chore(repo): initialize Cowork Local Gitea repository
CI / test (push) Canceled after 0s

This commit is contained in:
thanhnv
2026-08-09 20:12:05 +07:00
commit 414eaddca3
192 changed files with 48160 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
# Auto Model Assessment & Routing
Tự động **đánh giá** từng model (provider/model do user cấu hình), **chấm điểm phù hợp**
cho mỗi loại task, rồi **định tuyến** mỗi lượt chat/agent tới model phù hợp nhất — theo
3 chế độ **Off / Auto / Manual** bật ngay trên màn hình chat (Cowork, Co4E, AI-Edit).
Module này được xây dựng để **hoà vào đúng stack sẵn có** của Cowork-Local (PySide6
desktop app), thay vì dựng một service FastAPI riêng:
| Bản mô tả gốc (đề bài) | Hiện thực trong app này |
|---|---|
| Config YAML | Config JSON `~/.cowork_local/config.json` (chuẩn của app) + file assessment riêng |
| FastAPI REST endpoints | `RoutingService` (Python facade) — các method ánh xạ 1-1 với endpoint |
| `httpx` async + `asyncio.Semaphore` | Tái dùng `providers/` (requests) + `ThreadPoolExecutor` với **semaphore theo từng provider** |
| APScheduler | `QTimer` (giống `core/task_scheduler.py`) — không thêm dependency |
| `clients.py` (Anthropic/OpenAI) | `AppProbeClient` bọc `AppContext.build_provider_for` (đã có sẵn TLS-trust, retry 429, gateway) |
## Kiến trúc
```
core/routing/
models.py # Pydantic v2: ModelMetadata, ProbeResult, ModelAssessment,
# SwitchDecision, PendingSwitch, TaskType/Policy/SwitchMode
store.py # AssessmentStore: JSON, atomic write (temp+rename), history backup
metadata.py # STATIC_METADATA + enrich() (dùng lại core/model_pricing cho giá)
clients.py # AppProbeClient (bọc Provider có sẵn) + ProbeClient protocol
prober.py # BENCHMARK_TASKS, probe_model(), make_judge(), semaphore/provider
scorer.py # compute_fit_score() + POLICY_WEIGHTS
selector.py # rank_models()/best_model() — tính lại fit theo policy, không probe lại
classifier.py # classify(prompt) -> TaskType (heuristic, fallback LLM tuỳ chọn)
switch_controller.py # decide() (thuần) + PendingSwitchRegistry (TTL, idempotent)
orchestrator.py # check_and_update(): enrich -> probe -> score -> store
service.py # RoutingService — facade UI gọi
scheduler.py # RoutingScheduler (QTimer): reassess định kỳ + dọn pending hết hạn
```
## Công thức fit score
```
fit = w_quality * quality
+ w_cost * 1/(1 + cost)
+ w_latency * 1/(1 + latency_s)
```
`POLICY_WEIGHTS` (mỗi hàng cộng = 1.0):
| Policy | quality | cost | latency |
|---|---|---|---|
| `quality` | 0.80 | 0.10 | 0.10 |
| `cost` | 0.20 | 0.70 | 0.10 |
| `latency` | 0.20 | 0.10 | 0.70 |
| `balanced` | 0.50 | 0.25 | 0.25 |
- Probe **fail** → fit = 0 (model không dùng được thì không bao giờ được chọn).
- Giá **không rõ** → để `None`, đánh dấu `metadata_incomplete=True` (không đoán bừa).
## Config (trong `config.json`, mục `routing`)
```jsonc
"routing": {
"switch_mode": "off", // mặc định toàn cục: "off" | "auto" | "manual"
"policy": "balanced", // "quality" | "cost" | "latency" | "balanced"
"min_score_gain": 0.05, // chỉ chuyển nếu model mới hơn model hiện tại ≥ ngưỡng này
"confirm_timeout_sec": 60, // (manual) hết giờ chờ confirm → giữ model hiện tại
"reassess_interval_hours": 24, // lịch reassess; 0 = tắt
"per_provider_concurrency": 2, // số probe song song tối đa mỗi provider (chống rate limit)
"judge_provider": "", // provider của judge ("" → active provider)
"judge_model": "", // model chấm điểm cố định ("" → default rẻ theo provider)
"candidates": [ // model muốn đánh giá; rỗng → tự lấy model đang cấu hình
{"provider": "anthropic", "model_id": "claude-opus-4-8", "tier": "powerful"},
{"provider": "anthropic", "model_id": "claude-haiku-4-5", "tier": "fast"}
],
"auto_reassess_on_add": true, // thêm model mới → reassess ngay
"surface_modes": { // toggle Off/Auto/Manual của TỪNG màn hình ("" = theo switch_mode)
"cowork": "", "co4e": "", "ai_edit": ""
}
}
```
Kết quả assessment **KHÔNG** nằm trong `config.json` mà ở file riêng:
`~/.cowork_local/assessments.json` (+ backup lịch sử ở `assessments_history/<timestamp>.json`).
## Toggle Off / Auto / Manual (trên màn hình chat)
Mỗi màn hình chat có một toggle nhỏ cạnh ô chọn model:
- **Off** — tắt định tuyến, luôn dùng model đang chọn.
- **Auto** — tự động chuyển sang model phù hợp nhất (nếu `gain ≥ min_score_gain`),
chạy luôn, hiện dòng thông báo `↪ Auto-routed to …`.
- **Manual** — hiện hộp thoại xác nhận (có đếm ngược `confirm_timeout_sec`); user
đồng ý mới chuyển, từ chối / hết giờ thì giữ model hiện tại.
Toggle được lưu **riêng cho từng màn hình** (`surface_modes`) và ghi đè `switch_mode` toàn cục.
## Logical API (RoutingService)
| Method | Tương đương REST trong đề bài |
|---|---|
| `reassess(policy=None)` / `reassess_background()` | `POST /models/reassess` |
| `best_for(task_type, policy)` | `GET /models/best` |
| `status()` / `assessments()` | `GET /models/assessments` |
| `add_candidate(provider, model_id, tier)` | `POST /models/add` (tự trigger reassess) |
| `route(surface, prompt, provider, model)` | phần quyết định của `POST /task/execute` |
| `create_pending()` / `resolve_pending(id, approve, run)` | `POST /task/confirm-switch` (idempotent) |
| `get_routing_config()` / `update_routing_config(**)` | `GET`/`PATCH /routing/config` |
## Bảo mật & chi phí
- **API key** đọc từ env (qua `api_key_env` của provider) — không ghi key vào config/log.
- **Probe tốn tiền** → chỉ chạy theo lịch / khi thêm model / khi bấm "Reassess now".
Mỗi lần reassess ghi log số lượng API call.
- **Idempotent** — reassess ổn định (chỉ latency dao động ~µs, dưới xa `min_score_gain`);
ghi atomic nên ngắt giữa chừng không hỏng config.
- **Không gọi API thật trong test** — `clients.py`/`judge()` được mock hoàn toàn.
## Chạy test
```bash
# từ thư mục cha của package (…/cowork_local_20260722)
python -m pytest cowork_local/tests/routing/ -q
```
Bao phủ: `scorer`, `store` (atomic + history), `selector`, `switch_controller`
(Auto/Manual/Off, timeout, idempotent), `orchestrator` (mock client), `classifier`,
và `service` (end-to-end reassess → route → confirm).