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
77 changes: 77 additions & 0 deletions bitnet_tools/web.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,87 @@
from __future__ import annotations

from http import HTTPStatus
from concurrent.futures import Future, ThreadPoolExecutor
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
from pathlib import Path
import subprocess
import tempfile
import threading
import uuid
from typing import Any
from urllib.parse import urlparse

from .analysis import build_analysis_payload_from_csv_text
from .multi_csv import analyze_multiple_csv
from .visualize import create_multi_charts


UI_DIR = Path(__file__).parent / "ui"


CHART_JOB_DIR = Path('.bitnet_cache') / 'chart_jobs'
_CHART_EXECUTOR = ThreadPoolExecutor(max_workers=2)
_CHART_JOBS: dict[str, Future] = {}
_CHART_LOCK = threading.Lock()


def _run_chart_job(job_id: str, files: list[dict[str, str]]) -> dict[str, Any]:
CHART_JOB_DIR.mkdir(parents=True, exist_ok=True)
job_input_dir = CHART_JOB_DIR / f"{job_id}_input"
out_dir = CHART_JOB_DIR / f"{job_id}_charts"
job_input_dir.mkdir(parents=True, exist_ok=True)

csv_paths: list[Path] = []
for i, item in enumerate(files):
name = str(item.get('name', f'file_{i}.csv'))
text = str(item.get('csv_text', ''))
if not text.strip():
continue
if not name.endswith('.csv'):
name = f"{name}.csv"
path = job_input_dir / name
path.write_text(text, encoding='utf-8')
csv_paths.append(path)

if not csv_paths:
raise ValueError('valid csv_text files are required')

charts = create_multi_charts(csv_paths, out_dir)
return {
'job_id': job_id,
'status': 'done',
'chart_count': sum(len(v) for v in charts.values()),
'charts': charts,
'output_dir': str(out_dir),
}


def submit_chart_job(files: list[dict[str, str]]) -> str:
if not isinstance(files, list) or not files:
raise ValueError('files is required')
job_id = uuid.uuid4().hex
future = _CHART_EXECUTOR.submit(_run_chart_job, job_id, files)
with _CHART_LOCK:
_CHART_JOBS[job_id] = future
return job_id


def get_chart_job(job_id: str) -> dict[str, Any]:
with _CHART_LOCK:
future = _CHART_JOBS.get(job_id)

if future is None:
return {'job_id': job_id, 'status': 'not_found'}
if not future.done():
return {'job_id': job_id, 'status': 'running'}
try:
return future.result()
except Exception as exc:
return {'job_id': job_id, 'status': 'failed', 'error': str(exc)}



def run_ollama(model: str, prompt: str) -> str:
proc = subprocess.run(
["ollama", "run", model, prompt],
Expand Down Expand Up @@ -55,6 +122,11 @@ def do_GET(self) -> None:
return self._send_file(UI_DIR / "app.js", "application/javascript; charset=utf-8")
if route == "/styles.css":
return self._send_file(UI_DIR / "styles.css", "text/css; charset=utf-8")
if route.startswith('/api/charts/jobs/'):
job_id = route.split('/')[-1].strip()
if not job_id:
return self._send_json({'error': 'job id is required'}, HTTPStatus.BAD_REQUEST)
return self._send_json(get_chart_job(job_id))
self.send_error(HTTPStatus.NOT_FOUND)

def do_POST(self) -> None:
Expand Down Expand Up @@ -113,6 +185,11 @@ def do_POST(self) -> None:
)
return self._send_json(result)

if route == "/api/charts/jobs":
files = payload.get('files', [])
job_id = submit_chart_job(files)
return self._send_json({'job_id': job_id, 'status': 'queued'}, HTTPStatus.ACCEPTED)

if route == "/api/run":
model = str(payload.get("model", "")).strip()
prompt = str(payload.get("prompt", "")).strip()
Expand Down
36 changes: 36 additions & 0 deletions tests/test_web.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import time
from pathlib import Path

import bitnet_tools.web as web


def test_submit_and_get_chart_job_done(monkeypatch, tmp_path):
monkeypatch.setattr(web, "CHART_JOB_DIR", tmp_path / "jobs")

def fake_create_multi_charts(csv_paths, out_dir):
out_dir.mkdir(parents=True, exist_ok=True)
outputs = {}
for p in csv_paths:
chart = out_dir / f"{Path(p).stem}.png"
chart.write_text("ok", encoding="utf-8")
outputs[str(p)] = [str(chart)]
return outputs

monkeypatch.setattr(web, "create_multi_charts", fake_create_multi_charts)

job_id = web.submit_chart_job([{"name": "a.csv", "csv_text": "x\n1\n"}])
result = web.get_chart_job(job_id)
for _ in range(20):
if result["status"] != "running":
break
time.sleep(0.01)
result = web.get_chart_job(job_id)

assert result["status"] == "done"
assert result["chart_count"] == 1
assert result["output_dir"].endswith("_charts")


def test_get_chart_job_not_found():
result = web.get_chart_job("missing")
assert result["status"] == "not_found"