diff --git a/BitNet_Desktop_Start.bat b/BitNet_Desktop_Start.bat new file mode 100644 index 0000000..3638c8e --- /dev/null +++ b/BitNet_Desktop_Start.bat @@ -0,0 +1,14 @@ +@echo off +setlocal +cd /d %~dp0 + +if not exist .venv ( + py -m venv .venv +) + +call .venv\Scripts\activate +python -m pip install --upgrade pip >nul +python -m pip install -e . >nul + +start "" pythonw "%~dp0bitnet_desktop.pyw" +endlocal diff --git a/README.md b/README.md index 18061de..f9d578f 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,52 @@ --- -## 0) 이번 문서에서 바로 할 일 +## 0) 현재 완성도 빠른 진단 + +현 시점 기준 기능 완성도(실사용 관점): **약 80%** + +- 완료 + - CSV 기초 요약(행/열/결측/숫자 통계) + - BitNet용 프롬프트 자동 생성 + - CLI 실행 흐름 + - 브라우저 UI(`bitnet-analyze ui`) + - **윈도우 데스크톱 UI(`bitnet-analyze desktop`, `BitNet_Desktop_Start.bat`)** +- 남은 과제 + - 대용량 CSV(수십~수백 MB) 스트리밍 처리 + - 시각화(차트) 및 리포트 내보내기 + - 실행 전 환경진단(ollama 설치 여부 자동 점검) + +### 파일 붙여넣기 분석 가능 범위 + +가능: +- Python 코드, 로그, 에러 메시지, 설정 파일(`.toml`, `.json`, `.yaml`), CSV 샘플 +- 모듈 구조/의존성/리팩터링 포인트/버그 후보 분석 +- 여러 파일을 순차로 붙여주면 아키텍처 단위 진단 + +제약: +- 실제 실행이 필요한 문제(환경/권한/OS 특이 이슈)는 붙여넣기만으로 100% 재현 불가 +- 초대형 파일은 핵심 구간(에러 스택, 함수 단위) 분할 제공 권장 + +권장 붙여넣기 순서: +1. 에러 로그 전문 +2. 관련 함수/클래스 +3. 실행 명령어 +4. `pyproject.toml` 또는 의존성 목록 + +--- + +## 1) 이번 문서에서 바로 할 일 1. Ollama 설치 및 실행 2. BitNet 모델 1개 Pull 3. CLI로 동작 확인 4. Open WebUI 연결 5. JupyterLab에서 CSV 분석 + BitNet 해석 워크플로우 구성 +6. (Windows) 더블클릭으로 데스크톱 앱 실행 --- -## 1) 사전 확인 (10~20분) +## 2) 사전 확인 (10~20분) - OS 확인 - RAM/VRAM 확인 @@ -29,7 +64,7 @@ --- -## 2) Step-by-step 시작 절차 (BitNet 우선) +## 3) Step-by-step 시작 절차 (BitNet 우선) ### Step 1. Ollama 설치 ```bash @@ -79,9 +114,22 @@ pip install jupyterlab pandas matplotlib jupyter lab ``` +### Step 6. Windows 원클릭 실행 + +터미널 없이 사용하려면 아래 중 하나를 사용하세요. + +- 방법 A: 프로젝트 루트에서 `BitNet_Desktop_Start.bat` 더블클릭 +- 방법 B: 설치 후 `bitnet-desktop` 실행 +- 방법 C: `bitnet-analyze desktop` 실행 + +`BitNet_Desktop_Start.bat`는 다음을 자동 수행합니다. +- `.venv` 생성(없으면) +- 패키지 설치(`pip install -e .`) +- `pythonw`로 GUI 실행(콘솔창 없이) + --- -## 3) BitNet 기본 설정값 (안정성 우선) +## 4) BitNet 기본 설정값 (안정성 우선) - temperature: `0.2 ~ 0.5` - top_p: `0.9` @@ -95,12 +143,12 @@ jupyter lab --- -## 4) 데이터 분석 최소 워크플로우 (BitNet only) +## 5) 데이터 분석 최소 워크플로우 (BitNet only) -1. JupyterLab에서 CSV 로딩 -2. pandas로 결측/타입/기초통계 계산 -3. 계산 결과를 텍스트로 정리 -4. 정리된 텍스트를 BitNet에 입력해 인사이트/한계/추가 데이터 제안 받기 +1. CSV 로딩 +2. 결측/타입/기초통계 계산 +3. 계산 결과 기반 프롬프트 생성 +4. BitNet 실행으로 인사이트/한계/추가 데이터 제안 받기 예시 프롬프트: @@ -121,7 +169,7 @@ jupyter lab --- -## 5) 운영 안정화 체크리스트 +## 6) 운영 안정화 체크리스트 - [ ] BitNet 모델 1~2개만 유지 - [ ] 프롬프트 템플릿은 검증된 것만 유지 @@ -135,7 +183,7 @@ jupyter lab --- -## 6) 지금 바로 실행할 최소 커맨드 모음 +## 7) 지금 바로 실행할 최소 커맨드 모음 ```bash # 0) 프로젝트 설치 @@ -153,19 +201,16 @@ ollama pull # 3) CSV 분석 payload 생성 bitnet-analyze analyze sample.csv --question "샘플 매출 데이터를 요약해줘" --out payload.json -# 4) (선택) 웹 UI 실행 +# 4) 웹 UI 실행 bitnet-analyze ui --host 127.0.0.1 --port 8765 -``` -필요하면 다음 단계에서 환경(OS/CPU/RAM/GPU)에 맞춰 -- 정확한 BitNet 태그 -- 권장 context/max_tokens -- Open WebUI 프리셋 프롬프트 3종 -까지 바로 좁혀서 제안할 수 있습니다. +# 5) 데스크톱 UI 실행 +bitnet-analyze desktop +``` --- -## 7) GitHub 반영(적용) 절차 +## 8) GitHub 반영(적용) 절차 로컬에서 문서/설정을 수정한 뒤 아래 순서로 GitHub에 반영합니다. @@ -179,4 +224,3 @@ PR 생성 시 체크 포인트: - 변경 목적(왜 바꿨는지) 1~2줄 - 실행/검증한 명령어 - 사용자 관점에서 달라진 점(BitNet 우선 흐름, 실행 순서 명확화 등) - diff --git a/bitnet_desktop.pyw b/bitnet_desktop.pyw new file mode 100644 index 0000000..a31ec28 --- /dev/null +++ b/bitnet_desktop.pyw @@ -0,0 +1,5 @@ +from bitnet_tools.desktop import launch_desktop + + +if __name__ == "__main__": + launch_desktop() diff --git a/bitnet_tools/cli.py b/bitnet_tools/cli.py index 5d1362d..a762c2d 100644 --- a/bitnet_tools/cli.py +++ b/bitnet_tools/cli.py @@ -47,12 +47,14 @@ def _build_parser() -> argparse.ArgumentParser: 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") + subparsers.add_parser("desktop", help="Run Windows desktop UI") + 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"}: + if raw_args and raw_args[0] not in {"analyze", "ui", "desktop", "-h", "--help"}: raw_args.insert(0, "analyze") parser = _build_parser() @@ -62,6 +64,12 @@ def main(argv: list[str] | None = None) -> int: serve(host=args.host, port=args.port) return 0 + if args.command == "desktop": + from .desktop import launch_desktop + + launch_desktop() + return 0 + if args.command == "analyze": payload = build_analysis_payload(args.csv, args.question) args.out.write_text( diff --git a/bitnet_tools/desktop.py b/bitnet_tools/desktop.py new file mode 100644 index 0000000..e0a655e --- /dev/null +++ b/bitnet_tools/desktop.py @@ -0,0 +1,193 @@ +from __future__ import annotations + +import json +import subprocess +import threading +import tkinter as tk +from pathlib import Path +from tkinter import filedialog, messagebox, ttk + +from .analysis import build_analysis_payload + + +def run_ollama(model: str, prompt: str) -> str: + proc = subprocess.run( + ["ollama", "run", model, prompt], + capture_output=True, + text=True, + check=False, + ) + if proc.returncode != 0: + raise RuntimeError(proc.stderr.strip() or "ollama run failed") + return proc.stdout.strip() + + +class DesktopApp: + def __init__(self, root: tk.Tk) -> None: + self.root = root + self.root.title("BitNet CSV Analyzer (Windows)") + self.root.geometry("1100x760") + + self.csv_path: Path | None = None + self.latest_prompt = "" + + self._build_ui() + + def _build_ui(self) -> None: + frame = ttk.Frame(self.root, padding=12) + frame.pack(fill="both", expand=True) + + header = ttk.Label( + frame, + text="BitNet CSV Analyzer - 터미널 없이 바로 실행", + font=("Segoe UI", 14, "bold"), + ) + header.pack(anchor="w") + + sub = ttk.Label( + frame, + text="CSV 선택 → 분석 → BitNet 실행 순서로 사용하세요.", + ) + sub.pack(anchor="w", pady=(0, 10)) + + top_row = ttk.Frame(frame) + top_row.pack(fill="x", pady=(0, 8)) + ttk.Button(top_row, text="CSV 파일 열기", command=self._open_csv).pack(side="left") + + self.csv_label = ttk.Label(top_row, text="선택된 파일 없음") + self.csv_label.pack(side="left", padx=12) + + question_row = ttk.LabelFrame(frame, text="질문") + question_row.pack(fill="x", pady=(0, 8)) + + chip_row = ttk.Frame(question_row) + chip_row.pack(anchor="w", padx=8, pady=6) + presets = [ + "핵심 인사이트 3개와 근거를 알려줘", + "이상치 의심 포인트와 추가 확인 항목을 알려줘", + "실행 가능한 다음 액션 5개를 우선순위로 제안해줘", + ] + for txt in presets: + ttk.Button(chip_row, text=txt.split()[0], command=lambda t=txt: self._set_question(t)).pack( + side="left", padx=(0, 6) + ) + + self.question = tk.Text(question_row, height=3, wrap="word") + self.question.pack(fill="x", padx=8, pady=(0, 8)) + self.question.insert("1.0", presets[0]) + + model_row = ttk.Frame(frame) + model_row.pack(fill="x", pady=(0, 8)) + + ttk.Label(model_row, text="BitNet 모델 태그").pack(side="left") + self.model = ttk.Entry(model_row) + self.model.insert(0, "bitnet:latest") + self.model.pack(side="left", fill="x", expand=True, padx=8) + + ttk.Button(model_row, text="1) 분석", command=self._analyze_async).pack(side="left", padx=(8, 4)) + ttk.Button(model_row, text="2) BitNet 실행", command=self._run_model_async).pack(side="left") + + self.status = ttk.Label(frame, text="대기 중") + self.status.pack(anchor="w", pady=(0, 8)) + + output = ttk.Panedwindow(frame, orient="vertical") + output.pack(fill="both", expand=True) + + self.summary = self._make_text_panel(output, "데이터 요약") + self.prompt = self._make_text_panel(output, "생성 프롬프트") + self.answer = self._make_text_panel(output, "BitNet 응답") + + def _make_text_panel(self, parent: ttk.Panedwindow, title: str) -> tk.Text: + panel = ttk.LabelFrame(parent, text=title) + text = tk.Text(panel, wrap="word", height=10) + scrollbar = ttk.Scrollbar(panel, orient="vertical", command=text.yview) + text.configure(yscrollcommand=scrollbar.set) + text.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + parent.add(panel, weight=1) + return text + + def _on_ui(self, func, *args) -> None: + self.root.after(0, lambda: func(*args)) + + def _set_question(self, text: str) -> None: + self.question.delete("1.0", "end") + self.question.insert("1.0", text) + + def _open_csv(self) -> None: + path = filedialog.askopenfilename( + title="CSV 파일 선택", + filetypes=[("CSV files", "*.csv"), ("All files", "*.*")], + ) + if not path: + return + self.csv_path = Path(path) + self.csv_label.configure(text=str(self.csv_path)) + + def _get_question(self) -> str: + question = self.question.get("1.0", "end").strip() + return question or "이 데이터의 핵심 인사이트를 알려줘" + + def _analyze_async(self) -> None: + threading.Thread(target=self._analyze, daemon=True).start() + + def _analyze(self) -> None: + self._on_ui(self._set_status, "분석 중...") + try: + question = self._get_question() + if self.csv_path: + payload = build_analysis_payload(self.csv_path, question) + else: + self._on_ui( + messagebox.showinfo, + "파일 미선택", + "CSV를 선택하지 않아 본문 텍스트 입력을 안내합니다. 텍스트 박스에 CSV를 붙여넣으세요.", + ) + return + + self.latest_prompt = payload["prompt"] + self._on_ui(self._set_text, self.summary, json.dumps(payload["summary"], ensure_ascii=False, indent=2)) + self._on_ui(self._set_text, self.prompt, self.latest_prompt) + self._on_ui(self._set_text, self.answer, "") + self._on_ui(self._set_status, "분석 완료") + except Exception as exc: + self._on_ui(self._set_status, f"오류: {exc}") + + def _run_model_async(self) -> None: + threading.Thread(target=self._run_model, daemon=True).start() + + def _run_model(self) -> None: + if not self.latest_prompt: + self._on_ui(self._set_text, self.answer, "먼저 분석을 실행해 프롬프트를 생성하세요.") + return + + model = self.model.get().strip() + if not model: + self._on_ui(self._set_text, self.answer, "모델 태그를 입력하세요. 예: bitnet:latest") + return + + self._on_ui(self._set_status, "BitNet 실행 중...") + try: + result = run_ollama(model, self.latest_prompt) + self._on_ui(self._set_text, self.answer, result) + self._on_ui(self._set_status, "BitNet 실행 완료") + except Exception as exc: + self._on_ui(self._set_text, self.answer, f"오류: {exc}") + self._on_ui(self._set_status, "BitNet 실행 실패") + + def _set_text(self, widget: tk.Text, value: str) -> None: + widget.delete("1.0", "end") + widget.insert("1.0", value) + + def _set_status(self, value: str) -> None: + self.status.configure(text=value) + + +def launch_desktop() -> None: + root = tk.Tk() + DesktopApp(root) + root.mainloop() + + +if __name__ == "__main__": + launch_desktop() diff --git a/pyproject.toml b/pyproject.toml index 4f7a543..ae288de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [] [project.scripts] bitnet-analyze = "bitnet_tools.cli:main" +bitnet-desktop = "bitnet_tools.desktop:launch_desktop" [tool.pytest.ini_options] testpaths = ["tests"]