From 3b2ea2ebd51de9279c05c839c50133652a586f40 Mon Sep 17 00:00:00 2001 From: HONGDAE KIM Date: Fri, 13 Feb 2026 23:31:42 +0900 Subject: [PATCH] feat: align CLI workflow with beginner guide --- README.md | 12 ++++++-- bitnet_tools/__init__.py | 6 ---- bitnet_tools/analysis.py | 38 +++++++++++++++++++------ bitnet_tools/cli.py | 61 +++++++++++++++++++++++++++++----------- tests/test_analysis.py | 15 +++++++++- tests/test_cli.py | 29 +++++++++++++++++++ 6 files changed, 126 insertions(+), 35 deletions(-) create mode 100644 tests/test_cli.py diff --git a/README.md b/README.md index 27213e7..18061de 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,11 @@ 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 @@ -145,8 +150,11 @@ ollama serve # 2) BitNet pull ollama pull -# 3) 테스트 -ollama run "샘플 매출 데이터를 요약해줘" +# 3) CSV 분석 payload 생성 +bitnet-analyze analyze sample.csv --question "샘플 매출 데이터를 요약해줘" --out payload.json + +# 4) (선택) 웹 UI 실행 +bitnet-analyze ui --host 127.0.0.1 --port 8765 ``` 필요하면 다음 단계에서 환경(OS/CPU/RAM/GPU)에 맞춰 diff --git a/bitnet_tools/__init__.py b/bitnet_tools/__init__.py index a36b517..9e8880d 100644 --- a/bitnet_tools/__init__.py +++ b/bitnet_tools/__init__.py @@ -1,6 +1,5 @@ """Utilities for BitNet-focused local data analysis workflows.""" - from .analysis import ( build_analysis_payload, build_analysis_payload_from_csv_text, @@ -14,8 +13,3 @@ "build_prompt", "summarize_rows", ] - -from .analysis import build_analysis_payload, summarize_rows - -__all__ = ["build_analysis_payload", "summarize_rows"] - diff --git a/bitnet_tools/analysis.py b/bitnet_tools/analysis.py index 94dfa3f..5a615ab 100644 --- a/bitnet_tools/analysis.py +++ b/bitnet_tools/analysis.py @@ -2,6 +2,7 @@ from dataclasses import dataclass import csv +import io import json from pathlib import Path from statistics import mean @@ -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(): @@ -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": "", + "question": question, + "summary": summary.to_dict(), + "prompt": build_prompt(summary, question), } diff --git a/bitnet_tools/cli.py b/bitnet_tools/cli.py index eb0f063..5d1362d 100644 --- a/bitnet_tools/cli.py +++ b/bitnet_tools/cli.py @@ -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: @@ -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__": diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 19b3bef..d9d6454 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -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(): @@ -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"] == "" + assert payload["summary"]["row_count"] == 2 diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..56d148c --- /dev/null +++ b/tests/test_cli.py @@ -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}