Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,23 @@ jupyter lab
## 6) 지금 바로 실행할 최소 커맨드 모음

```bash
# 0) 프로젝트 설치
python -m venv .venv
source .venv/bin/activate
pip install -e .

# 1) Ollama
curl -fsSL https://ollama.com/install.sh | sh
ollama serve

# 2) BitNet pull
ollama pull <bitnet-model-tag>

# 3) 테스트
ollama run <bitnet-model-tag> "샘플 매출 데이터를 요약해줘"
# 3) CSV 분석 payload 생성
bitnet-analyze analyze sample.csv --question "샘플 매출 데이터를 요약해줘" --out payload.json
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add sample CSV creation before analyze command

The new quick-start command block now calls bitnet-analyze analyze sample.csv ... without creating or pointing to an existing sample.csv, so users following section 6 verbatim will hit FileNotFoundError instead of completing the advertised “minimum commands” flow. This regresses the onboarding path introduced by this commit; please add a prior step that creates a sample CSV (or reference a concrete existing file path).

Useful? React with 👍 / 👎.


# 4) (선택) 웹 UI 실행
bitnet-analyze ui --host 127.0.0.1 --port 8765
```

필요하면 다음 단계에서 환경(OS/CPU/RAM/GPU)에 맞춰
Expand Down
6 changes: 0 additions & 6 deletions bitnet_tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Utilities for BitNet-focused local data analysis workflows."""


from .analysis import (
build_analysis_payload,
build_analysis_payload_from_csv_text,
Expand All @@ -14,8 +13,3 @@
"build_prompt",
"summarize_rows",
]

from .analysis import build_analysis_payload, summarize_rows

__all__ = ["build_analysis_payload", "summarize_rows"]

38 changes: 29 additions & 9 deletions bitnet_tools/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from dataclasses import dataclass
import csv
import io
import json
from pathlib import Path
from statistics import mean
Expand Down Expand Up @@ -80,6 +81,16 @@ def summarize_rows(rows: list[dict[str, str]], columns: list[str]) -> DataSummar
)


def build_prompt(summary: DataSummary, question: str) -> str:
return (
"너는 BitNet 기반 데이터 분석 보조자야.\n"
"아래 데이터 요약을 바탕으로 답변해.\n"
"출력 형식: 핵심요약 / 근거 / 한계 / 다음행동\n\n"
f"사용자 질문: {question}\n\n"
f"데이터 요약(JSON):\n{json.dumps(summary.to_dict(), ensure_ascii=False, indent=2)}"
)


def build_analysis_payload(csv_path: str | Path, question: str) -> dict[str, Any]:
path = Path(csv_path)
if not path.exists():
Expand All @@ -94,17 +105,26 @@ def build_analysis_payload(csv_path: str | Path, question: str) -> dict[str, Any

summary = summarize_rows(rows, columns)

prompt = (
"너는 BitNet 기반 데이터 분석 보조자야.\n"
"아래 데이터 요약을 바탕으로 답변해.\n"
"출력 형식: 핵심요약 / 근거 / 한계 / 다음행동\n\n"
f"사용자 질문: {question}\n\n"
f"데이터 요약(JSON):\n{json.dumps(summary.to_dict(), ensure_ascii=False, indent=2)}"
)

return {
"csv_path": str(path),
"question": question,
"summary": summary.to_dict(),
"prompt": prompt,
"prompt": build_prompt(summary, question),
}


def build_analysis_payload_from_csv_text(csv_text: str, question: str) -> dict[str, Any]:
reader = csv.DictReader(io.StringIO(csv_text))
if reader.fieldnames is None:
raise ValueError("CSV header not found")

columns = [str(c) for c in reader.fieldnames]
rows = list(reader)
summary = summarize_rows(rows, columns)

return {
"csv_path": "<inline_csv>",
"question": question,
"summary": summary.to_dict(),
"prompt": build_prompt(summary, question),
}
61 changes: 44 additions & 17 deletions bitnet_tools/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import argparse
import json
import subprocess
import sys
from pathlib import Path

from .analysis import build_analysis_payload
from .web import serve


def run_ollama(model: str, prompt: str) -> str:
Expand All @@ -20,37 +22,62 @@ def run_ollama(model: str, prompt: str) -> str:
return proc.stdout.strip()


def main() -> int:
parser = argparse.ArgumentParser(
description="Build BitNet-focused analysis prompt from a CSV file"
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="BitNet-focused CSV analysis helper")
subparsers = parser.add_subparsers(dest="command")

analyze_parser = subparsers.add_parser(
"analyze", help="Build analysis payload from a CSV file"
)
parser.add_argument("csv", type=Path, help="Input CSV path")
parser.add_argument("--question", required=True, help="Analysis question")
parser.add_argument(
analyze_parser.add_argument("csv", type=Path, help="Input CSV path")
analyze_parser.add_argument("--question", required=True, help="Analysis question")
analyze_parser.add_argument(
"--model",
default=None,
help="Optional Ollama model tag to run immediately (example: bitnet:latest)",
)
parser.add_argument(
analyze_parser.add_argument(
"--out",
type=Path,
default=Path("analysis_payload.json"),
help="Where to store generated payload JSON",
)

args = parser.parse_args()
payload = build_analysis_payload(args.csv, args.question)
ui_parser = subparsers.add_parser("ui", help="Run local web UI")
ui_parser.add_argument("--host", default="127.0.0.1", help="Bind host")
ui_parser.add_argument("--port", default=8765, type=int, help="Bind port")

return parser


def main(argv: list[str] | None = None) -> int:
raw_args = list(sys.argv[1:] if argv is None else argv)
if raw_args and raw_args[0] not in {"analyze", "ui", "-h", "--help"}:
raw_args.insert(0, "analyze")

parser = _build_parser()
args = parser.parse_args(raw_args)

if args.command == "ui":
serve(host=args.host, port=args.port)
return 0

args.out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"payload saved: {args.out}")
if args.command == "analyze":
payload = build_analysis_payload(args.csv, args.question)
args.out.write_text(
json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
)
print(f"payload saved: {args.out}")

if args.model:
print(f"running ollama model: {args.model}")
answer = run_ollama(args.model, payload["prompt"])
print("\n=== BitNet answer ===")
print(answer)
if args.model:
print(f"running ollama model: {args.model}")
answer = run_ollama(args.model, payload["prompt"])
print("\n=== BitNet answer ===")
print(answer)
return 0

return 0
parser.print_help()
return 2


if __name__ == "__main__":
Expand Down
15 changes: 14 additions & 1 deletion tests/test_analysis.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from bitnet_tools.analysis import build_analysis_payload, summarize_rows
from bitnet_tools.analysis import (
build_analysis_payload,
build_analysis_payload_from_csv_text,
summarize_rows,
)


def test_summarize_rows_basic():
Expand All @@ -25,3 +29,12 @@ def test_build_analysis_payload(tmp_path):
assert payload["csv_path"].endswith("sample.csv")
assert payload["summary"]["row_count"] == 2
assert "핵심요약 / 근거 / 한계 / 다음행동" in payload["prompt"]


def test_build_analysis_payload_from_csv_text():
payload = build_analysis_payload_from_csv_text(
"x,y\n1,2\n3,4\n", "합계를 설명해줘"
)

assert payload["csv_path"] == "<inline_csv>"
assert payload["summary"]["row_count"] == 2
29 changes: 29 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from pathlib import Path

from bitnet_tools import cli


def test_cli_analyze_legacy_mode(tmp_path):
csv_path = tmp_path / "sample.csv"
out_path = tmp_path / "result.json"
csv_path.write_text("a,b\n1,2\n", encoding="utf-8")

code = cli.main([str(csv_path), "--question", "요약해줘", "--out", str(out_path)])

assert code == 0
assert out_path.exists()


def test_cli_ui_mode(monkeypatch):
called = {}

def fake_serve(host: str, port: int):
called["host"] = host
called["port"] = port

monkeypatch.setattr(cli, "serve", fake_serve)

code = cli.main(["ui", "--host", "0.0.0.0", "--port", "9999"])

assert code == 0
assert called == {"host": "0.0.0.0", "port": 9999}
Loading