GitHub Actions commited on
Commit
7f9dfed
·
0 Parent(s):

Initial ZeroGPU deployment with spaces shim

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.github/workflows/ci.yml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test-quality:
9
+ runs-on: windows-latest
10
+
11
+ steps:
12
+ - name: Checkout
13
+ uses: actions/checkout@v4
14
+
15
+ - name: Set up Python
16
+ uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.11"
19
+
20
+ - name: Install dependencies
21
+ run: |
22
+ python -m pip install --upgrade pip setuptools
23
+ python -m pip install -r requirements.txt -r requirements-dev.txt
24
+
25
+ - name: Tests, coverage, lint, type, security
26
+ shell: pwsh
27
+ run: .\scripts\run_quality.ps1
.gitignore ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .coverage
6
+ htmlcov/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .gradio/
10
+ node_modules/
11
+ #test-results/
12
+ #playwright-report/
13
+ blob-report/
14
+ #assets/e2e/playwright-report/
15
+ #assets/e2e/plant-playwright-report/
16
+ gradio.out.log
17
+ gradio.err.log
18
+ data/*
19
+ !data/.gitkeep
20
+ exports/*
21
+ !exports/.gitkeep
22
+ models_cache/
23
+ hf_cache/
24
+ wandb/
25
+ *.gguf
26
+ *.safetensors
27
+ *.bin
28
+ *.ckpt
29
+ *.onnx
30
+ *.tflite
31
+ *.pt
32
+ *.pth
33
+ *.model
34
+ .env
35
+ .env.*
36
+ *.pem
37
+ *.key
AGENTS.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AGENTS.md
2
+
3
+ ## Project Goal
4
+ Build a Gradio-based OpenBMB Local AI Workbench for the Build Small Hackathon.
5
+ The app should run locally first, deploy as a Hugging Face Space, and document the
6
+ small-model choices clearly.
7
+
8
+ ## Project Memory
9
+ - Start with `docs/README.md`.
10
+ - Use `docs/TASKS.md` as the task checklist.
11
+ - Use `docs/IMPLEMENTATION_STATUS.md` to decide whether something is really done.
12
+ - Use `docs/PRD_IMPLEMENTATION_MATRIX.md` before claiming PRD or extension PRD completion.
13
+ - Use `docs/ACCEPTANCE_CRITERIA.md` before marking work complete.
14
+ - Use `docs/ROADMAP.md` to prioritize hackathon work over stretch work.
15
+ - Use `docs/ROADMAP_V2_CRITICAL_IMPROVEMENT_PLAN.md` for hard judge-oriented prioritization.
16
+ - Use `docs/TEMPLATE_HOWTO.md` before creating a new domain app from the template.
17
+ - Use `docs/PLANT_DISCOVERY_APP_PLAN.md` before changing the Plant Discovery reference app.
18
+ - Use `docs/PLANT_MODEL_AND_TRAINING_HOWTO.md` before changing Plant Discovery model or training behavior.
19
+ - Use `docs/ARCHITECTURE.md` before changing app structure.
20
+ - Use `docs/EXTENDING.md` before adding models, tabs, services, or training.
21
+ - Main PRD: `HF_PRD_v1.md`.
22
+ - Extension PRD: `HF_PRD_ext.md`.
23
+
24
+ ## Hackathon Constraints
25
+ - Use Gradio as the app surface.
26
+ - Keep all model choices at or below 32B parameters.
27
+ - Prefer local/offline inference paths when practical.
28
+ - Avoid cloud APIs unless the user explicitly approves them.
29
+ - Make the README useful for judges: goal, setup, model choice, local run, Space deployment, demo flow.
30
+
31
+ ## Current MVP Scope
32
+ - Start with a working Gradio app and deterministic placeholder service.
33
+ - Add real inference incrementally through Ollama, llama.cpp, Transformers, or SGLang.
34
+ - Keep training, GGUF export, Trackio, MCP, and agent functionality as documented extension points until implemented.
35
+ - Never claim the full PRD is done while placeholders or unchecked PRD tasks remain.
36
+
37
+ ## Commands
38
+ - Create venv: `python -m venv .venv`
39
+ - Activate venv: `.venv\Scripts\Activate.ps1`
40
+ - Install: `python -m pip install -r requirements.txt`
41
+ - Install dev tools: `python -m pip install -r requirements-dev.txt`
42
+ - Run locally: `python app.py`
43
+ - Run tests: `.\scripts\run_tests.ps1`
44
+ - Run performance tests: `.\scripts\run_performance.ps1`
45
+ - Run quality checks: `.\scripts\run_quality.ps1`
46
+
47
+ ## Code Style
48
+ - Prefer small modules with clear responsibilities.
49
+ - Keep model IDs in `config/models.yaml`; avoid hardcoding them in UI logic.
50
+ - Keep user-facing text concise and demo-oriented.
51
+ - Do not download model weights automatically on app startup.
52
+
53
+ ## Verification
54
+ - Before claiming the app runs, verify `python app.py` starts locally.
55
+ - If Python or dependencies are missing, say exactly what is missing.
56
+ - Keep generated caches, model weights, exports, and virtualenvs out of git.
57
+ - After implementing a task, update `docs/TASKS.md` and `docs/IMPLEMENTATION_STATUS.md`.
58
+ - Every feature needs tests. Add or update unit tests for logic and user-story tests for workflows.
59
+ - Whenever a bug, failed check, or regression is found, create or update a test that would catch it before fixing the code.
60
+ - Do not mark a feature done until its tests pass or the test blocker is documented.
61
+ - Avoid blanket coverage escapes. Do not add `pragma: no cover` without a specific documented reason.
62
+ - For PRD work, update `docs/PRD_IMPLEMENTATION_MATRIX.md` along with task/status docs.
HF_PRD_ext.md ADDED
@@ -0,0 +1,1631 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenBMB Workbench — Planned Extensions: Detailed Spec
2
+ > 2026-06-05 | Companion to PRD v2.0
3
+
4
+ ---
5
+
6
+ ## Table of Contents
7
+
8
+ 1. [vLLM Serving Tab](#1-vllm-serving-tab)
9
+ 2. [Ollama Quick-Start](#2-ollama-quick-start)
10
+ 3. [Reward Model Eval](#3-reward-model-eval)
11
+ 4. [Synthetic Data Gen](#4-synthetic-data-gen)
12
+ 5. [Paper-to-Code Agent](#5-paper-to-code-agent)
13
+ 6. [HF Spaces Deploy](#6-hf-spaces-deploy)
14
+ 7. [VINDEX Integration](#7-vindex-integration)
15
+ 8. [OCR Pipeline Hook](#8-ocr-pipeline-hook)
16
+ 9. [MiniCPM Desk-Pet](#9-minicpm-desk-pet)
17
+ 10. [MiniCPM-o Audio Tab](#10-minicpm-o-audio-tab)
18
+ 11. [Cross-Extension Wiring](#11-cross-extension-wiring)
19
+
20
+ ---
21
+
22
+ ## 1. vLLM Serving Tab
23
+
24
+ ### What it is
25
+
26
+ vLLM is a production-grade inference engine built around *PagedAttention* — a KV-cache management
27
+ algorithm that treats GPU memory like virtual memory pages. The result is dramatically higher
28
+ throughput when multiple requests run concurrently, compared to naive Transformers inference.
29
+
30
+ In the workbench context, vLLM adds a fourth inference mode alongside llama.cpp, SGLang, and
31
+ Ollama. You use it when you want OpenAI-compatible HTTP endpoints, continuous batching, or when
32
+ benchmarking production serving latency.
33
+
34
+ ### Why it matters
35
+
36
+ | Scenario | Benefit |
37
+ |----------|---------|
38
+ | Benchmarking fine-tuned LoRA | Compare throughput before/after fine-tune |
39
+ | Multi-user demo | Queue and batch concurrent requests |
40
+ | Production deployment | OpenAI-compatible API, drop-in for existing tooling |
41
+ | MiniCPM4.1-8B long context | PagedAttention shines on 128K context — avoids OOM |
42
+
43
+ ### Architecture
44
+
45
+ ```
46
+ models/vllm_runner.py
47
+ VLLMRunner
48
+ .start(model_id, cfg) → subprocess: vllm serve ...
49
+ .stop() → terminate subprocess
50
+ .chat(messages) → str → POST /v1/chat/completions
51
+ .batch(prompts) → list[str]→ concurrent POST via asyncio
52
+ .stats() → dict → GET /metrics (Prometheus)
53
+ ```
54
+
55
+ ```python
56
+ # models/vllm_runner.py
57
+ import subprocess, asyncio, requests
58
+ from openai import AsyncOpenAI
59
+
60
+ class VLLMRunner:
61
+ def __init__(self, cfg: dict):
62
+ self.model_id = cfg["hf_id"]
63
+ self.port = cfg.get("port", 8000)
64
+ self.gpu_memory = cfg.get("gpu_memory_utilization", 0.85)
65
+ self.trust_rc = cfg.get("trust_remote_code", False)
66
+ self._proc = None
67
+ self._client = AsyncOpenAI(
68
+ base_url=f"http://localhost:{self.port}/v1",
69
+ api_key="vllm-local"
70
+ )
71
+
72
+ def start(self):
73
+ cmd = [
74
+ "vllm", "serve", self.model_id,
75
+ "--port", str(self.port),
76
+ "--gpu-memory-utilization", str(self.gpu_memory),
77
+ ]
78
+ if self.trust_rc:
79
+ cmd.append("--trust-remote-code")
80
+ self._proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
81
+ self._wait_ready()
82
+
83
+ def _wait_ready(self, timeout: int = 60):
84
+ import time
85
+ for _ in range(timeout):
86
+ try:
87
+ r = requests.get(f"http://localhost:{self.port}/health")
88
+ if r.status_code == 200:
89
+ return
90
+ except Exception:
91
+ pass
92
+ time.sleep(1)
93
+ raise RuntimeError("vLLM server did not become healthy")
94
+
95
+ async def chat(self, messages: list[dict], **kwargs) -> str:
96
+ resp = await self._client.chat.completions.create(
97
+ model=self.model_id,
98
+ messages=messages,
99
+ max_tokens=kwargs.get("max_tokens", 512),
100
+ )
101
+ return resp.choices[0].message.content
102
+
103
+ async def batch(self, prompts: list[str], system: str = "") -> list[str]:
104
+ tasks = [
105
+ self.chat([
106
+ {"role": "system", "content": system},
107
+ {"role": "user", "content": p},
108
+ ])
109
+ for p in prompts
110
+ ]
111
+ return await asyncio.gather(*tasks)
112
+
113
+ def stats(self) -> dict:
114
+ """Prometheus metrics parsed to dict. Returns throughput + latency."""
115
+ r = requests.get(f"http://localhost:{self.port}/metrics")
116
+ lines = r.text.splitlines()
117
+ metrics = {}
118
+ for line in lines:
119
+ if line.startswith("vllm:") and not line.startswith("#"):
120
+ key, val = line.rsplit(" ", 1)
121
+ metrics[key] = float(val)
122
+ return metrics
123
+
124
+ def stop(self):
125
+ if self._proc:
126
+ self._proc.terminate()
127
+ self._proc = None
128
+ ```
129
+
130
+ ### UI tab (models_tab.py → extend)
131
+
132
+ Add a "vLLM" section to the models tab:
133
+
134
+ ```python
135
+ with gr.Tab("⚡ vLLM"):
136
+ model_dd = gr.Dropdown(label="Model", choices=list_vllm_compatible())
137
+ gpu_mem_sl = gr.Slider(0.5, 0.95, value=0.85, label="GPU memory utilization")
138
+ start_btn = gr.Button("Start server")
139
+ stop_btn = gr.Button("Stop server")
140
+ status_lbl = gr.Label(label="Status")
141
+ metrics_json = gr.JSON(label="Live metrics")
142
+
143
+ def start_vllm(model_id, gpu_mem):
144
+ cfg = {**model_registry.get(model_id).cfg,
145
+ "gpu_memory_utilization": gpu_mem}
146
+ runner = VLLMRunner(cfg)
147
+ runner.start()
148
+ model_registry.register(f"{model_id}_vllm", runner)
149
+ return "Running", runner.stats()
150
+
151
+ start_btn.click(start_vllm, [model_dd, gpu_mem_sl], [status_lbl, metrics_json])
152
+ ```
153
+
154
+ ### Trackio events fired
155
+
156
+ ```python
157
+ trackio.init(project="workbench", run_name="vllm_benchmark")
158
+ trackio.log({"throughput_tok_per_s": ..., "p50_latency_ms": ..., "gpu_mem_used": ...})
159
+ trackio.finish()
160
+ ```
161
+
162
+ ---
163
+
164
+ ## 2. Ollama Quick-Start
165
+
166
+ ### What it is
167
+
168
+ Ollama is zero-configuration local model serving. One `ollama pull` downloads a quantized model
169
+ and one `ollama serve` runs it. No CUDA setup, no Python environment issues. The REST API is
170
+ OpenAI-compatible on port 11434.
171
+
172
+ MiniCPM-V-4.6 and MiniCPM5-1B are both in the Ollama registry:
173
+ ```bash
174
+ ollama pull openbmb/minicpm-v4.6
175
+ ollama pull openbmb/minicpm5-1b
176
+ ```
177
+
178
+ ### Why it matters
179
+
180
+ Ollama is the fastest path from "nothing" to "running model" — ideal for demos, non-GPU machines
181
+ (Apple Silicon is well-optimized), and users who shouldn't need to understand quantization.
182
+
183
+ ### Architecture
184
+
185
+ ```
186
+ models/ollama_runner.py
187
+ OllamaRunner
188
+ .pull(model_id) → subprocess: ollama pull ...
189
+ .chat(messages) → str → POST http://localhost:11434/api/chat
190
+ .generate(prompt) → str → POST http://localhost:11434/api/generate (streaming)
191
+ .list() → list[str] → GET /api/tags
192
+ ```
193
+
194
+ ```python
195
+ # models/ollama_runner.py
196
+ import requests, subprocess, json
197
+ from typing import Generator
198
+
199
+ class OllamaRunner:
200
+ BASE = "http://localhost:11434"
201
+
202
+ def __init__(self, model_id: str):
203
+ # Ollama uses "openbmb/minicpm-v4.6" style IDs directly
204
+ self.model_id = model_id
205
+
206
+ @staticmethod
207
+ def pull(model_id: str):
208
+ subprocess.run(["ollama", "pull", model_id], check=True)
209
+
210
+ @staticmethod
211
+ def list_local() -> list[str]:
212
+ r = requests.get(f"{OllamaRunner.BASE}/api/tags")
213
+ return [m["name"] for m in r.json().get("models", [])]
214
+
215
+ def chat(self, messages: list[dict], stream: bool = False) -> str | Generator:
216
+ payload = {"model": self.model_id, "messages": messages, "stream": stream}
217
+ r = requests.post(f"{self.BASE}/api/chat", json=payload, stream=stream)
218
+ if not stream:
219
+ return r.json()["message"]["content"]
220
+ # Generator for Gradio streaming
221
+ def _stream():
222
+ for line in r.iter_lines():
223
+ if line:
224
+ chunk = json.loads(line)
225
+ yield chunk["message"]["content"]
226
+ if chunk.get("done"):
227
+ break
228
+ return _stream()
229
+
230
+ def vision_chat(self, image_b64: str, prompt: str) -> str:
231
+ """Multimodal chat — Ollama passes images as base64 in the message."""
232
+ messages = [{
233
+ "role": "user",
234
+ "content": prompt,
235
+ "images": [image_b64],
236
+ }]
237
+ return self.chat(messages)
238
+ ```
239
+
240
+ ### UI integration
241
+
242
+ The models tab gets an "Ollama" subtab with a model browser, pull button, and instant chat
243
+ that requires zero setup — the simplest entry point to the whole workbench.
244
+
245
+ ```python
246
+ with gr.Tab("🦙 Ollama"):
247
+ available = gr.Dropdown(label="Pull model",
248
+ choices=["openbmb/minicpm-v4.6", "openbmb/minicpm5-1b"],
249
+ allow_custom_value=True)
250
+ pull_btn = gr.Button("Pull")
251
+ local_list = gr.JSON(label="Locally available")
252
+ pull_status= gr.Textbox(label="Status")
253
+
254
+ pull_btn.click(
255
+ lambda m: (OllamaRunner.pull(m), OllamaRunner.list_local()),
256
+ [available],
257
+ [pull_status, local_list]
258
+ )
259
+ ```
260
+
261
+ ---
262
+
263
+ ## 3. Reward Model Eval
264
+
265
+ ### What it is
266
+
267
+ A reward model is a model trained to score (prompt, response) pairs — answering "how good is this
268
+ output?" It's the missing piece between fine-tuning and verified alignment improvement. Without it
269
+ you can train a LoRA and only know quantitatively that loss went down, not whether outputs actually
270
+ got better by human-relevant criteria.
271
+
272
+ ### Why it matters
273
+
274
+ - Validates that LoRA fine-tuning improved quality (not just minimized loss)
275
+ - Enables best-of-N sampling: generate N responses, keep highest-scored
276
+ - Enables DPO data creation: generate response pairs, reward model labels preferences
277
+ - Closes the RLHF loop within the workbench itself
278
+
279
+ ### Reward model options
280
+
281
+ | Model | Size | Focus |
282
+ |-------|------|-------|
283
+ | `OpenAssistant/reward-model-deberta-v3-large-v2` | 450M | General helpfulness |
284
+ | `Salesforce/SFR-Reward-FsfairX-LLaMA3-RM-v0.1` | 8B | Instruction following |
285
+ | MiniCPM5-1B itself (self-eval) | 1B | Domain-specific, via prompt |
286
+
287
+ For the workbench, using MiniCPM5-1B as a judge (LLM-as-judge pattern) is the lowest-friction
288
+ option since the model is already loaded.
289
+
290
+ ### Architecture
291
+
292
+ ```
293
+ training/reward_eval.py
294
+ RewardEvaluator
295
+ .score(prompt, response) → float
296
+ .best_of_n(prompt, n, generator) → str
297
+ .create_dpo_pairs(dataset, generator, n=4) → Dataset
298
+ .eval_lora_vs_base(base_svc, lora_svc, eval_ds) → dict
299
+ ```
300
+
301
+ ```python
302
+ # training/reward_eval.py
303
+ from transformers import pipeline, AutoModelForSequenceClassification, AutoTokenizer
304
+ import torch
305
+
306
+ class RewardEvaluator:
307
+ def __init__(self, reward_model_id: str = "OpenAssistant/reward-model-deberta-v3-large-v2"):
308
+ self.pipe = pipeline(
309
+ "text-classification",
310
+ model=reward_model_id,
311
+ device=0 if torch.cuda.is_available() else -1,
312
+ )
313
+
314
+ def score(self, prompt: str, response: str) -> float:
315
+ """Returns a scalar reward score (higher = better)."""
316
+ text = f"Human: {prompt}\n\nAssistant: {response}"
317
+ result = self.pipe(text, truncation=True, max_length=512)
318
+ return result[0]["score"]
319
+
320
+ def best_of_n(
321
+ self,
322
+ prompt: str,
323
+ n: int,
324
+ generator_fn, # callable: prompt -> str
325
+ ) -> tuple[str, float]:
326
+ """Generate N responses, return the one with highest reward."""
327
+ candidates = [(generator_fn(prompt), ) for _ in range(n)]
328
+ scored = [(r[0], self.score(prompt, r[0])) for r in candidates]
329
+ return max(scored, key=lambda x: x[1])
330
+
331
+ def create_dpo_pairs(
332
+ self,
333
+ prompts: list[str],
334
+ generator_fn,
335
+ n: int = 4,
336
+ ):
337
+ """
338
+ For each prompt: generate n responses, rank by reward,
339
+ return (prompt, chosen, rejected) triplets for DPO training.
340
+ """
341
+ from datasets import Dataset
342
+ rows = []
343
+ for prompt in prompts:
344
+ responses = [(generator_fn(prompt), ) for _ in range(n)]
345
+ scored = sorted(
346
+ [(r[0], self.score(prompt, r[0])) for r in responses],
347
+ key=lambda x: x[1], reverse=True
348
+ )
349
+ if len(scored) >= 2:
350
+ rows.append({
351
+ "prompt": prompt,
352
+ "chosen": scored[0][0],
353
+ "rejected": scored[-1][0],
354
+ "reward_gap": scored[0][1] - scored[-1][1],
355
+ })
356
+ return Dataset.from_list(rows)
357
+
358
+ def eval_lora_vs_base(
359
+ self,
360
+ base_svc,
361
+ lora_svc,
362
+ eval_prompts: list[str],
363
+ ) -> dict:
364
+ """
365
+ Compare base vs LoRA checkpoint by average reward score.
366
+ Returns win rate and per-prompt scores.
367
+ """
368
+ base_scores = [self.score(p, base_svc.generate(p)) for p in eval_prompts]
369
+ lora_scores = [self.score(p, lora_svc.generate(p)) for p in eval_prompts]
370
+ wins = sum(l > b for l, b in zip(lora_scores, base_scores))
371
+ return {
372
+ "base_mean": sum(base_scores) / len(base_scores),
373
+ "lora_mean": sum(lora_scores) / len(lora_scores),
374
+ "lora_win_rate": wins / len(eval_prompts),
375
+ "per_prompt": list(zip(eval_prompts, base_scores, lora_scores)),
376
+ }
377
+ ```
378
+
379
+ ### Trackio logging
380
+
381
+ ```python
382
+ results = evaluator.eval_lora_vs_base(base_svc, lora_svc, eval_prompts)
383
+ trackio.init(project="workbench", run_name="reward_eval")
384
+ trackio.log({
385
+ "base_reward_mean": results["base_mean"],
386
+ "lora_reward_mean": results["lora_mean"],
387
+ "lora_win_rate": results["lora_win_rate"],
388
+ })
389
+ trackio.finish()
390
+ ```
391
+
392
+ ---
393
+
394
+ ## 4. Synthetic Data Gen
395
+
396
+ ### What it is
397
+
398
+ The ml-intern finding: *when real data is insufficient, have an LLM generate training data*.
399
+ This module does exactly that — it uses a capable model (MiniCPM4.1-8B or a cloud model via
400
+ HF Router) to generate diverse, high-quality (prompt, response) pairs on demand.
401
+
402
+ ### Why it matters
403
+
404
+ Real-world fine-tuning is often blocked not by compute but by data. You have 50 good examples
405
+ but need 5000. Synthetic gen + quality filtering bridges that gap, especially for specialized
406
+ domains (plant species, historical OCR corrections, industrial inspection defect labels).
407
+
408
+ ### Architecture
409
+
410
+ ```
411
+ datasets/synthetic.py
412
+ SyntheticGenerator
413
+ .generate(topic, n, schema) → Dataset
414
+ .augment(existing_ds, n) → Dataset
415
+ .filter_quality(ds, min_score) → Dataset
416
+ .generate_dpo_pairs(topic, n) → Dataset
417
+ ```
418
+
419
+ ```python
420
+ # datasets/synthetic.py
421
+ import json
422
+ from datasets import Dataset
423
+
424
+ GENERATION_PROMPT = """You are a training data generator. Generate {n} diverse, high-quality
425
+ training examples for the topic: {topic}.
426
+
427
+ Output ONLY a valid JSON array. Each item must have these fields: {schema}
428
+ No explanation, no markdown, no preamble. Raw JSON array only."""
429
+
430
+ class SyntheticGenerator:
431
+ def __init__(self, generator_svc):
432
+ """generator_svc: any loaded ModelService with a .generate(prompt) method."""
433
+ self.gen = generator_svc
434
+
435
+ def generate(
436
+ self,
437
+ topic: str,
438
+ n: int = 100,
439
+ schema: dict | None = None,
440
+ ) -> Dataset:
441
+ """
442
+ Generate n training examples on a topic.
443
+ schema: dict of field_name → description, e.g.
444
+ {"instruction": "task to perform", "response": "ideal answer"}
445
+ """
446
+ schema = schema or {"instruction": "user task", "response": "ideal answer"}
447
+ schema_str = ", ".join(f'"{k}": "{v}"' for k, v in schema.items())
448
+
449
+ # Generate in batches of 20 to stay within context
450
+ rows = []
451
+ for batch_start in range(0, n, 20):
452
+ batch_n = min(20, n - batch_start)
453
+ prompt = GENERATION_PROMPT.format(
454
+ n=batch_n, topic=topic, schema="{" + schema_str + "}"
455
+ )
456
+ raw = self.gen.generate(prompt)
457
+ try:
458
+ # Strip any accidental markdown fences
459
+ clean = raw.strip().lstrip("```json").lstrip("```").rstrip("```")
460
+ batch = json.loads(clean)
461
+ rows.extend(batch)
462
+ except json.JSONDecodeError:
463
+ # Skip malformed batches; log the failure
464
+ continue
465
+
466
+ return Dataset.from_list(rows)
467
+
468
+ def augment(self, existing_ds: Dataset, n: int) -> Dataset:
469
+ """
470
+ Use existing examples as few-shot demonstrations to generate n more.
471
+ Samples up to 5 examples from existing_ds as context.
472
+ """
473
+ import random
474
+ samples = existing_ds.shuffle().select(range(min(5, len(existing_ds))))
475
+ examples_str = json.dumps(samples.to_list(), indent=2)
476
+
477
+ prompt = f"""Here are {len(samples)} example training items:
478
+ {examples_str}
479
+
480
+ Generate {n} MORE diverse examples in the exact same JSON format.
481
+ Output only the JSON array, no explanation."""
482
+
483
+ raw = self.gen.generate(prompt)
484
+ try:
485
+ clean = raw.strip().lstrip("```json").rstrip("```")
486
+ new_rows = json.loads(clean)
487
+ except json.JSONDecodeError:
488
+ return existing_ds
489
+ return Dataset.from_list(existing_ds.to_list() + new_rows)
490
+
491
+ def filter_quality(
492
+ self,
493
+ ds: Dataset,
494
+ reward_evaluator=None,
495
+ min_score: float = 0.6,
496
+ ) -> Dataset:
497
+ """
498
+ Filter with reward model if available, else heuristic filters.
499
+ Heuristics: min length, no repeated n-grams, valid JSON fields.
500
+ """
501
+ if reward_evaluator:
502
+ def _score(row):
503
+ return reward_evaluator.score(
504
+ row.get("instruction", ""),
505
+ row.get("response", "")
506
+ ) >= min_score
507
+ return ds.filter(_score)
508
+ else:
509
+ # Basic heuristics
510
+ def _heuristic(row):
511
+ resp = row.get("response", "")
512
+ return (
513
+ len(resp) >= 20 and # not too short
514
+ len(resp) <= 4096 and # not too long
515
+ resp.count(resp[:20]) < 3 # not repetitive
516
+ )
517
+ return ds.filter(_heuristic)
518
+
519
+ def generate_for_domain(
520
+ self,
521
+ domain: str,
522
+ output_path: str,
523
+ n: int = 500,
524
+ ):
525
+ """
526
+ Convenience method: generate, augment, filter, save to disk.
527
+ Use for plant ID: domain="Plant species identification from photo descriptions"
528
+ """
529
+ ds = self.generate(topic=domain, n=n // 2)
530
+ ds = self.augment(ds, n=n // 2)
531
+ ds = self.filter_quality(ds)
532
+ ds.save_to_disk(output_path)
533
+ return ds
534
+ ```
535
+
536
+ ### Domain-specific example: plant ID
537
+
538
+ ```python
539
+ gen = SyntheticGenerator(model_registry.get("minicpm41_8b"))
540
+ plant_ds = gen.generate_for_domain(
541
+ domain="Identifying plant species from visual descriptions. "
542
+ "Include common name, latin name, family, key visual features, and care tips.",
543
+ output_path="data/synthetic_plants",
544
+ n=2000,
545
+ )
546
+ # → 2000 synthetic (description → species JSON) training pairs
547
+ ```
548
+
549
+ ---
550
+
551
+ ## 5. Paper-to-Code Agent
552
+
553
+ ### What it is
554
+
555
+ An autonomous agent that takes an arXiv paper URL or title, reads the methodology section,
556
+ and implements the described technique within the workbench codebase. Directly inspired by
557
+ the ml-intern architecture (Research → Plan → Implement → Trace).
558
+
559
+ ### Why it matters
560
+
561
+ The gap between reading a paper and running an experiment is usually days of engineering. This
562
+ agent compresses that to minutes for techniques that fit the workbench's model family.
563
+ Practical use cases: implement a new PEFT variant, add a new evaluation metric, adapt a new
564
+ data augmentation from a recent VLM paper.
565
+
566
+ ### Architecture
567
+
568
+ ```
569
+ agent/paper_agent.py
570
+ PaperAgent
571
+ .run(paper_ref) → AgentResult
572
+ → Phase 1: Research (fetch + parse paper)
573
+ → Phase 2: Plan (identify workbench integration points)
574
+ → Phase 3: Implement (generate + write code)
575
+ → Phase 4: Test (run + log to Trackio)
576
+ → Phase 5: Trace (upload session to HF Dataset)
577
+ ```
578
+
579
+ ```python
580
+ # agent/paper_agent.py
581
+ import re
582
+ from dataclasses import dataclass, field
583
+ from huggingface_hub import HfApi
584
+ from smolagents import CodeAgent, HfApiModel
585
+ import trackio
586
+
587
+ @dataclass
588
+ class AgentResult:
589
+ paper_title: str = ""
590
+ summary: str = ""
591
+ files_modified: list[str] = field(default_factory=list)
592
+ test_results: dict = field(default_factory=dict)
593
+ trace_url: str = ""
594
+
595
+ class PaperAgent:
596
+ SYSTEM_PROMPT = """You are an ML engineer working inside the OpenBMB Workbench codebase.
597
+ Given a research paper, your job is to:
598
+ 1. Understand the core algorithm or technique.
599
+ 2. Identify which module in the workbench it extends (training/, models/, datasets/, tools/).
600
+ 3. Implement it as a new class or function, following the existing patterns.
601
+ 4. Write a simple test that runs within the workbench and logs results to Trackio.
602
+
603
+ The workbench uses: transformers, peft, trl, trackio, mcp, gradio.
604
+ All new code must: fire events via the EventBus, log to Trackio, register in the Registry."""
605
+
606
+ def __init__(self, orchestrator_model: str = "openbmb/MiniCPM4.1-8B"):
607
+ self.model = HfApiModel(orchestrator_model)
608
+ self.api = HfApi()
609
+ self._log = []
610
+
611
+ def run(self, paper_ref: str) -> AgentResult:
612
+ """
613
+ paper_ref: arXiv URL like "https://arxiv.org/abs/2106.09685"
614
+ or paper title like "LoRA: Low-Rank Adaptation of Large Language Models"
615
+ """
616
+ result = AgentResult()
617
+
618
+ # Phase 1: Research
619
+ paper_text = self._fetch_paper(paper_ref)
620
+ result.paper_title = self._extract_title(paper_text)
621
+
622
+ # Phase 2: Plan
623
+ plan = self._plan(paper_text)
624
+
625
+ # Phase 3: Implement
626
+ code_files = self._implement(plan, paper_text)
627
+ result.files_modified = list(code_files.keys())
628
+ for path, code in code_files.items():
629
+ self._write_file(path, code)
630
+
631
+ # Phase 4: Test
632
+ trackio.init(project="workbench", run_name=f"paper_agent_{result.paper_title[:30]}")
633
+ test_result = self._test(code_files)
634
+ result.test_results = test_result
635
+ trackio.log({"test_passed": test_result.get("passed", False), **test_result})
636
+ trackio.finish()
637
+
638
+ # Phase 5: Trace (ml-intern pattern)
639
+ result.trace_url = self._upload_trace(result)
640
+
641
+ return result
642
+
643
+ def _fetch_paper(self, paper_ref: str) -> str:
644
+ """Fetch paper text via HF Papers API or arXiv."""
645
+ import requests
646
+ if "arxiv.org" in paper_ref:
647
+ arxiv_id = paper_ref.split("/abs/")[-1]
648
+ r = requests.get(f"https://export.arxiv.org/abs/{arxiv_id}")
649
+ return r.text
650
+ # Fall back to HF Papers search
651
+ from huggingface_hub import list_papers
652
+ results = list(list_papers(query=paper_ref, limit=1))
653
+ return str(results[0]) if results else ""
654
+
655
+ def _plan(self, paper_text: str) -> str:
656
+ """Ask the LLM to analyze the paper and produce an integration plan."""
657
+ agent = CodeAgent(tools=[], model=self.model, max_steps=5)
658
+ return agent.run(
659
+ f"Read this paper excerpt and produce a 5-step integration plan "
660
+ f"for the OpenBMB Workbench:\n\n{paper_text[:8000]}"
661
+ )
662
+
663
+ def _implement(self, plan: str, paper_text: str) -> dict[str, str]:
664
+ """Generate code files from the plan."""
665
+ agent = CodeAgent(tools=[], model=self.model, max_steps=15)
666
+ code = agent.run(
667
+ f"Implementation plan:\n{plan}\n\n"
668
+ f"Paper details:\n{paper_text[:4000]}\n\n"
669
+ f"Generate the Python file(s). Return a JSON dict: "
670
+ f"{{\"path/to/file.py\": \"file_content\", ...}}"
671
+ )
672
+ import json
673
+ try:
674
+ return json.loads(code)
675
+ except Exception:
676
+ return {}
677
+
678
+ def _write_file(self, path: str, content: str):
679
+ import os
680
+ os.makedirs(os.path.dirname(path), exist_ok=True)
681
+ with open(path, "w") as f:
682
+ f.write(content)
683
+
684
+ def _test(self, code_files: dict) -> dict:
685
+ """Run a quick import + instantiation test on generated files."""
686
+ results = {}
687
+ for path in code_files:
688
+ try:
689
+ module_name = path.replace("/", ".").replace(".py", "")
690
+ import importlib.util, sys
691
+ spec = importlib.util.spec_from_file_location(module_name, path)
692
+ mod = importlib.util.module_from_spec(spec)
693
+ spec.loader.exec_module(mod)
694
+ results[path] = "imported_ok"
695
+ except Exception as e:
696
+ results[path] = f"error: {e}"
697
+ results["passed"] = all("ok" in v for v in results.values())
698
+ return results
699
+
700
+ def _upload_trace(self, result: AgentResult) -> str:
701
+ """Upload session trace to private HF Dataset (ml-intern pattern)."""
702
+ import json, datetime
703
+ session = {
704
+ "timestamp": datetime.datetime.utcnow().isoformat(),
705
+ "paper": result.paper_title,
706
+ "files_modified": result.files_modified,
707
+ "test_results": result.test_results,
708
+ }
709
+ user = self.api.whoami()["name"]
710
+ dataset_id = f"{user}/workbench-paper-sessions"
711
+ # Upload as JSONL
712
+ # ... (HF Dataset API)
713
+ return f"https://huggingface.co/datasets/{dataset_id}"
714
+ ```
715
+
716
+ ---
717
+
718
+ ## 6. HF Spaces Deploy
719
+
720
+ ### What it is
721
+
722
+ One-click packaging and deployment of the current workbench state as a Hugging Face Space.
723
+ The Space runs the same Gradio app on HF infrastructure, shareable via URL, with ZeroGPU
724
+ support for serverless GPU access.
725
+
726
+ ### Why it matters
727
+
728
+ Sharing a workbench state with a collaborator currently requires: git push, environment setup,
729
+ model download, config sync. With one-click deploy: copy URL → colleague sees the live app.
730
+ For hackathons this is especially valuable — deploy a domain-specific variant as a demo Space
731
+ in under 2 minutes.
732
+
733
+ ### Architecture
734
+
735
+ ```
736
+ deploy/spaces.py
737
+ SpacesDeployer
738
+ .prepare_repo() → creates/updates HF Space repo
739
+ .upload_code() → pushes app code (not model weights)
740
+ .set_hardware(tier) → sets GPU tier in README
741
+ .configure_secrets(env_vars) → sets HF Space secrets
742
+ .deploy() → trigger Space rebuild
743
+ .get_url() → returns live Space URL
744
+ ```
745
+
746
+ ```python
747
+ # deploy/spaces.py
748
+ import os, shutil, tempfile
749
+ from pathlib import Path
750
+ from huggingface_hub import HfApi, SpaceHardware
751
+
752
+ class SpacesDeployer:
753
+ HARDWARE_MAP = {
754
+ "cpu": SpaceHardware.CPU_BASIC,
755
+ "t4": SpaceHardware.T4_SMALL,
756
+ "t4_lg": SpaceHardware.T4_MEDIUM,
757
+ "a10": SpaceHardware.A10G_SMALL,
758
+ "a100": SpaceHardware.A100_LARGE,
759
+ "zero": SpaceHardware.CPU_BASIC, # ZeroGPU: uses CPU_BASIC + @spaces.GPU
760
+ }
761
+ EXCLUDE = {".git", "__pycache__", "exports", "data", "checkpoints",
762
+ "*.gguf", "*.bin", "*.safetensors", ".env"}
763
+
764
+ def __init__(self, space_id: str, hardware: str = "zero"):
765
+ self.api = HfApi()
766
+ self.space_id = space_id # "username/my-workbench"
767
+ self.hardware = hardware
768
+
769
+ def deploy(
770
+ self,
771
+ src_dir: str = ".",
772
+ env_vars: dict | None = None,
773
+ ) -> str:
774
+ """Full deploy pipeline. Returns live Space URL."""
775
+ self._create_or_update_repo()
776
+ self._upload_code(src_dir)
777
+ self._configure_secrets(env_vars or {})
778
+ self._patch_app_for_zerogpu()
779
+ return f"https://huggingface.co/spaces/{self.space_id}"
780
+
781
+ def _create_or_update_repo(self):
782
+ try:
783
+ self.api.create_repo(
784
+ repo_id=self.space_id,
785
+ repo_type="space",
786
+ space_sdk="gradio",
787
+ private=False,
788
+ exist_ok=True,
789
+ )
790
+ except Exception as e:
791
+ print(f"Repo create/update: {e}")
792
+
793
+ def _upload_code(self, src_dir: str):
794
+ with tempfile.TemporaryDirectory() as tmp:
795
+ src = Path(src_dir)
796
+ dest = Path(tmp)
797
+ # Copy only non-excluded files
798
+ for item in src.rglob("*"):
799
+ if any(item.match(pat) for pat in self.EXCLUDE):
800
+ continue
801
+ rel = item.relative_to(src)
802
+ target = dest / rel
803
+ target.parent.mkdir(parents=True, exist_ok=True)
804
+ if item.is_file():
805
+ shutil.copy2(item, target)
806
+ self.api.upload_folder(
807
+ folder_path=str(dest),
808
+ repo_id=self.space_id,
809
+ repo_type="space",
810
+ commit_message="workbench deploy",
811
+ )
812
+
813
+ def _configure_secrets(self, env_vars: dict):
814
+ for key, val in env_vars.items():
815
+ self.api.add_space_secret(
816
+ repo_id=self.space_id,
817
+ key=key,
818
+ value=val,
819
+ )
820
+ # Always set hardware
821
+ self.api.request_space_hardware(
822
+ repo_id=self.space_id,
823
+ hardware=self.HARDWARE_MAP[self.hardware],
824
+ )
825
+
826
+ def _patch_app_for_zerogpu(self):
827
+ """
828
+ If hardware=zero, wrap inference functions with @spaces.GPU decorator.
829
+ Uploads a patched app.py.
830
+ """
831
+ if self.hardware != "zero":
832
+ return
833
+ # Read existing app.py from Space, inject @spaces.GPU, re-upload
834
+ app_content = self.api.hf_hub_download(
835
+ repo_id=self.space_id, filename="app.py", repo_type="space"
836
+ )
837
+ with open(app_content) as f:
838
+ code = f.read()
839
+ # Simple injection: add import and decorator before inference functions
840
+ patched = "import spaces\n" + code.replace(
841
+ "def run_inference(",
842
+ "@spaces.GPU\ndef run_inference("
843
+ )
844
+ self.api.upload_file(
845
+ path_or_fileobj=patched.encode(),
846
+ path_in_repo="app.py",
847
+ repo_id=self.space_id,
848
+ repo_type="space",
849
+ )
850
+ ```
851
+
852
+ ### UI: deploy button in any tab header
853
+
854
+ ```python
855
+ with gr.Row():
856
+ space_id_box = gr.Textbox(placeholder="username/my-workbench", label="Space ID")
857
+ hardware_dd = gr.Dropdown(["cpu","t4","a10","zero"], value="zero", label="Hardware")
858
+ deploy_btn = gr.Button("🚀 Deploy to HF Spaces")
859
+ deploy_url = gr.Textbox(label="Live URL", interactive=False)
860
+
861
+ def do_deploy(space_id, hw):
862
+ d = SpacesDeployer(space_id, hw)
863
+ url = d.deploy(env_vars={"HF_TOKEN": os.environ.get("HF_TOKEN","")})
864
+ return url
865
+
866
+ deploy_btn.click(do_deploy, [space_id_box, hardware_dd], deploy_url)
867
+ ```
868
+
869
+ ---
870
+
871
+ ## 7. VINDEX Integration
872
+
873
+ ### What it is
874
+
875
+ VINDEX is your own knowledge-editing engine (ki-fusion-labs.de). It exposes eight methods for
876
+ mechanistic interpretability and targeted weight editing on transformer models. Integrating it
877
+ into the workbench closes the loop between training a LoRA and verifying (or surgically fixing)
878
+ what the model actually knows.
879
+
880
+ VINDEX endpoints (from your PRD v1):
881
+ - `logit_lens` — per-layer prediction visualization
882
+ - `slot_neighbors` — embedding space neighbors
883
+ - `layer_contribution` — per-layer influence on final prediction
884
+ - `transition_spectrum` — attention head transition matrix
885
+ - `calibrated_edit` — targeted fact edit in weight space
886
+ - `derive_scale` — scaling factor derivation for safe edits
887
+ - `star_spread` — spread edit across semantically related slots
888
+ - `protect_relations` — guard against side effects
889
+
890
+ ### Why it matters for the workbench
891
+
892
+ After LoRA fine-tuning a model on plant data:
893
+ 1. `logit_lens` — does the model correctly predict "Rosa" at the right layer for a rose image?
894
+ 2. `calibrated_edit` — if it consistently misidentifies Acer as Quercus, edit that slot
895
+ 3. `star_spread` — propagate the Acer correction to closely related maple species
896
+ 4. `protect_relations` — verify the edit didn't break "plant → living thing → organism"
897
+
898
+ This is not possible with LoRA alone — LoRA changes weight statistics globally. VINDEX does
899
+ surgical point edits, making it a *complement* to LoRA, not a replacement.
900
+
901
+ ### Architecture
902
+
903
+ ```
904
+ tools/vindex_tool.py
905
+ VINDEXClient
906
+ .logit_lens(model, tokenizer, text) → dict[layer, prediction]
907
+ .calibrated_edit(model, subject, relation, old_obj, new_obj) → model
908
+ .star_spread(model, anchor_subject, n=5) → list[affected_slots]
909
+ .protect_relations(model, protected_triplets) → model
910
+ .layer_contribution(model, text) → dict[layer, score]
911
+ .slot_neighbors(model, token_id, n=10) → list[str]
912
+ ```
913
+
914
+ ```python
915
+ # tools/vindex_tool.py
916
+ """
917
+ VINDEX integration.
918
+ Assumes VINDEX FastAPI server is running locally on port 8765,
919
+ OR VINDEX modules are importable from your local install.
920
+ """
921
+ import requests
922
+ from mcp.server.fastmcp import FastMCP
923
+
924
+ VINDEX_BASE = "http://localhost:8765" # your local VINDEX FastAPI
925
+
926
+ mcp = FastMCP("VINDEXTools")
927
+
928
+ @mcp.tool()
929
+ async def logit_lens(
930
+ model_id: str,
931
+ text: str,
932
+ layer_range: tuple[int, int] = (0, -1),
933
+ ) -> dict:
934
+ """
935
+ Run logit lens on a loaded model for the given text.
936
+ Returns per-layer top-5 token predictions and probabilities.
937
+ Useful for finding the 'phase layer' where the model commits to an answer.
938
+ """
939
+ r = requests.post(f"{VINDEX_BASE}/logit_lens", json={
940
+ "model_id": model_id,
941
+ "text": text,
942
+ "layer_range": list(layer_range),
943
+ })
944
+ return r.json()
945
+
946
+ @mcp.tool()
947
+ async def calibrated_edit(
948
+ model_id: str,
949
+ subject: str,
950
+ relation: str,
951
+ old_obj: str,
952
+ new_obj: str,
953
+ causal_window: int = 3, # ±3 layers around logit lens phase layer
954
+ ) -> dict:
955
+ """
956
+ Perform a targeted knowledge edit: change the model's belief about
957
+ (subject, relation) from old_obj to new_obj.
958
+ causal_window: restrict causal search to ±N layers around phase layer.
959
+ Returns edit_success, layers_modified, side_effect_score.
960
+ """
961
+ r = requests.post(f"{VINDEX_BASE}/calibrated_edit", json={
962
+ "model_id": model_id,
963
+ "subject": subject,
964
+ "relation": relation,
965
+ "old_obj": old_obj,
966
+ "new_obj": new_obj,
967
+ "causal_window": causal_window,
968
+ })
969
+ return r.json()
970
+
971
+ @mcp.tool()
972
+ async def star_spread(
973
+ model_id: str,
974
+ anchor_subject: str,
975
+ n_neighbors: int = 5,
976
+ ) -> dict:
977
+ """
978
+ Find semantically related slots and spread a recent edit across them.
979
+ Example: after editing "Acer palmatum → maple", also update
980
+ "Acer japonicum", "Acer shirasawanum" etc.
981
+ Returns list of affected subjects and their edit scores.
982
+ """
983
+ r = requests.post(f"{VINDEX_BASE}/star_spread", json={
984
+ "model_id": model_id,
985
+ "anchor_subject": anchor_subject,
986
+ "n_neighbors": n_neighbors,
987
+ })
988
+ return r.json()
989
+
990
+ @mcp.tool()
991
+ async def protect_relations(
992
+ model_id: str,
993
+ protected_triplets: list[dict], # [{"s": ..., "r": ..., "o": ...}]
994
+ ) -> dict:
995
+ """
996
+ After a knowledge edit, verify that listed subject-relation-object triplets
997
+ remain intact. Returns a pass/fail table and a side_effect_score.
998
+ """
999
+ r = requests.post(f"{VINDEX_BASE}/protect_relations", json={
1000
+ "model_id": model_id,
1001
+ "protected_triplets": protected_triplets,
1002
+ })
1003
+ return r.json()
1004
+
1005
+ @mcp.tool()
1006
+ async def layer_contribution(
1007
+ model_id: str,
1008
+ text: str,
1009
+ ) -> dict:
1010
+ """
1011
+ Per-layer contribution score to the final prediction.
1012
+ Use to find which layers drive the target behavior before editing.
1013
+ """
1014
+ r = requests.post(f"{VINDEX_BASE}/layer_contribution", json={
1015
+ "model_id": model_id,
1016
+ "text": text,
1017
+ })
1018
+ return r.json()
1019
+ ```
1020
+
1021
+ ### Known bugs to fix before integration (from VINDEX PRD v1)
1022
+
1023
+ 1. **GPU memory leak** — after repeated edits, VRAM grows unbounded. Fix: explicitly call
1024
+ `torch.cuda.empty_cache()` after each `calibrated_edit` call and detach gradient graphs.
1025
+
1026
+ 2. **Dead-code blocks** — several helper functions in the weight-surgery path are unreachable
1027
+ after a recent refactor. Before integrating: `grep -n "def " vindex/core.py | xargs` and
1028
+ verify each function has at least one call site.
1029
+
1030
+ 3. **Weight imbalance** — `star_spread` can over-edit related slots if `n_neighbors > 5`.
1031
+ Hard-cap at 5 in the MCP tool until the scaling formula is validated.
1032
+
1033
+ 4. **Forward optimization** — restrict causal search to ±3 layers around the logit lens
1034
+ phase layer (already implemented as `causal_window` param above).
1035
+
1036
+ ### UI tab: "🧠 Knowledge Editor"
1037
+
1038
+ ```
1039
+ Inputs:
1040
+ Model selector (loaded models)
1041
+ Subject text (e.g. "Acer palmatum")
1042
+ Relation (e.g. "is a type of")
1043
+ Old object (e.g. "oak")
1044
+ New object (e.g. "maple")
1045
+ [Run Logit Lens] button → displays per-layer heatmap via gr.Plot
1046
+ [Apply Edit] button → runs calibrated_edit
1047
+ [Spread] button → runs star_spread
1048
+ [Verify] button → runs protect_relations on a default triplet set
1049
+
1050
+ Outputs:
1051
+ Per-layer prediction table
1052
+ Edit success / layers modified
1053
+ Side effect score (0 = safe, 1 = dangerous)
1054
+ ```
1055
+
1056
+ ---
1057
+
1058
+ ## 8. OCR Pipeline Hook
1059
+
1060
+ ### What it is
1061
+
1062
+ Your self-improving multilingual OCR pipeline (Latin, Arabic, Cyrillic) already exists and
1063
+ produces output files: image + predicted_text + confidence scores. This extension hooks those
1064
+ outputs directly into the workbench Field Notes system, creating a tight correction loop:
1065
+
1066
+ ```
1067
+ OCR pipeline outputs (uncertain predictions)
1068
+ ↓
1069
+ Auto-created Field Notes (image + OCR text + empty correction field)
1070
+ ↓
1071
+ Human reviews in UI → fills in correction
1072
+ ↓
1073
+ Accepted corrections auto-tagged "use_for_training=True"
1074
+ ↓
1075
+ LoRA training run on correction pairs
1076
+ ↓
1077
+ Better OCR model → fewer uncertain predictions
1078
+ ```
1079
+
1080
+ This is the active learning loop your OCR pipeline was designed for but didn't yet have
1081
+ a clean UI for corrections and retraining.
1082
+
1083
+ ### Architecture
1084
+
1085
+ ```
1086
+ datasets/ocr_loader.py
1087
+ OCRPipelineLoader
1088
+ .watch(output_dir, threshold) → poll for new low-confidence outputs
1089
+ .ingest(output_dir) → batch import all outputs
1090
+ .to_field_notes(threshold) → FieldNote[] (uncertain ones only)
1091
+ .to_training_dataset() → Dataset (corrected ones only)
1092
+ ```
1093
+
1094
+ ```python
1095
+ # datasets/ocr_loader.py
1096
+ import json, os
1097
+ from pathlib import Path
1098
+ from datasets.field_notes import FieldNote, FieldNoteStore
1099
+ from core.events import bus, EventType, Event
1100
+
1101
+ class OCRPipelineLoader:
1102
+ """
1103
+ Watches a directory written by the OCR pipeline.
1104
+ Expected format per document:
1105
+ <doc_id>.json → {"image_path": ..., "predicted_text": ...,
1106
+ "confidence": float, "script": "latin"|"arabic"|"cyrillic"}
1107
+ """
1108
+ def __init__(
1109
+ self,
1110
+ output_dir: str,
1111
+ store: FieldNoteStore,
1112
+ confidence_threshold: float = 0.85,
1113
+ ):
1114
+ self.output_dir = Path(output_dir)
1115
+ self.store = store
1116
+ self.threshold = confidence_threshold
1117
+
1118
+ def ingest(self, limit: int | None = None) -> int:
1119
+ """
1120
+ Read all pipeline outputs. Create Field Notes for uncertain predictions
1121
+ (confidence < threshold). Skip already-ingested docs.
1122
+ Returns number of new Field Notes created.
1123
+ """
1124
+ count = 0
1125
+ json_files = sorted(self.output_dir.glob("*.json"))
1126
+ if limit:
1127
+ json_files = json_files[:limit]
1128
+
1129
+ for jf in json_files:
1130
+ try:
1131
+ data = json.loads(jf.read_text())
1132
+ except json.JSONDecodeError:
1133
+ continue
1134
+
1135
+ # Skip high-confidence outputs
1136
+ if data.get("confidence", 1.0) >= self.threshold:
1137
+ continue
1138
+
1139
+ note = FieldNote(
1140
+ id=f"ocr_{jf.stem}",
1141
+ model_id="ocr_pipeline",
1142
+ modality="image",
1143
+ image_path=data["image_path"],
1144
+ prompt=(
1145
+ f"Transcribe this {data.get('script','latin')} text accurately. "
1146
+ f"OCR predicted: '{data['predicted_text']}'"
1147
+ ),
1148
+ response=data["predicted_text"],
1149
+ correction="", # human fills this in
1150
+ tags=[
1151
+ f"script:{data.get('script','unknown')}",
1152
+ f"conf:{data.get('confidence',0.0):.2f}",
1153
+ "source:ocr_pipeline",
1154
+ ],
1155
+ )
1156
+ self.store.save(note)
1157
+ count += 1
1158
+
1159
+ # Fire event
1160
+ import asyncio
1161
+ asyncio.run(bus.emit(Event(
1162
+ type=EventType.DATASET_LOADED,
1163
+ payload={"source": "ocr_pipeline", "new_notes": count}
1164
+ )))
1165
+ return count
1166
+
1167
+ def watch(self, poll_interval: int = 30):
1168
+ """
1169
+ Background thread: poll output_dir every N seconds, ingest new files.
1170
+ Use in production when OCR pipeline runs continuously.
1171
+ """
1172
+ import threading, time
1173
+ seen = set()
1174
+ def _poll():
1175
+ while True:
1176
+ for jf in self.output_dir.glob("*.json"):
1177
+ if jf.stem not in seen:
1178
+ seen.add(jf.stem)
1179
+ self.ingest.__wrapped__([jf]) # single-file ingest
1180
+ time.sleep(poll_interval)
1181
+ t = threading.Thread(target=_poll, daemon=True)
1182
+ t.start()
1183
+
1184
+ def to_training_dataset(self, script_filter: str | None = None):
1185
+ """
1186
+ Export corrected field notes as a training dataset.
1187
+ schema: {"image_path": ..., "instruction": ..., "response": ...}
1188
+ Ready to pass to LoRATextTrainer or a vision LoRA config.
1189
+ """
1190
+ from datasets import Dataset
1191
+ query = "SELECT data FROM notes WHERE json_extract(data,'$.correction') != ''"
1192
+ if script_filter:
1193
+ query += f" AND json_extract(data,'$.tags') LIKE '%script:{script_filter}%'"
1194
+
1195
+ rows = [
1196
+ json.loads(r[0])
1197
+ for r in self.store.conn.execute(query)
1198
+ ]
1199
+ training_rows = [
1200
+ {
1201
+ "image_path": r["image_path"],
1202
+ "instruction": r["prompt"],
1203
+ "response": r["correction"], # human-corrected
1204
+ "script": next(
1205
+ (t.split(":")[1] for t in r["tags"] if t.startswith("script:")),
1206
+ "unknown"
1207
+ ),
1208
+ }
1209
+ for r in rows
1210
+ ]
1211
+ return Dataset.from_list(training_rows)
1212
+ ```
1213
+
1214
+ ### UI: OCR correction view (Field Notes tab, new subtab)
1215
+
1216
+ ```
1217
+ [OCR Pipeline Output dir: ____] [Confidence threshold: 0.85] [Ingest]
1218
+
1219
+ Table of uncertain predictions:
1220
+ | Image | OCR text | Confidence | Your correction | Save |
1221
+ | [img] | "Rechung 18. Ap" | 0.73 | [____________] | [✓] |
1222
+ | [img] | "Beschlußprotoko" | 0.69 | [____________] | [✓] |
1223
+
1224
+ [Export corrections as training dataset] [Start LoRA retrain]
1225
+ ```
1226
+
1227
+ ### Connection to the active learning loop
1228
+
1229
+ Your OCR pipeline already has:
1230
+ - `abstention logic` (the "council abstains" on uncertain predictions)
1231
+ - `acceptance-gated fine-tuning`
1232
+ - `RAG-based post-correction`
1233
+
1234
+ The workbench hook provides the missing UI layer: human-in-the-loop corrections that feed
1235
+ the acceptance gate. The `FieldNoteStore.to_hf_dataset()` output plugs directly into the
1236
+ pipeline's `acceptance-gated fine-tuning` step.
1237
+
1238
+ ---
1239
+
1240
+ ## 9. MiniCPM Desk-Pet
1241
+
1242
+ ### What it is
1243
+
1244
+ OpenBMB ships `MiniCPM-Desk-Pet`, a desktop companion app powered by MiniCPM5-1B, alongside
1245
+ the model release (2026-05-19). Key features:
1246
+ - Runs locally on Apple Silicon, NVIDIA GPU, or CPU
1247
+ - LoRA persona switching — different personalities loaded as adapters
1248
+ - Integrates with coding agents (Cursor, Claude Code, Codex)
1249
+ - Tiny footprint (~2GB VRAM with Q4_K_M)
1250
+
1251
+ The workbench extension lets you train LoRA personas directly and export them to the Desk-Pet
1252
+ format.
1253
+
1254
+ ### What "persona" means here
1255
+
1256
+ A LoRA persona is a small adapter (rank 8–16) trained on ~100–500 conversation examples in a
1257
+ specific voice or style. Examples:
1258
+ - "Botanist assistant" — answers in scientific plant terminology
1259
+ - "Friendly field guide" — casual, encouraging tone for beginners
1260
+ - "Historical document expert" — formal, precise, citation-aware (connects to OCR pipeline)
1261
+
1262
+ Training data is small enough that synthetic gen (Extension 4) can produce it in minutes.
1263
+
1264
+ ### Architecture
1265
+
1266
+ ```
1267
+ agent/desk_pet.py
1268
+ DeskPetExporter
1269
+ .train_persona(name, style_desc, n_examples) → LoRA checkpoint
1270
+ .export_to_deskpet(checkpoint_path) → deskpet_compatible.gguf
1271
+ .list_personas() → [PersonaMeta]
1272
+ .load_persona(name) → activates adapter in current session
1273
+ ```
1274
+
1275
+ ```python
1276
+ # agent/desk_pet.py
1277
+ from dataclasses import dataclass
1278
+ from pathlib import Path
1279
+ import json, shutil
1280
+
1281
+ @dataclass
1282
+ class PersonaMeta:
1283
+ name: str
1284
+ description: str
1285
+ checkpoint: str
1286
+ gguf_path: str | None = None
1287
+ n_examples: int = 0
1288
+
1289
+ class DeskPetExporter:
1290
+ PERSONA_DIR = Path("data/personas")
1291
+
1292
+ def __init__(self, base_model_id: str = "openbmb/MiniCPM5-1B"):
1293
+ self.base_model_id = base_model_id
1294
+ self.PERSONA_DIR.mkdir(parents=True, exist_ok=True)
1295
+
1296
+ def train_persona(
1297
+ self,
1298
+ name: str,
1299
+ style_desc: str,
1300
+ n_examples: int = 200,
1301
+ lora_rank: int = 8, # small rank: personas need only ~50-100 examples
1302
+ ) -> str:
1303
+ """
1304
+ 1. Use SyntheticGenerator to create conversation examples in the persona style.
1305
+ 2. Fine-tune MiniCPM5-1B LoRA.
1306
+ 3. Save checkpoint.
1307
+ Returns checkpoint path.
1308
+ """
1309
+ from datasets.synthetic import SyntheticGenerator
1310
+ from training.lora import LoRATextTrainer
1311
+ from models.minicpm_text import MiniCPMTextService
1312
+ import torch
1313
+
1314
+ # Generate persona training data
1315
+ from transformers import AutoModelForCausalLM, AutoTokenizer
1316
+ base_model = AutoModelForCausalLM.from_pretrained(
1317
+ self.base_model_id, torch_dtype=torch.bfloat16, device_map="auto"
1318
+ )
1319
+ base_tok = AutoTokenizer.from_pretrained(self.base_model_id)
1320
+
1321
+ gen_svc = MiniCPMTextService.__new__(MiniCPMTextService)
1322
+ gen_svc.model, gen_svc.tokenizer = base_model, base_tok
1323
+
1324
+ synth = SyntheticGenerator(gen_svc)
1325
+ ds = synth.generate(
1326
+ topic=f"Conversation examples in the style of: {style_desc}. "
1327
+ f"Each example: a user message and a response in that persona's voice.",
1328
+ n=n_examples,
1329
+ schema={"instruction": "user message", "response": "persona reply"},
1330
+ )
1331
+
1332
+ # LoRA fine-tune
1333
+ trainer = LoRATextTrainer(cfg={
1334
+ "lora_rank": lora_rank,
1335
+ "lora_alpha": lora_rank * 2,
1336
+ "epochs": 2,
1337
+ "batch_size": 8,
1338
+ "grad_accum": 2,
1339
+ })
1340
+ output_dir = str(self.PERSONA_DIR / name / "checkpoint")
1341
+ trainer.train(base_model, base_tok, ds, run_name=f"persona_{name}")
1342
+
1343
+ # Save metadata
1344
+ meta = PersonaMeta(
1345
+ name=name, description=style_desc,
1346
+ checkpoint=output_dir, n_examples=n_examples
1347
+ )
1348
+ (self.PERSONA_DIR / name / "meta.json").write_text(
1349
+ json.dumps(meta.__dict__, indent=2)
1350
+ )
1351
+ return output_dir
1352
+
1353
+ def export_to_deskpet(self, persona_name: str) -> str:
1354
+ """
1355
+ Merge LoRA into base weights, then export as GGUF for Desk-Pet.
1356
+ Returns path to merged GGUF.
1357
+ """
1358
+ from peft import PeftModel
1359
+ from transformers import AutoModelForCausalLM, AutoTokenizer
1360
+ import torch
1361
+ from training.export import GGUFExporter
1362
+
1363
+ meta_path = self.PERSONA_DIR / persona_name / "meta.json"
1364
+ meta = PersonaMeta(**json.loads(meta_path.read_text()))
1365
+
1366
+ # Merge LoRA into base
1367
+ base = AutoModelForCausalLM.from_pretrained(
1368
+ self.base_model_id, torch_dtype=torch.bfloat16
1369
+ )
1370
+ peft_model = PeftModel.from_pretrained(base, meta.checkpoint)
1371
+ merged = peft_model.merge_and_unload()
1372
+
1373
+ merged_path = str(self.PERSONA_DIR / persona_name / "merged")
1374
+ merged.save_pretrained(merged_path)
1375
+ AutoTokenizer.from_pretrained(self.base_model_id).save_pretrained(merged_path)
1376
+
1377
+ # Export GGUF
1378
+ exporter = GGUFExporter()
1379
+ gguf_path = exporter.export(
1380
+ model_path=merged_path,
1381
+ output_dir=str(self.PERSONA_DIR / persona_name / "gguf"),
1382
+ quant="Q4_K_M",
1383
+ model_type="text",
1384
+ )[1] # [0] = F16, [1] = quantized
1385
+
1386
+ # Update metadata
1387
+ meta.gguf_path = gguf_path
1388
+ meta_path.write_text(json.dumps(meta.__dict__, indent=2))
1389
+ return gguf_path
1390
+ ```
1391
+
1392
+ ### Usage flow
1393
+
1394
+ ```
1395
+ 1. UI: "New persona" → enter name + style description
1396
+ 2. Synthetic gen: 200 examples of that voice → fine-tune LoRA (rank 8, ~10 min on RTX)
1397
+ 3. Export → Q4_K_M GGUF
1398
+ 4. Copy to Desk-Pet personas/ dir
1399
+ 5. Desk-Pet: switch persona → instant personality change
1400
+ ```
1401
+
1402
+ ---
1403
+
1404
+ ## 10. MiniCPM-o Audio Tab
1405
+
1406
+ ### What it is
1407
+
1408
+ MiniCPM-o-4.5 (released 2026-05-17) is a true omnimodal model — it sees, listens, and speaks
1409
+ simultaneously in real-time. It supports proactive interactions (proactive reminding) and
1410
+ real-time conversation with both visual and audio input.
1411
+
1412
+ This extension adds a new Gradio tab with a microphone + camera (or image) interface, streaming
1413
+ audio output, and real-time MiniCPM-o inference.
1414
+
1415
+ ### Architecture
1416
+
1417
+ ```
1418
+ ui/audio_tab.py
1419
+ OmnimodalTab
1420
+ .build() → gr.Column with audio+image inputs and streaming output
1421
+
1422
+ models/minicpm_omni.py
1423
+ MiniCPMOmniService
1424
+ .stream_chat(audio_bytes, image=None, text=None) → Generator[str]
1425
+ .speak(text) → bytes (TTS for audio output)
1426
+ ```
1427
+
1428
+ ```python
1429
+ # models/minicpm_omni.py
1430
+ import torch
1431
+ import numpy as np
1432
+ from transformers import AutoProcessor, AutoModel
1433
+
1434
+ class MiniCPMOmniService:
1435
+ """
1436
+ MiniCPM-o-4.5: omnimodal service.
1437
+ Handles text + image + audio simultaneously.
1438
+ """
1439
+ MODEL_ID = "openbmb/MiniCPM-o-4.5"
1440
+
1441
+ def __init__(self, cfg: dict):
1442
+ self.processor = AutoProcessor.from_pretrained(
1443
+ self.MODEL_ID, trust_remote_code=True
1444
+ )
1445
+ self.model = AutoModel.from_pretrained(
1446
+ self.MODEL_ID,
1447
+ trust_remote_code=True,
1448
+ torch_dtype=torch.bfloat16,
1449
+ device_map="auto",
1450
+ )
1451
+ self.thinking = cfg.get("thinking_mode", False)
1452
+
1453
+ def chat(
1454
+ self,
1455
+ text: str | None = None,
1456
+ image=None, # PIL Image
1457
+ audio: np.ndarray | None = None,
1458
+ sample_rate: int = 16000,
1459
+ ) -> str:
1460
+ """
1461
+ Full omnimodal chat: pass any combination of text, image, audio.
1462
+ MiniCPM-o-4.5 handles them natively.
1463
+ """
1464
+ content = []
1465
+ if image is not None:
1466
+ content.append({"type": "image", "image": image})
1467
+ if audio is not None:
1468
+ content.append({
1469
+ "type": "audio",
1470
+ "audio": audio,
1471
+ "sample_rate": sample_rate,
1472
+ })
1473
+ if text:
1474
+ content.append({"type": "text", "text": text})
1475
+
1476
+ messages = [{"role": "user", "content": content}]
1477
+ inputs = self.processor.apply_chat_template(
1478
+ messages,
1479
+ add_generation_prompt=True,
1480
+ tokenize=True,
1481
+ return_tensors="pt",
1482
+ return_dict=True,
1483
+ chat_template_kwargs={"enable_thinking": self.thinking},
1484
+ ).to(self.model.device)
1485
+
1486
+ with torch.no_grad():
1487
+ outputs = self.model.generate(**inputs, max_new_tokens=512)
1488
+ return self.processor.decode(
1489
+ outputs[0][inputs["input_ids"].shape[1]:],
1490
+ skip_special_tokens=True
1491
+ )
1492
+ ```
1493
+
1494
+ ```python
1495
+ # ui/audio_tab.py
1496
+ import gradio as gr
1497
+ import numpy as np
1498
+
1499
+ def build_audio_tab(model_registry):
1500
+ with gr.Column():
1501
+ gr.Markdown("### 🎙️ Omnimodal — MiniCPM-o-4.5")
1502
+ gr.Markdown("Speak, show an image, or type — all at once.")
1503
+
1504
+ with gr.Row():
1505
+ audio_in = gr.Audio(
1506
+ sources=["microphone"],
1507
+ type="numpy",
1508
+ label="Microphone input",
1509
+ streaming=True,
1510
+ )
1511
+ image_in = gr.Image(
1512
+ type="pil",
1513
+ label="Optional image",
1514
+ )
1515
+
1516
+ text_in = gr.Textbox(label="Optional text", placeholder="Or type here...")
1517
+ submit = gr.Button("Send")
1518
+ output = gr.Textbox(label="Response", lines=8)
1519
+ audio_out = gr.Audio(label="Spoken response (TTS)", autoplay=True)
1520
+
1521
+ def respond(audio_data, image, text):
1522
+ svc = model_registry.get("minicpm_o45")
1523
+ audio_arr = None
1524
+ if audio_data is not None:
1525
+ sr, arr = audio_data
1526
+ audio_arr = arr.astype(np.float32) / 32768.0
1527
+ response = svc.chat(text=text, image=image, audio=audio_arr)
1528
+ # Optional: TTS for spoken output
1529
+ # spoken = tts(response)
1530
+ return response, None # None = no audio out yet
1531
+
1532
+ submit.click(respond, [audio_in, image_in, text_in], [output, audio_out])
1533
+ ```
1534
+
1535
+ ### Real-time streaming version (advanced)
1536
+
1537
+ For true real-time conversation (proactive reminding, interrupt detection):
1538
+
1539
+ ```python
1540
+ # streaming audio inference using gradio's streaming audio + SSE
1541
+ demo = gr.Interface(
1542
+ fn=stream_omni_response,
1543
+ inputs=[
1544
+ gr.Audio(streaming=True, sources=["microphone"]),
1545
+ gr.Image(type="pil"),
1546
+ ],
1547
+ outputs=gr.Textbox(),
1548
+ live=True,
1549
+ )
1550
+ ```
1551
+
1552
+ ### TTS for audio output
1553
+
1554
+ MiniCPM-o-4.5 has its own audio generation capability — check the model card for the
1555
+ `generate_audio` method. Fallback: use `kokoro-82M` (Apache 2.0, 82M, runs locally) for TTS.
1556
+
1557
+ ```python
1558
+ # Kokoro TTS fallback (lightweight, local)
1559
+ from kokoro import KPipeline
1560
+ tts_pipe = KPipeline(lang_code="en-us")
1561
+ audio, sr = tts_pipe(response_text, voice="af_heart")
1562
+ ```
1563
+
1564
+ ---
1565
+
1566
+ ## 11. Cross-Extension Wiring
1567
+
1568
+ Most extensions are independent, but several combinations unlock powerful compound workflows:
1569
+
1570
+ ### OCR → VINDEX
1571
+
1572
+ Low-confidence OCR outputs → Field Notes → human corrections → LoRA retrain. But additionally:
1573
+ if the OCR model consistently misreads a specific character class, use VINDEX's `logit_lens` to
1574
+ identify which layer is responsible, then `calibrated_edit` to target that slot directly — a
1575
+ faster fix than a full retraining cycle.
1576
+
1577
+ ### Synthetic Gen → Reward Model → DPO
1578
+
1579
+ ```
1580
+ SyntheticGenerator.generate(topic, n=1000)
1581
+ → RewardEvaluator.create_dpo_pairs(prompts, generator, n=4)
1582
+ → DPO training via TRL DPOTrainer
1583
+ → Trackio logs win rate
1584
+ → VINDEX verify alignment not broken
1585
+ ```
1586
+
1587
+ ### Paper Agent → Desk-Pet Persona
1588
+
1589
+ ```
1590
+ PaperAgent reads: "Persona-based dialogue systems for domain experts"
1591
+ → Implements: persona training data format
1592
+ → DeskPetExporter.train_persona("expert_botanist", "...")
1593
+ → Export GGUF → load in Desk-Pet
1594
+ ```
1595
+
1596
+ ### HF Spaces + vLLM + Trackio
1597
+
1598
+ ```
1599
+ SpacesDeployer.deploy(hardware="a10") # production GPU
1600
+ → app.py runs VLLMRunner on A10G
1601
+ → All requests logged via Trackio with space_id=deployed_space
1602
+ → Dashboard visible to collaborators at trackio Space URL
1603
+ ```
1604
+
1605
+ ### Full active-learning loop (all extensions combined)
1606
+
1607
+ ```
1608
+ MiniCPM-o Audio Tab: user speaks + shows image of plant
1609
+ ↓
1610
+ OmnimodalService: identify species (low confidence)
1611
+ ↓
1612
+ OCRPipelineLoader: auto-create Field Note (uncertain prediction)
1613
+ ↓
1614
+ Human: corrects species name in UI
1615
+ ↓
1616
+ SyntheticGenerator: augment with 50 similar examples
1617
+ ↓
1618
+ RewardEvaluator: filter synthetic examples
1619
+ ↓
1620
+ LoRATextTrainer + TRL + Trackio: fine-tune
1621
+ ↓
1622
+ VINDEX: verify the target species slot was corrected
1623
+ ↓
1624
+ DeskPetExporter: export updated persona
1625
+ ↓
1626
+ SpacesDeployer: push updated app to HF Spaces
1627
+ ```
1628
+
1629
+ ---
1630
+
1631
+ *Extensions spec v1.0 — Christof Kaller / ki-fusion-labs.de — 2026-06-05*
HF_PRD_v1.md ADDED
@@ -0,0 +1,1361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenBMB Local AI Workbench — Technical PRD v2.0
2
+ > Revised: 2026-06-05 | Corrections from online research embedded inline
3
+
4
+ ---
5
+
6
+ ## Table of Contents
7
+
8
+ 1. [Purpose & Design Philosophy](#1-purpose--design-philosophy)
9
+ 2. [Design Principles (Template Architecture)](#2-design-principles-template-architecture)
10
+ 3. [System Architecture](#3-system-architecture)
11
+ 4. [Model Registry](#4-model-registry)
12
+ 5. [Inference Stack — Five Modes](#5-inference-stack--five-modes)
13
+ 6. [Tracking: Trackio (Corrected API)](#6-tracking-trackio-corrected-api)
14
+ 7. [MCP Layer (Three Integration Paths)](#7-mcp-layer-three-integration-paths)
15
+ 8. [Training Pipeline](#8-training-pipeline)
16
+ 9. [Export & Quantization](#9-export--quantization)
17
+ 10. [Agent Mode (ml-intern Inspired)](#10-agent-mode-ml-intern-inspired)
18
+ 11. [UI — Tab Specification](#11-ui--tab-specification)
19
+ 12. [Field Notes & Correction Loop](#12-field-notes--correction-loop)
20
+ 13. [Directory Structure](#13-directory-structure)
21
+ 14. [Configuration Schema](#14-configuration-schema)
22
+ 15. [Dependencies](#15-dependencies)
23
+ 16. [Hackathon Demo Flow](#16-hackathon-demo-flow)
24
+ 17. [Corrections from PRD v1](#17-corrections-from-prd-v1)
25
+ 18. [Roadmap & Extension Points](#18-roadmap--extension-points)
26
+
27
+ ---
28
+
29
+ ## 1. Purpose & Design Philosophy
30
+
31
+ **One-line:** A modular AI experimentation platform for the OpenBMB model family that covers
32
+ dataset ingestion → LoRA training → evaluation → GGUF export → llama.cpp/SGLang deployment →
33
+ multimodal inference → MCP tool exposure → trace sharing — in a single Gradio app.
34
+
35
+ **Broader framing:** The platform is deliberately designed as a *template*. The `config/models.yaml`
36
+ and `config/training.yaml` files drive almost all model-specific behaviour. Replacing the OpenBMB
37
+ section with any other model family (Qwen, Phi, Gemma …) should require only config changes and
38
+ a matching service class, never core rewrites.
39
+
40
+ **Why OpenBMB?**
41
+ - MiniCPM5-1B (2026-05-19): SOTA 1B on-device, 128K context, LlamaForCausalLM — no custom kernels
42
+ - MiniCPM4.1-8B (2025-09): sparse InfLLM-v2 attention, ~7× long-context speedup vs Qwen3-8B
43
+ - MiniCPM-V-4.6 (2026-05-11): 1.3B VLM, SigLIP2-400M + Qwen3.5-0.8B, 262K ctx, mixed 4×/16× token compression
44
+ - MiniCPM-V-4.6-Thinking: same architecture + long-CoT for multimodal reasoning
45
+ - MiniCPM-o 4.5 (2026-05-17): omnimodal (vision + audio), real-time conversation
46
+ - Apache 2.0 across the board; vLLM / SGLang / llama.cpp / Ollama native support
47
+
48
+ ---
49
+
50
+ ## 2. Design Principles (Template Architecture)
51
+
52
+ | Principle | Implementation |
53
+ |-----------|----------------|
54
+ | Config-driven | `models.yaml` + `training.yaml` define all models; no model names hardcoded in logic |
55
+ | Event-driven | `core/events.py` — typed event bus; all cross-module comms via events |
56
+ | Registry pattern | `core/registry.py` — all services register by name; swap without restart |
57
+ | Separation of concerns | `models/` = loading; `training/` = train/eval; `ui/` = Gradio; `mcp/` = protocol |
58
+ | Plugin tools | Any Python function decorated `@mcp.tool()` is auto-exposed; no wiring needed |
59
+ | Async-first | FastAPI backend, Gradio queuing, non-blocking inference |
60
+ | Observability | Every significant action fires a Trackio log + event |
61
+ | Template portability | Replace `config/models.yaml` → different model family; keep all else |
62
+
63
+ ---
64
+
65
+ ## 3. System Architecture
66
+
67
+ ```
68
+ ┌─────────────────────────────────────────────────────────────────────────────┐
69
+ │ OpenBMB Local AI Workbench │
70
+ ├───────────────────────────┬─────────────────────────────────────────────────┤
71
+ │ Gradio UI Layer │ Backend Service Layer │
72
+ │ ┌─────────────────────┐ │ ┌─────────────────────────────────────────┐ │
73
+ │ │ gr.Blocks / Tabs │ │ │ gradio.Server (FastAPI-based) │ │
74
+ │ │ chat_tab.py │ │ │ @app.api() — Gradio-queued endpoints │ │
75
+ │ │ vision_tab.py │ │ │ @app.mcp.tool() — MCP registration │ │
76
+ │ │ train_tab.py │ │ │ @app.get("/") — static/custom frontend │ │
77
+ │ │ dataset_tab.py │ │ └─────────────────────────────────────────┘ │
78
+ │ │ export_tab.py │ │ │
79
+ │ │ traces_tab.py │ │ ┌─────────────────────────────────────────┐ │
80
+ │ │ agent_tab.py │ │ │ FastMCP server (standalone / bridged) │ │
81
+ │ └─────────────────────┘ │ │ mcp = FastMCP("OpenBMBWorkbench") │ │
82
+ │ │ │ Registered tools (see §7) │ │
83
+ │ SSE MCP endpoint: │ └─────────────────────────────────────────┘ │
84
+ │ /gradio_api/mcp/sse │ │
85
+ └───────────────────────────┴─────────────────────────────────────────────────┘
86
+ │ │
87
+ ▼ ▼
88
+ ┌─────────────────────┐ ┌────────────────────────┐
89
+ │ Model Services │ │ Tracking & Storage │
90
+ │ minicpm_text.py │ │ trackio (local/Space) │
91
+ │ minicpm_vision.py │ │ field_notes/ (SQLite) │
92
+ │ llama_cpp_runner.py│ │ exports/ (GGUF files) │
93
+ │ sglang_runner.py │ │ data/ (HF datasets) │
94
+ │ ollama_runner.py │ └────────────────────────┘
95
+ └─────────────────────┘
96
+ │
97
+ ▼
98
+ ┌───────────────────────────────────────────────────────────┐
99
+ │ Inference Backends │
100
+ │ Transformers │ llama.cpp │ SGLang │ vLLM │ Ollama│
101
+ └───────────────────────────────────────────────────────────┘
102
+ ```
103
+
104
+ ### 3.1 Event Bus (core/events.py)
105
+
106
+ ```python
107
+ from dataclasses import dataclass, field
108
+ from enum import Enum
109
+ from typing import Any, Callable
110
+ import asyncio
111
+
112
+ class EventType(str, Enum):
113
+ DATASET_LOADED = "dataset_loaded"
114
+ TRAINING_STARTED = "training_started"
115
+ TRAINING_STEP = "training_step"
116
+ TRAINING_FINISHED = "training_finished"
117
+ EVAL_FINISHED = "eval_finished"
118
+ INFERENCE_REQUEST = "inference_request"
119
+ INFERENCE_RESPONSE = "inference_response"
120
+ TOOL_CALL = "tool_call"
121
+ MODEL_LOADED = "model_loaded"
122
+ MODEL_SWITCHED = "model_switched"
123
+ EXPORT_STARTED = "export_started"
124
+ EXPORT_FINISHED = "export_finished"
125
+ FIELD_NOTE_SAVED = "field_note_saved"
126
+ AGENT_STEP = "agent_step"
127
+
128
+ @dataclass
129
+ class Event:
130
+ type: EventType
131
+ payload: dict[str, Any] = field(default_factory=dict)
132
+ run_id: str = ""
133
+
134
+ class EventBus:
135
+ _handlers: dict[EventType, list[Callable]] = {}
136
+
137
+ def on(self, event_type: EventType):
138
+ """Decorator to register an event handler."""
139
+ def decorator(fn: Callable):
140
+ self._handlers.setdefault(event_type, []).append(fn)
141
+ return fn
142
+ return decorator
143
+
144
+ async def emit(self, event: Event):
145
+ for handler in self._handlers.get(event.type, []):
146
+ await handler(event)
147
+
148
+ bus = EventBus()
149
+ ```
150
+
151
+ ### 3.2 Service Registry (core/registry.py)
152
+
153
+ ```python
154
+ from typing import TypeVar, Generic, Type
155
+
156
+ T = TypeVar("T")
157
+
158
+ class Registry(Generic[T]):
159
+ def __init__(self):
160
+ self._services: dict[str, T] = {}
161
+
162
+ def register(self, name: str, service: T):
163
+ self._services[name] = service
164
+
165
+ def get(self, name: str) -> T:
166
+ if name not in self._services:
167
+ raise KeyError(f"Service '{name}' not registered. Available: {list(self._services)}")
168
+ return self._services[name]
169
+
170
+ def list(self) -> list[str]:
171
+ return list(self._services.keys())
172
+
173
+ # Singleton registries
174
+ model_registry = Registry() # model services
175
+ dataset_registry = Registry() # dataset loaders
176
+ tool_registry = Registry() # MCP tools
177
+ ```
178
+
179
+ ---
180
+
181
+ ## 4. Model Registry
182
+
183
+ ### 4.1 Text Models
184
+
185
+ | Config ID | HF ID | Architecture | Context | Notes |
186
+ |-----------|-------|-------------|---------|-------|
187
+ | `minicpm5_1b` | `openbmb/MiniCPM5-1B` | LlamaForCausalLM | 128K | Standard; no custom kernels; XML tool calls; SGLang `minicpm5` parser |
188
+ | `minicpm5_1b_thinking` | `openbmb/MiniCPM5-1B-Thinking` | LlamaForCausalLM | 128K | CoT mode; `chat_template_kwargs={"enable_thinking": True}` |
189
+ | `minicpm41_8b` | `openbmb/MiniCPM4.1-8B` | Sparse InfLLM-v2 | 128K (YaRN) | `--trust-remote-code` required; 7× faster long-ctx vs Qwen3-8B |
190
+
191
+ ### 4.2 Vision Models
192
+
193
+ | Config ID | HF ID | Backbone | Context | Notes |
194
+ |-----------|-------|---------|---------|-------|
195
+ | `minicpm_v46` | `openbmb/MiniCPM-V-4.6` | SigLIP2-400M + Qwen3.5-0.8B | 262K | `transformers>=5.7.0` + `torchcodec`; mixed 4×/16× compression |
196
+ | `minicpm_v46_thinking` | `openbmb/MiniCPM-V-4.6-Thinking` | same + CoT head | 262K | `enable_thinking=True` in chat_template_kwargs |
197
+ | `minicpm_o45` | `openbmb/MiniCPM-o-4.5` | Omnimodal | - | Vision + Audio; real-time conversation; see §4.4 |
198
+
199
+ ### 4.3 Model Architecture Notes (critical for correct code)
200
+
201
+ **MiniCPM5-1B** — standard Llama, loads with:
202
+ ```python
203
+ from transformers import AutoModelForCausalLM, AutoTokenizer
204
+ model = AutoModelForCausalLM.from_pretrained("openbmb/MiniCPM5-1B", trust_remote_code=True)
205
+ ```
206
+
207
+ **MiniCPM-V-4.6** — vision-language, loads with:
208
+ ```python
209
+ # ⚠️ CORRECTION from v1: NOT AutoModelForCausalLM — use AutoModelForImageTextToText
210
+ from transformers import AutoProcessor, AutoModelForImageTextToText
211
+
212
+ processor = AutoProcessor.from_pretrained("openbmb/MiniCPM-V-4.6", trust_remote_code=True)
213
+ model = AutoModelForImageTextToText.from_pretrained(
214
+ "openbmb/MiniCPM-V-4.6",
215
+ trust_remote_code=True,
216
+ torch_dtype=torch.bfloat16,
217
+ device_map="auto"
218
+ )
219
+ ```
220
+
221
+ Chat format (vision):
222
+ ```python
223
+ messages = [
224
+ {
225
+ "role": "user",
226
+ "content": [
227
+ {"type": "image", "image": pil_image}, # PIL Image
228
+ {"type": "text", "text": prompt}
229
+ ]
230
+ }
231
+ ]
232
+ inputs = processor.apply_chat_template(
233
+ messages,
234
+ add_generation_prompt=True,
235
+ tokenize=True,
236
+ return_tensors="pt",
237
+ return_dict=True,
238
+ chat_template_kwargs={"enable_thinking": False} # True for Thinking variant
239
+ ).to(model.device)
240
+ ```
241
+
242
+ **MiniCPM4.1-8B** — requires `trust_remote_code=True` for sparse attention kernel.
243
+
244
+ ### 4.4 models.yaml Schema
245
+
246
+ ```yaml
247
+ # config/models.yaml
248
+ # ─── Template: add any HF model here ──────────────────────────────────────
249
+ models:
250
+ minicpm5_1b:
251
+ hf_id: openbmb/MiniCPM5-1B
252
+ type: text # text | vision | omnimodal
253
+ architecture: llama # llama | minicpm41 | minicpm_v
254
+ context_length: 131072
255
+ dtype: bfloat16
256
+ trust_remote_code: false # LlamaForCausalLM — no fork
257
+ thinking_mode: false # set true for Thinking checkpoints
258
+ tool_call_parser: minicpm5 # for SGLang serving
259
+ gguf:
260
+ available: true
261
+ repo: openbmb/MiniCPM5-1B-GGUF
262
+ default_quant: Q4_K_M
263
+
264
+ minicpm41_8b:
265
+ hf_id: openbmb/MiniCPM4.1-8B
266
+ type: text
267
+ architecture: minicpm41 # sparse InfLLM-v2
268
+ context_length: 131072
269
+ dtype: bfloat16
270
+ trust_remote_code: true
271
+ thinking_mode: false
272
+ gguf:
273
+ available: true
274
+ repo: openbmb/MiniCPM4.1-8B-GGUF
275
+ default_quant: Q4_K_M
276
+
277
+ minicpm_v46:
278
+ hf_id: openbmb/MiniCPM-V-4.6
279
+ type: vision
280
+ architecture: minicpm_v
281
+ context_length: 262144
282
+ dtype: bfloat16
283
+ trust_remote_code: true
284
+ thinking_mode: false
285
+ vision_encoder: siglip2_400m
286
+ token_compression: "4x_16x" # mixed compression
287
+ gguf:
288
+ available: true
289
+ repo: openbmb/MiniCPM-V-4.6-gguf # official repo — no surgery script needed
290
+ main_file: MiniCPM-V-4.6-Q4_K_M.gguf
291
+ mmproj: mmproj-MiniCPM-V-4.6-F16.gguf
292
+ default_quant: Q4_K_M
293
+
294
+ minicpm_v46_thinking:
295
+ hf_id: openbmb/MiniCPM-V-4.6-Thinking
296
+ type: vision
297
+ architecture: minicpm_v
298
+ context_length: 262144
299
+ dtype: bfloat16
300
+ trust_remote_code: true
301
+ thinking_mode: true
302
+ gguf:
303
+ available: true
304
+ repo: openbmb/MiniCPM-V-4.6-Thinking-gguf
305
+ default_quant: Q4_K_M
306
+
307
+ minicpm_o45:
308
+ hf_id: openbmb/MiniCPM-o-4.5
309
+ type: omnimodal
310
+ architecture: minicpm_o
311
+ context_length: 131072
312
+ dtype: bfloat16
313
+ trust_remote_code: true
314
+ thinking_mode: false
315
+ note: "Vision + Audio; real-time conversation mode"
316
+ ```
317
+
318
+ ---
319
+
320
+ ## 5. Inference Stack — Five Modes
321
+
322
+ ### Mode A: Transformers (train + eval + fine-tuning)
323
+
324
+ Used for: LoRA training, evaluation, PEFT
325
+ Load text: `AutoModelForCausalLM`
326
+ Load vision: `AutoModelForImageTextToText` ← corrected from v1
327
+
328
+ ```python
329
+ # models/minicpm_text.py
330
+ class MiniCPMTextService:
331
+ def __init__(self, model_id: str, cfg: dict):
332
+ self.model = AutoModelForCausalLM.from_pretrained(
333
+ model_id,
334
+ torch_dtype=torch.bfloat16,
335
+ device_map="auto",
336
+ trust_remote_code=cfg.get("trust_remote_code", False)
337
+ )
338
+ self.tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
339
+ ```
340
+
341
+ ```python
342
+ # models/minicpm_vision.py ← new dedicated service
343
+ class MiniCPMVisionService:
344
+ def __init__(self, model_id: str, cfg: dict):
345
+ self.processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
346
+ self.model = AutoModelForImageTextToText.from_pretrained(
347
+ model_id,
348
+ torch_dtype=torch.bfloat16,
349
+ device_map="auto",
350
+ trust_remote_code=cfg.get("trust_remote_code", True)
351
+ )
352
+ self.thinking = cfg.get("thinking_mode", False)
353
+
354
+ def chat(self, messages: list[dict], max_new_tokens: int = 512) -> str:
355
+ inputs = self.processor.apply_chat_template(
356
+ messages,
357
+ add_generation_prompt=True,
358
+ tokenize=True,
359
+ return_tensors="pt",
360
+ return_dict=True,
361
+ chat_template_kwargs={"enable_thinking": self.thinking}
362
+ ).to(self.model.device)
363
+ with torch.no_grad():
364
+ outputs = self.model.generate(**inputs, max_new_tokens=max_new_tokens)
365
+ response = self.processor.decode(
366
+ outputs[0][inputs["input_ids"].shape[1]:],
367
+ skip_special_tokens=True
368
+ )
369
+ return response
370
+ ```
371
+
372
+ ### Mode B: llama.cpp (local deployment, CPU/GPU offload)
373
+
374
+ > ⚠️ CORRECTION from v1: For MiniCPM-V 4.6, official GGUFs are already published.
375
+ > Download from `openbmb/MiniCPM-V-4.6-gguf` — no need to run `convert_hf_to_gguf.py` yourself.
376
+ > The `minicpmv-surgery.py` script was for the older MiniCPM-o-2.6; **do not use it for V4.6**.
377
+
378
+ **Text models** → `llama-server` (standard OpenAI-compatible endpoint)
379
+ **Vision models** → `llama-server --mmproj <mmproj.gguf>` (multimodal server)
380
+
381
+ ```python
382
+ # models/llama_cpp_runner.py
383
+ import subprocess, requests, time
384
+
385
+ class LlamaCppDeployment:
386
+ def __init__(self, cfg: dict):
387
+ self.model_path = cfg["model_path"]
388
+ self.mmproj_path = cfg.get("mmproj_path") # None for text models
389
+ self.port = cfg.get("port", 8080)
390
+ self._proc = None
391
+
392
+ def start(self):
393
+ cmd = [
394
+ "llama-server",
395
+ "--model", self.model_path,
396
+ "--port", str(self.port),
397
+ "--ctx-size", "8192",
398
+ "--n-gpu-layers", "-1", # full GPU offload if available
399
+ ]
400
+ if self.mmproj_path:
401
+ cmd += ["--mmproj", self.mmproj_path] # enables vision
402
+ self._proc = subprocess.Popen(cmd)
403
+ self._wait_ready()
404
+
405
+ def _wait_ready(self, timeout: int = 30):
406
+ for _ in range(timeout):
407
+ try:
408
+ r = requests.get(f"http://127.0.0.1:{self.port}/health")
409
+ if r.status_code == 200:
410
+ return
411
+ except Exception:
412
+ pass
413
+ time.sleep(1)
414
+ raise RuntimeError("llama-server did not start in time")
415
+
416
+ def chat(self, messages: list[dict]) -> str:
417
+ r = requests.post(
418
+ f"http://127.0.0.1:{self.port}/v1/chat/completions",
419
+ json={"messages": messages, "max_tokens": 512}
420
+ )
421
+ return r.json()["choices"][0]["message"]["content"]
422
+
423
+ def stop(self):
424
+ if self._proc:
425
+ self._proc.terminate()
426
+ ```
427
+
428
+ **GGUF Export** (only needed if building custom quants):
429
+ ```bash
430
+ # For MiniCPM-V 4.6 — convert_hf_to_gguf.py (NOT legacy surgery scripts)
431
+ python convert_hf_to_gguf.py openbmb/MiniCPM-V-4.6 --outtype f16
432
+ # Output: MiniCPM-V-4.6-F16.gguf + mmproj-MiniCPM-V-4.6-F16.gguf
433
+
434
+ # Quantize
435
+ ./llama-quantize MiniCPM-V-4.6-F16.gguf MiniCPM-V-4.6-Q4_K_M.gguf Q4_K_M
436
+ ```
437
+
438
+ Supported quants: `Q4_K_M`, `Q5_K_M`, `Q8_0`, `Q2_K`, `F16`
439
+
440
+ ### Mode C: SGLang (tool-use, function calling, recommended for MiniCPM5)
441
+
442
+ MiniCPM5-1B emits XML-style tool calls. SGLang has a native `minicpm5` parser:
443
+
444
+ ```bash
445
+ python -m sglang.launch_server \
446
+ --model-path openbmb/MiniCPM5-1B \
447
+ --port 30000 \
448
+ --tool-call-parser minicpm5 # or: --tool-call-parser auto
449
+ ```
450
+
451
+ For MiniCPM-V-4.6 vision:
452
+ ```bash
453
+ python -m sglang.launch_server \
454
+ --model-path openbmb/MiniCPM-V-4.6-Thinking \
455
+ --port 30000 \
456
+ --enable-auto-tool-choice \
457
+ --tool-call-parser qwen3_coder \
458
+ --default-chat-template-kwargs '{"enable_thinking": true}'
459
+ ```
460
+
461
+ ### Mode D: vLLM (batch serving, production)
462
+
463
+ ```bash
464
+ vllm serve openbmb/MiniCPM4.1-8B --trust-remote-code
465
+ vllm serve openbmb/MiniCPM-V-4.6 --trust-remote-code
466
+ ```
467
+
468
+ ### Mode E: Ollama (zero-config, consumer hardware)
469
+
470
+ ```bash
471
+ ollama run openbmb/minicpm-v4.6 # vision + text
472
+ ollama run openbmb/minicpm5-1b # text only
473
+ ```
474
+
475
+ ### Mode Selection Matrix
476
+
477
+ | Use case | Recommended mode |
478
+ |----------|-----------------|
479
+ | LoRA training | A (Transformers) |
480
+ | Evaluation | A (Transformers) |
481
+ | Local chat (fast) | B (llama.cpp) |
482
+ | Tool/function calling | C (SGLang) |
483
+ | Production batch | D (vLLM) |
484
+ | Consumer demo | E (Ollama) |
485
+ | Vision inference local | B (llama-server --mmproj) |
486
+ | Vision fine-tuning | A (SWIFT / LLaMA-Factory) |
487
+
488
+ ---
489
+
490
+ ## 6. Tracking: Trackio (Corrected API)
491
+
492
+ > ⚠️ CRITICAL CORRECTION from v1: `trackio.trace()` **does not exist**.
493
+ > There is no context-manager trace API in Trackio. The v1 PRD example was wrong.
494
+ > Correct API: `init()` / `log()` / `finish()` / `alert()` / `Table()` / `Markdown()`
495
+
496
+ Trackio is a wandb drop-in replacement, local-first, free, Gradio dashboard, HF Spaces sync.
497
+
498
+ ```python
499
+ # tracking/trackio_service.py
500
+ import trackio
501
+ import time
502
+ from core.events import bus, EventType, Event
503
+
504
+ class TrackioService:
505
+ def __init__(self, project: str, space_id: str | None = None):
506
+ self.project = project
507
+ self.space_id = space_id
508
+ self._active = False
509
+ self._register_handlers()
510
+
511
+ def _register_handlers(self):
512
+ @bus.on(EventType.TRAINING_STARTED)
513
+ async def on_training_started(event: Event):
514
+ trackio.init(
515
+ project=self.project,
516
+ space_id=self.space_id,
517
+ run_name=event.payload.get("run_name", "training"),
518
+ )
519
+ self._active = True
520
+
521
+ @bus.on(EventType.TRAINING_STEP)
522
+ async def on_step(event: Event):
523
+ if self._active:
524
+ trackio.log(event.payload)
525
+
526
+ @bus.on(EventType.TRAINING_FINISHED)
527
+ async def on_done(event: Event):
528
+ if self._active:
529
+ trackio.log({"status": "finished", **event.payload})
530
+ trackio.finish()
531
+ self._active = False
532
+
533
+ @bus.on(EventType.EVAL_FINISHED)
534
+ async def on_eval(event: Event):
535
+ trackio.init(
536
+ project=self.project,
537
+ space_id=self.space_id,
538
+ run_name=event.payload.get("eval_run_name", "eval"),
539
+ )
540
+ trackio.log(event.payload)
541
+ trackio.finish()
542
+
543
+ @bus.on(EventType.INFERENCE_RESPONSE)
544
+ async def on_inference(event: Event):
545
+ # Log inference latency and token count
546
+ trackio.init(
547
+ project=self.project,
548
+ space_id=self.space_id,
549
+ run_name="inference",
550
+ )
551
+ trackio.log({
552
+ "model": event.payload.get("model"),
553
+ "latency_ms": event.payload.get("latency_ms"),
554
+ "tokens_out": event.payload.get("tokens_out"),
555
+ "mode": event.payload.get("mode", "text"),
556
+ })
557
+ trackio.finish()
558
+
559
+ @bus.on(EventType.TOOL_CALL)
560
+ async def on_tool(event: Event):
561
+ trackio.alert(
562
+ title=f"Tool call: {event.payload.get('tool_name')}",
563
+ level=trackio.AlertLevel.INFO,
564
+ )
565
+ ```
566
+
567
+ **TRL LoRA integration** (recommended for training):
568
+ ```python
569
+ from trl import SFTConfig
570
+ training_args = SFTConfig(
571
+ output_dir="./checkpoints",
572
+ report_to="trackio", # ← one-liner Trackio integration via TRL
573
+ run_name="lora_minicpm5_1b",
574
+ )
575
+ ```
576
+
577
+ **SQL query via CLI** (LLM-friendly):
578
+ ```bash
579
+ trackio query project --project workbench --sql "SELECT run_name, loss FROM metrics ORDER BY loss LIMIT 5"
580
+ ```
581
+
582
+ ---
583
+
584
+ ## 7. MCP Layer (Three Integration Paths)
585
+
586
+ ### Path 1: Gradio Blocks + `launch(mcp_server=True)` ← simplest
587
+
588
+ ```python
589
+ # app.py (simple path)
590
+ import gradio as gr
591
+ from mcp.server.fastmcp import FastMCP
592
+
593
+ demo = gr.Blocks()
594
+ # ... tabs ...
595
+ demo.launch(
596
+ mcp_server=True, # exposes /gradio_api/mcp/sse
597
+ server_port=7860,
598
+ )
599
+ ```
600
+
601
+ Every `gr.Interface` function and `gr.api()` endpoint becomes an MCP tool automatically.
602
+ Docstrings become tool descriptions. Type hints become parameter schemas.
603
+
604
+ ### Path 2: `gradio.Server` (custom frontend + MCP)
605
+
606
+ > New API published 2026-04-01. Extends FastAPI. Better for custom dashboards.
607
+
608
+ ```python
609
+ # app.py (Server path)
610
+ from gradio import Server
611
+ from fastapi.responses import HTMLResponse
612
+
613
+ app = Server()
614
+
615
+ @app.api(name="run_inference") # Gradio-queued + MCP-compatible
616
+ def run_inference(prompt: str, model_id: str) -> str:
617
+ """Run text inference on a MiniCPM text model."""
618
+ svc = model_registry.get(model_id)
619
+ return svc.generate(prompt)
620
+
621
+ @app.mcp.tool() # explicit MCP tool registration
622
+ async def export_gguf(model_id: str, quant: str = "Q4_K_M") -> str:
623
+ """Export a Transformers model to GGUF format with specified quantization."""
624
+ return await export_service.run(model_id, quant)
625
+
626
+ @app.get("/", response_class=HTMLResponse) # custom frontend
627
+ async def homepage():
628
+ return open("ui/index.html").read()
629
+
630
+ app.launch(show_error=True)
631
+ ```
632
+
633
+ ### Path 3: FastMCP standalone + Gradio client bridge
634
+
635
+ Use when you need stateful MCP tools, or when running multiple Gradio apps and want to save memory:
636
+
637
+ ```python
638
+ # mcp/server.py
639
+ from mcp.server.fastmcp import FastMCP
640
+ from gradio_client import Client
641
+
642
+ mcp = FastMCP("OpenBMBWorkbench")
643
+ _clients: dict[str, Client] = {}
644
+
645
+ def _gradio_client(space_id: str) -> Client:
646
+ if space_id not in _clients:
647
+ _clients[space_id] = Client(space_id)
648
+ return _clients[space_id]
649
+
650
+ @mcp.tool()
651
+ async def calculate(expression: str) -> str:
652
+ """Evaluate a mathematical expression."""
653
+ import math
654
+ return str(eval(expression, {"__builtins__": {}}, vars(math)))
655
+
656
+ @mcp.tool()
657
+ async def dataset_stats(dataset_name: str, split: str = "train") -> dict:
658
+ """Return basic statistics for a HuggingFace dataset."""
659
+ from datasets import load_dataset
660
+ ds = load_dataset(dataset_name, split=split)
661
+ return {
662
+ "rows": len(ds),
663
+ "columns": ds.column_names,
664
+ "features": str(ds.features),
665
+ }
666
+
667
+ @mcp.tool()
668
+ async def search_dataset(query: str, max_results: int = 5) -> list[dict]:
669
+ """Search HuggingFace Hub for datasets matching a query."""
670
+ from huggingface_hub import list_datasets
671
+ results = list(list_datasets(search=query, limit=max_results))
672
+ return [{"id": d.id, "downloads": d.downloads, "tags": d.tags} for d in results]
673
+
674
+ @mcp.tool()
675
+ async def start_training(
676
+ model_id: str,
677
+ dataset_name: str,
678
+ lora_rank: int = 16,
679
+ epochs: int = 3,
680
+ ) -> dict:
681
+ """Start a LoRA fine-tuning run on the specified model and dataset."""
682
+ # Dispatch to training service via event bus
683
+ await bus.emit(Event(
684
+ type=EventType.TRAINING_STARTED,
685
+ payload={"model_id": model_id, "dataset": dataset_name,
686
+ "lora_rank": lora_rank, "epochs": epochs}
687
+ ))
688
+ return {"status": "started", "model_id": model_id}
689
+
690
+ @mcp.tool()
691
+ async def evaluate_model(model_id: str, dataset_name: str) -> dict:
692
+ """Evaluate a model on a dataset and return metrics."""
693
+ svc = model_registry.get(model_id)
694
+ return await eval_service.run(svc, dataset_name)
695
+
696
+ @mcp.tool()
697
+ async def export_gguf(model_id: str, quant: str = "Q4_K_M") -> str:
698
+ """Convert a fine-tuned model to GGUF format."""
699
+ return await export_service.run(model_id, quant)
700
+
701
+ @mcp.tool()
702
+ async def vision_chat(image_path: str, prompt: str, model_id: str = "minicpm_v46") -> str:
703
+ """Run multimodal inference on an image with a prompt."""
704
+ svc: MiniCPMVisionService = model_registry.get(model_id)
705
+ from PIL import Image
706
+ img = Image.open(image_path).convert("RGB")
707
+ messages = [{"role": "user", "content": [
708
+ {"type": "image", "image": img},
709
+ {"type": "text", "text": prompt},
710
+ ]}]
711
+ return svc.chat(messages)
712
+
713
+ if __name__ == "__main__":
714
+ mcp.run(transport="sse", port=8081)
715
+ ```
716
+
717
+ MCP SSE endpoint: `http://localhost:8081/sse`
718
+ Gradio MCP SSE: `http://localhost:7860/gradio_api/mcp/sse`
719
+
720
+ ---
721
+
722
+ ## 8. Training Pipeline
723
+
724
+ ### 8.1 LoRA — Text Models (PEFT)
725
+
726
+ ```python
727
+ # training/lora.py
728
+ from peft import LoraConfig, get_peft_model, TaskType
729
+ from trl import SFTTrainer, SFTConfig
730
+
731
+ class LoRATextTrainer:
732
+ DEFAULT_TARGET_MODULES = ["q_proj", "k_proj", "v_proj", "o_proj",
733
+ "gate_proj", "up_proj", "down_proj"]
734
+
735
+ def __init__(self, cfg: dict):
736
+ self.cfg = cfg
737
+
738
+ def train(self, model, tokenizer, dataset, run_name: str):
739
+ lora_cfg = LoraConfig(
740
+ r=self.cfg.get("lora_rank", 16),
741
+ lora_alpha=self.cfg.get("lora_alpha", 32),
742
+ target_modules=self.cfg.get("target_modules", self.DEFAULT_TARGET_MODULES),
743
+ lora_dropout=self.cfg.get("lora_dropout", 0.05),
744
+ bias="none",
745
+ task_type=TaskType.CAUSAL_LM,
746
+ )
747
+ model = get_peft_model(model, lora_cfg)
748
+ model.print_trainable_parameters()
749
+
750
+ training_args = SFTConfig(
751
+ output_dir=f"./checkpoints/{run_name}",
752
+ num_train_epochs=self.cfg.get("epochs", 3),
753
+ per_device_train_batch_size=self.cfg.get("batch_size", 4),
754
+ gradient_accumulation_steps=self.cfg.get("grad_accum", 4),
755
+ learning_rate=self.cfg.get("lr", 2e-4),
756
+ fp16=False,
757
+ bf16=True,
758
+ report_to="trackio", # ← Trackio via TRL
759
+ run_name=run_name,
760
+ save_steps=100,
761
+ logging_steps=10,
762
+ )
763
+ trainer = SFTTrainer(
764
+ model=model,
765
+ args=training_args,
766
+ train_dataset=dataset,
767
+ tokenizer=tokenizer,
768
+ )
769
+ trainer.train()
770
+ return model
771
+ ```
772
+
773
+ ### 8.2 LoRA — Vision Models (SWIFT / LLaMA-Factory recommended)
774
+
775
+ > OpenBMB recommends SWIFT or LLaMA-Factory for MiniCPM-V fine-tuning.
776
+ > Vision tower is frozen; only language backbone layers are adapted.
777
+
778
+ ```python
779
+ # training/lora_vision.py
780
+ class LoRAVisionConfig:
781
+ """
782
+ For MiniCPM-V 4.6 LoRA:
783
+ - Freeze vision encoder (SigLIP2-400M) + projector
784
+ - Apply LoRA only to Qwen3.5-0.8B language layers
785
+ - Use SWIFT CLI or LLaMA-Factory for full pipeline
786
+ """
787
+ LANGUAGE_TARGET_MODULES = ["q_proj", "k_proj", "v_proj", "o_proj"]
788
+ FROZEN_PREFIXES = ["vpm", "resampler", "vision_model"] # freeze vision
789
+
790
+ @staticmethod
791
+ def freeze_vision(model):
792
+ for name, param in model.named_parameters():
793
+ if any(name.startswith(pfx) for pfx in LoRAVisionConfig.FROZEN_PREFIXES):
794
+ param.requires_grad = False
795
+ return model
796
+ ```
797
+
798
+ **SWIFT CLI approach** (recommended for MiniCPM-V 4.6):
799
+ ```bash
800
+ swift sft \
801
+ --model openbmb/MiniCPM-V-4.6 \
802
+ --dataset <your-dataset> \
803
+ --lora_rank 16 \
804
+ --freeze_vit true \
805
+ --output_dir ./checkpoints/minicpm_v46_lora
806
+ ```
807
+
808
+ ### 8.3 HF Jobs Offload (ml-intern pattern)
809
+
810
+ For long training runs, offload to HF Jobs (free `cpu-basic`, paid `gpu-a100`):
811
+
812
+ ```python
813
+ # training/hf_jobs.py
814
+ from huggingface_hub import HfApi
815
+
816
+ class HFJobsTrainer:
817
+ def submit(self, script_path: str, hardware: str = "gpu-a100") -> str:
818
+ api = HfApi()
819
+ job = api.create_job(
820
+ repo_id=f"{api.whoami()['name']}/ml-workbench-jobs",
821
+ script=script_path,
822
+ hardware=hardware,
823
+ )
824
+ return job.job_id
825
+ ```
826
+
827
+ ### 8.4 Evaluation
828
+
829
+ ```python
830
+ # training/evaluation.py
831
+ class Evaluator:
832
+ def run(self, model, tokenizer, dataset, metrics=("perplexity", "accuracy")) -> dict:
833
+ results = {}
834
+ if "perplexity" in metrics:
835
+ results["perplexity"] = self._perplexity(model, tokenizer, dataset)
836
+ if "accuracy" in metrics:
837
+ results["accuracy"] = self._accuracy(model, tokenizer, dataset)
838
+ return results
839
+ ```
840
+
841
+ ---
842
+
843
+ ## 9. Export & Quantization
844
+
845
+ ```python
846
+ # training/export.py
847
+ import subprocess
848
+ from pathlib import Path
849
+
850
+ class GGUFExporter:
851
+ SUPPORTED_QUANTS = ["F16", "Q4_K_M", "Q5_K_M", "Q8_0", "Q2_K"]
852
+
853
+ def export(
854
+ self,
855
+ model_path: str,
856
+ output_dir: str,
857
+ quant: str = "Q4_K_M",
858
+ model_type: str = "text", # "text" | "vision"
859
+ ) -> list[str]:
860
+ output_dir = Path(output_dir)
861
+ output_dir.mkdir(parents=True, exist_ok=True)
862
+
863
+ # Step 1: Convert HF → GGUF F16
864
+ # For MiniCPM-V 4.6: convert_hf_to_gguf.py produces main gguf + mmproj
865
+ # For text models: same script, no mmproj
866
+ f16_path = output_dir / "model-f16.gguf"
867
+ subprocess.run([
868
+ "python", "convert_hf_to_gguf.py",
869
+ model_path,
870
+ "--outtype", "f16",
871
+ "--outfile", str(f16_path),
872
+ ], check=True)
873
+
874
+ outputs = [str(f16_path)]
875
+
876
+ # Step 2: Quantize
877
+ if quant != "F16":
878
+ quant_path = output_dir / f"model-{quant}.gguf"
879
+ subprocess.run([
880
+ "llama-quantize",
881
+ str(f16_path),
882
+ str(quant_path),
883
+ quant,
884
+ ], check=True)
885
+ outputs.append(str(quant_path))
886
+
887
+ return outputs
888
+ ```
889
+
890
+ > Note: For MiniCPM-V 4.6, official GGUF files are already published at
891
+ > `openbmb/MiniCPM-V-4.6-gguf`. Download them directly unless you need custom quants.
892
+
893
+ ---
894
+
895
+ ## 10. Agent Mode (ml-intern Inspired)
896
+
897
+ The `agent/` module implements a lightweight research-plan-implement loop inspired by HF's
898
+ ml-intern (April 2026). This gives the workbench autonomous ML experimentation capabilities.
899
+
900
+ ```
901
+ ml-intern architecture (reference):
902
+ Research → browse arXiv/HF Papers, read methodology, traverse citations
903
+ Plan → break task down, verify resources (models, datasets, hardware)
904
+ Implement → execute scripts on HF Jobs / local GPU
905
+ Trace → auto-upload sessions as JSONL to HF Dataset
906
+ ```
907
+
908
+ ```python
909
+ # agent/loop.py
910
+ from smolagents import CodeAgent, HfApiModel
911
+ from huggingface_hub import HfApi
912
+
913
+ class WorkbenchAgent:
914
+ """
915
+ Lightweight agent loop for autonomous experimentation.
916
+ Uses smolagents + HF ecosystem + registered MCP tools.
917
+ """
918
+ def __init__(self, model_id: str = "claude-sonnet-4-6"):
919
+ self.model = HfApiModel(model_id)
920
+ self.api = HfApi()
921
+ self._session_log = []
922
+
923
+ def run(self, prompt: str, max_steps: int = 20) -> str:
924
+ agent = CodeAgent(
925
+ tools=self._get_tools(),
926
+ model=self.model,
927
+ max_steps=max_steps,
928
+ )
929
+ result = agent.run(prompt)
930
+ self._save_session(prompt, result)
931
+ return result
932
+
933
+ def _save_session(self, prompt: str, result: str):
934
+ import json, datetime
935
+ session = {
936
+ "timestamp": datetime.datetime.utcnow().isoformat(),
937
+ "prompt": prompt,
938
+ "result": result,
939
+ "log": self._session_log,
940
+ }
941
+ # Auto-upload to private HF dataset (ml-intern pattern)
942
+ # Dataset: {username}/workbench-sessions
943
+ pass
944
+
945
+ def _get_tools(self):
946
+ # Expose MCP tools as smolagents tools
947
+ from mcp.server.fastmcp import FastMCP
948
+ # ...
949
+ return []
950
+ ```
951
+
952
+ ---
953
+
954
+ ## 11. UI — Tab Specification
955
+
956
+ Using `gr.Blocks` with tabs. Optionally swap to `gradio.Server` for custom frontend.
957
+
958
+ ```python
959
+ # app.py
960
+ import gradio as gr
961
+ from ui.chat_tab import build_chat_tab
962
+ from ui.vision_tab import build_vision_tab
963
+ from ui.train_tab import build_train_tab
964
+ from ui.dataset_tab import build_dataset_tab
965
+ from ui.export_tab import build_export_tab
966
+ from ui.traces_tab import build_traces_tab
967
+ from ui.agent_tab import build_agent_tab
968
+ from ui.notes_tab import build_notes_tab
969
+
970
+ with gr.Blocks(title="OpenBMB Workbench", theme=gr.themes.Ocean()) as demo:
971
+ gr.Markdown("# 🔬 OpenBMB Local AI Workbench")
972
+
973
+ with gr.Tabs():
974
+ with gr.Tab("💬 Chat"): build_chat_tab()
975
+ with gr.Tab("👁️ Vision"): build_vision_tab()
976
+ with gr.Tab("📊 Dataset"): build_dataset_tab()
977
+ with gr.Tab("🏋️ Train"): build_train_tab()
978
+ with gr.Tab("📦 Export"): build_export_tab()
979
+ with gr.Tab("📈 Traces"): build_traces_tab()
980
+ with gr.Tab("🤖 Agent"): build_agent_tab()
981
+ with gr.Tab("📝 Field Notes"):build_notes_tab()
982
+
983
+ demo.launch(
984
+ mcp_server=True, # SSE at /gradio_api/mcp/sse
985
+ server_port=7860,
986
+ share=False,
987
+ )
988
+ ```
989
+
990
+ ### Tab Summary
991
+
992
+ | Tab | Purpose | Key components |
993
+ |-----|---------|----------------|
994
+ | Chat | Text inference on all text models | Model selector, streaming output, system prompt |
995
+ | Vision | Image + text inference on V-4.6/Thinking | Image upload, thinking toggle, bounding box overlay |
996
+ | Dataset | HF Hub browser + local CSV/JSON loader | Search bar, split preview, schema inspector |
997
+ | Train | LoRA training launcher | Config form, live loss chart (Trackio), checkpoint browser |
998
+ | Export | GGUF export + quantization | Model selector, quant dropdown, file download |
999
+ | Traces | Trackio run browser + comparison | Run table, metric plots, SQL query box |
1000
+ | Agent | ml-intern style agent loop | Prompt, step log, paper browser, HF Jobs monitor |
1001
+ | Field Notes | Correction capture → retrain trigger | Image+prompt+response+correction form, JSONL export |
1002
+
1003
+ ### Vision Tab — key detail
1004
+
1005
+ ```python
1006
+ # ui/vision_tab.py
1007
+ def build_vision_tab():
1008
+ with gr.Column():
1009
+ model_dd = gr.Dropdown(
1010
+ choices=["minicpm_v46", "minicpm_v46_thinking", "minicpm_o45"],
1011
+ value="minicpm_v46",
1012
+ label="Vision Model"
1013
+ )
1014
+ thinking_cb = gr.Checkbox(label="Enable Thinking Mode", value=False)
1015
+ image_in = gr.Image(type="pil", label="Upload Image")
1016
+ video_in = gr.Video(label="Upload Video (MiniCPM-V-4.6 supports video)")
1017
+ prompt_in = gr.Textbox(label="Prompt")
1018
+ submit_btn = gr.Button("Run")
1019
+ output_txt = gr.Textbox(label="Response", lines=10)
1020
+
1021
+ def infer(model_id, thinking, image, video, prompt):
1022
+ svc = model_registry.get(model_id)
1023
+ content = []
1024
+ if image:
1025
+ content.append({"type": "image", "image": image})
1026
+ if video:
1027
+ content.append({"type": "video", "video": video})
1028
+ content.append({"type": "text", "text": prompt})
1029
+ msgs = [{"role": "user", "content": content}]
1030
+ svc.thinking = thinking
1031
+ return svc.chat(msgs)
1032
+
1033
+ submit_btn.click(infer, [model_dd, thinking_cb, image_in, video_in, prompt_in], output_txt)
1034
+ ```
1035
+
1036
+ ---
1037
+
1038
+ ## 12. Field Notes & Correction Loop
1039
+
1040
+ ```python
1041
+ # datasets/field_notes.py
1042
+ import sqlite3, json
1043
+ from dataclasses import dataclass, field, asdict
1044
+ from datetime import datetime
1045
+
1046
+ @dataclass
1047
+ class FieldNote:
1048
+ id: str = field(default_factory=lambda: datetime.utcnow().isoformat())
1049
+ model_id: str = ""
1050
+ modality: str = "text" # "text" | "image" | "video" | "multimodal"
1051
+ image_path: str | None = None
1052
+ video_path: str | None = None
1053
+ prompt: str = ""
1054
+ response: str = ""
1055
+ correction: str = "" # human-corrected output
1056
+ tags: list = field(default_factory=list)
1057
+ used_in_train: bool = False
1058
+
1059
+ class FieldNoteStore:
1060
+ def __init__(self, db_path: str = "data/field_notes.db"):
1061
+ self.conn = sqlite3.connect(db_path, check_same_thread=False)
1062
+ self._init_db()
1063
+
1064
+ def _init_db(self):
1065
+ self.conn.execute("""
1066
+ CREATE TABLE IF NOT EXISTS notes (
1067
+ id TEXT PRIMARY KEY,
1068
+ data JSON NOT NULL,
1069
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
1070
+ )
1071
+ """)
1072
+ self.conn.commit()
1073
+
1074
+ def save(self, note: FieldNote):
1075
+ self.conn.execute(
1076
+ "INSERT OR REPLACE INTO notes (id, data) VALUES (?, ?)",
1077
+ (note.id, json.dumps(asdict(note)))
1078
+ )
1079
+ self.conn.commit()
1080
+
1081
+ def to_hf_dataset(self, output_path: str):
1082
+ """Export uncorrected notes as HF Dataset for retraining."""
1083
+ from datasets import Dataset
1084
+ rows = [json.loads(r[0]) for r in
1085
+ self.conn.execute("SELECT data FROM notes WHERE json_extract(data,'$.correction') != ''")]
1086
+ ds = Dataset.from_list(rows)
1087
+ ds.save_to_disk(output_path)
1088
+ return ds
1089
+ ```
1090
+
1091
+ ---
1092
+
1093
+ ## 13. Directory Structure
1094
+
1095
+ ```
1096
+ ml-workbench/
1097
+ ├── app.py # Entry point: gr.Blocks or gradio.Server
1098
+ ├── requirements.txt
1099
+ │
1100
+ ├── config/
1101
+ │ ├── models.yaml # ← TEMPLATE AXIS: change this for any model family
1102
+ │ └── training.yaml # LoRA hyperparams, export settings
1103
+ │
1104
+ ├── core/
1105
+ │ ├── events.py # EventBus, EventType enum
1106
+ │ ├── registry.py # Registry[T] generic
1107
+ │ └── state.py # Global AppState dataclass
1108
+ │
1109
+ ├── models/
1110
+ │ ├── base.py # Abstract ModelService
1111
+ │ ├── hf_loader.py # Generic HF Hub downloader
1112
+ │ ├── minicpm_text.py # MiniCPM5-1B, MiniCPM4.1-8B service
1113
+ │ ├── minicpm_vision.py # MiniCPM-V-4.6 / Thinking service
1114
+ │ ├── minicpm_omni.py # MiniCPM-o-4.5 service
1115
+ │ ├── llama_cpp_runner.py # llama-server wrapper
1116
+ │ ├── sglang_runner.py # SGLang server wrapper (tool-use)
1117
+ │ └── ollama_runner.py # Ollama client
1118
+ │
1119
+ ├── datasets/
1120
+ │ ├── loader.py # Abstract DatasetLoader
1121
+ │ ├── hf_datasets.py # HF Hub datasets.load_dataset()
1122
+ │ ├── field_notes.py # SQLite-backed correction store
1123
+ │ └── synthetic.py # ml-intern style synthetic data gen
1124
+ │
1125
+ ├── training/
1126
+ │ ├── lora.py # LoRA text trainer (PEFT + TRL)
1127
+ │ ├── lora_vision.py # LoRA vision config (SWIFT/LLaMA-Factory)
1128
+ │ ├── evaluation.py # Perplexity, accuracy, custom metrics
1129
+ │ ├── export.py # GGUF exporter (convert_hf_to_gguf.py)
1130
+ │ └── hf_jobs.py # HF Jobs offload (ml-intern pattern)
1131
+ │
1132
+ ├── tools/
1133
+ │ ├── calculator.py # Safe math eval tool
1134
+ │ ├── dataset_stats.py # Dataset statistics tool
1135
+ │ ├── hf_search.py # HF Hub search tool
1136
+ │ └── paper_search.py # arXiv / HF Papers search (ml-intern)
1137
+ │
1138
+ ├── tracking/
1139
+ │ └── trackio_service.py # Trackio integration (init/log/finish/alert)
1140
+ │
1141
+ ├── mcp/
1142
+ │ ├── server.py # FastMCP standalone server
1143
+ │ └── tools.py # All @mcp.tool() decorators
1144
+ │
1145
+ ├── agent/
1146
+ │ ├── loop.py # Research → Plan → Implement agent
1147
+ │ └── prompts/
1148
+ │ └── system_prompt.yaml # Agent system prompt (ml-intern style)
1149
+ │
1150
+ ├── ui/
1151
+ │ ├── chat_tab.py
1152
+ │ ├── vision_tab.py
1153
+ │ ├── train_tab.py
1154
+ │ ├── dataset_tab.py
1155
+ │ ├── export_tab.py
1156
+ │ ├── traces_tab.py
1157
+ │ ├── agent_tab.py
1158
+ │ └── notes_tab.py
1159
+ │
1160
+ ├── exports/ # GGUF + quantized model outputs
1161
+ └── data/ # Cached datasets, field_notes.db
1162
+ ```
1163
+
1164
+ ---
1165
+
1166
+ ## 14. Configuration Schema
1167
+
1168
+ ### training.yaml
1169
+
1170
+ ```yaml
1171
+ # config/training.yaml
1172
+ lora:
1173
+ rank: 16
1174
+ alpha: 32
1175
+ dropout: 0.05
1176
+ target_modules_text:
1177
+ - q_proj
1178
+ - k_proj
1179
+ - v_proj
1180
+ - o_proj
1181
+ - gate_proj
1182
+ - up_proj
1183
+ - down_proj
1184
+ target_modules_vision:
1185
+ - q_proj
1186
+ - k_proj
1187
+ - v_proj
1188
+ - o_proj # language layers only; vision tower frozen
1189
+
1190
+ training:
1191
+ epochs: 3
1192
+ batch_size: 4
1193
+ grad_accum: 4
1194
+ lr: 2.0e-4
1195
+ bf16: true
1196
+ report_to: trackio # or: wandb, tensorboard, none
1197
+
1198
+ export:
1199
+ default_quant: Q4_K_M
1200
+ supported_quants:
1201
+ - F16
1202
+ - Q4_K_M
1203
+ - Q5_K_M
1204
+ - Q8_0
1205
+ - Q2_K
1206
+
1207
+ trackio:
1208
+ project: ml-workbench
1209
+ space_id: null # set to "username/trackio-space" for HF sync
1210
+
1211
+ hf_jobs:
1212
+ enabled: false
1213
+ hardware: gpu-a100
1214
+ ```
1215
+
1216
+ ---
1217
+
1218
+ ## 15. Dependencies
1219
+
1220
+ ```text
1221
+ # requirements.txt
1222
+ # Core
1223
+ gradio[mcp]>=5.38.0
1224
+ mcp[cli]>=1.0.0
1225
+ fastapi
1226
+ uvicorn[standard]
1227
+
1228
+ # Models
1229
+ transformers>=5.7.0 # required for MiniCPM-V-4.6
1230
+ torch>=2.1.0
1231
+ torchvision
1232
+ torchcodec # video decoding for MiniCPM-V-4.6
1233
+ accelerate
1234
+ bitsandbytes
1235
+
1236
+ # Training
1237
+ peft>=0.10.0
1238
+ trl>=0.9.0
1239
+ datasets>=2.18.0
1240
+
1241
+ # Tracking
1242
+ trackio>=0.1.0
1243
+
1244
+ # MCP + Agents
1245
+ mcp>=1.0.0
1246
+ smolagents>=1.0.0
1247
+
1248
+ # Serving backends (optional, install as needed)
1249
+ # sglang # pip install sglang
1250
+ # vllm # pip install vllm
1251
+ # ollama # install separately
1252
+
1253
+ # Hub
1254
+ huggingface_hub>=0.23.0
1255
+ Pillow
1256
+ requests
1257
+ pyyaml
1258
+ ```
1259
+
1260
+ ---
1261
+
1262
+ ## 16. Hackathon Demo Flow
1263
+
1264
+ ```
1265
+ 1. DATASET
1266
+ Load HF dataset via Dataset tab (e.g. "tatsu-lab/alpaca", 10K rows)
1267
+ → EventType.DATASET_LOADED
1268
+ → Trackio: trackio.init("demo-run") + trackio.log({"dataset": ..., "rows": ...})
1269
+
1270
+ 2. TRAIN
1271
+ Start LoRA on MiniCPM5-1B (rank=16, 1 epoch demo)
1272
+ → EventType.TRAINING_STARTED
1273
+ → TRL SFTTrainer + report_to="trackio" → live loss in Traces tab
1274
+
1275
+ 3. EXPORT
1276
+ Click "Export GGUF" → Q4_K_M
1277
+ → convert_hf_to_gguf.py → llama-quantize
1278
+ → EventType.EXPORT_FINISHED → file download
1279
+
1280
+ 4. LLAMA.CPP CHAT
1281
+ Load exported GGUF in llama-server
1282
+ → Chat tab, backend: llama.cpp
1283
+ → Verify text generation quality
1284
+
1285
+ 5. SWITCH TO VISION
1286
+ Select MiniCPM-V-4.6 in Vision tab
1287
+ Upload image → enter prompt → Run
1288
+
1289
+ 6. THINKING MODE
1290
+ Toggle "Enable Thinking Mode" → switch to MiniCPM-V-4.6-Thinking
1291
+ → Observe explicit reasoning trace in response
1292
+
1293
+ 7. FIELD NOTE
1294
+ Response is wrong → open Field Notes tab
1295
+ → Enter correction → Save
1296
+ → EventType.FIELD_NOTE_SAVED
1297
+
1298
+ 8. AGENT MODE
1299
+ Agent tab: "Improve MiniCPM-V-4.6 on [domain] using field notes"
1300
+ → Research phase: browse HF Papers
1301
+ → Plan: dataset selection, LoRA config
1302
+ → Implement: trigger SWIFT fine-tune
1303
+
1304
+ 9. SHARE TRACES
1305
+ Traces tab → set space_id → Sync to HF Space
1306
+ → Shareable dashboard URL
1307
+ ```
1308
+
1309
+ ---
1310
+
1311
+ ## 17. Corrections from PRD v1
1312
+
1313
+ | v1 Claim | Correct v2 |
1314
+ |----------|-----------|
1315
+ | `trackio.trace("vision_inference")` context manager | **Does not exist.** Use `trackio.init()` + `trackio.log()` + `trackio.finish()` |
1316
+ | `AutoModelForCausalLM` for MiniCPM-V-4.6 | **Wrong.** Use `AutoModelForImageTextToText` with `transformers>=5.7.0` |
1317
+ | `llama-mtmd-cli` for vision deployment | Use `llama-server --mmproj <mmproj.gguf>` for server; `llama-mtmd-cli` for CLI-only |
1318
+ | Run `convert_hf_to_gguf.py` for V4.6 | Official GGUFs at `openbmb/MiniCPM-V-4.6-gguf` — download directly; run script only for custom quants |
1319
+ | Surgery scripts for V4.6 GGUF | Surgery scripts are for MiniCPM-o-2.6 only; **do not use for V4.6** |
1320
+ | `future MiniCPM-o` (vague) | **MiniCPM-o-4.5** released 2026-05-17; specific HF ID available |
1321
+ | `app.launch(mcp_server=True)` only option | Two paths: `gr.Blocks` + `mcp_server=True`, or `gradio.Server` with `@app.mcp.tool()` |
1322
+ | FastMCP as only MCP approach | Three paths: Gradio native, gradio.Server, standalone FastMCP (see §7) |
1323
+ | MiniCPM4.1-8B: standard architecture | Has sparse InfLLM-v2 attention; requires `--trust-remote-code` |
1324
+ | LoRA for vision: PEFT only | OpenBMB recommends **SWIFT** or **LLaMA-Factory** for MiniCPM-V fine-tuning |
1325
+ | No HF Jobs mention | HF Jobs integration (ml-intern pattern) enables GPU cloud offload |
1326
+ | No SGLang mention | SGLang is the **recommended** backend for MiniCPM5-1B tool calling |
1327
+ | No video support | MiniCPM-V-4.6 supports **video understanding** (`torchcodec` required) |
1328
+
1329
+ ---
1330
+
1331
+ ## 18. Roadmap & Extension Points
1332
+
1333
+ ### Swap the model family (template portability)
1334
+
1335
+ To build a "Qwen3 Workbench":
1336
+ 1. Edit `config/models.yaml` — add Qwen3 model IDs
1337
+ 2. Create `models/qwen3_service.py` extending `ModelService`
1338
+ 3. Register in `app.py` — done
1339
+
1340
+ No changes to training, export, MCP, Trackio, or UI layers.
1341
+
1342
+ ### Planned extensions
1343
+
1344
+ | Feature | Module | Notes |
1345
+ |---------|--------|-------|
1346
+ | vLLM serving tab | `models/vllm_runner.py` | Batch inference, production |
1347
+ | Ollama quick-start | `models/ollama_runner.py` | Zero-config consumer demo |
1348
+ | Reward model eval | `training/reward_eval.py` | For RLHF experiments |
1349
+ | Synthetic data gen | `datasets/synthetic.py` | ml-intern pattern: LLM writes training data |
1350
+ | Paper-to-code agent | `agent/paper_agent.py` | Read arXiv → implement in workbench |
1351
+ | HF Spaces deploy | `deploy/spaces.py` | One-click push as HF Space |
1352
+ | VINDEX integration | `tools/vindex_tool.py` | Knowledge editing via your VINDEX engine |
1353
+ | OCR pipeline hook | `datasets/ocr_loader.py` | Feed your OCR pipeline output as field notes |
1354
+ | MiniCPM Desk-Pet | `agent/desk_pet.py` | LoRA persona switching via MiniCPM5-1B |
1355
+ | MiniCPM-o audio | `ui/audio_tab.py` | Real-time omnimodal via MiniCPM-o-4.5 |
1356
+
1357
+ ---
1358
+
1359
+ *PRD v2.0 — Christof Kaller / ki-fusion-labs.de — 2026-06-05*
1360
+ *Research sources: openbmb/MiniCPM-V, openbmb/MiniCPM, gradio-app/trackio,*
1361
+ *huggingface/ml-intern, huggingface.co/blog/introducing-gradio-server*
README.md ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: OpenBMB Local AI Workbench
3
+ colorFrom: green
4
+ colorTo: gray
5
+ sdk: gradio
6
+ app_file: app.py
7
+ pinned: false
8
+ ---
9
+
10
+ # OpenBMB Local AI Workbench
11
+
12
+ A Gradio workbench for the Build Small Hackathon: small local models, practical experimentation,
13
+ and a clear path from local demo to Hugging Face Space.
14
+
15
+ ## What This Is
16
+
17
+ The project turns the PRD in `HF_PRD_v1.md` into a staged implementation:
18
+
19
+ 1. A working Gradio app shell.
20
+ 2. Config-driven OpenBMB model registry.
21
+ 3. Local-first inference path through real backends: Transformers, Ollama, llama.cpp,
22
+ LM Studio/OpenAI-compatible, SGLang, and vLLM.
23
+ 4. Field notes for collecting corrections.
24
+ 5. Extension points for training, GGUF export, Trackio traces, MCP tools, and agent workflows.
25
+
26
+ ## Hackathon Fit
27
+
28
+ - **Track:** Backyard AI or Thousand Token Wood, depending on the final user story.
29
+ - **Canvas:** Gradio app, deployable to Hugging Face Spaces.
30
+ - **Small model rule:** target models stay at or below 32B parameters.
31
+ - **Bonus quests:** local-first, field notes/report, possible llama.cpp and trace sharing.
32
+
33
+ ## Quick Start
34
+
35
+ Python is not currently available on PATH in this workspace shell. Install Python first, then:
36
+
37
+ ```powershell
38
+ .\scripts\verify_structure.ps1
39
+ & "$env:LOCALAPPDATA\Microsoft\WindowsApps\python3.11.exe" -m venv .venv
40
+ .venv\Scripts\Activate.ps1
41
+ python -m pip install -r requirements.txt
42
+ python -m pip install -r requirements-dev.txt
43
+ .\scripts\run_tests.ps1
44
+ python app.py
45
+ ```
46
+
47
+ ## Browser E2E Screenshots
48
+
49
+ After installing Node.js:
50
+
51
+ ```powershell
52
+ npm install
53
+ npm run e2e:install
54
+ npm run e2e
55
+ ```
56
+
57
+ The Playwright user-story tests run separate Workbench and Plant flows and save documentation
58
+ screenshots under `assets/e2e/workbench/` and `assets/e2e/plant/`. The Workbench screenshot now runs
59
+ local GGUF chat through `llama-cpp-python`; the Plant screenshot can run OpenBMB MiniCPM-V on
60
+ `assets/plant_sample.jpg` with `RUN_REAL_MODEL_E2E=1`.
61
+ only when `RUN_REAL_MODEL_E2E=1` is set; otherwise the browser tests verify real-backend setup
62
+ surfaces without using mock responses. To record or edit the browser flow manually, run
63
+ `npm run e2e:record`.
64
+
65
+ Generated screenshot sets:
66
+
67
+ - [Workbench home](assets/e2e/workbench/01-workbench-home.png)
68
+ - [Workbench backend status](assets/e2e/workbench/05-backend-status.png)
69
+ - [Plant tool home](assets/e2e/plant/01-plant-home.png)
70
+ - [Plant corrections export](assets/e2e/plant/03-corrections-export.png)
71
+
72
+ ## Template And Reference Apps
73
+
74
+ This repo is also a template for focused local-first Gradio apps. The first reference app is
75
+ Plant Discovery under `plant/`.
76
+
77
+ ```powershell
78
+ .venv\Scripts\python.exe -m plant.app --no-model --port 7861
79
+ ```
80
+
81
+ Use the real OpenBMB VLM path after installing optional plant dependencies:
82
+
83
+ ```powershell
84
+ .venv\Scripts\python.exe -m plant.app --model-mode openbmb --port 7861
85
+ ```
86
+
87
+ The detailed build guide is [docs/TEMPLATE_HOWTO.md](docs/TEMPLATE_HOWTO.md), and the Plant
88
+ Discovery checklist is [docs/PLANT_DISCOVERY_APP_PLAN.md](docs/PLANT_DISCOVERY_APP_PLAN.md).
89
+ Model and adapter training steps are in
90
+ [docs/PLANT_MODEL_AND_TRAINING_HOWTO.md](docs/PLANT_MODEL_AND_TRAINING_HOWTO.md).
91
+
92
+ If `python` is not recognized, install Python 3.11+ from python.org or the Windows Store,
93
+ then reopen the terminal so PATH is refreshed.
94
+
95
+ Open the local URL shown by Gradio, usually `http://127.0.0.1:7860`.
96
+
97
+ ## Project Structure
98
+
99
+ ```text
100
+ .
101
+ |-- app.py
102
+ |-- AGENTS.md
103
+ |-- README.md
104
+ |-- requirements.txt
105
+ |-- config/
106
+ | |-- models.yaml
107
+ | `-- training.yaml
108
+ |-- core/
109
+ | |-- events.py
110
+ | `-- registry.py
111
+ |-- datasets/
112
+ | `-- field_notes.py
113
+ |-- models/
114
+ | |-- placeholder_service.py
115
+ | `-- model_catalog.py
116
+ |-- ui/
117
+ | |-- chat_tab.py
118
+ | |-- dataset_tab.py
119
+ | |-- export_tab.py
120
+ | |-- notes_tab.py
121
+ | |-- traces_tab.py
122
+ | |-- agent_tab.py
123
+ | |-- status_tab.py
124
+ | |-- train_tab.py
125
+ | `-- vision_tab.py
126
+ |-- data/
127
+ | `-- .gitkeep
128
+ |-- exports/
129
+ | `-- .gitkeep
130
+ |-- HF_PRD_v1.md
131
+ `-- HF_PRD_ext.md
132
+ ```
133
+
134
+ ## Project Docs
135
+
136
+ The working docs live in [docs/README.md](docs/README.md).
137
+
138
+ - [Task checklist](docs/TASKS.md)
139
+ - [Implementation status](docs/IMPLEMENTATION_STATUS.md)
140
+ - [PRD implementation matrix](docs/PRD_IMPLEMENTATION_MATRIX.md)
141
+ - [Acceptance criteria](docs/ACCEPTANCE_CRITERIA.md)
142
+ - [Roadmap](docs/ROADMAP.md)
143
+ - [Template how-to](docs/TEMPLATE_HOWTO.md)
144
+ - [Plant Discovery plan](docs/PLANT_DISCOVERY_APP_PLAN.md)
145
+ - [Usage guide](docs/USAGE.md)
146
+ - [Architecture guide](docs/ARCHITECTURE.md)
147
+ - [Extension guide](docs/EXTENDING.md)
148
+ - [Hackathon submission package](docs/HACKATHON_SUBMISSION.md)
149
+ - [Test and quality policy](docs/ACCEPTANCE_CRITERIA.md)
150
+
151
+ ## Current Truth
152
+
153
+ The full PRD is not implemented yet. The current app is a tested, quality-gated scaffold moving
154
+ from placeholder-first local verification to real-backend Workbench deployment. GitHub push is
155
+ complete at `https://github.com/Ckal/codex`. LM Studio/OpenAI-compatible text inference has been
156
+ verified previously; OpenBMB Transformers, Ollama OpenBMB, llama.cpp, MiniCPM-V, Space builds,
157
+ training execution, served MCP, and most extension PRD items still need proof before being claimed
158
+ done.
159
+
160
+ ## Model Plan
161
+
162
+ Initial candidates from the PRD:
163
+
164
+ | Config ID | Model | Purpose |
165
+ | --- | --- | --- |
166
+ | `minicpm5_1b` | `openbmb/MiniCPM5-1B` | text chat, LoRA, local-first baseline |
167
+ | `minicpm_v46` | `openbmb/MiniCPM-V-4.6` | image/video understanding |
168
+ | `minicpm_o45` | `openbmb/MiniCPM-o-4.5` | omnimodal stretch goal |
169
+
170
+ The app does not download large model files automatically. In deployed Space mode, placeholder
171
+ backend choices are hidden and model calls require real backend configuration. llama.cpp,
172
+ llama-cpp-python, Ollama, LM Studio/OpenAI-compatible, SGLang, Nemotron Nano 9B v2, and Transformers text can be selected as
173
+ backends, but the backend tool/package/server must be installed and populated with the selected
174
+ model explicitly by the user.
175
+
176
+ ## Deployment Target
177
+
178
+ For Hugging Face Spaces, keep these files at repo root:
179
+
180
+ - `app.py`
181
+ - `requirements.txt`
182
+ - `README.md`
183
+ - `config/`
184
+ - `core/`
185
+ - `models/`
186
+ - `ui/`
187
+
188
+ Workbench Space target:
189
+
190
+ ```text
191
+ https://huggingface.co/spaces/build-small-hackathon/workbench
192
+ ```
193
+
194
+ ## Spaces
195
+
196
+ - Workbench Space: https://huggingface.co/spaces/build-small-hackathon/workbench
197
+ - Plant Identification Tool Space: https://huggingface.co/spaces/build-small-hackathon/plant_identification_tool
198
+
199
+ Both Spaces have been pushed. At the latest local poll they were still in Hugging Face `BUILDING`
200
+ state on `zero-a10g`, so final build/run smoke verification is still open.
201
+
202
+ Plant Identification Tool Space target:
203
+
204
+ ```text
205
+ https://huggingface.co/spaces/build-small-hackathon/plant_identification_tool
206
+ ```
207
+
208
+ Use a freshly generated token through `hf auth login`; do not paste tokens into files or commit
209
+ them.
210
+
211
+ Later deployment commands:
212
+
213
+ ```powershell
214
+ hf auth login
215
+ git remote add space-workbench https://huggingface.co/spaces/build-small-hackathon/workbench
216
+ git push space-workbench main
217
+ git remote add space-plant https://huggingface.co/spaces/build-small-hackathon/plant_identification_tool
218
+ git push space-plant main
219
+ ```
220
+
221
+ ## Next Implementation Steps
222
+
223
+ 1. Decide the exact hackathon story and user.
224
+ 2. Add screenshot/demo media and Space submission URLs.
225
+ 3. Push and verify the two Hugging Face Spaces, then finish llama.cpp MiniCPM-V mmproj vision verification.
226
+ 4. Add field-note export to JSONL/HF Dataset.
227
+ 5. Polish README with screenshots, demo video script, and submission links.
agent/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Local deterministic agent helpers."""
agent/runner.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import asdict, dataclass
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from core.file_exports import copy_text_file_or_empty
9
+ from mcp_tools.tools import safe_calculator_tool, tool_registry
10
+
11
+ AGENT_SYSTEM_PROMPT = (
12
+ "You are a local workbench agent. Research the request, draft a small plan, "
13
+ "name tools you would use, and require verification before marking work done."
14
+ )
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class AgentStep:
19
+ """One deterministic agent trace step."""
20
+
21
+ phase: str
22
+ content: str
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class AgentSession:
27
+ """Agent session trace."""
28
+
29
+ task: str
30
+ steps: list[AgentStep]
31
+ tools: list[str]
32
+ limitations: list[str]
33
+ safety_gates: list[str]
34
+
35
+ def as_dict(self) -> dict[str, Any]:
36
+ return {
37
+ "task": self.task,
38
+ "steps": [asdict(step) for step in self.steps],
39
+ "tools": self.tools,
40
+ "limitations": self.limitations,
41
+ "safety_gates": self.safety_gates,
42
+ "system_prompt": AGENT_SYSTEM_PROMPT,
43
+ }
44
+
45
+ def as_markdown(self) -> str:
46
+ lines = [f"Task: {self.task or '(none)'}", ""]
47
+ for step in self.steps:
48
+ lines.append(f"{step.phase}: {step.content}")
49
+ lines.append("")
50
+ lines.append(f"Tools: {', '.join(self.tools)}")
51
+ lines.append(f"Limitations: {'; '.join(self.limitations)}")
52
+ lines.append(f"Safety gates: {'; '.join(self.safety_gates)}")
53
+ return "\n".join(lines)
54
+
55
+
56
+ def run_agent_loop(task: str) -> AgentSession:
57
+ tools = sorted(tool_registry())
58
+ steps = [
59
+ AgentStep("research", _research_summary(task)),
60
+ AgentStep("plan", _plan_summary(task)),
61
+ AgentStep("implement", _implementation_summary(task)),
62
+ AgentStep("verify", "Run unit tests, smoke checks, quality gates, and update docs/tasks."),
63
+ ]
64
+
65
+ calculator_result = _maybe_calculate(task)
66
+ if calculator_result is not None:
67
+ steps.insert(
68
+ 1,
69
+ AgentStep(
70
+ "tool:safe_calculator",
71
+ json.dumps(calculator_result.payload, ensure_ascii=False),
72
+ ),
73
+ )
74
+
75
+ return AgentSession(
76
+ task=task,
77
+ steps=steps,
78
+ tools=tools,
79
+ limitations=[
80
+ "Does not execute shell commands.",
81
+ "Does not commit, push, deploy, download models, or call external services.",
82
+ "Requires Codex or a human to apply and verify implementation changes.",
83
+ ],
84
+ safety_gates=default_safety_gates(),
85
+ )
86
+
87
+
88
+ def run_paper_to_code_loop(
89
+ paper_title: str,
90
+ paper_notes: str,
91
+ implementation_goal: str,
92
+ ) -> AgentSession:
93
+ task = f"Paper-to-code: {paper_title.strip() or 'untitled paper'}"
94
+ steps = [
95
+ AgentStep("research", _paper_research_summary(paper_title, paper_notes)),
96
+ AgentStep("plan", _paper_plan_summary(implementation_goal)),
97
+ AgentStep("implement", _paper_implementation_trace(implementation_goal)),
98
+ AgentStep("verify", "Map claims to tests, run quality gates, and document gaps."),
99
+ ]
100
+ return AgentSession(
101
+ task=task,
102
+ steps=steps,
103
+ tools=sorted(tool_registry()),
104
+ limitations=[
105
+ "Does not read remote papers automatically.",
106
+ "Does not execute code changes autonomously.",
107
+ "Requires human/Codex review before implementation claims are marked done.",
108
+ ],
109
+ safety_gates=default_safety_gates(),
110
+ )
111
+
112
+
113
+ def default_safety_gates() -> list[str]:
114
+ return [
115
+ "No shell commands are executed by the agent trace.",
116
+ "No model weights, datasets, or papers are downloaded automatically.",
117
+ "Every implementation claim needs a matching test or documented blocker.",
118
+ "External services require explicit user credentials and approval.",
119
+ ]
120
+
121
+
122
+ def save_agent_trace(
123
+ session: AgentSession,
124
+ path: str | Path = "data/agent_traces.jsonl",
125
+ ) -> Path:
126
+ output = Path(path)
127
+ output.parent.mkdir(parents=True, exist_ok=True)
128
+ with output.open("a", encoding="utf-8") as f:
129
+ f.write(json.dumps(session.as_dict(), ensure_ascii=False) + "\n")
130
+ return output
131
+
132
+
133
+ def export_agent_traces(
134
+ source_path: str | Path = "data/agent_traces.jsonl",
135
+ output_path: str | Path = "exports/agent_traces.jsonl",
136
+ ) -> Path:
137
+ return copy_text_file_or_empty(source_path, output_path)
138
+
139
+
140
+ def export_agent_traces_hf_dataset(
141
+ source_path: str | Path = "data/agent_traces.jsonl",
142
+ output_dir: str | Path = "exports/agent_traces_dataset",
143
+ ) -> Path:
144
+ target = Path(output_dir)
145
+ target.mkdir(parents=True, exist_ok=True)
146
+ data_file = target / "data.jsonl"
147
+ if Path(source_path).exists():
148
+ data_file.write_text(Path(source_path).read_text(encoding="utf-8"), encoding="utf-8")
149
+ else:
150
+ data_file.write_text("", encoding="utf-8")
151
+ (target / "README.md").write_text(
152
+ "# Agent Traces Dataset\n\n"
153
+ "Local Hugging Face Dataset-style export for OpenBMB Local AI Workbench traces.\n",
154
+ encoding="utf-8",
155
+ )
156
+ return target
157
+
158
+
159
+ def _research_summary(task: str) -> str:
160
+ if not task.strip():
161
+ return "No task provided. Ask for a concrete task before implementation."
162
+ return "Inspect PRD/tasks/docs, identify affected modules, and check existing tests."
163
+
164
+
165
+ def _plan_summary(task: str) -> str:
166
+ if any(word in task.casefold() for word in ["deploy", "push", "github", "huggingface"]):
167
+ return "Prepare repo/deploy steps, verify auth/remotes, then push only after tests pass."
168
+ return "Make a focused implementation slice, add or update tests, then update docs."
169
+
170
+
171
+ def _implementation_summary(task: str) -> str:
172
+ if "model" in task.casefold():
173
+ return "Use configured backend services and avoid startup downloads."
174
+ return "Apply changes in the smallest relevant modules and keep unrelated files untouched."
175
+
176
+
177
+ def _paper_research_summary(paper_title: str, paper_notes: str) -> str:
178
+ title = paper_title.strip() or "untitled paper"
179
+ notes = paper_notes.strip()
180
+ if not notes:
181
+ return f"Summarize the claims, assumptions, and reproducibility risks for {title}."
182
+ return f"Extract implementation claims from {title}: {notes[:240]}"
183
+
184
+
185
+ def _paper_plan_summary(implementation_goal: str) -> str:
186
+ goal = implementation_goal.strip() or "create a minimal local reproduction plan"
187
+ return f"Break the goal into local modules, tests, data assumptions, and blockers: {goal}."
188
+
189
+
190
+ def _paper_implementation_trace(implementation_goal: str) -> str:
191
+ goal = implementation_goal.strip() or "minimal reproducible scaffold"
192
+ return (
193
+ "Draft a non-executing implementation trace for "
194
+ f"{goal}; keep dependencies explicit and update docs before claiming completion."
195
+ )
196
+
197
+
198
+ def _maybe_calculate(task: str):
199
+ prefix = "calculate:"
200
+ if task.casefold().strip().startswith(prefix):
201
+ expression = task.split(":", 1)[1].strip()
202
+ return safe_calculator_tool(expression)
203
+ return None
app.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import cast
4
+
5
+ import gradio as gr
6
+
7
+ from core.app_logging import configure_app_logging
8
+ from core.deployment import current_policy
9
+ from models.model_catalog import load_model_catalog
10
+ from ui.agent_tab import build_agent_tab
11
+ from ui.chat_tab import build_chat_tab
12
+ from ui.dataset_tab import build_dataset_tab
13
+ from ui.export_tab import build_export_tab
14
+ from ui.notes_tab import build_notes_tab
15
+ from ui.status_tab import build_status_tab
16
+ from ui.traces_tab import build_traces_tab
17
+ from ui.train_tab import build_train_tab
18
+ from ui.vision_tab import build_vision_tab
19
+ from ui.vllm_tab import build_vllm_tab
20
+
21
+ APP_THEME = gr.themes.Soft(primary_hue="teal", neutral_hue="slate")
22
+ APP_CSS = """
23
+ .app-title {
24
+ margin-bottom: 0.25rem;
25
+ }
26
+
27
+ .gradio-container {
28
+ max-width: 1180px !important;
29
+ }
30
+
31
+ .tabs {
32
+ overflow-x: auto;
33
+ }
34
+
35
+ button {
36
+ min-height: 2.5rem;
37
+ }
38
+
39
+ textarea,
40
+ input,
41
+ .wrap {
42
+ max-width: 100%;
43
+ }
44
+
45
+ @media (max-width: 720px) {
46
+ .gradio-container {
47
+ padding-left: 0.75rem !important;
48
+ padding-right: 0.75rem !important;
49
+ }
50
+
51
+ .app-title h1 {
52
+ font-size: 1.55rem;
53
+ line-height: 1.2;
54
+ }
55
+ }
56
+ """
57
+
58
+
59
+ def build_app() -> gr.Blocks:
60
+ configure_app_logging()
61
+ policy = current_policy()
62
+ catalog = load_model_catalog("config/models.yaml")
63
+
64
+ with gr.Blocks(title="OpenBMB Local AI Workbench", analytics_enabled=False) as demo:
65
+ gr.Markdown(
66
+ """
67
+ # OpenBMB Local AI Workbench
68
+ Real small-model experimentation for Gradio + Hugging Face Spaces.
69
+ """,
70
+ elem_classes=["app-title"],
71
+ )
72
+ if policy.is_space:
73
+ gr.Markdown(
74
+ "Deployment mode: Space. Placeholder and demo inference are disabled; "
75
+ "select a configured real backend before running model calls."
76
+ )
77
+
78
+ with gr.Tabs():
79
+ with gr.Tab("Chat"):
80
+ build_chat_tab(catalog, policy)
81
+ with gr.Tab("Vision"):
82
+ build_vision_tab(catalog, policy)
83
+ with gr.Tab("Dataset"):
84
+ build_dataset_tab()
85
+ with gr.Tab("Train"):
86
+ build_train_tab()
87
+ with gr.Tab("vLLM"):
88
+ build_vllm_tab(catalog)
89
+ with gr.Tab("Export"):
90
+ build_export_tab(catalog)
91
+ with gr.Tab("Field Notes"):
92
+ build_notes_tab(catalog)
93
+ with gr.Tab("Traces"):
94
+ build_traces_tab()
95
+ with gr.Tab("Agent"):
96
+ build_agent_tab()
97
+ with gr.Tab("Status"):
98
+ build_status_tab(catalog, policy)
99
+
100
+ return cast(gr.Blocks, demo)
101
+
102
+
103
+ if __name__ == "__main__":
104
+ build_app().launch(
105
+ server_port=7860,
106
+ share=False,
107
+ theme=APP_THEME,
108
+ css=APP_CSS,
109
+ mcp_server=True,
110
+ )
assets/e2e/README.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # E2E Documentation Assets
2
+
3
+ Run the Playwright user-story test to generate screenshots for the README and hackathon docs:
4
+
5
+ ```powershell
6
+ npm install
7
+ npm run e2e:install
8
+ npm run e2e
9
+ ```
10
+
11
+ The test starts or reuses the local Gradio app at `http://127.0.0.1:7860`, walks all major tabs,
12
+ executes safe local actions, and writes screenshots plus `user-story.md` into this folder.
13
+
14
+ To record or edit the flow manually:
15
+
16
+ ```powershell
17
+ npm run e2e:record
18
+ ```
19
+
20
+ Generated images are intentionally stored under `assets/e2e/` so they can be linked from
21
+ `README.md`, `docs/HACKATHON_SUBMISSION.md`, and demo notes.
assets/e2e/plant-playwright-report/index.html ADDED
The diff for this file is too large to render. See raw diff
 
assets/e2e/playwright-report/index.html ADDED
The diff for this file is too large to render. See raw diff
 
assets/samples/README.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Public Sample Assets
2
+
3
+ These files are intentionally small and public-safe. They are used for local verification,
4
+ Playwright screenshots, and documentation examples.
5
+
6
+ - `plant_leaf.ppm` and `plant_leaf.png` are generated bitmap-style plant samples for upload workflow testing.
7
+ It is not a botanical benchmark image and should not be used to claim model accuracy.
assets/samples/plant_leaf.png ADDED
assets/samples/plant_leaf.ppm ADDED
config/models.yaml ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ models:
2
+ minicpm5_1b:
3
+ hf_id: openbmb/MiniCPM5-1B
4
+ display_name: MiniCPM5 1B
5
+ type: text
6
+ parameters_b: 1
7
+ backend: transformers
8
+ backend_capabilities:
9
+ placeholder: [text]
10
+ ollama: [text, streaming]
11
+ llama.cpp: [text, gguf]
12
+ llama-cpp-python: [text, gguf]
13
+ transformers: [text, lora]
14
+ openai-compatible: [text, openai-compatible]
15
+ sglang: [text, tools]
16
+ vllm: [text, openai-compatible]
17
+ context_length: 131072
18
+ local_first: true
19
+ thinking_mode: false
20
+ gguf:
21
+ available: true
22
+ repo: openbmb/MiniCPM5-1B-GGUF
23
+ default_quant: Q4_K_M
24
+ notes: Text baseline for local chat, LoRA experiments, and GGUF export.
25
+
26
+ minicpm5_1b_thinking:
27
+ hf_id: openbmb/MiniCPM5-1B-Thinking
28
+ display_name: MiniCPM5 1B Thinking
29
+ type: text
30
+ parameters_b: 1
31
+ backend: transformers
32
+ backend_capabilities:
33
+ placeholder: [text]
34
+ ollama: [text, thinking, streaming]
35
+ llama.cpp: [text, gguf]
36
+ llama-cpp-python: [text, gguf]
37
+ transformers: [text, thinking, lora]
38
+ openai-compatible: [text, thinking, openai-compatible]
39
+ sglang: [text, thinking, tools]
40
+ vllm: [text, openai-compatible]
41
+ context_length: 131072
42
+ local_first: true
43
+ thinking_mode: true
44
+ gguf:
45
+ available: true
46
+ repo: openbmb/MiniCPM5-1B-Thinking-GGUF
47
+ default_quant: Q4_K_M
48
+ notes: Thinking variant for explicit reasoning experiments.
49
+
50
+ minicpm41_8b:
51
+ hf_id: openbmb/MiniCPM4.1-8B
52
+ display_name: MiniCPM4.1 8B
53
+ type: text
54
+ parameters_b: 8
55
+ backend: transformers
56
+ backend_capabilities:
57
+ placeholder: [text]
58
+ ollama: [text, streaming]
59
+ llama.cpp: [text, gguf]
60
+ llama-cpp-python: [text, gguf]
61
+ transformers: [text, long-context]
62
+ openai-compatible: [text, long-context, openai-compatible]
63
+ sglang: [text, long-context, tools]
64
+ vllm: [text, long-context, openai-compatible]
65
+ context_length: 131072
66
+ local_first: true
67
+ thinking_mode: false
68
+ trust_remote_code: true
69
+ gguf:
70
+ available: true
71
+ repo: openbmb/MiniCPM4.1-8B-GGUF
72
+ default_quant: Q4_K_M
73
+ notes: Long-context sparse-attention text model; real backend will require trust_remote_code.
74
+
75
+ nemotron_nano_9b_v2:
76
+ hf_id: nvidia/NVIDIA-Nemotron-Nano-9B-v2
77
+ display_name: NVIDIA Nemotron Nano 9B v2
78
+ type: text
79
+ parameters_b: 9
80
+ backend: transformers
81
+ backend_capabilities:
82
+ placeholder: [text]
83
+ llama.cpp: [text, gguf]
84
+ llama-cpp-python: [text, gguf]
85
+ transformers: [text, custom-code]
86
+ openai-compatible: [text, openai-compatible]
87
+ sglang: [text]
88
+ vllm: [text, openai-compatible]
89
+ context_length: 131072
90
+ local_first: true
91
+ thinking_mode: false
92
+ trust_remote_code: true
93
+ gguf:
94
+ available: true
95
+ repo: bartowski/nvidia_NVIDIA-Nemotron-Nano-9B-v2-GGUF
96
+ default_quant: Q4_K_M
97
+ notes: Small Nemotron text option under the 32B hackathon limit; verify license and hardware before using in a public Space.
98
+
99
+ minicpm_v46:
100
+ hf_id: openbmb/MiniCPM-V-4.6
101
+ display_name: MiniCPM-V 4.6
102
+ type: vision
103
+ parameters_b: 1.3
104
+ backend: transformers
105
+ backend_capabilities:
106
+ placeholder: [vision]
107
+ ollama: [vision]
108
+ llama.cpp: [vision, gguf, mmproj]
109
+ transformers: [vision, image-text-to-text]
110
+ sglang: [vision, openai-compatible]
111
+ context_length: 262144
112
+ local_first: true
113
+ thinking_mode: false
114
+ gguf:
115
+ available: true
116
+ repo: openbmb/MiniCPM-V-4.6-gguf
117
+ main_file: MiniCPM-V-4.6-Q4_K_M.gguf
118
+ mmproj: mmproj-MiniCPM-V-4.6-F16.gguf
119
+ default_quant: Q4_K_M
120
+ notes: Vision-language model candidate for image and video understanding.
121
+
122
+ minicpm_v46_thinking:
123
+ hf_id: openbmb/MiniCPM-V-4.6-Thinking
124
+ display_name: MiniCPM-V 4.6 Thinking
125
+ type: vision
126
+ parameters_b: 1.3
127
+ backend: transformers
128
+ backend_capabilities:
129
+ placeholder: [vision, thinking]
130
+ ollama: [vision, thinking]
131
+ llama.cpp: [vision, gguf, mmproj]
132
+ transformers: [vision, thinking, image-text-to-text]
133
+ sglang: [vision, thinking, openai-compatible]
134
+ context_length: 262144
135
+ local_first: true
136
+ thinking_mode: true
137
+ gguf:
138
+ available: true
139
+ repo: openbmb/MiniCPM-V-4.6-Thinking-gguf
140
+ default_quant: Q4_K_M
141
+ notes: Vision-language thinking variant for multimodal reasoning demos.
142
+
143
+ minicpm_o45:
144
+ hf_id: openbmb/MiniCPM-o-4.5
145
+ display_name: MiniCPM-o 4.5
146
+ type: omnimodal
147
+ parameters_b: 8
148
+ backend: transformers
149
+ backend_capabilities:
150
+ placeholder: [text, vision, audio]
151
+ transformers: [text, vision, audio]
152
+ context_length: 131072
153
+ local_first: true
154
+ thinking_mode: false
155
+ notes: Omnimodal stretch goal for audio, image, and text.
config/training.yaml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ lora:
2
+ rank: 16
3
+ alpha: 32
4
+ dropout: 0.05
5
+
6
+ training:
7
+ epochs: 1
8
+ batch_size: 2
9
+ grad_accum: 4
10
+ lr: 0.0002
11
+ report_to: none
12
+
13
+ export:
14
+ default_quant: Q4_K_M
15
+ supported_quants:
16
+ - F16
17
+ - Q4_K_M
18
+ - Q5_K_M
19
+ - Q8_0
20
+
21
+ trackio:
22
+ enabled: false
23
+ project: openbmb-local-ai-workbench
24
+ local_path: data/traces.jsonl
core/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Core shared helpers for events, state, and registries."""
core/app_logging.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+
6
+ def configure_app_logging() -> None:
7
+ """Configure compact application logging once."""
8
+
9
+ if logging.getLogger().handlers:
10
+ return
11
+
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format="%(asctime)s %(levelname)s %(name)s %(message)s",
15
+ )
core/app_state.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import asdict
5
+
6
+ from core.events import Event, EventBus, EventType
7
+ from tracking.trackio_client import TrackingClient
8
+
9
+
10
+ class AppState:
11
+ """Shared in-process state for UI events and lightweight traces."""
12
+
13
+ def __init__(
14
+ self,
15
+ event_bus: EventBus | None = None,
16
+ tracking_client: TrackingClient | None = None,
17
+ ) -> None:
18
+ self.event_bus = event_bus or EventBus()
19
+ self.events: list[Event] = []
20
+ self.logger = logging.getLogger("openbmb_workbench")
21
+ self.tracking_client = tracking_client or TrackingClient()
22
+ self.tracking_client.init()
23
+
24
+ def emit(self, event: Event) -> None:
25
+ self.events.append(event)
26
+ self.logger.info("event=%s payload=%s", event.type.value, event.payload)
27
+ self.tracking_client.log(event.type.value, event.payload)
28
+ self.event_bus.emit(event)
29
+
30
+ def recent_events(self, limit: int = 20) -> list[dict]:
31
+ recent = self.events[-limit:]
32
+ return [asdict(event) for event in recent]
33
+
34
+
35
+ def emit_inference_response(
36
+ mode: str,
37
+ model_id: str,
38
+ backend: str,
39
+ response: str,
40
+ state: AppState | None = None,
41
+ ) -> None:
42
+ target_state = state or APP_STATE
43
+ target_state.emit(
44
+ Event(
45
+ EventType.INFERENCE_RESPONSE,
46
+ {
47
+ "mode": mode,
48
+ "model_id": model_id,
49
+ "backend": backend,
50
+ "response_chars": len(response),
51
+ },
52
+ )
53
+ )
54
+
55
+
56
+ APP_STATE = AppState()
core/deployment.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+
6
+ LOCAL_DEPLOYMENT = "local"
7
+ SPACE_DEPLOYMENT = "space"
8
+ VALID_DEPLOYMENTS = {LOCAL_DEPLOYMENT, SPACE_DEPLOYMENT}
9
+ PLACEHOLDER_BACKEND = "placeholder"
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class DeploymentPolicy:
14
+ mode: str = LOCAL_DEPLOYMENT
15
+
16
+ @property
17
+ def is_space(self) -> bool:
18
+ return self.mode == SPACE_DEPLOYMENT
19
+
20
+ @property
21
+ def allow_placeholder_backend(self) -> bool:
22
+ return not self.is_space
23
+
24
+ @property
25
+ def allow_demo_mode(self) -> bool:
26
+ return not self.is_space
27
+
28
+
29
+ def deployment_mode(env_value: str | None = None) -> str:
30
+ raw_value = env_value if env_value is not None else os.getenv("WORKBENCH_DEPLOYMENT", "")
31
+ value = raw_value.strip().lower() or LOCAL_DEPLOYMENT
32
+ if value not in VALID_DEPLOYMENTS:
33
+ raise ValueError(
34
+ "WORKBENCH_DEPLOYMENT must be 'local' or 'space'; "
35
+ f"got {raw_value!r}."
36
+ )
37
+ return value
38
+
39
+
40
+ def current_policy() -> DeploymentPolicy:
41
+ return DeploymentPolicy(deployment_mode())
42
+
43
+
44
+ def filter_backends_for_policy(backends: list[str], policy: DeploymentPolicy) -> list[str]:
45
+ if policy.allow_placeholder_backend:
46
+ return backends
47
+ return [backend for backend in backends if backend != PLACEHOLDER_BACKEND]
48
+
49
+
50
+ def default_backend_for_policy(
51
+ backends: list[str],
52
+ preferred: str,
53
+ policy: DeploymentPolicy,
54
+ ) -> str:
55
+ allowed = filter_backends_for_policy(backends, policy)
56
+ if preferred in allowed:
57
+ return preferred
58
+ if allowed:
59
+ return allowed[0]
60
+ raise ValueError("No real backend is available for this deployment policy.")
61
+
62
+
63
+ def ensure_backend_allowed(backend: str, policy: DeploymentPolicy) -> None:
64
+ if backend == PLACEHOLDER_BACKEND and not policy.allow_placeholder_backend:
65
+ raise ValueError(
66
+ "Placeholder inference is disabled in WORKBENCH_DEPLOYMENT=space. "
67
+ "Select a real backend such as transformers, Ollama, llama.cpp, "
68
+ "SGLang, or an OpenAI-compatible local server."
69
+ )
70
+
71
+
72
+ def ensure_demo_mode_allowed(policy: DeploymentPolicy) -> None:
73
+ if not policy.allow_demo_mode:
74
+ raise ValueError(
75
+ "Demo/no-model mode is disabled in WORKBENCH_DEPLOYMENT=space. "
76
+ "Use a real OpenBMB model configuration for the deployed app."
77
+ )
core/events.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+ from typing import Any
7
+
8
+
9
+ class StrEnum(str, Enum):
10
+ """Python 3.10-compatible subset of enum.StrEnum."""
11
+
12
+ def __str__(self) -> str:
13
+ return str(self.value)
14
+
15
+
16
+ class EventType(StrEnum):
17
+ """Event names shared across future services."""
18
+
19
+ DATASET_LOADED = "dataset_loaded"
20
+ INFERENCE_REQUEST = "inference_request"
21
+ INFERENCE_RESPONSE = "inference_response"
22
+ UI_ERROR = "ui_error"
23
+ FIELD_NOTE_SAVED = "field_note_saved"
24
+ TRAINING_STARTED = "training_started"
25
+ EXPORT_STARTED = "export_started"
26
+
27
+
28
+ @dataclass
29
+ class Event:
30
+ """A lightweight event payload."""
31
+
32
+ type: EventType
33
+ payload: dict[str, Any] = field(default_factory=dict)
34
+
35
+
36
+ class EventBus:
37
+ """In-process event dispatcher for low-volume app events."""
38
+
39
+ def __init__(self) -> None:
40
+ self._handlers: dict[EventType, list[Callable[[Event], None]]] = {}
41
+
42
+ def on(self, event_type: EventType) -> Callable:
43
+ def decorator(fn: Callable[[Event], None]) -> Callable[[Event], None]:
44
+ self._handlers.setdefault(event_type, []).append(fn)
45
+ return fn
46
+
47
+ return decorator
48
+
49
+ def emit(self, event: Event) -> None:
50
+ for handler in self._handlers.get(event.type, []):
51
+ handler(event)
52
+
53
+
54
+ bus = EventBus()
core/file_exports.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def copy_text_file_or_empty(source_path: str | Path, output_path: str | Path) -> Path:
7
+ """Copy a text file to an export path, or create an empty export."""
8
+
9
+ source = Path(source_path)
10
+ output = Path(output_path)
11
+ output.parent.mkdir(parents=True, exist_ok=True)
12
+ if source.exists():
13
+ output.write_text(source.read_text(encoding="utf-8"), encoding="utf-8")
14
+ else:
15
+ output.write_text("", encoding="utf-8")
16
+ return output
core/registry.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Generic, TypeVar
4
+
5
+ T = TypeVar("T")
6
+
7
+
8
+ class Registry(Generic[T]):
9
+ """Small name-to-service registry."""
10
+
11
+ def __init__(self) -> None:
12
+ self._items: dict[str, T] = {}
13
+
14
+ def register(self, name: str, item: T) -> None:
15
+ self._items[name] = item
16
+
17
+ def get(self, name: str) -> T:
18
+ if name not in self._items:
19
+ available = ", ".join(self._items)
20
+ raise KeyError(f"{name!r} is not registered. Available: {available}")
21
+ return self._items[name]
22
+
23
+ def list(self) -> list[str]:
24
+ return list(self._items)
core/spaces_runtime.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from importlib import import_module
5
+ from types import SimpleNamespace
6
+ from typing import Any, TypeVar
7
+
8
+ F = TypeVar("F", bound=Callable[..., Any])
9
+ spaces: Any
10
+
11
+ try:
12
+ spaces = import_module("spaces")
13
+ except ImportError:
14
+
15
+ def _gpu(*_args: Any, **_kwargs: Any) -> Callable[[F], F]:
16
+ def decorator(func: F) -> F:
17
+ return func
18
+
19
+ return decorator
20
+
21
+ spaces = SimpleNamespace(GPU=_gpu)
core/tab_feedback.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from core.app_state import APP_STATE, AppState
6
+ from core.events import Event, EventType
7
+
8
+
9
+ def status_ok(message: str) -> str:
10
+ return f"Status: {message}"
11
+
12
+
13
+ def emit_tab_error(
14
+ tab: str,
15
+ message: str,
16
+ context: dict[str, Any] | None = None,
17
+ state: AppState | None = None,
18
+ ) -> str:
19
+ target_state = state or APP_STATE
20
+ payload = {"tab": tab, "message": message}
21
+ if context:
22
+ payload.update(context)
23
+ target_state.emit(Event(EventType.UI_ERROR, payload))
24
+ return f"Error: {message}"
data/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
datasets/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Dataset and correction-loop helpers."""
datasets/field_notes.py ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import datetime as dt
5
+ import json
6
+ import sqlite3
7
+ from contextlib import closing
8
+ from dataclasses import asdict, dataclass
9
+ from pathlib import Path
10
+
11
+ UTC = getattr(dt, "UTC", dt.timezone.utc) # noqa: UP017
12
+
13
+
14
+ @dataclass
15
+ class FieldNote:
16
+ """One human correction record."""
17
+
18
+ created_at: str
19
+ model_id: str
20
+ prompt: str
21
+ response: str
22
+ correction: str
23
+ tags: str
24
+ image_path: str = ""
25
+ video_path: str = ""
26
+ use_for_training: bool = True
27
+
28
+ @classmethod
29
+ def create(
30
+ cls,
31
+ model_id: str,
32
+ prompt: str,
33
+ response: str,
34
+ correction: str,
35
+ tags: str,
36
+ image_path: str = "",
37
+ video_path: str = "",
38
+ use_for_training: bool = True,
39
+ ) -> FieldNote:
40
+ return cls(
41
+ created_at=dt.datetime.now(UTC).isoformat(),
42
+ model_id=model_id,
43
+ prompt=prompt,
44
+ response=response,
45
+ correction=correction,
46
+ tags=tags,
47
+ image_path=image_path,
48
+ video_path=video_path,
49
+ use_for_training=use_for_training,
50
+ )
51
+
52
+ @classmethod
53
+ def from_row(cls, row: dict[str, str]) -> FieldNote:
54
+ use_for_training = str(row.get("use_for_training", "true")).lower() in {
55
+ "1",
56
+ "true",
57
+ "yes",
58
+ }
59
+ return cls(
60
+ created_at=row["created_at"],
61
+ model_id=row["model_id"],
62
+ prompt=row["prompt"],
63
+ response=row["response"],
64
+ correction=row["correction"],
65
+ tags=row["tags"],
66
+ image_path=row.get("image_path", ""),
67
+ video_path=row.get("video_path", ""),
68
+ use_for_training=use_for_training,
69
+ )
70
+
71
+ def to_dict(self) -> dict[str, object]:
72
+ return asdict(self)
73
+
74
+
75
+ class FieldNoteStore:
76
+ """CSV-backed field note storage."""
77
+
78
+ def __init__(self, path: str | Path = "data/field_notes.csv") -> None:
79
+ self.path = Path(path)
80
+
81
+ def save(self, note: FieldNote) -> Path:
82
+ self.path.parent.mkdir(parents=True, exist_ok=True)
83
+ is_new = not self.path.exists()
84
+
85
+ with self.path.open("a", newline="", encoding="utf-8") as f:
86
+ writer = csv.DictWriter(f, fieldnames=list(note.to_dict()))
87
+ if is_new:
88
+ writer.writeheader()
89
+ writer.writerow(note.to_dict())
90
+
91
+ return self.path
92
+
93
+ def list_notes(
94
+ self,
95
+ corrected_only: bool = False,
96
+ tag: str = "",
97
+ training_only: bool = False,
98
+ ) -> list[FieldNote]:
99
+ if not self.path.exists():
100
+ return []
101
+
102
+ with self.path.open(newline="", encoding="utf-8") as f:
103
+ rows = list(csv.DictReader(f))
104
+
105
+ notes = [FieldNote.from_row(row) for row in rows]
106
+ if corrected_only:
107
+ notes = [note for note in notes if note.correction.strip()]
108
+ if tag:
109
+ notes = [note for note in notes if tag in _split_tags(note.tags)]
110
+ if training_only:
111
+ notes = [note for note in notes if note.use_for_training]
112
+ return notes
113
+
114
+ def export_jsonl(
115
+ self,
116
+ output_path: str | Path = "data/field_notes.jsonl",
117
+ corrected_only: bool = True,
118
+ training_only: bool = True,
119
+ ) -> Path:
120
+ output = Path(output_path)
121
+ output.parent.mkdir(parents=True, exist_ok=True)
122
+
123
+ notes = self.list_notes(
124
+ corrected_only=corrected_only,
125
+ training_only=training_only,
126
+ )
127
+ with output.open("w", encoding="utf-8") as f:
128
+ for note in notes:
129
+ f.write(json.dumps(note.to_dict(), ensure_ascii=False) + "\n")
130
+
131
+ return output
132
+
133
+ def export_hf_dataset(
134
+ self,
135
+ output_dir: str | Path = "data/hf_field_notes",
136
+ corrected_only: bool = True,
137
+ training_only: bool = True,
138
+ ) -> Path:
139
+ target = Path(output_dir)
140
+ target.mkdir(parents=True, exist_ok=True)
141
+ data_file = self.export_jsonl(
142
+ target / "data.jsonl",
143
+ corrected_only=corrected_only,
144
+ training_only=training_only,
145
+ )
146
+ (target / "README.md").write_text(
147
+ "# Field Notes Dataset\n\n"
148
+ "Local export generated by OpenBMB Local AI Workbench.\n\n"
149
+ f"- Data file: `{data_file.name}`\n"
150
+ "- Intended split: `train`\n",
151
+ encoding="utf-8",
152
+ )
153
+ return target
154
+
155
+
156
+ class SQLiteFieldNoteStore:
157
+ """SQLite-backed field note storage for larger correction loops."""
158
+
159
+ def __init__(self, path: str | Path = "data/field_notes.sqlite") -> None:
160
+ self.path = Path(path)
161
+ self.path.parent.mkdir(parents=True, exist_ok=True)
162
+ self._init_schema()
163
+
164
+ def _connect(self) -> sqlite3.Connection:
165
+ return sqlite3.connect(self.path)
166
+
167
+ def _init_schema(self) -> None:
168
+ with closing(self._connect()) as conn:
169
+ conn.execute(
170
+ """
171
+ CREATE TABLE IF NOT EXISTS field_notes (
172
+ created_at TEXT NOT NULL,
173
+ model_id TEXT NOT NULL,
174
+ prompt TEXT NOT NULL,
175
+ response TEXT NOT NULL,
176
+ correction TEXT NOT NULL,
177
+ tags TEXT NOT NULL,
178
+ image_path TEXT NOT NULL,
179
+ video_path TEXT NOT NULL,
180
+ use_for_training INTEGER NOT NULL
181
+ )
182
+ """
183
+ )
184
+ conn.commit()
185
+
186
+ def save(self, note: FieldNote) -> Path:
187
+ with closing(self._connect()) as conn:
188
+ conn.execute(
189
+ """
190
+ INSERT INTO field_notes (
191
+ created_at, model_id, prompt, response, correction, tags,
192
+ image_path, video_path, use_for_training
193
+ )
194
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
195
+ """,
196
+ (
197
+ note.created_at,
198
+ note.model_id,
199
+ note.prompt,
200
+ note.response,
201
+ note.correction,
202
+ note.tags,
203
+ note.image_path,
204
+ note.video_path,
205
+ int(note.use_for_training),
206
+ ),
207
+ )
208
+ conn.commit()
209
+ return self.path
210
+
211
+ def list_notes(
212
+ self,
213
+ corrected_only: bool = False,
214
+ tag: str = "",
215
+ training_only: bool = False,
216
+ ) -> list[FieldNote]:
217
+ with closing(self._connect()) as conn:
218
+ conn.row_factory = sqlite3.Row
219
+ rows = conn.execute(
220
+ """
221
+ SELECT created_at, model_id, prompt, response, correction, tags,
222
+ image_path, video_path, use_for_training
223
+ FROM field_notes
224
+ ORDER BY created_at
225
+ """
226
+ ).fetchall()
227
+
228
+ notes = [
229
+ FieldNote(
230
+ created_at=str(row["created_at"]),
231
+ model_id=str(row["model_id"]),
232
+ prompt=str(row["prompt"]),
233
+ response=str(row["response"]),
234
+ correction=str(row["correction"]),
235
+ tags=str(row["tags"]),
236
+ image_path=str(row["image_path"]),
237
+ video_path=str(row["video_path"]),
238
+ use_for_training=bool(row["use_for_training"]),
239
+ )
240
+ for row in rows
241
+ ]
242
+ if corrected_only:
243
+ notes = [note for note in notes if note.correction.strip()]
244
+ if tag:
245
+ notes = [note for note in notes if tag in _split_tags(note.tags)]
246
+ if training_only:
247
+ notes = [note for note in notes if note.use_for_training]
248
+ return notes
249
+
250
+
251
+ def _split_tags(tags: str) -> set[str]:
252
+ return {tag.strip() for tag in tags.split(",") if tag.strip()}
datasets/loader.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import json
5
+ from dataclasses import dataclass
6
+ from importlib import import_module
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class DatasetPreview:
13
+ """Small local dataset preview result."""
14
+
15
+ source: str
16
+ rows: int
17
+ columns: list[str]
18
+ samples: list[dict[str, Any]]
19
+
20
+ def as_table(self) -> list[list[str]]:
21
+ table = [
22
+ ["source", self.source],
23
+ ["rows", str(self.rows)],
24
+ ["columns", ", ".join(self.columns)],
25
+ ]
26
+ for index, sample in enumerate(self.samples, start=1):
27
+ table.append([f"sample_{index}", json.dumps(sample, ensure_ascii=False)])
28
+ return table
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class DatasetStats:
33
+ """Basic dataset statistics for local preview and tools."""
34
+
35
+ source: str
36
+ rows: int
37
+ columns: int
38
+ column_names: list[str]
39
+ non_empty_by_column: dict[str, int]
40
+
41
+ def as_dict(self) -> dict[str, Any]:
42
+ return {
43
+ "source": self.source,
44
+ "rows": self.rows,
45
+ "columns": self.columns,
46
+ "column_names": self.column_names,
47
+ "non_empty_by_column": self.non_empty_by_column,
48
+ }
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class HuggingFaceDatasetPreview:
53
+ """Small preview result for optional Hugging Face datasets integration."""
54
+
55
+ dataset_id: str
56
+ split: str
57
+ rows: int
58
+ columns: list[str]
59
+ samples: list[dict[str, Any]]
60
+ status: str = "loaded"
61
+
62
+ def as_table(self) -> list[list[str]]:
63
+ table = [
64
+ ["dataset_id", self.dataset_id],
65
+ ["split", self.split],
66
+ ["rows", str(self.rows)],
67
+ ["columns", ", ".join(self.columns)],
68
+ ["status", self.status],
69
+ ]
70
+ for index, sample in enumerate(self.samples, start=1):
71
+ table.append([f"sample_{index}", json.dumps(sample, ensure_ascii=False)])
72
+ return table
73
+
74
+
75
+ def preview_local_dataset(path: str | Path, limit: int = 5) -> DatasetPreview:
76
+ dataset_path = Path(path)
77
+ if not dataset_path.exists():
78
+ raise FileNotFoundError(f"Dataset path not found: {dataset_path}")
79
+ if dataset_path.suffix.lower() == ".csv":
80
+ return _preview_csv(dataset_path, limit)
81
+ if dataset_path.suffix.lower() in {".jsonl", ".ndjson"}:
82
+ return _preview_jsonl(dataset_path, limit)
83
+ raise ValueError(f"Unsupported dataset format: {dataset_path.suffix}")
84
+
85
+
86
+ def dataset_statistics(path: str | Path) -> DatasetStats:
87
+ preview = preview_local_dataset(path, limit=0)
88
+ non_empty_by_column = {column: 0 for column in preview.columns}
89
+
90
+ dataset_path = Path(path)
91
+ if dataset_path.suffix.lower() == ".csv":
92
+ with dataset_path.open(newline="", encoding="utf-8") as f:
93
+ for row in csv.DictReader(f):
94
+ for column in preview.columns:
95
+ if str(row.get(column, "")).strip():
96
+ non_empty_by_column[column] += 1
97
+ else:
98
+ with dataset_path.open(encoding="utf-8") as f:
99
+ for line in f:
100
+ if not line.strip():
101
+ continue
102
+ row = json.loads(line)
103
+ for column in preview.columns:
104
+ if str(row.get(column, "")).strip():
105
+ non_empty_by_column[column] += 1
106
+
107
+ return DatasetStats(
108
+ source=preview.source,
109
+ rows=preview.rows,
110
+ columns=len(preview.columns),
111
+ column_names=preview.columns,
112
+ non_empty_by_column=non_empty_by_column,
113
+ )
114
+
115
+
116
+ def preview_huggingface_dataset(
117
+ dataset_id: str,
118
+ split: str = "train",
119
+ limit: int = 5,
120
+ ) -> HuggingFaceDatasetPreview:
121
+ datasets_module = import_module("datasets")
122
+ load_dataset = getattr(datasets_module, "load_dataset", None)
123
+ if load_dataset is None:
124
+ return HuggingFaceDatasetPreview(
125
+ dataset_id=dataset_id,
126
+ split=split,
127
+ rows=0,
128
+ columns=[],
129
+ samples=[],
130
+ status="Hugging Face package 'datasets' is not installed.",
131
+ )
132
+
133
+ dataset = load_dataset(dataset_id, split=split)
134
+ rows = len(dataset)
135
+ columns = list(dataset.column_names)
136
+ samples = [dict(dataset[index]) for index in range(min(limit, rows))]
137
+ return HuggingFaceDatasetPreview(dataset_id, split, rows, columns, samples)
138
+
139
+
140
+ def _preview_csv(path: Path, limit: int) -> DatasetPreview:
141
+ samples: list[dict[str, Any]] = []
142
+ rows = 0
143
+ columns: list[str] = []
144
+
145
+ with path.open(newline="", encoding="utf-8") as f:
146
+ reader = csv.DictReader(f)
147
+ columns = list(reader.fieldnames or [])
148
+ for row in reader:
149
+ rows += 1
150
+ if len(samples) < limit:
151
+ samples.append(dict(row))
152
+
153
+ return DatasetPreview(str(path), rows, columns, samples)
154
+
155
+
156
+ def _preview_jsonl(path: Path, limit: int) -> DatasetPreview:
157
+ samples: list[dict[str, Any]] = []
158
+ rows = 0
159
+ columns: set[str] = set()
160
+
161
+ with path.open(encoding="utf-8") as f:
162
+ for line in f:
163
+ if not line.strip():
164
+ continue
165
+ rows += 1
166
+ item = json.loads(line)
167
+ if not isinstance(item, dict):
168
+ raise ValueError("JSONL rows must be objects")
169
+ columns.update(item)
170
+ if len(samples) < limit:
171
+ samples.append(item)
172
+
173
+ return DatasetPreview(str(path), rows, sorted(columns), samples)
datasets/ocr.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import json
5
+ from dataclasses import asdict, dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from datasets.field_notes import FieldNote, FieldNoteStore
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class OCRPrediction:
14
+ """One local OCR prediction that may need human correction."""
15
+
16
+ source_path: str
17
+ text: str
18
+ confidence: float
19
+ page: str = ""
20
+
21
+ @classmethod
22
+ def from_row(cls, row: dict[str, Any]) -> OCRPrediction:
23
+ source_path = _first_present(row, ["source_path", "image_path", "file_path", "path"])
24
+ text = _first_present(row, ["text", "prediction", "ocr_text", "response"])
25
+ confidence_raw = _first_present(row, ["confidence", "score", "probability"], "0")
26
+ return cls(
27
+ source_path=source_path,
28
+ text=text,
29
+ confidence=_parse_confidence(confidence_raw),
30
+ page=str(row.get("page", "")),
31
+ )
32
+
33
+ def to_dict(self) -> dict[str, object]:
34
+ return asdict(self)
35
+
36
+
37
+ def load_ocr_predictions(path: str | Path) -> list[OCRPrediction]:
38
+ """Load OCR predictions from local CSV, JSONL, or NDJSON files."""
39
+
40
+ source = Path(path)
41
+ if not source.exists():
42
+ raise FileNotFoundError(f"OCR prediction file not found: {source}")
43
+
44
+ suffix = source.suffix.lower()
45
+ if suffix == ".csv":
46
+ with source.open(newline="", encoding="utf-8") as f:
47
+ return [OCRPrediction.from_row(row) for row in csv.DictReader(f)]
48
+ if suffix in {".jsonl", ".ndjson"}:
49
+ return [
50
+ OCRPrediction.from_row(json.loads(line))
51
+ for line in source.read_text(encoding="utf-8").splitlines()
52
+ if line.strip()
53
+ ]
54
+ raise ValueError("OCR predictions must be a .csv, .jsonl, or .ndjson file.")
55
+
56
+
57
+ def uncertain_predictions(
58
+ predictions: list[OCRPrediction],
59
+ confidence_threshold: float = 0.8,
60
+ ) -> list[OCRPrediction]:
61
+ return [
62
+ prediction
63
+ for prediction in predictions
64
+ if prediction.confidence <= confidence_threshold or not prediction.text.strip()
65
+ ]
66
+
67
+
68
+ def import_uncertain_predictions(
69
+ store: FieldNoteStore,
70
+ predictions: list[OCRPrediction],
71
+ model_id: str,
72
+ confidence_threshold: float = 0.8,
73
+ tags: str = "ocr,uncertain",
74
+ ) -> int:
75
+ imported = 0
76
+ for prediction in uncertain_predictions(predictions, confidence_threshold):
77
+ page_note = f" page {prediction.page}" if prediction.page else ""
78
+ note = FieldNote.create(
79
+ model_id=model_id,
80
+ prompt=f"Review OCR text for {prediction.source_path}{page_note}.",
81
+ response=prediction.text,
82
+ correction="",
83
+ tags=tags,
84
+ image_path=prediction.source_path,
85
+ use_for_training=False,
86
+ )
87
+ store.save(note)
88
+ imported += 1
89
+ return imported
90
+
91
+
92
+ def export_corrected_ocr_notes(
93
+ store: FieldNoteStore,
94
+ output_path: str | Path = "data/ocr_corrections.jsonl",
95
+ ) -> Path:
96
+ output = Path(output_path)
97
+ output.parent.mkdir(parents=True, exist_ok=True)
98
+ notes = store.list_notes(corrected_only=True, tag="ocr")
99
+ with output.open("w", encoding="utf-8") as f:
100
+ for note in notes:
101
+ row = {
102
+ "source_path": note.image_path,
103
+ "predicted_text": note.response,
104
+ "corrected_text": note.correction,
105
+ "model_id": note.model_id,
106
+ "created_at": note.created_at,
107
+ "tags": note.tags,
108
+ }
109
+ f.write(json.dumps(row, ensure_ascii=False) + "\n")
110
+ return output
111
+
112
+
113
+ def ocr_import_summary(path: str | Path, confidence_threshold: float = 0.8) -> dict[str, object]:
114
+ predictions = load_ocr_predictions(path)
115
+ uncertain = uncertain_predictions(predictions, confidence_threshold)
116
+ return {
117
+ "source": str(path),
118
+ "rows": len(predictions),
119
+ "uncertain_rows": len(uncertain),
120
+ "confidence_threshold": confidence_threshold,
121
+ "sample": [prediction.to_dict() for prediction in uncertain[:5]],
122
+ }
123
+
124
+
125
+ def _first_present(row: dict[str, Any], names: list[str], default: str = "") -> str:
126
+ for name in names:
127
+ value = row.get(name)
128
+ if value is not None:
129
+ return str(value)
130
+ return default
131
+
132
+
133
+ def _parse_confidence(value: str) -> float:
134
+ try:
135
+ return float(value)
136
+ except ValueError:
137
+ return 0.0
datasets/synthetic.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import asdict, dataclass
5
+ from pathlib import Path
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class SyntheticExample:
10
+ prompt: str
11
+ response: str
12
+ correction: str
13
+ tags: list[str]
14
+ source: str = "synthetic"
15
+
16
+ def as_dict(self) -> dict:
17
+ return asdict(self)
18
+
19
+
20
+ def generate_synthetic_examples(
21
+ topic: str,
22
+ count: int,
23
+ tag: str = "synthetic",
24
+ ) -> list[SyntheticExample]:
25
+ if count < 0:
26
+ raise ValueError("count must be non-negative")
27
+ cleaned_topic = topic.strip() or "local AI workflow"
28
+ return [
29
+ SyntheticExample(
30
+ prompt=f"Write a concise field note about {cleaned_topic} #{index}.",
31
+ response=f"Draft note {index}: {cleaned_topic}.",
32
+ correction=f"Corrected note {index}: {cleaned_topic} with concrete evidence.",
33
+ tags=[tag, cleaned_topic.replace(" ", "-").lower()],
34
+ )
35
+ for index in range(1, count + 1)
36
+ ]
37
+
38
+
39
+ def validate_synthetic_example(example: SyntheticExample) -> list[str]:
40
+ errors = []
41
+ if not example.prompt.strip():
42
+ errors.append("prompt is required")
43
+ if not example.response.strip():
44
+ errors.append("response is required")
45
+ if not example.correction.strip():
46
+ errors.append("correction is required")
47
+ if not example.tags:
48
+ errors.append("at least one tag is required")
49
+ return errors
50
+
51
+
52
+ def quality_filter_examples(examples: list[SyntheticExample]) -> list[SyntheticExample]:
53
+ return [
54
+ example
55
+ for example in examples
56
+ if not validate_synthetic_example(example)
57
+ and example.prompt != example.response
58
+ and example.response != example.correction
59
+ ]
60
+
61
+
62
+ def augment_examples(
63
+ examples: list[SyntheticExample],
64
+ extra_tag: str = "augmented",
65
+ ) -> list[SyntheticExample]:
66
+ augmented = list(examples)
67
+ for example in examples:
68
+ tags = list(dict.fromkeys([*example.tags, extra_tag]))
69
+ augmented.append(
70
+ SyntheticExample(
71
+ prompt=f"{example.prompt} Include one verification detail.",
72
+ response=example.response,
73
+ correction=f"{example.correction} Verification detail recorded.",
74
+ tags=tags,
75
+ source=example.source,
76
+ )
77
+ )
78
+ return augmented
79
+
80
+
81
+ def export_synthetic_jsonl(
82
+ examples: list[SyntheticExample],
83
+ output_path: str | Path = "data/synthetic_examples.jsonl",
84
+ ) -> Path:
85
+ output = Path(output_path)
86
+ output.parent.mkdir(parents=True, exist_ok=True)
87
+ with output.open("w", encoding="utf-8") as f:
88
+ for example in examples:
89
+ f.write(json.dumps(example.as_dict(), ensure_ascii=False) + "\n")
90
+ return output
deployment/__init__.py ADDED
File without changes
deployment/hf_space.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from pathlib import Path
5
+
6
+ SPACE_REQUIRED_FILES = [
7
+ "app.py",
8
+ "plant_space_app.py",
9
+ "requirements.txt",
10
+ "README.md",
11
+ "config/models.yaml",
12
+ "config/training.yaml",
13
+ ]
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class SpaceBuildStatus:
18
+ required_files_present: bool
19
+ missing_files: list[str]
20
+ readme_has_space_metadata: bool
21
+ has_space_remote: bool
22
+
23
+ def as_dict(self) -> dict:
24
+ return asdict(self)
25
+
26
+
27
+ def missing_required_files(root: str | Path = ".") -> list[str]:
28
+ repo_root = Path(root)
29
+ return [path for path in SPACE_REQUIRED_FILES if not (repo_root / path).exists()]
30
+
31
+
32
+ def readme_has_space_metadata(root: str | Path = ".") -> bool:
33
+ readme_path = Path(root) / "README.md"
34
+ if not readme_path.exists():
35
+ return False
36
+ text = readme_path.read_text(encoding="utf-8")
37
+ return text.startswith("---\n") and "\nsdk: gradio\n" in text and "\napp_file: app.py\n" in text
38
+
39
+
40
+ def has_space_remote(remote_output: str, remote_name: str = "space") -> bool:
41
+ return any(line.startswith(f"{remote_name}\t") for line in remote_output.splitlines())
42
+
43
+
44
+ def space_build_status(root: str | Path = ".", remote_output: str = "") -> SpaceBuildStatus:
45
+ missing = missing_required_files(root)
46
+ return SpaceBuildStatus(
47
+ required_files_present=not missing,
48
+ missing_files=missing,
49
+ readme_has_space_metadata=readme_has_space_metadata(root),
50
+ has_space_remote=(
51
+ has_space_remote(remote_output, "space")
52
+ or has_space_remote(remote_output, "space-workbench")
53
+ or has_space_remote(remote_output, "space-plant")
54
+ ),
55
+ )
56
+
57
+
58
+ def create_space_commands(
59
+ hf_user: str,
60
+ space_name: str = "openbmb-local-ai-workbench",
61
+ branch: str = "main",
62
+ remote_name: str = "space",
63
+ app_file: str = "app.py",
64
+ ) -> list[str]:
65
+ if not hf_user.strip():
66
+ raise ValueError("hf_user is required")
67
+ if not space_name.strip():
68
+ raise ValueError("space_name is required")
69
+ if not remote_name.strip():
70
+ raise ValueError("remote_name is required")
71
+ if not app_file.strip():
72
+ raise ValueError("app_file is required")
73
+ return [
74
+ "hf auth login",
75
+ f"hf repo create {hf_user}/{space_name} --type space --space-sdk gradio",
76
+ f"git remote add {remote_name} https://huggingface.co/spaces/{hf_user}/{space_name}",
77
+ f"git push {remote_name} {branch}",
78
+ f"Space metadata app_file must be {app_file} for this deployment.",
79
+ ]
80
+
81
+
82
+ def hackathon_space_commands(branch: str = "main") -> dict[str, list[str]]:
83
+ return {
84
+ "workbench": create_space_commands(
85
+ "build-small-hackathon",
86
+ "workbench",
87
+ branch,
88
+ remote_name="space-workbench",
89
+ app_file="app.py",
90
+ ),
91
+ "plant": create_space_commands(
92
+ "build-small-hackathon",
93
+ "plant_identification_tool",
94
+ branch,
95
+ remote_name="space-plant",
96
+ app_file="plant_space_app.py",
97
+ ),
98
+ }
docs/ACCEPTANCE_CRITERIA.md ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Acceptance Criteria
2
+
3
+ This file defines how we decide whether something is really done.
4
+
5
+ ## Global Rule
6
+
7
+ Do not mark a feature done unless:
8
+
9
+ 1. The files exist.
10
+ 2. The feature is linked from docs where relevant.
11
+ 3. The behavior was tested locally, or the blocker is written in `IMPLEMENTATION_STATUS.md`.
12
+ 4. The checklist in `TASKS.md` is updated.
13
+ 5. Unit and user-story tests are added or updated.
14
+
15
+ If a bug or failed check is discovered, add or update a test that reproduces it before marking
16
+ the fix done.
17
+
18
+ ## MVP App
19
+
20
+ Done when:
21
+
22
+ - `python app.py` launches without errors.
23
+ - The Gradio UI opens locally.
24
+ - Chat tab returns a response.
25
+ - Vision tab accepts an image and returns a response.
26
+ - Field Notes tab saves a row to `data/field_notes.csv`.
27
+ - README explains that current inference is placeholder/local-safe.
28
+ - Unit and user-story tests pass.
29
+
30
+ ## Testing And Quality
31
+
32
+ Done when:
33
+
34
+ - Unit tests cover new logic.
35
+ - User-story tests cover new workflows.
36
+ - `scripts/run_tests.ps1` passes.
37
+ - `ruff`, `mypy`, `pylint`, `bandit`, and `pip-audit` are installed or blockers are documented.
38
+ - `scripts/run_quality.ps1` passes before release or documents exact blockers.
39
+
40
+ ## Real Local Inference
41
+
42
+ Done when:
43
+
44
+ - A user can choose a backend.
45
+ - Model loading happens only after an explicit click.
46
+ - The app does not download weights on startup.
47
+ - At least one small model returns a real response.
48
+ - Setup steps are documented.
49
+
50
+ ## Hugging Face Space
51
+
52
+ Done when:
53
+
54
+ - Space exists.
55
+ - Repo is pushed.
56
+ - Build succeeds.
57
+ - Public/private Space URL is added to README.
58
+ - Any hardware or model limitations are documented.
59
+
60
+ ## GitHub
61
+
62
+ Done when:
63
+
64
+ - GitHub repo exists.
65
+ - Initial commit is pushed.
66
+ - README includes GitHub URL.
67
+ - Large files and secrets are excluded.
68
+
69
+ ## Training
70
+
71
+ Done when:
72
+
73
+ - Training input format is documented.
74
+ - A dry-run or actual LoRA run completes.
75
+ - Output checkpoint path is shown.
76
+ - Metrics or logs are captured.
77
+ - Hardware assumptions are documented.
78
+
79
+ ## Field Notes
80
+
81
+ Done when:
82
+
83
+ - Corrections save reliably.
84
+ - Corrected records can be exported.
85
+ - Export schema is documented.
86
+ - Export can feed the training pipeline.
87
+
88
+ ## Hackathon Submission
89
+
90
+ Done when:
91
+
92
+ - Space URL is live.
93
+ - Demo video is recorded.
94
+ - Social post is published or drafted.
95
+ - Submission form fields are complete.
96
+ - Model-size compliance is documented.
docs/ARCHITECTURE.md ADDED
@@ -0,0 +1,575 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Architecture
2
+
3
+ The project is intentionally small at first. The PRD describes a large workbench; this repo starts
4
+ with the smallest version that can grow into it.
5
+
6
+ ## High-Level Flow
7
+
8
+ ```text
9
+ app.py
10
+ loads config/models.yaml
11
+ configures lightweight logging
12
+ builds Gradio tabs
13
+ passes model catalog to UI modules
14
+
15
+ ui/*
16
+ defines each Gradio tab
17
+ calls service classes
18
+ emits local app events for inference, datasets, and field notes
19
+ uses shared progress settings for callback loading indicators
20
+
21
+ agent/*
22
+ holds deterministic local agent planning and trace export helpers
23
+
24
+ models/*
25
+ holds model catalog, local backend config, and inference services
26
+
27
+ datasets/*
28
+ stores dataset, synthetic data, and correction-loop helpers
29
+
30
+ mcp_tools/*
31
+ holds local tool functions, VINDEX call planning, and Gradio-native MCP bridge metadata
32
+
33
+ config/*
34
+ holds model and training settings
35
+
36
+ training/*
37
+ holds non-executing training, LoRA request, evaluation, and export planning helpers
38
+
39
+ tracking/*
40
+ holds local JSONL tracing and optional Trackio integration
41
+
42
+ deployment/*
43
+ holds Hugging Face Space deployment planning and validation helpers
44
+
45
+ plant/*
46
+ holds the first reference domain app built from the template
47
+ can run standalone with python -m plant.app --no-model
48
+ keeps heavy model dependencies optional
49
+
50
+ core/*
51
+ shared app state, event, logging, and registry helpers
52
+ ```
53
+
54
+ ## Files And Classes
55
+
56
+ ### `app.py`
57
+
58
+ Builds and launches the Gradio app.
59
+
60
+ - `build_app()` creates the Gradio `Blocks` app.
61
+ - Loads the model catalog from `config/models.yaml`.
62
+ - Registers the current UI tabs.
63
+ - `APP_CSS` defines compact responsive layout rules for app width, mobile padding, scrollable tabs,
64
+ and button touch targets.
65
+
66
+ ### `plant/app.py`
67
+
68
+ Standalone Plant Discovery reference app built from the template.
69
+
70
+ - `build_app(no_model=True)` creates a Gradio app without loading model weights.
71
+ - Loads `plant/models.yaml`.
72
+ - Builds a local species index.
73
+ - Reuses `datasets.field_notes.FieldNoteStore` for corrections.
74
+ - Uses `DemoPlantVisionService` for screenshots/tests or `PlantVisionService` for OpenBMB
75
+ MiniCPM-V zero-shot and fine-tuned adapter inference.
76
+
77
+ ### `plant/plant_service.py`
78
+
79
+ Domain service and schema for Plant Discovery.
80
+
81
+ - `PlantID` is the structured output schema.
82
+ - `DemoPlantVisionService` provides deterministic no-model results.
83
+ - `PlantVisionService` lazy-loads optional MiniCPM-V dependencies only during identification.
84
+ - `PlantVisionService.from_config(..., "plant_vlm_finetuned")` can load a PEFT adapter after a real
85
+ adapter repo is configured.
86
+ - `extract_json_object()` and `parse_plant_response()` make model JSON output testable.
87
+
88
+ ### `plant/training.py`
89
+
90
+ Non-executing training planner for Plant Discovery.
91
+
92
+ - `build_plant_training_plan()` returns SWIFT and LLaMA-Factory command previews.
93
+ - `plant_training_dependency_report()` reports optional training dependency availability.
94
+ - `write_llamafactory_dataset_info()` writes a dataset-info preview for LLaMA-Factory workflows.
95
+ - Training is never started by the Gradio UI or script.
96
+
97
+ ### `plant/plant_loader.py`
98
+
99
+ Domain data and export helpers for Plant Discovery.
100
+
101
+ - `PlantRecord` normalizes plant examples into training rows.
102
+ - `LocalFolderLoader` maps species folders to image metadata.
103
+ - `SpeciesIndexBuilder` builds a no-network species index with demo fallback.
104
+ - `FieldNotesPlantExporter` exports corrected field notes to plant training JSONL.
105
+
106
+ ### `plant/plant_tab.py`
107
+
108
+ Focused Gradio UI for Plant Discovery.
109
+
110
+ - Identify tab uploads images and renders a safe escaped result card.
111
+ - Field Guide tab searches the species index.
112
+ - Corrections tab saves and exports training-ready corrections.
113
+ - Stats tab summarizes species and correction counts.
114
+ - Training is represented as a non-executing plan, not a subprocess.
115
+
116
+ ### `plant/plant_tools.py`
117
+
118
+ Optional local/MCP tools for Plant Discovery.
119
+
120
+ - Pure functions can be tested without an MCP server.
121
+ - `build_mcp_server()` imports `mcp` only when explicitly requested.
122
+ - Tools expose identify, species search, correction save/export, stats, and training plan.
123
+
124
+ ### `models/model_catalog.py`
125
+
126
+ Reads model configuration and turns it into typed Python objects.
127
+
128
+ - `ModelInfo` describes one configured model.
129
+ - `load_model_catalog(path)` reads YAML and returns all configured models.
130
+ - `model_choices(catalog, model_type)` filters models for a UI dropdown.
131
+ - `model_summary(model)` returns display metadata for the Gradio JSON panel.
132
+ - `backend_capabilities` maps each model to supported local backend capabilities.
133
+
134
+ ### `models/placeholder_service.py`
135
+
136
+ Deterministic placeholder model service used before real inference is wired.
137
+
138
+ - `PlaceholderModelService.chat()` returns a deterministic text response.
139
+ - `PlaceholderModelService.vision_chat()` returns a deterministic image/prompt response.
140
+
141
+ This file should be replaced or complemented by real services such as:
142
+
143
+ - `ollama_service.py`
144
+ - `llama_cpp_service.py`
145
+ - `openai_compatible_service.py`
146
+ - `sglang_runner.py`
147
+ - `minicpm_vision.py`
148
+ - `transformers_text.py`
149
+ - `sglang_service.py`
150
+
151
+ ### `models/base.py`
152
+
153
+ Defines service contracts and backend status records.
154
+
155
+ - `BackendStatus` describes whether a backend is available.
156
+ - `TextModelService` is the text chat protocol.
157
+ - `VisionModelService` is the vision chat protocol.
158
+
159
+ ### `models/ollama_service.py`
160
+
161
+ Ollama-backed local inference client.
162
+
163
+ - Checks whether `ollama` is installed and reachable.
164
+ - Sends text and vision chat requests to `http://127.0.0.1:11434/api/chat`.
165
+ - Lists locally available Ollama models through `/api/tags`.
166
+ - Builds explicit `ollama pull <model>` commands for the Status tab.
167
+ - Does not pull or download models automatically.
168
+
169
+ ### `models/llama_cpp_service.py`
170
+
171
+ llama.cpp HTTP client for local GGUF inference.
172
+
173
+ - Checks whether `llama-server` is installed and reachable.
174
+ - Builds explicit `llama-server -m <model.gguf>` commands.
175
+ - Supports `--mmproj <mmproj.gguf>` command metadata for multimodal models.
176
+ - Sends text chat requests to `/v1/chat/completions`.
177
+ - Does not download GGUF files or start background servers automatically.
178
+
179
+ ### `models/local_backend_config.py`
180
+
181
+ User-local backend settings stored under ignored `data/local_backends.yaml`.
182
+
183
+ - `LocalBackendConfig` stores llama.cpp server URL, OpenAI-compatible base URL, optional served
184
+ model name, GGUF path, mmproj path, context length, and GPU layers.
185
+ - `save_local_backend_config()` writes local-only settings without touching tracked model config.
186
+ - `build_llama_server_command()` returns the explicit command the user can run.
187
+ - `local_backend_summary()` reports file status and confirms no startup downloads or automatic model loads.
188
+
189
+ ### `models/openai_compatible_service.py`
190
+
191
+ Local OpenAI-compatible chat client for LM Studio, vLLM-style servers, or similar local endpoints.
192
+
193
+ - Checks `/v1/models` for reachability.
194
+ - Sends text chat requests to `/v1/chat/completions`.
195
+ - Supports an optional served-model-name override for tools such as LM Studio.
196
+ - Returns visible unavailable/request-failed messages instead of crashing the Gradio callback.
197
+ - Does not call cloud APIs or download model weights.
198
+
199
+ ### `models/llama_cpp_python_service.py`
200
+
201
+ Optional direct Python binding backend for GGUF inference.
202
+
203
+ - Checks whether `llama_cpp` is importable.
204
+ - Requires an explicit local GGUF path.
205
+ - Does not download model files.
206
+ - Provides text chat through `Llama.create_chat_completion()`.
207
+ - Vision support remains routed through llama-server until mmproj/image serialization is wired.
208
+
209
+ ### `models/minicpm_vision.py`
210
+
211
+ Optional MiniCPM vision backend.
212
+
213
+ - Checks whether the `transformers` package is available.
214
+ - Lazy-loads `AutoProcessor` and `AutoModelForImageTextToText` only when selected.
215
+ - Formats image/text messages for image-text-to-text generation.
216
+ - Maps thinking mode into the prompt template.
217
+ - Provides a video support plan for future local frame sampling.
218
+
219
+ ### `models/sglang_runner.py`
220
+
221
+ SGLang local server planner and OpenAI-compatible chat client.
222
+
223
+ - Builds an explicit `python -m sglang.launch_server` command.
224
+ - Includes MiniCPM tool parser configuration.
225
+ - Checks `/health`, sends chat requests to `/v1/chat/completions`, and can request `/shutdown`.
226
+ - Does not install SGLang, start a process, download model weights, or load a model on app startup.
227
+
228
+ ### `models/vllm_runner.py`
229
+
230
+ vLLM local server planner and OpenAI-compatible chat client.
231
+
232
+ - Builds explicit `vllm serve <model>` command plans.
233
+ - Checks `/health`, parses Prometheus-style `/metrics`, and sends chat requests to
234
+ `/v1/chat/completions`.
235
+ - Logs parsed benchmark metrics through `TrackingClient`.
236
+ - Does not install vLLM, start a process, download model weights, or load a model on app startup.
237
+
238
+ ### `models/transformers_text.py`
239
+
240
+ Optional Transformers text backend.
241
+
242
+ - Checks whether the `transformers` package is installed.
243
+ - Lazy-loads `AutoTokenizer` and `AutoModelForCausalLM` only when the backend is selected.
244
+ - Reads `trust_remote_code`, device map, dtype, max token, and temperature settings from explicit config.
245
+ - Provides a simple token-list streaming helper for future Gradio streaming wiring.
246
+ - Does not download model weights on startup.
247
+
248
+ ### `models/service_factory.py`
249
+
250
+ Creates the selected backend service for the UI.
251
+
252
+ - `TEXT_SERVICE_REGISTRY` registers available text backend factories.
253
+ - `VISION_SERVICE_REGISTRY` registers available vision backend factories.
254
+ - `create_text_service()` chooses placeholder, llama.cpp, llama-cpp-python, Ollama,
255
+ OpenAI-compatible, SGLang, or Transformers text service.
256
+ - `create_vision_service()` chooses placeholder, llama.cpp, llama-cpp-python, Ollama, or
257
+ Transformers MiniCPM vision service.
258
+ - `backend_statuses()` reports current backend availability.
259
+ - llama.cpp, llama-cpp-python, and OpenAI-compatible services read ignored local backend settings
260
+ when selected.
261
+
262
+ ### `ui/chat_tab.py`
263
+
264
+ Builds the text chat tab.
265
+
266
+ - Shows text models from the catalog.
267
+ - Displays selected model metadata.
268
+ - Calls the selected backend service.
269
+ - Emits inference request and response events.
270
+
271
+ ### `ui/vision_tab.py`
272
+
273
+ Builds the vision tab.
274
+
275
+ - Shows vision models from the catalog.
276
+ - Accepts an image and prompt.
277
+ - Calls the selected backend service.
278
+ - Emits inference request and response events.
279
+
280
+ ### `ui/dataset_tab.py`
281
+
282
+ Local dataset preview surface.
283
+
284
+ - Previews local CSV, JSONL, and NDJSON files.
285
+ - Previews Hugging Face datasets when the optional external `datasets` package is installed.
286
+ - Shows source, row count, columns, and sample rows.
287
+ - Calculates basic local dataset statistics.
288
+ - Emits dataset loaded events.
289
+
290
+ Future behavior:
291
+
292
+ - Serve dataset tools through the selected MCP path.
293
+
294
+ ### `ui/train_tab.py`
295
+
296
+ Training planning and local evaluation surface.
297
+
298
+ - Builds a LoRA dry-run training plan without launching training.
299
+ - Builds a non-executing LoRA trainer request with dependency status.
300
+ - Shows SWIFT/LLaMA-Factory vision fine-tuning plan.
301
+ - Shows checkpoint output path, validation status, and hardware notes.
302
+ - Runs local base-vs-tuned evaluation from newline-separated response text.
303
+ - Shows exact-match summary and a qualitative eval table.
304
+ - Logs tuned evaluation reports to `data/eval_results.jsonl`.
305
+
306
+ Future behavior:
307
+
308
+ - Start LoRA training.
309
+ - Show loss and metrics.
310
+ - Write Trackio traces.
311
+
312
+ ### `ui/vllm_tab.py`
313
+
314
+ vLLM local serving planner.
315
+
316
+ - Builds explicit `vllm serve` command plans.
317
+ - Checks local vLLM `/health`.
318
+ - Fetches and parses `/metrics`.
319
+ - Logs vLLM benchmark metrics through local JSONL/Trackio fallback tracking.
320
+ - Does not install vLLM, start a process, download models, or load weights on startup.
321
+
322
+ ### `ui/export_tab.py`
323
+
324
+ GGUF export planning surface.
325
+
326
+ - Selects a configured model and quantization.
327
+ - Shows official GGUF download command plans when the model has GGUF metadata.
328
+ - Shows local HF-to-GGUF conversion and llama.cpp quantization command plans.
329
+ - Lists files already present under the selected export directory.
330
+ - Exposes existing exported files through a Gradio download output.
331
+ - Does not execute downloads, conversion, or quantization.
332
+
333
+ Future behavior:
334
+
335
+ - Execute downloads and conversions after explicit user action.
336
+
337
+ ### `ui/notes_tab.py`
338
+
339
+ Field notes implementation.
340
+
341
+ - Saves prompt, model response, correction, and tags to `data/field_notes.csv`.
342
+ - Captures optional image path, video path, and a use-for-training flag.
343
+ - Exports corrected notes to JSONL.
344
+ - Exports local Hugging Face Dataset-style files under `data/hf_field_notes`.
345
+ - Imports uncertain OCR predictions for human correction.
346
+ - Exports corrected OCR rows to JSONL.
347
+ - Emits field note saved events.
348
+
349
+ Future behavior:
350
+
351
+ - Push corrected notes to a remote Hugging Face Dataset after login.
352
+ - Feed notes into fine-tuning.
353
+
354
+ ### `ui/traces_tab.py`
355
+
356
+ Local trace and tracking preview.
357
+
358
+ - Shows manual trace event previews.
359
+ - Shows recent local app events.
360
+ - Shows JSONL trace rows and tracking status.
361
+ - Exports local traces to `exports/traces.jsonl`.
362
+ - Calls Trackio only when the optional package is installed and enabled.
363
+
364
+ ### `ui/agent_tab.py`
365
+
366
+ Local non-autonomous agent mode.
367
+
368
+ - Drafts a research-plan-implement-verify trace.
369
+ - Saves agent traces to `data/agent_traces.jsonl`.
370
+ - Exports trace JSONL and local HF Dataset-style trace files.
371
+ - Does not execute shell commands, commit, push, deploy, download models, or call external services.
372
+
373
+ ### `ui/status_tab.py`
374
+
375
+ Shows configured models and backend metadata.
376
+
377
+ - Helps verify model-size compliance and backend status.
378
+ - Provides local llama.cpp settings, GGUF/mmproj file pickers, and command generation.
379
+ - Provides LM Studio/OpenAI-compatible base URL, optional model-name storage, and reachability check.
380
+ - Provides SGLang command planning, health check, and shutdown request controls.
381
+
382
+ ### `datasets/field_notes.py`
383
+
384
+ Field note data model and CSV store.
385
+
386
+ - `FieldNote` captures prompt, response, correction, tags, and timestamp.
387
+ - `FieldNote` also captures optional image/video paths and a training inclusion flag.
388
+ - `FieldNoteStore.save()` persists notes to `data/field_notes.csv`.
389
+ - `FieldNoteStore.list_notes()` filters by correction, tag, and training inclusion.
390
+ - `FieldNoteStore.export_jsonl()` writes training-ready JSONL.
391
+ - `FieldNoteStore.export_hf_dataset()` writes local HF Dataset-style files.
392
+ - `SQLiteFieldNoteStore` stores and lists notes in SQLite for larger correction loops.
393
+
394
+ ### `datasets/loader.py`
395
+
396
+ Dataset preview and statistics helpers.
397
+
398
+ - `preview_local_dataset()` previews CSV, JSONL, and NDJSON files.
399
+ - `dataset_statistics()` reports row count, column count, names, and non-empty counts.
400
+ - `preview_huggingface_dataset()` optionally uses the external Hugging Face `datasets` package.
401
+
402
+ ### `datasets/synthetic.py`
403
+
404
+ Deterministic local synthetic data helpers.
405
+
406
+ - `generate_synthetic_examples()` creates local prompt/response/correction examples.
407
+ - `validate_synthetic_example()` checks schema requirements.
408
+ - `quality_filter_examples()` removes incomplete or low-value examples.
409
+ - `augment_examples()` creates deterministic variants for workflow testing.
410
+ - `export_synthetic_jsonl()` writes JSONL without external services.
411
+
412
+ ### `datasets/ocr.py`
413
+
414
+ Local OCR correction helpers.
415
+
416
+ - `OCRPrediction` stores source path, predicted text, confidence, and optional page.
417
+ - `load_ocr_predictions()` loads local `.csv`, `.jsonl`, and `.ndjson` prediction files.
418
+ - `uncertain_predictions()` filters rows at or below a confidence threshold or with empty text.
419
+ - `import_uncertain_predictions()` creates Field Notes correction tasks for uncertain rows.
420
+ - `export_corrected_ocr_notes()` writes corrected OCR examples to JSONL for evaluation or training.
421
+ - `ocr_import_summary()` previews uncertain rows for the Field Notes tab.
422
+
423
+ ### `mcp_tools/tools.py`
424
+
425
+ Local MCP-style tools.
426
+
427
+ - `dataset_stats_tool()` returns local dataset statistics.
428
+ - `hf_dataset_preview_tool()` previews Hugging Face datasets when optional dependencies exist.
429
+ - `safe_calculator_tool()` evaluates numeric arithmetic only.
430
+ - `model_inference_tool()` routes text prompts through the selected model service.
431
+ - `tool_registry()` returns the local tool map for a future MCP endpoint.
432
+
433
+ ### `mcp_tools/vindex_tool.py`
434
+
435
+ Non-executing VINDEX integration boundary.
436
+
437
+ - Defines the eight VINDEX PRD methods and their local FastAPI paths.
438
+ - `build_vindex_call_plan()` validates method names and builds endpoint/payload plans.
439
+ - Caps `star_spread.n_neighbors` at 5 and `calibrated_edit.causal_window` at 3 based on the PRD
440
+ safety notes.
441
+ - `vindex_dependency_report()` checks whether the optional `vindex` package or local health
442
+ endpoint is available.
443
+ - `vindex_verification_report()` combines dependency status with a safe call plan and keeps
444
+ execution disabled until the local VINDEX install is verified.
445
+
446
+ ### `mcp_tools/bridge.py`
447
+
448
+ Gradio-native MCP bridge metadata and local invocation helper.
449
+
450
+ - `MCP_PATH` documents `/gradio_api/mcp/sse`.
451
+ - `mcp_manifest()` returns the selected mode, path, and tool definitions.
452
+ - `invoke_mcp_tool()` verifies local tool invocation by name.
453
+
454
+ ### `agent/runner.py`
455
+
456
+ Deterministic local agent trace runner.
457
+
458
+ - `AGENT_SYSTEM_PROMPT` defines the agent behavior contract.
459
+ - `run_agent_loop()` produces research, plan, implement, and verify trace steps.
460
+ - `run_paper_to_code_loop()` produces paper-to-code research, plan, implement, and verify trace steps.
461
+ - `default_safety_gates()` lists the non-autonomous safety requirements.
462
+ - `save_agent_trace()` appends traces to JSONL.
463
+ - `export_agent_traces()` exports trace JSONL.
464
+ - `export_agent_traces_hf_dataset()` writes local HF Dataset-style trace files.
465
+ - The runner can call safe local tools, but it is not autonomous.
466
+
467
+ ### `core/file_exports.py`
468
+
469
+ Shared export helper.
470
+
471
+ - `copy_text_file_or_empty()` copies a text artifact to an export path or creates an empty one.
472
+
473
+ ### `training/export.py`
474
+
475
+ Non-executing GGUF export planning.
476
+
477
+ - `detect_llama_cpp_tools()` checks `llama-server`, `llama-cli`, and `llama-quantize`.
478
+ - `build_export_plan()` creates explicit download, conversion, and quantization command plans.
479
+ - `list_exported_files()` lists generated/local export files.
480
+ - `ExportPlan.as_dict()` marks that commands are not executed and no startup downloads occur.
481
+
482
+ ### `training/evaluation.py`
483
+
484
+ Local deterministic evaluation helpers.
485
+
486
+ - `default_prompt_cases()` returns a small built-in prompt test set.
487
+ - `load_prompt_cases()` loads prompt/expected pairs from JSONL.
488
+ - `evaluate_responses()` computes exact-match rows and a qualitative table.
489
+ - `perplexity_from_losses()` computes perplexity from explicit negative log likelihood values.
490
+ - `compare_base_vs_tuned()` reports exact-match delta.
491
+ - `log_eval_report()` appends JSONL evaluation results.
492
+
493
+ ### `training/lora_trainer.py`
494
+
495
+ Non-executing LoRA trainer request builder.
496
+
497
+ - `lora_dependency_report()` reports PEFT, TRL, Transformers, and Torch availability.
498
+ - `build_lora_training_request()` combines the training plan with dependency status and a command
499
+ preview.
500
+ - `vision_finetuning_plan()` documents SWIFT/LLaMA-Factory as the future MiniCPM-V fine-tuning path.
501
+ - Keeps `execute_training` false until dependencies, hardware, and dataset schema are approved.
502
+
503
+ ### `training/reward_eval.py`
504
+
505
+ Deterministic local reward-style evaluation helpers.
506
+
507
+ - `RewardEvaluator.evaluate()` scores supplied responses with transparent lexical heuristics.
508
+ - `best_of_n()` selects the highest-scoring candidate without model calls.
509
+ - `create_dpo_pairs()` creates chosen/rejected pairs for DPO-style datasets.
510
+ - `eval_lora_vs_base()` compares base and LoRA response rewards.
511
+
512
+ ### `training/planner.py`
513
+
514
+ Non-executing LoRA training planner.
515
+
516
+ - `load_training_config()` reads LoRA and training settings from `config/training.yaml`.
517
+ - `build_training_plan()` creates a dry-run plan with checkpoint output path.
518
+ - `validate_training_plan()` checks dataset existence and numeric training settings.
519
+ - `training_hardware_notes()` documents practical local hardware expectations.
520
+
521
+ ### `tracking/trackio_client.py`
522
+
523
+ Tracking client with JSONL fallback.
524
+
525
+ - `load_tracking_config()` reads Trackio settings from `config/training.yaml`.
526
+ - `TrackingClient.init()` starts Trackio only when enabled and installed.
527
+ - `TrackingClient.log()` always writes local JSONL and optionally forwards to Trackio.
528
+ - `TrackingClient.finish()` closes optional Trackio state.
529
+ - `export_traces()` copies local traces to `exports/traces.jsonl`.
530
+ - `read_trace_rows()` returns recent local trace rows for the UI.
531
+
532
+ ### `core/events.py`
533
+
534
+ Small event bus reserved for future cross-module events.
535
+
536
+ - `EventType` names app events.
537
+ - `UI_ERROR` records visible tab-level failures.
538
+ - `Event` carries event data.
539
+ - `EventBus` registers handlers and emits events.
540
+
541
+ ### `core/app_state.py`
542
+
543
+ Shared local app state.
544
+
545
+ - `AppState.emit()` records events, logs them, and dispatches them through `EventBus`.
546
+ - `AppState.emit()` also writes trace events through `TrackingClient`.
547
+ - `AppState.recent_events()` returns local trace previews for the Traces tab.
548
+ - `emit_inference_response()` records shared response metadata.
549
+
550
+ ### `core/tab_feedback.py`
551
+
552
+ Formats tab status text and emits `ui_error` events for visible tab-level failures.
553
+
554
+ ### `ui/progress.py`
555
+
556
+ Defines the shared Gradio progress mode used by tab button callbacks.
557
+
558
+ ### `core/app_logging.py`
559
+
560
+ Lightweight logging setup.
561
+
562
+ - `configure_app_logging()` configures compact process logging once.
563
+
564
+ ### `core/registry.py`
565
+
566
+ Generic registry helper.
567
+
568
+ - `Registry.register(name, item)` stores a service.
569
+ - `Registry.get(name)` retrieves a service.
570
+ - `Registry.list()` lists registered services.
571
+
572
+ ## Current Design Rule
573
+
574
+ The app must not download model weights on startup. Model loading should happen only after the
575
+ user chooses a backend/model and clicks an explicit action.
docs/EXTENDING.md ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # How To Extend The Workbench
2
+
3
+ ## Add A New Model
4
+
5
+ 1. Open `config/models.yaml`.
6
+ 2. Add a new model entry.
7
+ 3. Set `type` to `text`, `vision`, or `omnimodal`.
8
+ 4. Keep `parameters_b` at or below 32 for hackathon eligibility.
9
+ 5. Keep `backend: placeholder` until a real service supports it.
10
+
11
+ Example:
12
+
13
+ ```yaml
14
+ models:
15
+ my_model:
16
+ hf_id: org/model-name
17
+ display_name: My Model
18
+ type: text
19
+ parameters_b: 7
20
+ backend: placeholder
21
+ context_length: 32768
22
+ local_first: true
23
+ notes: Why this model is useful.
24
+ ```
25
+
26
+ ## Add A Real Backend
27
+
28
+ Create a service in `models/`, for example:
29
+
30
+ ```text
31
+ models/ollama_service.py
32
+ ```
33
+
34
+ The service should expose a small interface:
35
+
36
+ ```python
37
+ class OllamaService:
38
+ def chat(self, system_prompt: str, user_prompt: str) -> str:
39
+ ...
40
+ ```
41
+
42
+ Then update the service factory or relevant UI tab to choose between placeholder and real services.
43
+
44
+ ## Add A New Gradio Tab
45
+
46
+ 1. Create `ui/new_tab.py`.
47
+ 2. Add a `build_new_tab(...)` function.
48
+ 3. Import it in `app.py`.
49
+ 4. Add it inside the `gr.Tabs()` block.
50
+ 5. Update `docs/ARCHITECTURE.md`.
51
+ 6. Add a checklist item in `docs/TASKS.md`.
52
+ 7. Update `docs/IMPLEMENTATION_STATUS.md`.
53
+
54
+ ## Add Field Notes Export
55
+
56
+ Next useful field notes upgrade:
57
+
58
+ - Add a button to export `data/field_notes.csv` to JSONL.
59
+ - Add a button to upload that JSONL as a Hugging Face Dataset.
60
+ - Document the dataset schema in `README.md`.
61
+
62
+ Suggested JSONL schema:
63
+
64
+ ```json
65
+ {"model_id":"minicpm5_1b","prompt":"...","response":"...","correction":"...","tags":["demo"]}
66
+ ```
67
+
68
+ ## Add OCR Corrections
69
+
70
+ The local OCR extension starts from prediction files rather than running an OCR engine directly.
71
+ Use `.csv`, `.jsonl`, or `.ndjson` rows with fields like:
72
+
73
+ ```json
74
+ {"source_path":"receipt.png","text":"Tota1 12.30","confidence":0.54}
75
+ ```
76
+
77
+ The Field Notes tab can preview uncertain rows, import them as correction tasks, and export
78
+ corrected OCR rows to `data/ocr_corrections.jsonl`. The intended wiring is:
79
+
80
+ ```text
81
+ OCR predictions -> uncertain Field Notes -> corrected JSONL/HF Dataset -> training/evaluation
82
+ ```
83
+
84
+ ## Add VINDEX Execution
85
+
86
+ The current VINDEX integration is a safety boundary, not an edit runner. It validates the eight PRD
87
+ methods, builds non-executing local FastAPI call plans, and reports whether a local VINDEX package
88
+ or `http://127.0.0.1:8765/health` server is available.
89
+
90
+ Before allowing execution:
91
+
92
+ 1. Verify the local VINDEX package or FastAPI server.
93
+ 2. Re-check the PRD bug list: GPU cache cleanup, dead-code paths, star-spread over-editing, and
94
+ causal-window limits.
95
+ 3. Keep `star_spread.n_neighbors <= 5` and `calibrated_edit.causal_window <= 3` until the scaling
96
+ formula is validated.
97
+ 4. Add protected-relation tests for every edit workflow.
98
+ 5. Only then add an explicit user-triggered execute button or MCP tool.
99
+
100
+ ## Add Training
101
+
102
+ Training should be added only after local inference works.
103
+
104
+ Recommended order:
105
+
106
+ 1. Export field notes to JSONL.
107
+ 2. Load JSONL as a dataset.
108
+ 3. Add PEFT/TRL LoRA for text model.
109
+ 4. Add Trackio logging.
110
+ 5. Add checkpoint output folder.
111
+ 6. Add README instructions.
112
+
113
+ ## Add Hugging Face Space Deployment
114
+
115
+ After the local app runs:
116
+
117
+ ```powershell
118
+ .venv\Scripts\python.exe scripts\plan_hf_space.py --user <hf-user-or-org>
119
+ huggingface-cli login
120
+ huggingface-cli repo create openbmb-local-ai-workbench --type space --space-sdk gradio
121
+ git remote add space https://huggingface.co/spaces/<user>/openbmb-local-ai-workbench
122
+ git push space main
123
+ ```
124
+
125
+ Never commit Hugging Face tokens.
docs/HACKATHON_SUBMISSION.md ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hackathon Submission Package
2
+
3
+ This document collects the judge-facing story, demo flow, and submission assets for the Gradio
4
+ Hugging Face Build Small Hackathon.
5
+
6
+ ## Track
7
+
8
+ Recommended track: Backyard AI.
9
+
10
+ Reason: the app is local-first, small-model focused, and designed to help a solo builder inspect,
11
+ test, correct, and document OpenBMB model workflows on their own machine before moving to a Space.
12
+
13
+ ## Project Story
14
+
15
+ OpenBMB Local AI Workbench is a Gradio app for trying small OpenBMB models locally, capturing
16
+ human corrections as field notes, and turning those notes into training or evaluation artifacts.
17
+
18
+ ## Target User
19
+
20
+ The target user is a hackathon builder or small-team AI tinkerer who wants a practical local
21
+ workflow before committing to cloud deployment, GPU rentals, or a larger training stack.
22
+
23
+ ## Measurable Benefit
24
+
25
+ The app reduces setup uncertainty by keeping model choices, backend availability, field-note
26
+ exports, traces, and deployment next steps visible in one Gradio surface.
27
+
28
+ ## Final Model Family
29
+
30
+ Primary family: OpenBMB MiniCPM.
31
+
32
+ Current configured models:
33
+
34
+ | Config ID | Model | Parameters | Role |
35
+ | --- | --- | ---: | --- |
36
+ | `minicpm5_1b` | `openbmb/MiniCPM5-1B` | 1B | local text baseline |
37
+ | `minicpm5_1b_thinking` | `openbmb/MiniCPM5-1B-Thinking` | 1B | reasoning/text variant |
38
+ | `minicpm41_8b` | `openbmb/MiniCPM4.1-8B` | 8B | long-context text candidate |
39
+ | `minicpm_v46` | `openbmb/MiniCPM-V-4.6` | 1.3B | vision candidate |
40
+ | `minicpm_v46_thinking` | `openbmb/MiniCPM-V-4.6-Thinking` | 1.3B | vision reasoning candidate |
41
+ | `minicpm_o45` | `openbmb/MiniCPM-o-4.5` | 8B | omnimodal stretch candidate |
42
+
43
+ All configured models are at or below the 32B hackathon limit.
44
+
45
+ ## Badge Targets
46
+
47
+ - Local-first: yes, through verified llama.cpp CLI, llama-cpp-python GGUF, LM Studio/OpenAI-compatible, and OpenBMB MiniCPM-V Plant image paths; Ollama and SGLang remain setup paths until generation is verified.
48
+ - llama.cpp: target badge path; requires local llama.cpp install and GGUF model verification.
49
+ - Open trace: yes, through local JSONL tracking and trace export.
50
+ - Field notes/report: yes, through corrected field notes, JSONL export, and local HF Dataset-style export.
51
+
52
+ ## Demo Flow
53
+
54
+ 1. Open the Gradio app locally.
55
+ 2. Show the Status tab and explain model-size compliance plus backend availability.
56
+ 3. Use Chat with the local `llama-cpp-python` GGUF backend to show a visible real response.
57
+ 4. Use the Dataset tab to preview a local JSONL/CSV training candidate.
58
+ 5. Save a correction in Field Notes and export corrected rows to JSONL.
59
+ 6. Open Traces to show local event history and optional Trackio status.
60
+ 7. Open Export to show GGUF conversion and quantization planning.
61
+ 8. Explain the remaining llama.cpp mmproj, Ollama, SGLang, and Space build verification tasks.
62
+ 9. Show the GitHub repo and, when available, the Hugging Face Space URL.
63
+
64
+ ## Screenshot Assets
65
+
66
+ - Workbench home: `assets/e2e/workbench/01-workbench-home.png`
67
+ - Workbench backend status: `assets/e2e/workbench/05-backend-status.png`
68
+ - Plant tool home: `assets/e2e/plant/01-plant-home.png`
69
+ - Plant corrections export: `assets/e2e/plant/03-corrections-export.png`
70
+
71
+ ## Demo Video Script
72
+
73
+ 1. "This is OpenBMB Local AI Workbench, a Gradio app for small-model local experimentation."
74
+ 2. "The model registry keeps every configured model below 32B parameters."
75
+ 3. "The app starts safely in placeholder mode, so it never downloads model weights on startup."
76
+ 4. "The Status tab shows which local backends are configured or missing."
77
+ 5. "A user can try a prompt, capture a correction, and export those corrections as training data."
78
+ 6. "The Traces tab records local workflow events for reproducibility."
79
+ 7. "The Export tab prepares explicit GGUF conversion and quantization commands."
80
+ 8. "The next deployment step is pushing this same Gradio app to a Hugging Face Space."
81
+
82
+ ## Social Post Draft
83
+
84
+ Built OpenBMB Local AI Workbench for the Gradio Hugging Face Build Small Hackathon: a local-first
85
+ Gradio app for testing MiniCPM models, collecting field-note corrections, exporting training data,
86
+ planning GGUF/llama.cpp workflows, and keeping traceable evidence of small-model experiments.
87
+
88
+ GitHub: https://github.com/Ckal/codex
89
+ Workbench Space: https://huggingface.co/spaces/build-small-hackathon/workbench
90
+ Plant Space: https://huggingface.co/spaces/build-small-hackathon/plant_identification_tool
91
+
92
+ ## Submission Checklist
93
+
94
+ - GitHub URL: https://github.com/Ckal/codex
95
+ - Workbench Space URL: https://huggingface.co/spaces/build-small-hackathon/workbench
96
+ - Plant Identification Tool Space URL: https://huggingface.co/spaces/build-small-hackathon/plant_identification_tool
97
+ - Space build verification: blocked until `hf auth login --force` is run with a fresh token
98
+ - Demo video URL: pending
99
+ - Social post URL: pending
100
+ - Field notes/report URL: pending
101
+ - Final track: Backyard AI
102
+ - App name: OpenBMB Local AI Workbench
103
+ - Deadline: June 15, 2026
docs/IMPLEMENTATION_STATUS.md ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Implementation Status
2
+
3
+ This file answers: "How do we know what is done?"
4
+
5
+ An item is done only when:
6
+
7
+ 1. The code or document exists.
8
+ 2. It is linked from the relevant docs.
9
+ 3. It has been manually reviewed.
10
+ 4. If it is executable, it has been run locally or the blocker is documented.
11
+
12
+ ## Current Status
13
+
14
+ | Area | Status | Evidence |
15
+ | --- | --- | --- |
16
+ | PRDs | Done | `HF_PRD_v1.md`, `HF_PRD_ext.md` exist at repo root |
17
+ | Codex guidance | Done | `AGENTS.md` exists |
18
+ | Docs folder | Done | `docs/` exists with task, usage, architecture, extension docs |
19
+ | README | Done | `README.md` exists |
20
+ | Hackathon submission package | Drafted | `docs/HACKATHON_SUBMISSION.md` contains track, story, user, benefit, model compliance, demo flow, video script, social post draft, and submission checklist |
21
+ | Critical improvement roadmap | Done | `docs/ROADMAP_V2_CRITICAL_IMPROVEMENT_PLAN.md` gives a hard judge-oriented rating, architecture/security critique, and second roadmap |
22
+ | Template how-to | Done | `docs/TEMPLATE_HOWTO.md` documents how to build new domain apps from the template |
23
+ | Plant Discovery plan | Done | `docs/PLANT_DISCOVERY_APP_PLAN.md` tracks the first reference app and remaining work |
24
+ | Plant model/training how-to | Done | `docs/PLANT_MODEL_AND_TRAINING_HOWTO.md` documents demo, OpenBMB zero-shot, fine-tuned adapter mode, correction export, training plan, and adapter configuration |
25
+ | Gradio app shell | Implemented and launch-verified | `app.py` builds and foreground launch holds the server open; not currently running |
26
+ | Model config | Implemented | `config/models.yaml` exists |
27
+ | Expanded model config | Implemented | MiniCPM text, thinking, 4.1, V 4.6, V thinking, omnimodal entries, NVIDIA Nemotron Nano 9B v2, GGUF metadata, and backend capability metadata including OpenAI-compatible text serving |
28
+ | Catalog validation | Implemented | `validate_catalog()` and Status tab warnings |
29
+ | Training config | Implemented | `config/training.yaml` exists |
30
+ | Placeholder chat | Implemented | `ui/chat_tab.py`, `models/placeholder_service.py` |
31
+ | Placeholder vision | Implemented | `ui/vision_tab.py`, `models/placeholder_service.py` |
32
+ | Service abstraction | Implemented | `models/base.py`, `models/service_factory.py` |
33
+ | Local backend config | Implemented | `models/local_backend_config.py` saves ignored local settings in `data/local_backends.yaml` |
34
+ | llama.cpp backend | Implemented and locally verified for text CLI | `models/llama_cpp_service.py`; Status tab can pick GGUF/mmproj paths and build a command using `C:\llama-b9587-bin-win-cuda-13.3-x64\llama-server.exe`; `llama-cli.exe` generated a real response from a local GGUF |
35
+ | llama-cpp-python backend | Implemented and locally verified | `models/llama_cpp_python_service.py`; `llama_cpp 0.3.8` generated a real local response from `Llama-3.2-1B-Instruct-Q4_K_M.gguf`; Workbench Playwright captures this response |
36
+ | Ollama backend | Implemented, not locally verified | `models/ollama_service.py`; Status tab lists local Ollama models and prepares explicit `ollama pull` commands; Ollama executable not found on PATH |
37
+ | OpenAI-compatible backend | Implemented and live-verified | `models/openai_compatible_service.py`; Status tab stores LM Studio/vLLM-style base URL and optional served model name, checks `/v1/models`, and posts to `/v1/chat/completions` only when selected; verified `http://192.168.188.37:1234` with `llama-3.2-1b-instruct` |
38
+ | Transformers text backend | Implemented, package installed | `models/transformers_text.py`; lazy-loads tokenizer/model only when selected; `transformers 5.10.2` is installed for MiniCPM-V support |
39
+ | MiniCPM vision backend | Implemented and Plant-verified | `models/minicpm_vision.py` and `plant/plant_service.py` use `AutoProcessor` and `AutoModelForImageTextToText` lazily; `assets/plant_sample.jpg` produced a structured OpenBMB MiniCPM-V result |
40
+ | SGLang backend | Implemented, optional local backend | `models/sglang_runner.py`; builds explicit local start commands, reports health, sends OpenAI-compatible chat requests, and provides a shutdown request; SGLang server launch remains unverified and is not a root Space dependency |
41
+ | App state | Implemented | `core/app_state.py` records local events and dispatches through `EventBus`; `core/tab_feedback.py` emits tab-level UI errors |
42
+ | Service registry | Implemented | `models/service_factory.py` registers text and vision backend factories |
43
+ | Dataset tab | Partial | Local CSV/JSONL preview, optional HF dataset preview, schema, split selector, row count, samples, stats, dataset event emission, and tab-level error status |
44
+ | Local dataset preview | Implemented | CSV, JSONL, NDJSON preview and statistics via `datasets/loader.py` |
45
+ | MCP tools | Implemented locally, Gradio MCP path selected | `mcp_tools/tools.py` provides dataset stats, HF dataset preview/search-style helper, safe calculator, and model inference tool functions; `mcp_tools/bridge.py` documents `/gradio_api/mcp/sse` and verifies local invocation |
46
+ | VINDEX boundary | Implemented locally, execution disabled | `mcp_tools/vindex_tool.py` validates the eight PRD methods, builds non-executing call plans, caps risky edit parameters, and reports local package/server availability |
47
+ | Training tab | Partial | `ui/train_tab.py` builds a LoRA dry-run plan, checkpoint output path, hardware notes, local deterministic evaluation, and optional loss-based perplexity summary |
48
+ | Training planner | Implemented, non-executing | `training/planner.py` parses LoRA/training config, validates dry runs, and never starts training |
49
+ | LoRA trainer planner | Implemented locally, execution disabled | `training/lora_trainer.py` reports PEFT/TRL/Transformers/Torch availability, builds a non-executing LoRA request, and documents SWIFT/LLaMA-Factory vision fine-tuning path |
50
+ | Evaluation | Implemented, local-only | `training/evaluation.py` provides prompt cases, exact-match scoring, optional loss-based perplexity, qualitative table, base-vs-tuned comparison, and JSONL logging |
51
+ | Export tab | Partial | `ui/export_tab.py` builds GGUF download/conversion/quantization plans, lists exported files, and exposes existing exported files through a download output |
52
+ | Export planner | Implemented, non-executing | `training/export.py` detects llama.cpp tools, builds explicit commands, and does not run downloads/conversions |
53
+ | Reward evaluation | Implemented locally | `training/reward_eval.py` provides deterministic reward scoring, best-of-N selection, DPO pair generation, and LoRA-vs-base reward reports |
54
+ | Synthetic data generation | Implemented locally | `datasets/synthetic.py` provides deterministic generation, validation, quality filtering, augmentation, and JSONL export |
55
+ | Field notes | Partial | `ui/notes_tab.py` saves CSV, supports media paths/training flag, imports uncertain OCR predictions, emits field-note events, and exports JSONL/local HF Dataset files |
56
+ | Field note module | Implemented | CSV save, SQLite store, corrected/tag/training filters, JSONL export, local HF Dataset export via `datasets/field_notes.py` |
57
+ | OCR correction loop | Implemented locally | `datasets/ocr.py` loads local CSV/JSONL OCR predictions, filters by confidence threshold, imports uncertain rows to Field Notes, and exports corrected OCR JSONL |
58
+ | Tracking | Implemented with local fallback | `tracking/trackio_client.py` loads Trackio config, writes local JSONL traces, and calls Trackio when installed/enabled |
59
+ | Traces tab | Partial | `ui/traces_tab.py` previews app events, reads local trace rows, shows tracking status, and exports traces |
60
+ | Agent mode | Implemented locally, non-autonomous | `agent/runner.py` provides system prompt, deterministic research-plan-implement-verify trace, paper-to-code trace mode, safety gates, tool registry integration, JSONL trace save/export, and local HF Dataset-style export |
61
+ | Agent tab | Implemented locally | `ui/agent_tab.py` drafts task traces, paper-to-code traces, and exports trace files/datasets |
62
+ | Status tab | Implemented | `ui/status_tab.py` lists model config, backend status, local llama.cpp setup, LM Studio/OpenAI-compatible setup, SGLang setup, and Ollama list/pull planning |
63
+ | Tab-level error messages | Implemented | Chat, Vision, and Dataset tabs show status/error messages and emit `ui_error` trace events |
64
+ | Loading/progress states | Implemented | `ui/progress.py` applies full Gradio progress indicators to tab actions |
65
+ | Compact responsive layout | Implemented | `APP_CSS` constrains app width, keeps tabs scrollable, sizes touch targets, and adds mobile padding/type rules |
66
+ | Structure verification | Done | `scripts/verify_structure.ps1` passed |
67
+ | Unit tests | Passing | 187 unit/user-story tests pass |
68
+ | User-story tests | Passing | Included in the 187-test suite |
69
+ | Coverage | Passing | 68% line/branch coverage at current configured threshold |
70
+ | Performance tests | Passing | 2 lightweight performance tests pass |
71
+ | Playwright E2E | Passing with real response screenshots | Workbench Playwright captures a real local GGUF `llama-cpp-python` chat response; Plant Playwright with `RUN_REAL_MODEL_E2E=1` captures a real OpenBMB MiniCPM-V image result from `assets/plant_sample.jpg` |
72
+ | CI pipeline | Added, not run remotely | `.github/workflows/ci.yml` |
73
+ | Quality tooling | Passing | Tests, coverage, performance, ruff, mypy, pylint, bandit, and pip-audit pass through `scripts/run_quality.ps1` |
74
+ | Secrets and model-weight git policy | Implemented | `.gitignore` excludes env files, keys, caches, generated data/exports, and common model weight formats; policy has a unit test |
75
+ | Real model inference | Partial but materially verified | Verified paths: LM Studio/OpenAI-compatible text, llama.cpp CLI text, llama-cpp-python GGUF text, and OpenBMB MiniCPM-V Plant image inference. Remaining unverified paths: Ollama generation, SGLang server generation, llama.cpp MiniCPM-V mmproj vision, full Transformers text generation |
76
+ | Hugging Face Space deploy | Pushed, startup/build pending | Workbench pushed to `build-small-hackathon/workbench` at `6aafdc2083a9b82e9dca2cca5b87c3a1be05121b`; Plant pushed to `build-small-hackathon/plant_identification_tool` at `50897b3167a844b6a66ca0552b73a1791cdff926`; both include Python 3.10 compatibility fixes for HF Spaces |
77
+ | HF Space deployment helper | Implemented locally | `deployment/hf_space.py` and `scripts/plan_hf_space.py` validate required files, README Space metadata, Workbench/Plant remote status, and manual `hf` deployment commands |
78
+ | vLLM serving tab | Implemented locally, not locally verified | `models/vllm_runner.py` and `ui/vllm_tab.py` build explicit vLLM commands, check health, parse metrics, log benchmark metrics through local tracking, and use OpenAI-compatible chat when a server is running |
79
+ | Plant Discovery reference app | Implemented locally, no-model verified; Space wrapper added | `plant/` is a standalone template-built app with demo/no-model service for local tests, default OpenBMB MiniCPM-V mode, optional fine-tuned adapter mode, local species index, correction export, non-executing training plan, optional MCP tools, unit tests, HTTP smoke verification on port 7861, and `plant_space_app.py` for real-model Space launch |
80
+ | GitHub push | Done | GitHub remote `https://github.com/Ckal/codex.git`; commits pushed to `origin/main` |
81
+
82
+ ## Known Blockers
83
+
84
+ - `python` is available on PATH as Python 3.13 in the current shell. The documented `.venv` was not
85
+ visible during the latest verification run, so tests ran against the global Python environment
86
+ after reinstalling/updating `requirements.txt`.
87
+ - App launch was verified by Playwright through `python app.py`; no long-running server is currently active.
88
+ - llama.cpp tools are installed at `C:\llama-b9587-bin-win-cuda-13.3-x64`; they are not on PATH,
89
+ so the app stores/uses the explicit `llama-server.exe` path.
90
+ - Export planning works, but actual conversion/quantization remains blocked until a specific export
91
+ run is requested and verified.
92
+ - A GGUF path is configured locally for `Llama-3.2-1B-Instruct-Q4_K_M.gguf`; direct
93
+ `llama-cli.exe` and `llama-cpp-python` generation both produced real text.
94
+ - Ollama is installed on PATH (`ollama version is 0.24.0`). `ollama pull openbmb/minicpm-v4.6`
95
+ succeeded and `ollama list` shows `openbmb/minicpm-v4.6:latest`; a tiny `ollama run` prompt
96
+ currently fails with a 500 model-load error, so real Ollama generation remains unverified.
97
+ - LM Studio/OpenAI-compatible setup is implemented and verified for `http://192.168.188.37:1234`
98
+ with served model override `llama-3.2-1b-instruct`. This local override is stored in ignored
99
+ `data/local_backends.yaml`.
100
+ - `transformers 5.10.2`, `torch`, and MiniCPM-V dependencies are installed. This conflicts with
101
+ the currently installed `sentence-transformers 3.4.1` requirement of `transformers<5.0.0`, so
102
+ future quality runs should watch for that dependency edge.
103
+ - MiniCPM-V Plant inference is verified with `assets/plant_sample.jpg`; the app also supports
104
+ bounded E2E inference through `PLANT_MAX_NEW_TOKENS=320` and `PLANT_AUTO_THINKING=0`.
105
+ - SGLang command planning, health, stop, and chat client code is implemented. SGLang remains an
106
+ optional local backend, not a root Space dependency, because the local Python 3.13 package index
107
+ cannot install the newer non-vulnerable SGLang server stack.
108
+ - vLLM command planning, health, metrics, benchmark logging, and chat client code is implemented,
109
+ but the `vllm` package/server is not installed or running in this workspace.
110
+ - LoRA training request planning is implemented, but real execution remains blocked until PEFT/TRL,
111
+ Transformers, Torch, and final hardware are approved and installed.
112
+ - Trackio is optional; local JSONL tracing works without the `trackio` package, but remote Trackio/HF
113
+ sync still needs package availability and credentials.
114
+ - LoRA dry-run planning works locally, but real training remains blocked until a final backend,
115
+ PEFT/TRL or SWIFT/LLaMA-Factory path, and hardware are chosen.
116
+ - Hugging Face dataset preview is optional and requires the external `datasets` package; the app
117
+ reports a clear status when it is not installed.
118
+ - Hugging Face Space deployment is pushed with Python 3.10 compatibility fixes for `StrEnum` and
119
+ UTC timestamps. Final run verification and smoke workflows remain open until rebuilt Spaces
120
+ report `RUNNING`.
121
+ - Node.js and npm are installed (`node v23.11.0`, `npm 10.9.2`). `npm install`,
122
+ `npm run e2e:install`, and `npm run e2e` pass.
123
+ - Plant Discovery OpenBMB MiniCPM-V inference is implemented as the default real mode, and
124
+ `plant_space_app.py` launches that mode for Space deployment. `RUN_REAL_MODEL_E2E=1`
125
+ Playwright passed and captured a real response screenshot.
126
+ - Plant Discovery fine-tuned adapter mode is implemented, but no trained plant adapter exists in
127
+ this workspace yet.
128
+ - Plant Discovery public Space mode still needs path/url hardening and screenshots.
129
+ - Full PRD implementation is not complete. There are still unchecked tasks in `docs/TASKS.md`.
130
+ - Current unchecked task count needs recounting after the latest Workbench/Space changes. Several PRD/ext PRD items still
131
+ need real local setup, credentials, hardware, product decisions, or hackathon submission artifacts.
132
+
133
+ ## Latest Local Verification
134
+
135
+ - `.\scripts\run_tests.ps1` passed: 200 tests and 70% coverage.
136
+ - `.\scripts\run_quality.ps1` passed: tests, app smoke, 70% coverage, performance, Ruff, mypy,
137
+ Pylint, Bandit, and project-scoped pip-audit.
138
+ - Plant Discovery no-model HTTP smoke passed on `http://127.0.0.1:7861`; the process was stopped
139
+ after verification.
140
+ - `python scripts\plan_plant_training.py --corrected-examples 30` prints a non-executing SWIFT /
141
+ LLaMA-Factory adapter training plan; current environment is missing torch, transformers, PEFT,
142
+ TRL, and SWIFT.
143
+ - `pytest tests/unit/test_local_backend_config.py tests/unit/test_llama_cpp_service.py tests/unit/test_model_catalog.py tests/unit/test_service_factory.py -q` passed: 30 tests.
144
+ - `npm run e2e:workbench` passed and captured a visible local GGUF response through `llama-cpp-python`.
145
+ - `RUN_REAL_MODEL_E2E=1 PLANT_MAX_NEW_TOKENS=320 PLANT_AUTO_THINKING=0 npx playwright test tests/e2e/plant_real_model.spec.ts --config playwright.plant.config.ts --reporter=list` passed and captured a real MiniCPM-V response.
146
+ - `hf upload build-small-hackathon/workbench C:\tmp\workbench_space_payload . --repo-type space` pushed commit `6aafdc2083a9b82e9dca2cca5b87c3a1be05121b`.
147
+ - `hf upload build-small-hackathon/plant_identification_tool C:\tmp\plant_space_payload . --repo-type space` pushed commit `50897b3167a844b6a66ca0552b73a1791cdff926`.
148
+ - `hf spaces variables add` set `WORKBENCH_DEPLOYMENT=space` for Workbench and Plant; Plant also has `PLANT_MAX_NEW_TOKENS=320` and `PLANT_AUTO_THINKING=0`.
149
+ - `hf spaces info` showed Workbench in `APP_STARTING` and Plant in `BUILDING` on `zero-a10g`; final build/run verification remains pending.
150
+ - `pytest tests/unit tests/user_stories -q` passed earlier in this workstream: 197 tests.
151
+ - `node --version` returned `v23.11.0`; `npm --version` returned `10.9.2`.
152
+ - `ollama --version` returned `ollama version is 0.24.0`; `ollama list` returned `gemma4:latest`.
153
+ - `ollama pull openbmb/minicpm5-1b` failed because the registry manifest does not exist.
154
+ - `ollama pull openbmb/minicpm-v4.6` succeeded; `ollama run openbmb/minicpm-v4.6` failed with
155
+ a local 500 model-load error.
156
+ - `hf --version` returned `1.17.0`; `huggingface-cli --version` reports the legacy command is
157
+ deprecated and recommends `hf`.
158
+ - `hf auth whoami` failed with "Invalid user token"; Space push/build verification is blocked
159
+ until `hf auth login --force` is run with a fresh token.
160
+ - `llama-server --version` failed because `llama-server` is not on PATH.
161
+ - `git remote -v` shows `space-workbench` and `space-plant` remotes configured.
162
+ - Direct `ruff check .` passed; cache-write warnings were caused by OneDrive permissions.
163
+ - Direct `mypy . --no-incremental` passed when `MYPY_CACHE_DIR` was moved to `%TEMP%`.
164
+ - LM Studio `/v1/models` at `http://192.168.188.37:1234` returned
165
+ `text-embedding-nomic-embed-text-v1.5`, `qwen2.5-coder-3b-instruct`, and
166
+ `llama-3.2-1b-instruct`.
167
+ - LM Studio `/v1/chat/completions` returned a text response from `llama-3.2-1b-instruct`.
168
+ - Focused OCR callback and pipeline tests pass: 8 tests.
169
+ - Focused VINDEX/MCP tests pass: 13 tests.
170
+
171
+ ## Verification Commands
172
+
173
+ Run these after installing Python:
174
+
175
+ ```powershell
176
+ .\scripts\verify_structure.ps1
177
+ .venv\Scripts\python.exe --version
178
+ .venv\Scripts\Activate.ps1
179
+ python -m pip install -r requirements.txt
180
+ python -m pip install -r requirements-dev.txt
181
+ .\scripts\run_tests.ps1
182
+ .\scripts\run_quality.ps1
183
+ python app.py
184
+ ```
185
+
186
+ When the app starts, update this file and `docs/TASKS.md`.
docs/PLANT_DISCOVERY_APP_PLAN.md ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Plant Discovery Reference App Plan
2
+
3
+ This checklist turns the rough `plant/` sample into the first concrete app built around the
4
+ template. It is intentionally detailed so future apps can copy the pattern.
5
+
6
+ ## Product Story
7
+
8
+ Plant Discovery helps a gardener, teacher, or field-note collector:
9
+
10
+ 1. Upload one or more plant images.
11
+ 2. Get a structured plant identification.
12
+ 3. Correct mistakes.
13
+ 4. Browse a small field guide.
14
+ 5. Export corrections as local training data.
15
+ 6. Later fine-tune or evaluate a small vision model.
16
+
17
+ This is stronger than a generic workbench for hackathon judging because it has a real user outcome.
18
+
19
+ ## Current Implementation Status
20
+
21
+ - [x] Add `plant/` package.
22
+ - [x] Add standalone `plant/app.py`.
23
+ - [x] Add clean `plant/models.yaml`.
24
+ - [x] Add deterministic no-model demo service.
25
+ - [x] Add optional MiniCPM-V service adapter.
26
+ - [x] Add explicit `demo`, `openbmb`, and `finetuned` model modes.
27
+ - [x] Make OpenBMB MiniCPM-V the default real model mode.
28
+ - [x] Add fine-tuned adapter loading path through PEFT when configured.
29
+ - [x] Avoid hard `torch`/`transformers` imports at module import time.
30
+ - [x] Add plant schema/parser through `PlantID`.
31
+ - [x] Add local species index builder.
32
+ - [x] Add local folder loader.
33
+ - [x] Add field-note export to plant training JSONL.
34
+ - [x] Add focused Gradio UI with Identify, Field Guide, Corrections, and Stats.
35
+ - [x] Replace direct training execution with non-executing training plan.
36
+ - [x] Add optional pure tool functions and lazy MCP server builder.
37
+ - [x] Add non-executing plant training planner.
38
+ - [x] Add Plant model/training how-to.
39
+ - [x] Add plant unit tests.
40
+ - [x] Add no-model app build verification.
41
+
42
+ ## Remaining Work
43
+
44
+ ### P0 - Make The Demo Judgeable
45
+
46
+ - [x] Run `python -m plant.app --no-model --port 7861`.
47
+ - [ ] Install Node.js/npm.
48
+ - [ ] Generate Playwright screenshots for the plant app.
49
+ - [ ] Add Plant Discovery screenshots to README.
50
+ - [ ] Add a 60-90 second demo script focused on plant correction.
51
+ - [x] Decide whether the hackathon Space should launch `app.py` or `plant/app.py`.
52
+ - [x] If using `plant/app.py` for Space, add a Space-specific entrypoint or README note.
53
+
54
+ ### P1 - Real Model Path
55
+
56
+ - [ ] Install optional plant dependencies from `plant/requirements.txt`.
57
+ - [ ] Verify `PlantVisionService.dependency_report()` is fully available.
58
+ - [ ] Run one MiniCPM-V identification on a local public-safe plant image.
59
+ - [ ] Capture model ID, hardware, latency, and output JSON.
60
+ - [ ] Add a real-backend integration test profile that is skipped unless dependencies are present.
61
+ - [ ] Document that model weights are not downloaded on startup.
62
+
63
+ ### P2 - Data And Correction Loop
64
+
65
+ - [ ] Add a tiny public-safe sample plant image folder under ignored local data.
66
+ - [ ] Add a sample `plant/data/plantnet_labels.json` cache or document how to create it.
67
+ - [ ] Add path allowlist before public Space deployment.
68
+ - [ ] Add JSONL schema documentation for exported corrections.
69
+ - [ ] Add validation for minimum correction count before recommending training.
70
+
71
+ ### P3 - Template Extraction
72
+
73
+ - [ ] Identify reusable parts that should move from `plant/` to generic modules.
74
+ - [ ] Keep domain-specific prompt/schema/UI inside `plant/`.
75
+ - [ ] Consider `domains/<name>/` convention if more than one reference app is added.
76
+ - [ ] Add a cookiecutter-style checklist or scaffold script only after the second app proves the pattern.
77
+
78
+ ### P4 - Security And Public Mode
79
+
80
+ - [ ] Add public/local mode config.
81
+ - [ ] In public mode, disable arbitrary local file paths.
82
+ - [ ] In public mode, disable arbitrary backend URLs.
83
+ - [ ] Add max upload size and allowed image extension checks.
84
+ - [ ] Add tests for path traversal and malformed image/data inputs.
85
+ - [ ] Add a plant-specific disclaimer: identification can be wrong; do not use for medical, edible,
86
+ or toxicity-critical decisions without expert verification.
87
+
88
+ ### P5 - Training And Evaluation
89
+
90
+ - [ ] Collect at least 30 corrected examples before any LoRA experiment.
91
+ - [ ] Split exported corrections into train/eval.
92
+ - [ ] Add exact species-name evaluation.
93
+ - [ ] Add before/after model comparison.
94
+ - [ ] Run SWIFT or LLaMA-Factory locally only after dependencies and hardware are approved.
95
+ - [ ] Publish adapter only if it improves the evaluation set.
96
+ - [ ] Update `plant/models.yaml` with the real adapter repo.
97
+ - [ ] Verify `python -m plant.app --model-mode finetuned --port 7861`.
98
+
99
+ ## Recommended Next Command Sequence
100
+
101
+ ```powershell
102
+ .venv\Scripts\python.exe -m pytest tests/unit/test_plant_reference_app.py -q
103
+ .venv\Scripts\ruff.exe check plant tests/unit/test_plant_reference_app.py --no-cache
104
+ .venv\Scripts\python.exe -m mypy plant tests/unit/test_plant_reference_app.py --cache-dir "$env:TEMP\openbmb-workbench-mypy-cache"
105
+ .venv\Scripts\python.exe -m plant.app --no-model --port 7861
106
+ .venv\Scripts\python.exe scripts\plan_plant_training.py --corrected-examples 30
107
+ ```
108
+
109
+ For the deployed Plant Identification Tool Space, use `plant_space_app.py`. It launches
110
+ OpenBMB MiniCPM-V mode and does not enable `--no-model`/demo mode.
111
+
112
+ ## Template Lessons From Plant Discovery
113
+
114
+ - Domain app first screen should be the product, not the infrastructure.
115
+ - Demo/no-model mode is essential for tests and screenshots.
116
+ - Heavy model dependencies should be optional.
117
+ - Correction loops are more valuable than promising immediate fine-tuning.
118
+ - Tool/MCP functions should be pure and importable without a running server.
119
+ - Training should be a plan until data, hardware, and dependencies are verified.
docs/PLANT_MODEL_AND_TRAINING_HOWTO.md ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Plant Model And Training How-To
2
+
3
+ This answers three common questions:
4
+
5
+ 1. Does Plant Discovery use an LLM?
6
+ 2. Does it use a specially trained plant model?
7
+ 3. How do we train and then use one?
8
+
9
+ ## Current Model Modes
10
+
11
+ Plant Discovery has three explicit modes.
12
+
13
+ | Mode | Command | Uses an LLM/VLM? | Trained specifically for this app? |
14
+ | --- | --- | --- | --- |
15
+ | Demo | `python -m plant.app --model-mode demo --port 7861` | No | No |
16
+ | OpenBMB zero-shot | `python -m plant.app --model-mode openbmb --port 7861` | Yes, MiniCPM-V | No, uses base OpenBMB model |
17
+ | Fine-tuned adapter | `python -m plant.app --model-mode finetuned --port 7861` | Yes, MiniCPM-V plus adapter | Yes, after you train/publish an adapter |
18
+
19
+ The app defaults to `openbmb` mode. The `--no-model` flag is an alias for demo mode and exists for
20
+ tests, screenshots, and machines without GPU/model dependencies.
21
+
22
+ ## What Runs Today
23
+
24
+ - Demo mode is fully verified locally.
25
+ - OpenBMB mode is implemented but requires optional dependencies and model weights.
26
+ - Fine-tuned mode is implemented as an adapter-loading path, but there is no real adapter yet.
27
+
28
+ This means the app can use a real OpenBMB VLM, but this workspace has not installed the heavy
29
+ `plant/requirements.txt` dependencies or downloaded model weights.
30
+
31
+ ## Step 1 - Use OpenBMB MiniCPM-V Zero-Shot
32
+
33
+ Install optional plant dependencies:
34
+
35
+ ```powershell
36
+ .venv\Scripts\python.exe -m pip install -r plant\requirements.txt
37
+ ```
38
+
39
+ Run the app with the real OpenBMB model path:
40
+
41
+ ```powershell
42
+ .venv\Scripts\python.exe -m plant.app --model-mode openbmb --port 7861
43
+ ```
44
+
45
+ Open `http://127.0.0.1:7861`, upload a public-safe plant image, and click `Identify plant`.
46
+
47
+ Expected behavior:
48
+
49
+ - The UI shows model status with `uses_llm: true`.
50
+ - The first identify action loads `openbmb/MiniCPM-V-4.6`.
51
+ - If confidence is low and auto-thinking is enabled, the app can try
52
+ `openbmb/MiniCPM-V-4.6-Thinking`.
53
+ - Output is parsed into the `PlantID` JSON schema.
54
+
55
+ ## Step 2 - Collect Corrections
56
+
57
+ Use the app normally:
58
+
59
+ 1. Upload plant image.
60
+ 2. Identify.
61
+ 3. Add a human correction when the model is wrong.
62
+ 4. Save correction.
63
+ 5. Open `Corrections`.
64
+ 6. Click `Export training JSONL`.
65
+
66
+ The export path is:
67
+
68
+ ```text
69
+ data/plant_training.jsonl
70
+ ```
71
+
72
+ Do not train on one or two examples. The current minimum recommendation is 30 corrected examples,
73
+ and more is better.
74
+
75
+ ## Step 3 - Plan Training
76
+
77
+ Generate a non-executing training plan:
78
+
79
+ ```powershell
80
+ .venv\Scripts\python.exe scripts\plan_plant_training.py --corrected-examples 30
81
+ ```
82
+
83
+ This prints:
84
+
85
+ - dependency availability,
86
+ - SWIFT command preview,
87
+ - LLaMA-Factory command preview,
88
+ - publish commands,
89
+ - command to run the trained adapter.
90
+
91
+ The Gradio UI also exposes the same non-executing plan from the `Corrections` tab.
92
+
93
+ ## Step 4 - Train Locally
94
+
95
+ Preferred path for MiniCPM-V vision LoRA is SWIFT or another tool that supports multimodal LoRA.
96
+ The generated SWIFT command looks like this:
97
+
98
+ ```powershell
99
+ swift sft --model openbmb/MiniCPM-V-4.6 --dataset data/plant_training.jsonl --lora_rank 16 --num_train_epochs 3 --per_device_train_batch_size 4 --gradient_accumulation_steps 4 --learning_rate 2.0e-4 --freeze_vit true --output_dir checkpoints/plant_lora
100
+ ```
101
+
102
+ Run it only after:
103
+
104
+ - optional dependencies are installed,
105
+ - GPU memory is sufficient,
106
+ - exported JSONL has been reviewed,
107
+ - private images/notes are removed,
108
+ - you accept that training may take time and disk space.
109
+
110
+ ## Step 5 - Publish Or Configure The Adapter
111
+
112
+ After training, publish the adapter or keep it local. If publishing:
113
+
114
+ ```powershell
115
+ huggingface-cli repo create your-username/minicpm-v46-plant-lora --type model
116
+ huggingface-cli upload your-username/minicpm-v46-plant-lora checkpoints/plant_lora .
117
+ ```
118
+
119
+ Then edit `plant/models.yaml`:
120
+
121
+ ```yaml
122
+ plant_vlm_finetuned:
123
+ base_model: openbmb/MiniCPM-V-4.6
124
+ adapter_id: your-username/minicpm-v46-plant-lora
125
+ ```
126
+
127
+ Run:
128
+
129
+ ```powershell
130
+ .venv\Scripts\python.exe -m plant.app --model-mode finetuned --port 7861
131
+ ```
132
+
133
+ ## Important Honesty Rule
134
+
135
+ Do not claim Plant Discovery uses a specially trained model until:
136
+
137
+ - a real adapter exists,
138
+ - it is configured in `plant/models.yaml`,
139
+ - `--model-mode finetuned` has been run,
140
+ - evaluation shows it improves over the base OpenBMB model.
141
+
142
+ Until then, the honest claim is:
143
+
144
+ > Plant Discovery uses OpenBMB MiniCPM-V zero-shot, with a correction loop and documented path to
145
+ > fine-tune a plant adapter.
docs/PRD_IMPLEMENTATION_MATRIX.md ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PRD Implementation Matrix
2
+
3
+ This file maps the main PRD and extension PRD to current implementation status.
4
+
5
+ ## Summary
6
+
7
+ The full PRD and extension PRD are not fully implemented yet.
8
+
9
+ Current state:
10
+
11
+ - Foundation, docs, test policy, quality gates, CI, placeholder Gradio surfaces, and a Plant
12
+ Discovery reference app exist.
13
+ - Shared app state, service registries, local event logging, tab-level error events, and local
14
+ trace preview exist.
15
+ - Local llama.cpp settings, GGUF/mmproj pickers, and command generation exist without startup downloads.
16
+ - GGUF export planning exists with tool detection and explicit non-executing command plans.
17
+ - Local JSONL tracing and optional Trackio wrapper exist.
18
+ - Dataset statistics and local MCP tool functions exist.
19
+ - OCR correction loop exists locally for CSV/JSONL prediction imports into Field Notes.
20
+ - VINDEX integration boundary exists locally as non-executing MCP-style planning tools.
21
+ - Local non-autonomous agent mode exists with trace export.
22
+ - Real local model inference is partially implemented through llama.cpp, llama-cpp-python, Ollama,
23
+ OpenAI-compatible/LM Studio, SGLang, Transformers text, and MiniCPM vision services. Verified
24
+ local paths now include llama.cpp CLI text, llama-cpp-python GGUF text, LM Studio text, and
25
+ OpenBMB MiniCPM-V Plant image inference. The Status
26
+ tab includes llama.cpp setup, LM Studio/OpenAI-compatible setup, SGLang command/check/stop setup,
27
+ and Ollama local model listing plus explicit pull-command planning. LM Studio text generation is
28
+ live-verified with `llama-3.2-1b-instruct`; the other real backends still need local verification.
29
+ - `WORKBENCH_DEPLOYMENT=space` now hides placeholder backend choices and refuses placeholder/demo
30
+ service creation for deployed app paths.
31
+ - LoRA training execution, served MCP endpoint, deployment, and most extensions are not implemented.
32
+ - Placeholder services remain intentionally visible so the app never pretends to be real inference.
33
+
34
+ ## Main PRD
35
+
36
+ | PRD Area | Status | Evidence / Next Step |
37
+ | --- | --- | --- |
38
+ | Purpose and design philosophy | Documented | `README.md`, `docs/ROADMAP.md` |
39
+ | Template architecture | Partial | Config-driven model catalog exists; `docs/TEMPLATE_HOWTO.md` and `plant/` show the first domain-app pattern |
40
+ | System architecture | Partial | `app.py`, `core/`, `models/`, `ui/`, `datasets/`, local app state/events |
41
+ | Model registry | Partial | `config/models.yaml`, `models/model_catalog.py`; includes GGUF and backend capability metadata |
42
+ | Five inference modes | Partial | llama.cpp, llama-cpp-python, OpenAI-compatible/LM Studio, and MiniCPM-V Plant image inference are locally verified; Ollama generation, SGLang server generation, vLLM server generation, and llama.cpp mmproj vision remain unverified |
43
+ | Trackio | Partial | Local traces, optional Trackio wrapper, and HF Space sync docs exist; credentials/package setup still missing |
44
+ | MCP layer | Partial | Local tool functions, Gradio-native MCP path metadata, `mcp_server=True` launch flag, and local invocation tests exist; full external client verification still missing |
45
+ | Training pipeline | Partial | `training/` package supports dry-run planning, non-executing LoRA request planning, export planning, exact-match/perplexity evaluation, and local logging; real PEFT/TRL execution missing |
46
+ | Export and quantization | Partial | `training/export.py` and Export tab plan downloads/conversion/quantization and expose existing exported files for download; execution still missing |
47
+ | Agent mode | Partial | Local deterministic task and paper-to-code trace loops exist with safety gates; autonomous execution and remote uploads missing |
48
+ | UI tabs | Partial | Tabs exist; Chat/Vision/Dataset/Field Notes/Status have behavior; Status includes SGLang setup; tab actions have Gradio progress indicators; Chat/Vision/Dataset have tab-level status/error messages; compact responsive CSS exists; several tabs are still placeholders |
49
+ | Field notes | Partial | CSV save, SQLite store, corrected/tag/training filters, media paths, OCR uncertain import, JSONL export, and local HF Dataset export exist; remote HF upload missing |
50
+ | Directory structure | Partial | Foundation exists; many PRD packages missing |
51
+ | Configuration schema | Partial | Model/training config plus ignored local backend config exists; validation is lightweight |
52
+ | Dependencies | Partial | Runtime/dev deps exist for scaffold; full model/training deps not added |
53
+ | Hackathon demo flow | Partial | `docs/HACKATHON_SUBMISSION.md` drafts story, user, demo flow, script, social post, and URLs; real backend and Space URL still missing |
54
+ | Corrections from PRD v1 | Documented in PRD | Not all implemented |
55
+ | Roadmap and extension points | Documented | `docs/ROADMAP.md`, `docs/TASKS.md` |
56
+
57
+ ## Extension PRD
58
+
59
+ | Extension | Status | Evidence / Next Step |
60
+ | --- | --- | --- |
61
+ | vLLM serving tab | Implemented locally, not locally verified | `models/vllm_runner.py` and vLLM tab provide command planning, health checks, metrics parsing, benchmark logging, and OpenAI-compatible chat client; needs installed/running vLLM for real serving |
62
+ | Ollama quick-start | Partial | Service, UI backend selector, local model listing, explicit pull-command planning, and setup docs exist; local Ollama install/real model verification missing |
63
+ | Reward model eval | Implemented locally | `training/reward_eval.py` provides deterministic reward scoring, best-of-N, DPO pairs, and LoRA-vs-base reward reports |
64
+ | Synthetic data generation | Implemented locally | `datasets/synthetic.py` provides deterministic generation, validation, filtering, augmentation, and JSONL export |
65
+ | Paper-to-code agent | Implemented locally | Agent tab and `agent/runner.py` support paper input, research/plan/implementation/verify trace, and safety gates without autonomous execution |
66
+ | HF Spaces deploy | Partial | README metadata, deployment helper, command plan, required-file validation, Workbench/Plant target URLs, and remote/build status checks exist; HF auth/remote/push/build verification still missing |
67
+ | VINDEX integration | Implemented locally, execution disabled | `mcp_tools/vindex_tool.py` validates VINDEX methods, builds safe call plans, reports dependency/server status, and documents that actual edits require a verified local VINDEX install |
68
+ | OCR pipeline hook | Implemented locally | `datasets/ocr.py` and Field Notes tab support local OCR prediction loading, confidence thresholds, uncertain import, human correction, and corrected JSONL export |
69
+ | MiniCPM Desk-Pet | Not implemented | Needs persona schema/export |
70
+ | MiniCPM-o audio tab | Not implemented | Needs audio tab and omnimodal backend |
71
+ | Cross-extension wiring | Partial | OCR -> Field Notes -> Training, Synthetic Gen -> Reward Eval -> DPO, Agent -> Desk-Pet Persona, and HF Spaces -> Trackio are documented; remaining wiring depends on unimplemented runtime modules |
72
+
73
+ ## Quality Coverage
74
+
75
+ Current verified gates:
76
+
77
+ - Structure check passes.
78
+ - 187 unit/user-story tests pass.
79
+ - Coverage report passes at 68%, above the current 60% configured threshold.
80
+ - 2 lightweight performance tests pass.
81
+ - Ruff passes.
82
+ - Mypy passes.
83
+ - Pylint passes at 10/10.
84
+ - Bandit reports no issues.
85
+ - Pip-audit reports no known vulnerabilities in `.venv`.
86
+ - LM Studio `/v1/models` and `/v1/chat/completions` are verified locally for
87
+ `llama-3.2-1b-instruct`.
88
+ - Workbench and Plant Playwright screenshot flows pass through `npm run e2e`.
89
+ - CI workflow exists but has not run remotely.
90
+ - App launch has been verified locally through Playwright, but the server is not currently left running.
91
+
92
+ ## No Pretend-Done Rule
93
+
94
+ Any row marked `Partial`, `Placeholder`, or `Not implemented` must not be described as complete.
95
+ When a row is implemented, update this file, `docs/TASKS.md`, and `docs/IMPLEMENTATION_STATUS.md`.
docs/README.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Project Docs
2
+
3
+ This folder is the project memory. Use it before asking Codex to make larger changes.
4
+
5
+ ## Start Here
6
+
7
+ - [TASKS.md](TASKS.md) - checklist of hackathon tasks and subtasks.
8
+ - [IMPLEMENTATION_STATUS.md](IMPLEMENTATION_STATUS.md) - what is done, partial, or not started.
9
+ - [PRD_IMPLEMENTATION_MATRIX.md](PRD_IMPLEMENTATION_MATRIX.md) - exact PRD/ext implementation coverage.
10
+ - [ACCEPTANCE_CRITERIA.md](ACCEPTANCE_CRITERIA.md) - proof required before marking work done.
11
+ - [ROADMAP.md](ROADMAP.md) - hackathon path versus long-term PRD vision.
12
+ - [ROADMAP_V2_CRITICAL_IMPROVEMENT_PLAN.md](ROADMAP_V2_CRITICAL_IMPROVEMENT_PLAN.md) - hard
13
+ judge-oriented critique and second roadmap.
14
+ - [TEMPLATE_HOWTO.md](TEMPLATE_HOWTO.md) - how to build new domain apps from this template.
15
+ - [PLANT_DISCOVERY_APP_PLAN.md](PLANT_DISCOVERY_APP_PLAN.md) - detailed checklist for the first
16
+ reference app.
17
+ - [PLANT_MODEL_AND_TRAINING_HOWTO.md](PLANT_MODEL_AND_TRAINING_HOWTO.md) - model modes and
18
+ adapter training path for Plant Discovery.
19
+ - [PLANT_MODEL_AND_TRAINING_HOWTO.md](PLANT_MODEL_AND_TRAINING_HOWTO.md) - how Plant Discovery
20
+ uses OpenBMB MiniCPM-V and how to train an adapter.
21
+ - [USAGE.md](USAGE.md) - how to install, run, and use the app.
22
+ - [ARCHITECTURE.md](ARCHITECTURE.md) - how the code is organized and how pieces connect.
23
+ - [EXTENDING.md](EXTENDING.md) - how to add models, tabs, services, and features later.
24
+ - [HACKATHON_SUBMISSION.md](HACKATHON_SUBMISSION.md) - story, demo flow, video script, and submission checklist.
25
+
26
+ ## Source PRDs
27
+
28
+ - [HF_PRD_v1.md](../HF_PRD_v1.md) - main technical PRD.
29
+ - [HF_PRD_ext.md](../HF_PRD_ext.md) - planned extensions.
30
+
31
+ The PRDs describe the full vision. The task and status docs describe what is actually implemented
32
+ or planned for the hackathon MVP.
docs/ROADMAP.md ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Roadmap
2
+
3
+ The PRD is intentionally broad. This roadmap keeps the hackathon path separate from the larger
4
+ Workbench vision.
5
+
6
+ ## Must Ship For Hackathon
7
+
8
+ 1. Local app runs.
9
+ 2. One real small model backend works.
10
+ 3. User story is clear.
11
+ 4. Field notes or demo evidence shows practical value.
12
+ 5. Hugging Face Space is live.
13
+ 6. README is clear enough for judges.
14
+ 7. Demo video and social post are ready.
15
+
16
+ ## Should Ship If Time Allows
17
+
18
+ 1. Ollama quick-start.
19
+ 2. llama.cpp GGUF mode.
20
+ 3. Field notes JSONL export.
21
+ 4. Trackio trace logging.
22
+ 5. Better visual polish.
23
+ 6. Architecture/report write-up.
24
+
25
+ ## Stretch After Hackathon
26
+
27
+ 1. LoRA training.
28
+ 2. GGUF export and quantization.
29
+ 3. MCP tools.
30
+ 4. Agent mode.
31
+ 5. Reward model evaluation.
32
+ 6. Synthetic data generation.
33
+ 7. OCR pipeline hook.
34
+ 8. MiniCPM-o audio tab.
35
+ 9. Desk-Pet persona export.
36
+
37
+ ## Current Recommended Path
38
+
39
+ For a new Codex user, the best path is:
40
+
41
+ ```text
42
+ docs/TASKS.md
43
+ -> install Python
44
+ -> run placeholder app
45
+ -> add Ollama backend
46
+ -> push GitHub
47
+ -> push Hugging Face Space
48
+ -> polish submission
49
+ ```
docs/ROADMAP_V2_CRITICAL_IMPROVEMENT_PLAN.md ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Roadmap V2 - Critical Hackathon Improvement Plan
2
+
3
+ This document is intentionally hard on the project. It answers: "If a strong automated judge or
4
+ human judge reviews this repo, what will cost points, and what should be fixed first?"
5
+
6
+ Sources checked on 2026-06-08:
7
+
8
+ - Official Build Small Hackathon page: <https://huggingface.co/build-small-hackathon>
9
+ - Current repo status: `docs/IMPLEMENTATION_STATUS.md`
10
+ - PRD coverage: `docs/PRD_IMPLEMENTATION_MATRIX.md`
11
+ - Task checklist: `docs/TASKS.md`
12
+
13
+ ## Executive Verdict
14
+
15
+ This is not yet a SOTA hackathon entry. It is a serious engineering scaffold with good local
16
+ quality discipline, but it is not yet a sharp, shipped product.
17
+
18
+ Current estimated rating:
19
+
20
+ | Dimension | Score | Hard reason |
21
+ | --- | ---: | --- |
22
+ | Hackathon fit | 5/10 | It uses Gradio and small-model framing, but the user story is diluted by too many tabs. |
23
+ | Shipped product | 3/10 | No verified Hugging Face Space URL, no generated screenshots, no submitted demo media. |
24
+ | Real usefulness | 5/10 | Field Notes plus correction loop can be useful, but it needs one real user, one real workflow, and evidence. |
25
+ | Technical architecture | 6/10 | Modules are clean enough, but UI callbacks still orchestrate too much and global state is too central. |
26
+ | Security posture | 5/10 | Basic secret/model-weight policy exists, but URL/path inputs and future command execution need hardening. |
27
+ | Test discipline | 7/10 | Good Python quality gates and unit tests; missing executed browser E2E, visual checks, and real backend integration. |
28
+ | Design and UX | 4/10 | Functional Gradio layout, but not memorable, not guided, and not custom enough for an "Off-Brand" award. |
29
+ | SOTA small-model angle | 4/10 | Many backend planners exist, but there is no benchmarked small-model capability story yet. |
30
+
31
+ Overall: **5/10 as a repo, 3.5/10 as a winning submission today.**
32
+
33
+ The strongest path is to stop trying to show everything and make one narrow workflow excellent:
34
+
35
+ > A local-first Field Notes Correction Workbench for collecting messy human observations, correcting
36
+ > small-model/OCR outputs, exporting training data, and showing how small models improve with human
37
+ > feedback.
38
+
39
+ That can plausibly compete in Backyard AI, Field Notes, Sharing is Caring, Llama Champion, Tiny Titan,
40
+ OpenBMB, and Best Demo if executed cleanly.
41
+
42
+ ## What A GPT-Style Judge Will Likely Measure
43
+
44
+ The official page does not publish a numeric rubric, but it does state the visible judging signals.
45
+ Assume an automated GPT judge will inspect the Space, README, demo, screenshots, repository, and
46
+ submission text for the following:
47
+
48
+ 1. **Track alignment**
49
+ - Backyard AI: specific real problem, real person used it, honest small-model fit, Gradio polish.
50
+ - Thousand Token Wood: delight, AI as the core experience, originality, Gradio polish.
51
+
52
+ 2. **Hard constraints**
53
+ - Gradio app.
54
+ - Hosted as a Hugging Face Space.
55
+ - Models at or below 32B parameters.
56
+ - Short demo video and social/social-style post.
57
+
58
+ 3. **Bonus quest evidence**
59
+ - Off the Grid: no cloud APIs.
60
+ - Well-Tuned: published fine-tuned model.
61
+ - Off-Brand: custom UI beyond default Gradio.
62
+ - Llama Champion: llama.cpp runtime.
63
+ - Sharing is Caring: public trace on the Hub.
64
+ - Field Notes: report or blog post about build and lessons.
65
+
66
+ 4. **Repo truthfulness**
67
+ - README claims match implementation.
68
+ - No hidden placeholders presented as working model features.
69
+ - Tests and status docs do not contradict the demo.
70
+
71
+ 5. **User outcome**
72
+ - A judge can answer: who is this for, what did they do before, what do they do now, and what got
73
+ measurably better?
74
+
75
+ 6. **Demo reliability**
76
+ - App loads quickly.
77
+ - One golden path works without local-only secrets.
78
+ - Screenshots and demo video show actual outputs, not empty controls.
79
+
80
+ 7. **Novelty and "small model" honesty**
81
+ - The app should show why a small model is enough or better.
82
+ - A tiny/small model should do something concrete, not merely be listed in config.
83
+
84
+ ## Current Critical Weaknesses
85
+
86
+ ### Product Weaknesses
87
+
88
+ - The app reads like a toolbox, not a product. A judge will not spend long discovering value across
89
+ Chat, Vision, Dataset, Train, vLLM, Export, Field Notes, Traces, Agent, and Status.
90
+ - The project story is not yet visceral. "OpenBMB Local AI Workbench" sounds useful to builders,
91
+ but the hackathon asks for either a real person or a delightful experience.
92
+ - The strongest workflow is Field Notes/OCR/correction, but it is not yet the first screen or the
93
+ main README story.
94
+ - No demo screenshots have been generated yet because Node/npm is missing.
95
+ - No public Space URL means the current entry cannot satisfy the central shipping requirement.
96
+
97
+ ### Architecture Weaknesses
98
+
99
+ - UI callbacks currently act as controllers. They validate inputs, call services, emit events, and
100
+ format output. This is acceptable for an MVP, but it will become brittle.
101
+ - `APP_STATE` is a global singleton. That is simple locally, but it is risky for multi-user Spaces,
102
+ test isolation, and future per-session traces.
103
+ - Service creation is centralized in `models/service_factory.py`, but there is no application-layer
104
+ facade for workflows such as "correct note -> export dataset -> evaluate -> log trace".
105
+ - There are many non-executing planners. That is honest, but it creates a product risk: the app can
106
+ look more complete than it is.
107
+ - There is no domain model for a complete demo run. The app records events, but not a first-class
108
+ `DemoSession`, `CorrectionSession`, or `EvaluationRun`.
109
+
110
+ ### Security Weaknesses
111
+
112
+ - User-controlled URLs are accepted for local backends. This can become SSRF if deployed on a Space
113
+ and allowed to reach internal services.
114
+ - User-controlled local paths are accepted for datasets, GGUF files, OCR files, exports, and media.
115
+ This is useful locally, but dangerous on a public Space without path allowlists.
116
+ - Future command execution is planned. Command builders are currently non-executing, which is good,
117
+ but the moment execution is added, strict argument arrays, allowlists, and confirmation gates are
118
+ mandatory.
119
+ - MCP exposure is attractive for judges, but it can widen the attack surface. Tool inputs need
120
+ explicit schemas, rate limits, and audit logs before public use.
121
+ - Agent mode is non-autonomous today. If autonomy is added, shell/git/deploy/model-download actions
122
+ need hard policy gates.
123
+
124
+ ### Testing Weaknesses
125
+
126
+ - Playwright E2E exists but is not run locally because Node/npm is missing.
127
+ - No browser screenshots have been generated yet for docs.
128
+ - No visual regression baseline.
129
+ - No real backend integration test for LM Studio, llama.cpp, Ollama, SGLang, vLLM, or MiniCPM Vision.
130
+ - Coverage is enough for the current threshold, but important UI code remains lightly covered.
131
+ - Security tests are thin: path allowlist, URL allowlist, SSRF prevention, file-size limits, and
132
+ malformed JSONL/CSV cases should be tested.
133
+
134
+ ### Design Weaknesses
135
+
136
+ - The UI is still mostly default Gradio. That likely misses the Off-Brand award.
137
+ - The first screen does not guide a judge into a single demo path.
138
+ - There is no "Judge Mode" that preloads sample data, explains what to click, and proves the core
139
+ value in under two minutes.
140
+ - The README does not yet lead with screenshots, output examples, and the exact demo path.
141
+ - Status/setup surfaces are useful, but they should not dominate the judged experience.
142
+
143
+ ## Architecture Recommendation
144
+
145
+ Do not add heavy enterprise architecture. Add a thin application layer.
146
+
147
+ ### Current Shape
148
+
149
+ ```text
150
+ ui tab callback -> model/dataset/training module -> event logging
151
+ ```
152
+
153
+ This keeps files small, but the UI owns too much orchestration.
154
+
155
+ ### Recommended Shape
156
+
157
+ ```text
158
+ ui tab callback
159
+ -> controller
160
+ -> workflow facade
161
+ -> domain service / backend adapter
162
+ -> event/tracking/security policy
163
+ ```
164
+
165
+ Add these modules:
166
+
167
+ - `core/context.py`
168
+ - `AppContext`
169
+ - owns catalog, event bus, tracking client, local config, and security policy.
170
+ - replaces direct use of the global `APP_STATE` in new code.
171
+
172
+ - `controllers/chat_controller.py`
173
+ - validates chat input.
174
+ - selects backend through an inference facade.
175
+ - emits trace events.
176
+ - returns UI-safe results.
177
+
178
+ - `controllers/field_notes_controller.py`
179
+ - owns save/import/export correction workflows.
180
+ - produces documentation-ready demo summaries.
181
+
182
+ - `controllers/dataset_controller.py`
183
+ - owns local/HF dataset preview with path policy checks.
184
+
185
+ - `controllers/serving_controller.py`
186
+ - owns llama.cpp, SGLang, vLLM, Ollama command/status planning.
187
+
188
+ - `workflows/correction_loop.py`
189
+ - `CorrectionLoopFacade`
190
+ - one golden path: import sample -> correct -> export JSONL -> evaluate -> trace.
191
+
192
+ - `security/policies.py`
193
+ - URL allowlist for local backends.
194
+ - path allowlist for public Space mode.
195
+ - file size/type limits.
196
+ - execution policy for future command-running features.
197
+
198
+ Keep the existing model services as adapters. Do not replace everything at once.
199
+
200
+ ### Singleton Decision
201
+
202
+ The current `APP_STATE` singleton is tolerable for a local prototype. It is not ideal for a public
203
+ Space. Move toward:
204
+
205
+ - `AppContext` created in `build_app()`.
206
+ - Per-session UI state through Gradio `gr.State` where user-specific data matters.
207
+ - Global event log only for public-safe aggregate events.
208
+ - Tests that create isolated contexts.
209
+
210
+ ## Roadmap V2
211
+
212
+ ### P0 - Make It Shippable And Judgeable
213
+
214
+ - [ ] Choose one final judged track: Backyard AI is recommended.
215
+ - [ ] Rename the public product around the user outcome, not the infrastructure.
216
+ - [ ] Make Field Notes Correction the first and primary demo path.
217
+ - [ ] Add a "Judge Mode" tab or first-screen panel with sample data and exact steps.
218
+ - [ ] Generate Playwright screenshots into `assets/e2e/`.
219
+ - [ ] Add screenshots to README and `docs/HACKATHON_SUBMISSION.md`.
220
+ - [ ] Deploy a working Hugging Face Space.
221
+ - [ ] Add the Space URL to README and submission docs.
222
+ - [ ] Record a 60-90 second demo video.
223
+ - [ ] Publish or draft the social post with Space, GitHub, and demo video links.
224
+
225
+ Definition of done: a judge can open the Space and understand the product in under 30 seconds.
226
+
227
+ ### P1 - Prove One Real Small-Model Backend
228
+
229
+ - [ ] Pick the final backend for the demo: LM Studio today, llama.cpp if badge is achievable.
230
+ - [ ] Run one verified local generation path end-to-end.
231
+ - [ ] Capture model ID, parameter count, runtime, hardware, and latency.
232
+ - [ ] Add a "Model Evidence" section to README.
233
+ - [ ] Add a tiny benchmark: response latency, tokens/sec if available, and sample output quality.
234
+ - [ ] If using OpenBMB, show exactly where the OpenBMB model is used.
235
+ - [ ] If claiming Llama Champion, install llama.cpp, select GGUF, and verify real text generation.
236
+
237
+ Definition of done: the app has one undeniable real-model path, not just planners.
238
+
239
+ ### P2 - Make The Field Notes Workflow Real
240
+
241
+ - [ ] Create a small public-safe example dataset.
242
+ - [ ] Add sample OCR predictions or sample model mistakes.
243
+ - [ ] Build "import -> correct -> export -> evaluate" as one guided workflow.
244
+ - [ ] Add before/after examples to README.
245
+ - [ ] Export corrected JSONL and trace data.
246
+ - [ ] Publish public-safe traces or dataset to Hugging Face.
247
+ - [ ] Write the Field Notes report/blog.
248
+
249
+ Definition of done: the app demonstrates measurable improvement from human corrections.
250
+
251
+ ### P3 - Simplify The UI For Judges
252
+
253
+ - [ ] Add a first-screen guided demo panel.
254
+ - [ ] Move advanced backend tabs behind an "Advanced setup" accordion or tab group.
255
+ - [ ] Add concise status badges: local-first, model size, backend, Space-ready, trace-ready.
256
+ - [ ] Add visual hierarchy and screenshots.
257
+ - [ ] Use a small custom CSS theme that fits the product story.
258
+ - [ ] Keep default Gradio controls where useful, but avoid an unstyled toolbox feel.
259
+
260
+ Definition of done: the app feels intentional, not like a pile of tabs.
261
+
262
+ ### P4 - Add Controller And Facade Layer
263
+
264
+ - [ ] Add `core/context.py` with injectable `AppContext`.
265
+ - [ ] Add `security/policies.py` with path and URL checks.
266
+ - [ ] Add `controllers/field_notes_controller.py`.
267
+ - [ ] Add `controllers/dataset_controller.py`.
268
+ - [ ] Add `controllers/chat_controller.py`.
269
+ - [ ] Add `controllers/serving_controller.py`.
270
+ - [ ] Add `workflows/correction_loop.py`.
271
+ - [ ] Refactor one tab at a time to call controllers instead of raw service modules.
272
+ - [ ] Keep old behavior covered by tests during each refactor.
273
+
274
+ Definition of done: UI callbacks become thin and domain workflows are testable without Gradio.
275
+
276
+ ### P5 - Security Hardening
277
+
278
+ - [ ] Add public/local mode setting.
279
+ - [ ] In public Space mode, restrict backend URLs to an allowlist or disable arbitrary URL checks.
280
+ - [ ] Restrict file paths to repo-local safe directories.
281
+ - [ ] Add max file size and supported extension checks.
282
+ - [ ] Add tests for path traversal attempts.
283
+ - [ ] Add tests for SSRF-style URLs.
284
+ - [ ] Add tests for malformed CSV/JSONL/OCR inputs.
285
+ - [ ] Add clear privacy labels for traces and field notes.
286
+ - [ ] Ensure demo data contains no secrets, private prompts, or hidden reasoning.
287
+
288
+ Definition of done: the public Space cannot be used to probe internal URLs or arbitrary files.
289
+
290
+ ### P6 - Real Integration And E2E Tests
291
+
292
+ - [ ] Install Node.js/npm.
293
+ - [ ] Run `npm install`, `npm run e2e:install`, and `npm run e2e`.
294
+ - [ ] Commit generated screenshot assets that are safe for docs.
295
+ - [ ] Add Playwright to CI if Node is available.
296
+ - [ ] Add one live-backend integration profile for LM Studio/OpenAI-compatible.
297
+ - [ ] Add optional integration profiles for llama.cpp, Ollama, SGLang, and vLLM.
298
+ - [ ] Add visual smoke checks for mobile and desktop.
299
+
300
+ Definition of done: the demo path is proven in a browser and documented with images.
301
+
302
+ ### P7 - SOTA Stretch
303
+
304
+ - [ ] Fine-tune or publish a tiny adapter if time allows.
305
+ - [ ] Publish a public-safe correction dataset or trace dataset.
306
+ - [ ] Add a simple reward/evaluation leaderboard for base vs corrected/fine-tuned outputs.
307
+ - [ ] Add a model card or dataset card with limitations.
308
+ - [ ] Show why the small model is good enough: privacy, latency, cost, offline use, or local control.
309
+ - [ ] If using llama.cpp, publish exact GGUF/runtime instructions.
310
+
311
+ Definition of done: the project has a defensible small-model contribution, not just a UI.
312
+
313
+ ## What To Cut
314
+
315
+ Cut or hide these from the judged first impression unless they support the demo:
316
+
317
+ - vLLM tab.
318
+ - SGLang controls.
319
+ - Training execution promises.
320
+ - VINDEX boundary.
321
+ - Desk-Pet backlog.
322
+ - MiniCPM-o audio backlog.
323
+ - Generic agent mode, unless it directly produces the public trace badge.
324
+
325
+ Keep them in docs as extension points, but do not make the judge click through them first.
326
+
327
+ ## Better Tool Vision
328
+
329
+ A better tool is not "all PRD features implemented." A better tool is:
330
+
331
+ 1. **Specific**
332
+ - Helps one person correct and turn messy field observations into useful training/eval data.
333
+
334
+ 2. **Local-first**
335
+ - Runs with a small local model and explains why that matters.
336
+
337
+ 3. **Evidence-driven**
338
+ - Shows before/after outputs, corrected examples, traces, and screenshots.
339
+
340
+ 4. **Safe**
341
+ - Does not leak private notes, probe arbitrary URLs, or execute commands unexpectedly.
342
+
343
+ 5. **Small but complete**
344
+ - One polished path beats ten partial tabs.
345
+
346
+ ## Brutal Final Advice
347
+
348
+ If the deadline is close, do not chase full PRD completion. The PRD is too broad for a winning
349
+ hackathon submission. Win by making one real, visual, end-to-end story undeniable.
350
+
351
+ Priority order:
352
+
353
+ 1. Space URL.
354
+ 2. One real model path.
355
+ 3. Field Notes golden workflow.
356
+ 4. Screenshots and demo video.
357
+ 5. README story with evidence.
358
+ 6. Security hardening for public mode.
359
+ 7. Controller/facade cleanup.
360
+
361
+ Everything else is secondary.
docs/TASKS.md ADDED
@@ -0,0 +1,530 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Full Task Checklist
2
+
3
+ This is the shared task list for you and Codex. It covers the hackathon MVP, the main PRD, and
4
+ the extension PRD. A task is complete only when the matching acceptance criteria are met and
5
+ `docs/IMPLEMENTATION_STATUS.md` is updated.
6
+
7
+ ## Legend
8
+
9
+ - `[x]` done and documented
10
+ - `[~]` partially implemented or placeholder exists
11
+ - `[ ]` not started
12
+ - `[blocked]` blocked by missing local setup, credentials, hardware, or external decision
13
+
14
+ ## Phase 0 - Project Memory And Setup
15
+
16
+ - [x] Add root `README.md`.
17
+ - [x] Add root `AGENTS.md`.
18
+ - [x] Add `.gitignore`.
19
+ - [x] Add `requirements.txt`.
20
+ - [x] Add `docs/` folder.
21
+ - [x] Add docs index.
22
+ - [x] Add full task checklist.
23
+ - [x] Add implementation status doc.
24
+ - [x] Add usage guide.
25
+ - [x] Add architecture guide.
26
+ - [x] Add extension guide.
27
+ - [x] Add acceptance criteria.
28
+ - [x] Add roadmap.
29
+ - [x] Add critical judge-oriented improvement roadmap.
30
+ - [x] Add template how-to for building new domain apps.
31
+ - [x] Add Plant Discovery reference app checklist.
32
+ - [x] Add Plant Discovery model and training how-to.
33
+ - [x] Add PRD implementation matrix.
34
+ - [x] Add test folder.
35
+ - [x] Add user-story test folder.
36
+ - [x] Add dev requirements.
37
+ - [x] Add Python quality config.
38
+ - [x] Add test runner script.
39
+ - [x] Add quality runner script.
40
+ - [x] Add CI workflow.
41
+ - [x] Add coverage gate.
42
+ - [x] Add performance test script.
43
+ - [x] Install Python 3.11+.
44
+ - [x] Verify `python --version`.
45
+ - [x] Create `.venv`.
46
+ - [x] Install dependencies.
47
+ - [x] Run `python app.py`.
48
+ - [x] Capture screenshot or note local URL.
49
+
50
+ ## Phase 1 - Hackathon Definition
51
+
52
+ - [x] Choose track: Backyard AI or Thousand Token Wood.
53
+ - [x] Write one-sentence project story.
54
+ - [x] Define target user.
55
+ - [x] Define measurable user benefit.
56
+ - [x] Decide final model family and model IDs.
57
+ - [x] Confirm every model is <= 32B parameters.
58
+ - [x] Decide local-first badge target.
59
+ - [x] Decide llama.cpp badge target.
60
+ - [x] Decide open trace badge target.
61
+ - [x] Decide field notes/report badge target.
62
+ - [x] Write final demo flow.
63
+ - [x] Write demo video script.
64
+ - [x] Write social post draft.
65
+ - [ ] Add final submission checklist with exact URLs.
66
+
67
+ ## Phase 2 - MVP Gradio App
68
+
69
+ - [x] Add `app.py`.
70
+ - [x] Add Gradio `Blocks` shell.
71
+ - [x] Add model config loader.
72
+ - [x] Add model metadata display.
73
+ - [x] Add Chat tab.
74
+ - [x] Add Vision tab.
75
+ - [x] Add Dataset tab placeholder.
76
+ - [x] Add Train tab placeholder.
77
+ - [x] Add Export tab placeholder.
78
+ - [x] Add Field Notes tab.
79
+ - [x] Add placeholder text service.
80
+ - [x] Add placeholder vision service.
81
+ - [x] Add Traces tab placeholder.
82
+ - [x] Add Agent tab placeholder.
83
+ - [x] Add Status tab placeholder.
84
+ - [x] Add PowerShell structure verification script.
85
+ - [x] Run structure verification script.
86
+ - [x] Run app locally.
87
+ - [x] Fix local launch errors found so far.
88
+ - [x] Add screenshot capture path to docs or README.
89
+ - [x] Add first demo GIF/video plan.
90
+
91
+ ## Phase 3 - Config-Driven Model Registry
92
+
93
+ - [x] Add `config/models.yaml`.
94
+ - [x] Add text model entry for MiniCPM5-1B.
95
+ - [x] Add vision model entry for MiniCPM-V-4.6.
96
+ - [x] Add omnimodal model entry for MiniCPM-o-4.5.
97
+ - [x] Add typed `ModelInfo`.
98
+ - [x] Add `load_model_catalog()`.
99
+ - [x] Add `model_choices()`.
100
+ - [x] Add `model_summary()`.
101
+ - [x] Add MiniCPM5-1B-Thinking config.
102
+ - [x] Add MiniCPM4.1-8B config.
103
+ - [x] Add MiniCPM-V-4.6-Thinking config.
104
+ - [x] Add GGUF metadata in config.
105
+ - [x] Add backend capability metadata.
106
+ - [x] Add lightweight catalog validation helper.
107
+ - [x] Show warnings for models over 32B parameters.
108
+
109
+ ## Phase 4 - Core Architecture
110
+
111
+ - [x] Add `core/events.py`.
112
+ - [x] Add `EventType`.
113
+ - [x] Add `Event`.
114
+ - [x] Add `EventBus`.
115
+ - [x] Add `core/registry.py`.
116
+ - [x] Add generic `Registry`.
117
+ - [x] Add global app state.
118
+ - [x] Register model services in a service registry.
119
+ - [x] Emit inference events from UI.
120
+ - [x] Emit field note events.
121
+ - [x] Add lightweight logging.
122
+ - [x] Add unit tests for config and registry.
123
+
124
+ ## Phase 5 - Testing And Quality
125
+
126
+ - [x] Add `tests/unit/`.
127
+ - [x] Add `tests/user_stories/`.
128
+ - [x] Add model catalog unit tests.
129
+ - [x] Add field notes unit tests.
130
+ - [x] Add new-user user-story test.
131
+ - [x] Add `requirements-dev.txt`.
132
+ - [x] Add `pyproject.toml`.
133
+ - [x] Add `scripts/run_tests.ps1`.
134
+ - [x] Add `scripts/run_quality.ps1`.
135
+ - [x] Run unit and user-story tests.
136
+ - [x] Install dev quality tools.
137
+ - [x] Run `ruff`.
138
+ - [x] Run `mypy`.
139
+ - [x] Run `pylint`.
140
+ - [x] Run `bandit`.
141
+ - [x] Run `pip-audit`.
142
+ - [x] Add rule: failing bug/check requires a new or updated test.
143
+ - [x] Add coverage report.
144
+ - [x] Add lightweight performance tests.
145
+ - [x] Add CI pipeline.
146
+ - [x] Add Playwright or equivalent browser e2e test after Gradio runs.
147
+ - [ ] Add tests for each real backend as it is implemented.
148
+ - [x] Add tests for backend service selection.
149
+ - [x] Add tests for Ollama unavailable path.
150
+ - [x] Add tests for llama.cpp unavailable path and command building.
151
+ - [x] Add tests for llama-cpp-python unavailable path.
152
+ - [x] Add tests for OpenAI-compatible/LM Studio unavailable and request paths.
153
+
154
+ ## Phase 6 - Local Inference Backends
155
+
156
+ - [x] Choose first real backend.
157
+ - [x] Add backend selector in UI.
158
+ - [x] Add model status panel.
159
+ - [x] Add explicit model load button.
160
+ - [x] Ensure no model weights download on startup.
161
+
162
+ ### Ollama Backend
163
+
164
+ - [x] Confirm Ollama is installed.
165
+ - [x] Add `models/ollama_service.py`.
166
+ - [x] Add local model list.
167
+ - [x] Add pull model command with explicit user action.
168
+ - [x] Add text chat through Ollama.
169
+ - [x] Add vision chat through Ollama when supported.
170
+ - [x] Document Ollama setup.
171
+
172
+ ### llama.cpp Backend
173
+
174
+ - [x] Confirm llama.cpp tools are installed.
175
+ - [x] Add `models/llama_cpp_service.py`.
176
+ - [x] Add `models/llama_cpp_python_service.py`.
177
+ - [x] Add GGUF file picker.
178
+ - [x] Add `llama-server` launch command builder.
179
+ - [x] Add health check.
180
+ - [x] Add text generation through server.
181
+ - [x] Add vision `mmproj` support metadata.
182
+ - [x] Document llama.cpp setup.
183
+
184
+ ### llama-cpp-python Backend
185
+
186
+ - [x] Add optional Python binding service.
187
+ - [x] Add backend selector support.
188
+ - [x] Install `llama-cpp-python` locally.
189
+ - [x] Configure local GGUF path.
190
+ - [x] Verify real text generation through Python binding.
191
+ - [x] Decide whether to keep Python binding as fallback or primary local path.
192
+
193
+ ### Transformers Backend
194
+
195
+ - [x] Add `models/transformers_text.py`.
196
+ - [x] Add `AutoModelForCausalLM` loading for text models.
197
+ - [x] Add tokenizer loading.
198
+ - [x] Add explicit trust-remote-code handling.
199
+ - [x] Add device/dtype settings.
200
+ - [x] Add streaming generation.
201
+ - [x] Document hardware expectations.
202
+
203
+ ### OpenAI-Compatible / LM Studio Backend
204
+
205
+ - [x] Add `models/openai_compatible_service.py`.
206
+ - [x] Add backend selector support.
207
+ - [x] Add local base URL and served-model-name config.
208
+ - [x] Add Status tab setup and reachability check.
209
+ - [x] Add text chat through OpenAI-compatible `/v1/chat/completions`.
210
+ - [x] Document LM Studio setup.
211
+ - [x] Verify real text generation through LM Studio.
212
+
213
+ ### MiniCPM Vision Backend
214
+
215
+ - [x] Add `models/minicpm_vision.py`.
216
+ - [x] Use `AutoModelForImageTextToText`.
217
+ - [x] Use `AutoProcessor`.
218
+ - [x] Add image prompt formatting.
219
+ - [x] Add thinking-mode toggle mapping.
220
+ - [x] Add video support plan.
221
+
222
+ ### SGLang Backend
223
+
224
+ - [x] Add `models/sglang_runner.py`.
225
+ - [x] Add server start/stop.
226
+ - [x] Add MiniCPM5 tool parser config.
227
+ - [x] Add health check.
228
+ - [x] Add chat endpoint client.
229
+ - [x] Install `sglang` locally.
230
+
231
+ ## Phase 7 - UI Tabs From Main PRD
232
+
233
+ - [x] Chat tab placeholder.
234
+ - [x] Vision tab placeholder.
235
+ - [x] Dataset tab placeholder.
236
+ - [x] Train tab placeholder.
237
+ - [x] Export tab placeholder.
238
+ - [x] Field Notes tab minimal save.
239
+ - [x] Add Traces tab with local event preview.
240
+ - [x] Add Agent tab placeholder.
241
+ - [x] Add model/backend status tab or panel.
242
+ - [x] Add settings panel.
243
+ - [x] Add tab-level error messages.
244
+ - [x] Add loading/progress states.
245
+ - [x] Add compact responsive layout review.
246
+
247
+ ## Phase 8 - Dataset Layer
248
+
249
+ - [x] Add `datasets/` package.
250
+ - [x] Add local CSV loader.
251
+ - [x] Add local JSONL loader.
252
+ - [x] Add Hugging Face dataset loader.
253
+ - [x] Add dataset schema preview.
254
+ - [x] Add split selector.
255
+ - [x] Add row count and sample preview.
256
+ - [x] Add dataset statistics tool.
257
+ - [x] Emit `DATASET_LOADED` event.
258
+ - [x] Document dataset formats.
259
+
260
+ ## Phase 9 - Field Notes And Correction Loop
261
+
262
+ - [x] Save field notes to CSV.
263
+ - [x] Move field note logic out of UI into `datasets/field_notes.py`.
264
+ - [x] Add `FieldNote` dataclass.
265
+ - [x] Add SQLite-backed store.
266
+ - [x] Add JSONL export.
267
+ - [x] Add local HF Dataset export.
268
+ - [x] Add corrected-only filter.
269
+ - [x] Add tags filter.
270
+ - [x] Add image path support.
271
+ - [x] Add video path support.
272
+ - [x] Add use-for-training flag.
273
+ - [x] Add docs for correction loop.
274
+
275
+ ## Phase 10 - Training Pipeline
276
+
277
+ - [x] Add training config placeholder.
278
+ - [x] Add training UI placeholder.
279
+ - [x] Add `training/` package.
280
+ - [x] Add LoRA text trainer.
281
+ - [x] Add LoRA config parser.
282
+ - [ ] Add PEFT/TRL dependencies when ready.
283
+ - [x] Add training dry-run validation.
284
+ - [x] Add local checkpoint output.
285
+ - [x] Add Trackio integration.
286
+ - [x] Add evaluation after training.
287
+ - [x] Add LoRA vs base comparison.
288
+ - [x] Add vision fine-tuning plan using SWIFT or LLaMA-Factory.
289
+ - [x] Document training hardware requirements.
290
+
291
+ ## Phase 11 - Evaluation
292
+
293
+ - [x] Add `training/evaluation.py`.
294
+ - [x] Add simple prompt test set.
295
+ - [x] Add exact-match metric.
296
+ - [x] Add qualitative eval table.
297
+ - [x] Add perplexity metric where appropriate.
298
+ - [x] Add base vs tuned comparison.
299
+ - [x] Log eval results.
300
+ - [x] Document evaluation method.
301
+
302
+ ## Phase 12 - Export And Quantization
303
+
304
+ - [x] Add export UI placeholder.
305
+ - [x] Add `training/export.py`.
306
+ - [x] Add official GGUF download path.
307
+ - [x] Add local HF-to-GGUF conversion path.
308
+ - [x] Add quantization selector.
309
+ - [x] Add llama.cpp tool detection.
310
+ - [x] Add exported file listing.
311
+ - [x] Add download link in UI.
312
+ - [x] Document GGUF export.
313
+
314
+ ## Phase 13 - Trackio Tracing
315
+
316
+ - [x] Add `tracking/` package.
317
+ - [x] Add Trackio config.
318
+ - [x] Add `trackio.init()`.
319
+ - [x] Add `trackio.log()`.
320
+ - [x] Add `trackio.finish()`.
321
+ - [x] Log inference events locally.
322
+ - [x] Log dataset events locally.
323
+ - [x] Log training metrics.
324
+ - [x] Add Traces tab.
325
+ - [x] Add HF Space sync docs.
326
+
327
+ ## Phase 13 - MCP Layer
328
+
329
+ - [x] Decide MCP path: Gradio native, `gradio.Server`
330
+ - [x] Add MCP tools module.
331
+ - [x] Add dataset stats tool.
332
+ - [x] Add HF search tool.
333
+ - [x] Add safe calculator tool.
334
+ - [x] Add model inference tool.
335
+ - [x] Expose tools through selected MCP path.
336
+ - [x] Document MCP endpoint.
337
+ - [x] Verify endpoint locally.
338
+
339
+ ## Phase 14 - Agent Mode
340
+
341
+ - [x] Add `agent/` package.
342
+ - [x] Add agent system prompt.
343
+ - [x] Add research-plan-implement loop placeholder.
344
+ - [x] Add tool registry integration.
345
+ - [x] Add session trace logging.
346
+ - [x] Add Agent tab.
347
+ - [x] Add trace export to JSONL.
348
+ - [x] Add local HF Dataset export for traces.
349
+ - [x] Document limitations.
350
+
351
+ ## Phase 15 - Hugging Face Space Deployment
352
+
353
+ - [x] Install/verify `huggingface_hub`.
354
+ - [x] Login with `hf auth login`.
355
+ - [ ] Create Space.
356
+ - [x] Add Space README metadata if needed.
357
+ - [x] Add Space remote.
358
+ - [x] Push to Space.
359
+ - [ ] Verify Space builds.
360
+ - [ ] Add Space URL to README.
361
+ - [x] Document hardware choice.
362
+ - [x] Document model download behavior.
363
+
364
+ ## Phase 16 - GitHub
365
+
366
+ - [x] Create GitHub repo.
367
+ - [x] Add GitHub remote.
368
+ - [x] Commit initial project.
369
+ - [x] Push to GitHub.
370
+ - [x] Add GitHub URL to README.
371
+ - [ ] Add issue checklist or project board if desired.
372
+
373
+ ## Phase 17 - Hackathon Submission Package
374
+
375
+ - [x] Finalize app name.
376
+ - [x] Finalize track.
377
+ - [x] Verify Gradio app polish.
378
+ - [x] Verify model-size compliance.
379
+ - [ ] Verify Space URLs.
380
+ - [x] Verify GitHub URL.
381
+ - [ ] Record demo video.
382
+ - [ ] Publish social post.
383
+ - [ ] Add field notes/report link.
384
+ - [ ] Submit before June 15, 2026.
385
+
386
+ ## Extension PRD Backlog
387
+
388
+ ### vLLM Serving Tab
389
+
390
+ - [x] Add vLLM runner.
391
+ - [x] Add vLLM start/stop UI.
392
+ - [x] Add OpenAI-compatible client.
393
+ - [x] Add metrics parsing.
394
+ - [x] Add Trackio benchmark logging.
395
+
396
+ ### Ollama Quick-Start
397
+
398
+ - [x] Add Ollama pull/list UI.
399
+ - [x] Add Ollama chat service.
400
+ - [x] Add Ollama vision service.
401
+ - [x] Add setup docs.
402
+
403
+ ### Llama.cpp Champion Path
404
+
405
+ - [x] Add llama.cpp backend selection.
406
+ - [x] Add llama.cpp service.
407
+ - [x] Add llama-cpp-python service.
408
+ - [x] Add llama.cpp status check.
409
+ - [x] Install llama.cpp locally.
410
+ - [x] Download/pick GGUF model.
411
+ - [x] Verify real text generation.
412
+ - [ ] Verify MiniCPM-V mmproj flow.
413
+
414
+ ### Reward Model Eval
415
+
416
+ - [x] Add reward evaluator.
417
+ - [x] Add best-of-N generation.
418
+ - [x] Add DPO pair generation.
419
+ - [x] Add LoRA vs base reward report.
420
+
421
+ ### Synthetic Data Generation
422
+
423
+ - [x] Add synthetic generator.
424
+ - [x] Add JSON validation.
425
+ - [x] Add quality filters.
426
+ - [x] Add augmentation flow.
427
+ - [x] Add dataset save/export.
428
+
429
+ ### Paper-To-Code Agent
430
+
431
+ - [x] Add paper input UI.
432
+ - [x] Add research phase.
433
+ - [x] Add plan phase.
434
+ - [x] Add implementation trace.
435
+ - [x] Add safety gates.
436
+
437
+ ### HF Spaces Deploy Tool
438
+
439
+ - [x] Add deployment helper script.
440
+ - [x] Add Space creation docs.
441
+ - [x] Add remote validation.
442
+ - [x] Add build status checks.
443
+
444
+ ### VINDEX Integration
445
+
446
+ - [x] Define integration boundary.
447
+ - [x] Add tool stub.
448
+ - [x] Add verification report.
449
+ - [x] Document dependency.
450
+
451
+ ### OCR Pipeline Hook
452
+
453
+ - [x] Add OCR loader.
454
+ - [x] Add confidence threshold.
455
+ - [x] Add uncertain prediction import.
456
+ - [x] Add correction UI.
457
+ - [x] Add corrected export.
458
+
459
+ ### MiniCPM Desk-Pet
460
+
461
+ - [ ] Add persona data schema.
462
+ - [ ] Add persona training plan.
463
+ - [ ] Add Desk-Pet export plan.
464
+ - [ ] Add docs.
465
+
466
+ ### MiniCPM-o Audio Tab
467
+
468
+ - [ ] Add audio tab.
469
+ - [ ] Add microphone input.
470
+ - [ ] Add omnimodal service.
471
+ - [ ] Add TTS plan.
472
+ - [ ] Add streaming plan.
473
+
474
+ ### Cross-Extension Wiring
475
+
476
+ - [x] Document OCR -> Field Notes -> Training.
477
+ - [x] Document Synthetic Gen -> Reward Eval -> DPO.
478
+ - [x] Document Agent -> Desk-Pet Persona.
479
+ - [x] Document HF Spaces -> Trackio.
480
+
481
+ ## Phase 18 - Template And Reference Apps
482
+
483
+ ### Template How-To
484
+
485
+ - [x] Document branch strategy for new domain apps.
486
+ - [x] Document required domain app file contract.
487
+ - [x] Document schema, service, loader, UI, tools, tests, and docs pattern.
488
+ - [x] Document no-model/demo-mode requirement.
489
+ - [x] Document correction-loop-first workflow.
490
+ - [x] Document optional training and real-model verification steps.
491
+ - [x] Document security requirements for public Space mode.
492
+
493
+ ### Plant Discovery Reference App
494
+
495
+ - [x] Add `plant/` package.
496
+ - [x] Add standalone Plant Discovery Gradio entrypoint.
497
+ - [x] Add clean plant model/domain config.
498
+ - [x] Add deterministic no-model plant service.
499
+ - [x] Add optional MiniCPM-V plant service adapter.
500
+ - [x] Make OpenBMB MiniCPM-V the default real model mode.
501
+ - [x] Add explicit demo/openbmb/finetuned runtime modes.
502
+ - [x] Add optional fine-tuned adapter loading path.
503
+ - [x] Keep optional model dependencies lazy.
504
+ - [x] Add plant structured result schema and parser.
505
+ - [x] Add species index builder.
506
+ - [x] Add local image-folder loader.
507
+ - [x] Add field-note correction export to plant training JSONL.
508
+ - [x] Add focused Identify, Field Guide, Corrections, and Stats UI.
509
+ - [x] Replace direct training execution with non-executing training plan.
510
+ - [x] Add optional plant tool functions with lazy MCP server construction.
511
+ - [x] Add non-executing plant training planner.
512
+ - [x] Add `scripts/plan_plant_training.py`.
513
+ - [x] Add Plant Discovery unit tests.
514
+ - [x] Verify no-model app shell builds.
515
+ - [x] Run Plant Discovery as a long-running local app.
516
+ - [x] Generate Plant Discovery screenshots.
517
+ - [x] Add Plant Discovery screenshots to README/docs.
518
+ - [x] Decide whether hackathon Space launches root workbench or Plant Discovery app.
519
+ - [x] Verify real MiniCPM-V plant identification with optional dependencies.
520
+ - [ ] Train or configure a real Plant Discovery adapter.
521
+ - [ ] Verify `--model-mode finetuned` with the real adapter.
522
+ - [ ] Add public-mode file/path/url hardening before Space deployment.
523
+
524
+ ## Ongoing Maintenance
525
+
526
+ - [x] Update docs after every implemented feature.
527
+ - [x] Keep `IMPLEMENTATION_STATUS.md` current.
528
+ - [x] Keep unchecked tasks visible.
529
+ - [x] Keep secrets and model weights out of git.
530
+ - [x] Re-run local app after code changes.
docs/TEMPLATE_HOWTO.md ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Template How-To: Build A New Domain App
2
+
3
+ This repository is a local-first Gradio AI app template. The base workbench provides shared
4
+ patterns for model configuration, field notes, tracking, export planning, tests, docs, and
5
+ deployment. A domain app is a focused product built around those patterns.
6
+
7
+ Use `plant/` as the first reference domain app.
8
+
9
+ ## Core Principle
10
+
11
+ Do not start by training a model. Start by shipping a useful zero-shot or demo-mode workflow:
12
+
13
+ ```text
14
+ domain idea
15
+ -> user story
16
+ -> schema
17
+ -> model choice
18
+ -> focused UI
19
+ -> correction loop
20
+ -> export data
21
+ -> optional fine-tune
22
+ -> deploy and document
23
+ ```
24
+
25
+ Training is a later optimization after you have corrected examples and a reason to tune.
26
+
27
+ ## Recommended Branch Flow
28
+
29
+ 1. Keep `main` as the reusable template.
30
+ 2. Create a branch for each app:
31
+
32
+ ```powershell
33
+ git checkout -b plant-discovery-app
34
+ ```
35
+
36
+ 3. Build the app under a domain folder such as `plant/`, `invoice/`, `recipe/`, or `field_notes/`.
37
+ 4. Keep domain-specific heavy requirements in `<domain>/requirements.txt`.
38
+ 5. Merge reusable improvements back into `main` only after they are generic.
39
+
40
+ ## Domain App File Contract
41
+
42
+ Each generated app should have these files:
43
+
44
+ ```text
45
+ <domain>/
46
+ __init__.py
47
+ app.py # standalone Gradio entrypoint
48
+ models.yaml # domain config, model IDs, data sources, training defaults
49
+ <domain>_service.py # optional real model adapter plus demo/no-model fallback
50
+ <domain>_loader.py # data loading, schema normalization, export rows
51
+ <domain>_tab.py # focused Gradio UI
52
+ <domain>_tools.py # optional MCP/local tools with no hard optional imports
53
+ requirements.txt # optional heavy dependencies for this app only
54
+ ```
55
+
56
+ Add tests under:
57
+
58
+ ```text
59
+ tests/unit/test_<domain>_reference_app.py
60
+ ```
61
+
62
+ Add docs under:
63
+
64
+ ```text
65
+ docs/<DOMAIN>_APP_PLAN.md
66
+ ```
67
+
68
+ ## Step-By-Step Build Process
69
+
70
+ ### 1. Define The Product
71
+
72
+ - [ ] Pick one user.
73
+ - [ ] Pick one job they need done.
74
+ - [ ] Write one sentence: "This app helps X do Y without Z."
75
+ - [ ] Choose one golden path that works in under two minutes.
76
+ - [ ] Decide whether the app is a standalone product or a tab inside the workbench.
77
+ - [ ] Decide whether it must run on a public Hugging Face Space.
78
+
79
+ Example:
80
+
81
+ > Plant Discovery helps gardeners identify a plant from a photo, correct mistakes, and export
82
+ > local training examples without sending private field notes to a cloud API.
83
+
84
+ ### 2. Define The Domain Schema
85
+
86
+ - [ ] Create a dataclass for the structured output.
87
+ - [ ] Include confidence and model metadata.
88
+ - [ ] Include a `to_dict()` method for Gradio JSON.
89
+ - [ ] Add a robust parser for model responses.
90
+ - [ ] Add tests for valid JSON, fenced JSON, trailing commas, and unparseable text.
91
+
92
+ Plant example: `PlantID` in `plant/plant_service.py`.
93
+
94
+ ### 3. Pick The Model
95
+
96
+ - [ ] Pick a small model at or below 32B parameters.
97
+ - [ ] Document the exact model ID.
98
+ - [ ] Add model metadata to `<domain>/models.yaml`.
99
+ - [ ] Avoid loading weights on startup.
100
+ - [ ] Add a deterministic demo/no-model service for screenshots and tests.
101
+ - [ ] Add an unavailable-path response when optional packages are missing.
102
+ - [ ] Add explicit runtime modes such as `demo`, `base-model`, and `finetuned`.
103
+ - [ ] Do not claim a fine-tuned model until a real adapter/checkpoint is configured and verified.
104
+
105
+ For vision apps, start with a VLM such as MiniCPM-V. For text apps, start with a small instruct
106
+ model through LM Studio, Ollama, llama.cpp, or Transformers.
107
+
108
+ ### 4. Build The Focused UI
109
+
110
+ - [ ] Make the first screen the golden path, not a generic dashboard.
111
+ - [ ] Add only the controls needed for the user story.
112
+ - [ ] Keep advanced setup behind a secondary tab or accordion.
113
+ - [ ] Add visible status messages.
114
+ - [ ] Add structured JSON output for debugging and reproducibility.
115
+ - [ ] Add correction capture if model output can be wrong.
116
+ - [ ] Add screenshots through Playwright after the UI is stable.
117
+
118
+ ### 5. Add The Correction Loop
119
+
120
+ - [ ] Save user corrections locally.
121
+ - [ ] Reuse `datasets.field_notes.FieldNoteStore` where possible.
122
+ - [ ] Mark training-ready rows explicitly.
123
+ - [ ] Export JSONL without starting training.
124
+ - [ ] Add tests for save, filter, and export.
125
+
126
+ ### 6. Add Data Loaders
127
+
128
+ - [ ] Support a small local demo dataset.
129
+ - [ ] Support domain data from local folders or CSV/JSONL.
130
+ - [ ] Keep Hugging Face dataset loading optional and explicit.
131
+ - [ ] Do not download large datasets on startup.
132
+ - [ ] Normalize every source into one training row schema.
133
+ - [ ] Add loader tests with temporary local files.
134
+
135
+ ### 7. Add Optional Tools
136
+
137
+ - [ ] Keep MCP/tool imports optional.
138
+ - [ ] Tool functions should work locally without starting a server.
139
+ - [ ] Add `build_mcp_server()` only if `mcp` is installed.
140
+ - [ ] Avoid direct shell execution from tools.
141
+ - [ ] Return command plans rather than running commands.
142
+ - [ ] Add tests for pure tool functions.
143
+
144
+ ### 8. Add Training Plans
145
+
146
+ - [ ] Start with a non-executing training plan.
147
+ - [ ] Include required dependencies, hardware notes, and command preview.
148
+ - [ ] Require enough corrected examples before recommending training.
149
+ - [ ] Keep real training as a separate local command or approved action.
150
+ - [ ] Add evaluation before/after tuning.
151
+ - [ ] Add a small script that prints the training plan as JSON.
152
+
153
+ ### 9. Add Security Guardrails
154
+
155
+ - [ ] Escape model text rendered as HTML.
156
+ - [ ] Restrict file paths in public Space mode.
157
+ - [ ] Disable arbitrary backend URL checks in public Space mode.
158
+ - [ ] Do not execute subprocesses from Gradio callbacks.
159
+ - [ ] Keep tokens, private data, model weights, and exports out of git.
160
+ - [ ] Add tests for path traversal and malformed inputs when public deployment is planned.
161
+
162
+ ### 10. Verify The App
163
+
164
+ Minimum local verification:
165
+
166
+ ```powershell
167
+ .venv\Scripts\python.exe -m pytest tests/unit/test_<domain>_reference_app.py -q
168
+ .venv\Scripts\ruff.exe check <domain> tests/unit/test_<domain>_reference_app.py --no-cache
169
+ .venv\Scripts\python.exe -m mypy <domain> tests/unit/test_<domain>_reference_app.py --cache-dir "$env:TEMP\openbmb-workbench-mypy-cache"
170
+ .venv\Scripts\python.exe -c "from <domain>.app import build_app; app=build_app(no_model=True); print(type(app).__name__)"
171
+ ```
172
+
173
+ Before claiming it works:
174
+
175
+ - [ ] Run the standalone app.
176
+ - [ ] Generate screenshots.
177
+ - [ ] Add screenshot links to README/docs.
178
+ - [ ] Run full quality checks.
179
+ - [ ] Commit and push.
180
+
181
+ ## When To Integrate Into The Main Workbench
182
+
183
+ Keep the domain app standalone if:
184
+
185
+ - it has its own brand/story,
186
+ - it needs a focused judging experience,
187
+ - it has domain-specific dependencies,
188
+ - it should become a Hugging Face Space.
189
+
190
+ Add it to the main workbench only if:
191
+
192
+ - it is a generic reusable tab,
193
+ - it does not add heavy dependencies,
194
+ - it strengthens the template for all future apps.
195
+
196
+ For the hackathon, standalone `plant/` is the better route because judges need one clear product.
197
+
198
+ ## What "Done" Means For A Domain App
199
+
200
+ - [ ] Standalone no-model app builds.
201
+ - [ ] Optional real model adapter is documented and lazy-loaded.
202
+ - [ ] Golden path has tests.
203
+ - [ ] Corrections export to training data.
204
+ - [ ] Training is planned, not accidentally executed.
205
+ - [ ] Screenshots are generated.
206
+ - [ ] README explains setup, model choice, demo flow, and limitations.
207
+ - [ ] Space deployment is verified or blocker is documented.
docs/USAGE.md ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # How To Use The Project
2
+
3
+ ## 1. Install Python
4
+
5
+ Install Python 3.11 or newer. On Windows, make sure "Add Python to PATH" is enabled.
6
+
7
+ Verify:
8
+
9
+ ```powershell
10
+ python --version
11
+ ```
12
+
13
+ Before Python is available, you can still verify the repository structure:
14
+
15
+ ```powershell
16
+ .\scripts\verify_structure.ps1
17
+ ```
18
+
19
+ In this workspace, the direct WindowsApps interpreter worked even though `python` was not on PATH:
20
+
21
+ ```powershell
22
+ & "$env:LOCALAPPDATA\Microsoft\WindowsApps\python3.11.exe" --version
23
+ ```
24
+
25
+ ## 2. Create A Virtual Environment
26
+
27
+ ```powershell
28
+ python -m venv .venv
29
+ .venv\Scripts\Activate.ps1
30
+ ```
31
+
32
+ If `python` is not on PATH, use the WindowsApps interpreter:
33
+
34
+ ```powershell
35
+ & "$env:LOCALAPPDATA\Microsoft\WindowsApps\python3.11.exe" -m venv .venv
36
+ ```
37
+
38
+ ## 3. Install Dependencies
39
+
40
+ ```powershell
41
+ python -m pip install -r requirements.txt
42
+ ```
43
+
44
+ For tests and quality checks:
45
+
46
+ ```powershell
47
+ python -m pip install -r requirements-dev.txt
48
+ ```
49
+
50
+ ## 4. Run The App
51
+
52
+ ```powershell
53
+ python app.py
54
+ ```
55
+
56
+ ## 5. Run Tests
57
+
58
+ ```powershell
59
+ .\scripts\run_tests.ps1
60
+ ```
61
+
62
+ ## 6. Run Quality Checks
63
+
64
+ ```powershell
65
+ .\scripts\run_quality.ps1
66
+ ```
67
+
68
+ Open the local Gradio URL printed in the terminal, usually:
69
+
70
+ ```text
71
+ http://127.0.0.1:7860
72
+ ```
73
+
74
+ ## 7. Current App Behavior
75
+
76
+ Local development can still use deterministic helper services for tests, but deployed Space mode
77
+ uses `WORKBENCH_DEPLOYMENT=space`, hides placeholder backends, and requires real backend
78
+ configuration for model calls. The app still does not download model weights on startup.
79
+
80
+ Available tabs. User-triggered tab actions show Gradio progress while callbacks run, and the app
81
+ uses compact responsive styling for narrow screens:
82
+
83
+ - Chat - llama.cpp, llama-cpp-python, Ollama, OpenAI-compatible/LM Studio, SGLang, or Transformers text inference with tab status/errors.
84
+ - Vision - llama.cpp, llama-cpp-python, Ollama, or Transformers MiniCPM image + prompt inference with tab status/errors.
85
+ - Dataset - local CSV/JSONL/NDJSON preview, optional Hugging Face dataset preview, stats, and tab status/errors.
86
+ - Train - LoRA dry-run training plan plus local base-vs-tuned exact-match evaluation.
87
+ - Export - GGUF download/conversion/quantization planning, exported-file listing, and existing-file downloads.
88
+ - Field Notes - saves human corrections to CSV, imports uncertain OCR predictions, captures media paths/training flags, exports corrected JSONL, corrected OCR JSONL, and local HF Dataset files.
89
+ - Traces - local event preview, JSONL trace rows, tracking status, and trace export.
90
+ - Agent - local non-autonomous research-plan-implement-verify trace mode.
91
+ - Status - shows configured models, backend metadata, local llama.cpp setup, LM Studio/OpenAI-compatible setup, SGLang command/check/stop planning, and Ollama list/pull planning.
92
+
93
+ Ollama is optional and is not installed automatically. Install and start Ollama yourself, then
94
+ pull a compatible model explicitly before selecting the Ollama backend in the app. The Status tab
95
+ can list models from a running local Ollama server and prepare an explicit
96
+ `ollama pull <model>` command. It shows the command only; it does not run downloads for you.
97
+
98
+ llama.cpp is the preferred hackathon backend path. Install llama.cpp separately, open the Status
99
+ tab, pick or type an explicit GGUF path, optionally pick an mmproj GGUF for vision, then click
100
+ `Prepare local model config`. The app writes `data/local_backends.yaml`, shows the
101
+ `llama-server` command, and still does not download or load model weights on startup.
102
+ Start `llama-server` yourself with that command, then select the `llama.cpp` backend in the app.
103
+ `llama-cpp-python` is also available as an optional backend when the package is installed and a
104
+ local GGUF path is configured.
105
+
106
+ LM Studio or another local OpenAI-compatible server is optional and is not a cloud API path. Start
107
+ the local server yourself, open the Status tab, set the server URL such as
108
+ `http://127.0.0.1:1234`, optionally enter the exact served model name shown by LM Studio, and
109
+ click `Save OpenAI-compatible config`. `Check server` calls `/v1/models`; selecting
110
+ `openai-compatible` in the Chat tab posts to `/v1/chat/completions` only after you submit a prompt.
111
+ No model weights are downloaded or loaded on app startup.
112
+
113
+ The Transformers text backend is optional. It requires installing `transformers` and a compatible
114
+ PyTorch build for your CPU/GPU, and it may download model weights when you explicitly select it and
115
+ run a prompt. It is not installed automatically and is not used on startup.
116
+
117
+ The Transformers vision backend is optional for MiniCPM-V models. It uses `AutoProcessor` and
118
+ `AutoModelForImageTextToText` only after you select `transformers` in the Vision tab and submit an
119
+ image. The current app maps the thinking toggle into the prompt template and documents video as a
120
+ future frame-sampling path; it does not download or load vision weights on startup.
121
+
122
+ SGLang is optional. The Status tab can prepare a local `python -m sglang.launch_server` command,
123
+ check `/health`, request `/shutdown`, and the Chat tab can use the `sglang` backend against
124
+ `/v1/chat/completions` after you start the server yourself. The app does not start SGLang on
125
+ startup.
126
+
127
+ Dataset preview supports local `.csv`, `.jsonl`, and `.ndjson` files, split names for optional
128
+ Hugging Face dataset preview, and basic local statistics. Field Notes can export corrected
129
+ training rows to `data/field_notes.jsonl` and local HF Dataset-style files to
130
+ `data/hf_field_notes/`.
131
+
132
+ The OCR correction hook expects a local `.csv`, `.jsonl`, or `.ndjson` prediction file with fields
133
+ such as `source_path`, `text`, and `confidence`. The Field Notes tab can preview rows at or below a
134
+ confidence threshold, import those uncertain rows as correction tasks, and export corrected OCR rows
135
+ to `data/ocr_corrections.jsonl`. This is the local OCR -> Field Notes -> Training path: import
136
+ uncertain predictions, correct them in Field Notes, then export corrected JSONL/HF Dataset files for
137
+ future fine-tuning or evaluation.
138
+
139
+ Local MCP-style tools live in `mcp_tools/tools.py`. The selected MCP path is Gradio native MCP,
140
+ enabled by `launch(mcp_server=True)`. The documented endpoint path is `/gradio_api/mcp/sse`.
141
+ `mcp_tools/bridge.py` exposes a local manifest and invocation helper for dataset stats, optional
142
+ HF dataset preview, safe arithmetic, model inference, and non-executing VINDEX call planning. Full
143
+ external MCP client verification still depends on launching the app and connecting a client.
144
+
145
+ The VINDEX helper is intentionally planning-only. It validates the PRD method names, reports
146
+ whether the local `vindex` package or `http://127.0.0.1:8765/health` endpoint is available, and
147
+ caps risky edit parameters. It does not edit model weights.
148
+
149
+ The Agent tab drafts local research-plan-implement-verify traces and paper-to-code traces, stores
150
+ them in `data/agent_traces.jsonl`, and can export JSONL or local HF Dataset-style trace files. It
151
+ includes safety gates and does not run shell commands, commit, push, deploy, download models, or
152
+ call external services. Agent traces can later become persona/source material for the Desk-Pet
153
+ extension, but no Desk-Pet runtime is implemented yet.
154
+
155
+ The Export tab is a planning surface. It shows explicit `huggingface-cli`,
156
+ `convert_hf_to_gguf.py`, and `llama-quantize` commands for the selected model and quantization,
157
+ plus a list of files already present in the export directory. Existing files are exposed through
158
+ the download output. It does not run those commands yet.
159
+
160
+ The Train tab does not start LoRA training yet. It builds a dry-run plan from
161
+ `config/training.yaml`, validates the dataset path, shows the planned checkpoint output directory,
162
+ documents hardware expectations, and builds a non-executing LoRA trainer request with PEFT/TRL
163
+ dependency status. It can also run a local evaluation by comparing newline-separated base and tuned
164
+ responses against the built-in prompt cases. It reports exact match, can calculate perplexity from
165
+ optional negative log likelihood values, shows a qualitative table, and appends tuned results to
166
+ `data/eval_results.jsonl`.
167
+
168
+ The vLLM tab is a local serving planner. It prepares an explicit `vllm serve ...` command, checks
169
+ `/health`, fetches `/metrics`, logs parsed benchmark metrics through local tracking, and points the
170
+ chat client at `/v1/chat/completions` only after you start vLLM yourself. It does not install vLLM
171
+ or start a server on app startup.
172
+
173
+ Tracing writes local events to `data/traces.jsonl` by default. The Traces tab can show recent app
174
+ events, show JSONL trace rows, report whether optional Trackio is available, and export local
175
+ traces to `exports/traces.jsonl`. Remote Trackio/HF sync still requires credentials and setup.
176
+
177
+ ## 8. Hugging Face Space Deployment
178
+
179
+ The repo includes Hugging Face Space metadata in `README.md` and a local planning helper:
180
+
181
+ ```powershell
182
+ .venv\Scripts\python.exe scripts\plan_hf_space.py --user <hf-user-or-org>
183
+ ```
184
+
185
+ The helper validates required files and prints the manual commands for login, Space creation,
186
+ remote setup, and push. It does not login, create a repo, push, or store tokens.
187
+
188
+ Required Space files:
189
+
190
+ - `app.py`
191
+ - `requirements.txt`
192
+ - `README.md`
193
+ - `config/models.yaml`
194
+ - `config/training.yaml`
195
+
196
+ Target Spaces:
197
+
198
+ - Workbench: `https://huggingface.co/spaces/build-small-hackathon/workbench`
199
+ - Plant Identification Tool: `https://huggingface.co/spaces/build-small-hackathon/plant_identification_tool`
200
+
201
+ Use `hf auth login` with a freshly generated token. The app does not download model weights on
202
+ startup; model downloads happen only through explicit backend actions such as `ollama pull`,
203
+ `hf download`, or selecting a real Transformers backend and running a prompt. Upgrade Space
204
+ hardware if real OpenBMB text or vision inference cannot run on the default CPU tier.
205
+
206
+ Trackio/HF sync path: local traces are written to JSONL first, then optional Trackio can be enabled
207
+ in `config/training.yaml` after package availability and credentials are ready.
208
+
209
+ Synthetic data and reward evaluation helpers are local Python utilities. `datasets/synthetic.py`
210
+ can generate, validate, filter, augment, and export JSONL examples. `training/reward_eval.py` can
211
+ score supplied responses, select best-of-N candidates, create DPO chosen/rejected pairs, and compare
212
+ base responses against LoRA responses. They do not load reward models or call external services.
213
+
214
+ ## 9. How To Work With Codex
215
+
216
+ Useful prompts:
217
+
218
+ ```text
219
+ Read docs/TASKS.md and implement the next unchecked MVP task.
220
+ ```
221
+
222
+ ```text
223
+ Read docs/IMPLEMENTATION_STATUS.md and tell me what is blocked.
224
+ ```
225
+
226
+ ```text
227
+ Wire the next backend, but keep automatic model downloads disabled on startup.
228
+ ```
229
+
230
+ ```text
231
+ Update the docs after the change and mark the matching checklist item.
232
+ ```
233
+
234
+ ```text
235
+ If this failed, add or update a test that catches it, then fix the code.
236
+ ```
237
+
238
+ ## 10. What To Avoid
239
+
240
+ - Do not start with the whole PRD at once.
241
+ - Do not download huge models automatically.
242
+ - Do not mark tasks done before running or documenting the blocker.
243
+ - Do not add features without tests.
244
+ - Do not add broad coverage escapes such as pragma no cover without a documented reason.
245
+ - Do not push secrets, tokens, model caches, GGUF files, or virtual environments to git.
exports/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
mcp_tools/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Local tool functions that can later be exposed through an MCP endpoint."""