GitHub Actions commited on
Commit ·
7f9dfed
0
Parent(s):
Initial ZeroGPU deployment with spaces shim
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .github/workflows/ci.yml +27 -0
- .gitignore +37 -0
- AGENTS.md +62 -0
- HF_PRD_ext.md +1631 -0
- HF_PRD_v1.md +1361 -0
- README.md +227 -0
- agent/__init__.py +1 -0
- agent/runner.py +203 -0
- app.py +110 -0
- assets/e2e/README.md +21 -0
- assets/e2e/plant-playwright-report/index.html +0 -0
- assets/e2e/playwright-report/index.html +0 -0
- assets/samples/README.md +7 -0
- assets/samples/plant_leaf.png +0 -0
- assets/samples/plant_leaf.ppm +11 -0
- config/models.yaml +155 -0
- config/training.yaml +24 -0
- core/__init__.py +1 -0
- core/app_logging.py +15 -0
- core/app_state.py +56 -0
- core/deployment.py +77 -0
- core/events.py +54 -0
- core/file_exports.py +16 -0
- core/registry.py +24 -0
- core/spaces_runtime.py +21 -0
- core/tab_feedback.py +24 -0
- data/.gitkeep +1 -0
- datasets/__init__.py +1 -0
- datasets/field_notes.py +252 -0
- datasets/loader.py +173 -0
- datasets/ocr.py +137 -0
- datasets/synthetic.py +90 -0
- deployment/__init__.py +0 -0
- deployment/hf_space.py +98 -0
- docs/ACCEPTANCE_CRITERIA.md +96 -0
- docs/ARCHITECTURE.md +575 -0
- docs/EXTENDING.md +125 -0
- docs/HACKATHON_SUBMISSION.md +103 -0
- docs/IMPLEMENTATION_STATUS.md +186 -0
- docs/PLANT_DISCOVERY_APP_PLAN.md +119 -0
- docs/PLANT_MODEL_AND_TRAINING_HOWTO.md +145 -0
- docs/PRD_IMPLEMENTATION_MATRIX.md +95 -0
- docs/README.md +32 -0
- docs/ROADMAP.md +49 -0
- docs/ROADMAP_V2_CRITICAL_IMPROVEMENT_PLAN.md +361 -0
- docs/TASKS.md +530 -0
- docs/TEMPLATE_HOWTO.md +207 -0
- docs/USAGE.md +245 -0
- exports/.gitkeep +1 -0
- mcp_tools/__init__.py +1 -0
.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."""
|