Spaces:
Sleeping
Sleeping
| # Agent Notes โ Fin-EvalOps | |
| Notes left by coding agents for future agents. Read this before making changes. | |
| --- | |
| ## Architecture in 30 seconds | |
| - **Backend**: FastAPI + SQLAlchemy + SQLite (WAL). Lives in `backend/app/`. Routers: `testsets`, `runs`, `skills`, `dashboard`, `agent`, `annotations`, `sse`. Background eval work in `services/evaluator.py`. | |
| - **Frontend**: Vite + React + TS + AntD + ECharts. Lives in `frontend/src/`. Key page: `pages/TestSets/index.tsx`. | |
| - **Eval pipeline**: `TestCase` โ auto-route to a `Skill` (`services/skill_router.py`) โ LLM judge (`services/llm_client.py`) โ store on `Run` with `weight_assignment`, `dimension_scores`, `caps`, `root_causes`, `narrative_review`. | |
| - **Deployment**: | |
| - Frontend โ GitHub Pages (auto via `.github/workflows/deploy.yml` on push to `main` touching `frontend/**`). | |
| - Backend โ HF Space `appqqq/Fin-EvalOps-v2` (Docker SDK). NOT auto-synced from GitHub unless the user manually enables GitHub integration in the Space settings. | |
| - **Persistence**: SQLite DB lives in `/data` on the Space, snapshot-pushed to a HF Dataset repo via `persistence.py`. | |
| --- | |
| ## Recent work (2026-06 โ 2026-07) | |
| ### Feature: custom business categories | |
| Business asked: don't force every eval batch into one of the 13 seeded self-eval categories. Let users define their own. | |
| - Backend ([`routers/testsets.py`](backend/app/routers/testsets.py)): new `POST /api/testsets/categories` (with body validation: code charset = `^[\wไธ-้ฟฟ\-]+$`, length 1-32) and `DELETE /api/testsets/categories/{code}` (refuses seed `is_custom=false` with 409; refuses in-use categories). | |
| - Model ([`models.py`](backend/app/models.py)): `TestCategory.is_custom: Boolean` + inline migration. Widened `code` / `category_code` to `String(64)` โ SQLite type affinity means no DDL migration needed. | |
| - Slug auto-derived as `c-{slugified-code}` to keep custom in its own namespace and avoid colliding with the canonical English slugs. | |
| - Frontend ([`pages/TestSets/index.tsx`](frontend/src/pages/TestSets/index.tsx)): `+ ๆฐๅปบๅ็ฑป` button next to the category Select in all 3 modals (import / new / iwencai-pull); new top-toolbar `ๅ็ฑป็ฎก็` modal with delete popconfirm; tree node title appends ๐ท๏ธ for custom rows. | |
| ### Fix: `tags` lost across all JSON import paths | |
| `_normalize_raw` (Chinese-keyed branch) didn't extract `tags`; `import_file` / `scan_disk` / `import-from-iwencai` never wrote `tags=...` to the `TestCase` ORM. Result: any upstream metadata packed into `tags` (e.g. by `scripts/convert_part06.py`) silently disappeared. Fixed in the same PR โ `tags=norm.get("tags")` now propagates everywhere. | |
| ### Fix: scan-disk `500` when `testsets_root` is missing | |
| `ๆฐๆฎๆต่ฏ้/` is now uploaded at runtime via the web UI instead of bundled in the Docker image (see "Things you must know" below). Without data on disk, `POST /api/testsets/scan-disk` returned 500. Fixed: now returns `ScanDiskResponse(scanned=0, inserted=0, updated=0, skipped=0)` and the frontend's "็ฃ็ๅๆญฅ" button just shows 0 instead of erroring. | |
| ### Fix: eval pipeline โ parallel-array `root_causes` + tool_use text-block recovery | |
| Two recurring eval failures observed in user runs: | |
| 1. **Judge outputs `root_causes` as a dict of parallel arrays** (e.g. `{l1: ["a","b"], l2: ["x","y"], raw_score: ["40","60"], โฆ}`) when the schema wants a list of objects. Same pattern occasionally on `caps`, `skipped_dimensions`. Validator throws `SchemaValidationError`. | |
| 2. **minimax / other Anthropic-compatible APIs** put JSON in a markdown text block instead of a `tool_use` block. The fallback tried to validate strictly; on failure the entire eval died. | |
| Fix in [`services/llm_client.py`](backend/app/services/llm_client.py): | |
| - New `_sanitize_judge_for_validation()` runs **before** strict validation. Detects "dict of parallel arrays" shape and transposes to list of objects. Coerces `raw_score` / `score_ceiling` string-numbers to float. Splits string `matched_golden_cases` into list. | |
| - `call_with_schema` now wraps validation: if it fails, fills missing `required` fields with empty defaults, re-validates; if still failing, returns partial data with a warning instead of raising. Downstream evaluator sanitizers can recover the rest. | |
| - `_call_anthropic` text-block fallback: pre-sanitizes, then on residual mismatch logs a warning and returns data anyway. | |
| Duplicate sanitizer in [`services/evaluator.py`](backend/app/services/evaluator.py) `_sanitize_judge_output` โ defense in depth, handles cached / out-of-band payloads. | |
| ### Frontend: explicit "ๅผๅงๅฏผๅ ฅ" button on the JSON import modal | |
| The Antd `Upload.beforeUpload` was firing the import the moment a file was selected, and `Modal.footer={null}` hid the default OK/Cancel. Users had no way to confirm the action. Now: file is staged in state; `ๅผๅงๅฏผๅ ฅ` OK button (disabled until both category + file are set) triggers `importFile`; success message includes the destination category code. | |
| --- | |
| ## Things you must know (operational gotchas) | |
| ### HF Space sync โ there is NO direct git push from this environment | |
| `git push` to `huggingface.co/spaces/...` fails from this machine (network policy blocks port 443 to `huggingface.co`). The workaround is `huggingface_hub.HfApi`: | |
| ```python | |
| from huggingface_hub import HfApi | |
| api = HfApi(token=open("/Users/appstore/.cache/huggingface/token").read().strip()) | |
| api.upload_file( | |
| path_or_fileobj=open("local/file", "rb").read(), | |
| path_in_repo="remote/path", | |
| repo_id="appqqq/Fin-EvalOps-v2", | |
| repo_type="space", | |
| commit_message="...", | |
| ) | |
| ``` | |
| `HfApi` uses an HTTP API path that *is* reachable; raw `git` over SSH or HTTPS to `huggingface.co` is not. Endpoints like `model_info("gpt2")` work without auth; authenticated writes need the cached token at `~/.cache/huggingface/token`. | |
| `upload_folder` is for many files at once (uses LFS for large ones); `upload_file` is for a single file commit. `delete_files(repo_id, repo_type, delete_patterns=[...])` removes files with glob patterns. | |
| SSL is intermittent โ wrap writes in a retry loop. Each `upload_file` / `delete_files` creates its own commit, so failures mid-upload don't roll back earlier commits. | |
| If a fresh user runs into the same `git push` failure, use the script at `scripts/sync_to_hf_space.sh` which clones locally + rsyncs + commits (assumes the user can reach huggingface.co from their machine). | |
| ### Dockerfile: do NOT `COPY ๆฐๆฎๆต่ฏ้/` | |
| The Docker image must not bundle test data. The pattern is: | |
| ```dockerfile | |
| COPY backend/ /app/backend/ | |
| COPY skills/ /app/skills/ | |
| # ๆฐๆฎๆต่ฏ้/ is NOT bundled โ tests are uploaded via the web UI. | |
| ``` | |
| If you add the COPY line back, the build fails with `"/ๆฐๆฎๆต่ฏ้/่ช็ ่ฏๆตๆต่ฏ้": not found` (because the directory is excluded from the Space repo via the deletion commit `7d79f77a1f13`). | |
| ### `.hfignore` keeps test data out of HF uploads | |
| `/Users/appstore/AI-Code/Fin-EvalOps/.hfignore` excludes `ๆฐๆฎๆต่ฏ้/`, `*.jsonl`, and `_smoke_*.json`. Also `scripts/sync_to_hf_space.sh` has matching rsync excludes. Don't re-add these โ they were excluded for a reason (don't bundle customer test data into the Space image). | |
| ### SQLite migration pattern is `_run_inline_migrations()` | |
| Adding a column? Put it in the `MIGRATIONS` list in [`backend/app/db.py`](backend/app/db.py) with a predicate + action SQL. Predicate should return 1+ rows when migration is needed. SQLite ALTER TABLE only supports ADD COLUMN and RENAME COLUMN โ for anything more complex (e.g. widening column type), you need table rebuild or just leave the schema declaration alone (SQLite doesn't enforce VARCHAR length anyway). | |
| ### `TestCase` is referenced everywhere โ be careful with column changes | |
| `TestCase` is the central model. Test cases are created from JSON import, manual API, scan-disk, iwencai import. If you change a column, update: | |
| - The model in `models.py` | |
| - `_normalize_raw` (handles Chinese-keyed JSON) | |
| - All 4 creation paths: `import_file`, `scan_disk`, `import-from-iwencai`, `create_testcase` | |
| - The serializer (`schemas.py`) | |
| Missing any path โ silent data loss (this is exactly what bit us with `tags`). | |
| --- | |
| ## Testing | |
| ```bash | |
| cd backend && python3 -m pytest tests/ -v | |
| ``` | |
| 49 tests, all passing. Patterns: | |
| - `test_field_rename.py` โ DB-level migration tests (create temp DB with old schema, run migration, verify shape). | |
| - `test_custom_category.py` โ full-stack via `TestClient` with patched DB session. | |
| - `test_sanitize_judge_output.py` โ pure-function tests for LLM output reshaping (the 12 new cases cover parallel-array transpose + permissive fallback). | |
| - `test_skill_router.py`, `test_scorer.py`, `test_cache_headers.py`, `test_sanitize_judge_output.py` (legacy) โ unit tests. | |
| For end-to-end smoke tests against a live Space, you'll need an LLM API key. The HF Space has provider keys configured; for local testing set `MINIMAX_API_KEY` (or others) in env and run `uvicorn app.main:app --port 8000`. | |
| --- | |
| ## File map (for agents coming in cold) | |
| ``` | |
| Fin-EvalOps/ | |
| โโโ backend/app/ | |
| โ โโโ main.py # FastAPI app, lifespan (HF persistence + skill sync) | |
| โ โโโ config.py # Settings, env-driven | |
| โ โโโ db.py # SQLite engine, migrations | |
| โ โโโ models.py # ORM models (TestCase, Run, Skill, TestCategory, ...) | |
| โ โโโ schemas.py # Pydantic I/O schemas | |
| โ โโโ persistence.py # HF Datasets snapshot push/pull | |
| โ โโโ routers/ | |
| โ โ โโโ testsets.py # โ most custom-category work lives here | |
| โ โ โโโ runs.py # Run creation + polling | |
| โ โ โโโ skills.py # Skill catalog API | |
| โ โ โโโ dashboard.py | |
| โ โ โโโ agent.py # Data Agent (LLM chat over the DB) | |
| โ โ โโโ annotations.py | |
| โ โ โโโ sse.py # SSE channels for live progress | |
| โ โโโ services/ | |
| โ โโโ llm_client.py # โ sanitizer fixes live here | |
| โ โโโ evaluator.py # โ 5-step eval pipeline; _sanitize_judge_output here | |
| โ โโโ scorer.py # weighted-sum + cap rule application | |
| โ โโโ skill_router.py # auto-route question โ skill | |
| โ โโโ skill_loader.py # parses ่ช็ ่ฏๆตSkill/ markdown into Skill rows | |
| โ โโโ data_agent.py # Data Agent LLM prompt | |
| โ โโโ fetch_iwencai.py # iwencai EvalOps backend adapter | |
| โโโ frontend/src/ | |
| โ โโโ pages/TestSets/ # โ UX overhaul for custom categories lives here | |
| โ โโโ api/testsets.ts # frontend API client (createCategory, deleteCategory) | |
| โ โโโ components/, pages/{Agent,Annotations,Compare,Dashboard,Runs,Skills,Settings}/ | |
| โโโ skills/ # 13 self-eval + 14 competitor + 14 e2e skill markdown | |
| โโโ scripts/ | |
| โ โโโ convert_part06.py # JSONL โ platform-importable JSON converter | |
| โ โโโ sync_to_hf_space.sh # git-based HF Space sync (only works for users with network access) | |
| โโโ ๆฐๆฎๆต่ฏ้/ # โ ๏ธ NOT shipped to Space (excluded in .hfignore) | |
| โ โโโ ่ช็ ่ฏๆตๆต่ฏ้/ # 13 canonical test set files | |
| โ โโโ ็ซๅๅฏนๆฏๆต่ฏ้/ | |
| โ โโโ part_06.jsonl # new test data (user's) | |
| โ โโโ part_06-importable.json # generated by scripts/convert_part06.py | |
| โโโ .hfignore # excludes data from HF uploads | |
| โโโ Dockerfile | |
| ``` | |
| --- | |
| ## Common tasks | |
| ### Add a new eval metric dimension | |
| 1. Update the skill's markdown in `skills/่ช็ ่ฏๆตSkill/<NN>-<slug>/` (add dim to dimensions table, caps table, root_causes table). | |
| 2. Update the LLM schema in [`backend/app/services/evaluator.py`](backend/app/services/evaluator.py) `EVAL_OUTPUT_SCHEMA` `dimension_scores.additionalProperties` if you want strict validation. | |
| 3. The schema is shared across all 13 skills โ don't break the others. If a dimension is skill-specific, define it per-skill via a relaxed schema. | |
| ### Add a new business category via API | |
| ```bash | |
| curl -X POST https://appqqq-fin-evalops-v2.hf.space/api/testsets/categories \ | |
| -H 'Content-Type: application/json' \ | |
| -d '{"code":"my-batch-2026q3","name_zh":"2026Q3 ๅๅฝๆนๆฌก","description":"..."}' | |
| ``` | |
| Returns `TestCategoryOut` with `is_custom: true`, `slug: c-my-batch-2026q3`. | |
| ### Import a test set into a custom category | |
| ```bash | |
| curl -X POST 'https://appqqq-fin-evalops-v2.hf.space/api/testsets/import-file?category_code=my-batch-2026q3' \ | |
| -F 'file=@my_set.json' | |
| ``` | |
| Response: `{"inserted": N, "total_in_file": M}`. Returns 400 if the category doesn't exist; the import endpoint accepts both Chinese-keyed (`{้ฎ้ข, ็ญๆก, ้พ่ทฏๆฐๆฎ, ไธไธๆ, id, ๆฅๆบ, tags}`) and English-keyed (`{question, agent_answer, reasoning_trace, context_history, source_id, source, tags}`) JSON. | |
| ### Trigger a single eval | |
| ```bash | |
| curl -X POST https://appqqq-fin-evalops-v2.hf.space/api/runs \ | |
| -H 'Content-Type: application/json' \ | |
| -d '{"testcase_id":"<hex>","judge_model":"minimax-3"}' | |
| ``` | |
| Returns 201 with the run. Poll `GET /api/runs/{run_id}` for status. Auto-routes to the best matching skill unless `skill_id` is provided. | |
| --- | |
| ## Known rough edges (not bugs, just be aware) | |
| - `cost_usd` is always `null` โ `ModelSpec` has no pricing fields. If you add pricing, update `LLMResult` and the evaluator to compute `tokens_in * input_price + tokens_out * output_price`. | |
| - `matched_golden_cases` is often `[]` even when the question clearly matches a golden case โ judge models tend to be conservative. The golden case list is provided in the prompt (truncated to 6000 chars). | |
| - `scan-disk` always returns 0 on the Space (no bundled data). This is intentional after the 2026-07 cleanup. | |
| - HF Space file count is ~1598 because the previous sync included `.pytest_cache/` etc. These are harmless dead files but inflate the repo. Safe to ignore. |