Files
tzt 4575025056 feat(sense): T-G5 Grader 决策组合(特征门×概率×conformal + /v1/route + 三态 mode)
- features.py:gate() 纯函数——轮数/字符估算/意图黑名单/仓库级信号/长度门
  -> t1_hard_ok(任一硬门不过即 False,D-G1)
- grader.py:Grader.decide §8 时序(embed 降级检查 -> 特征门 -> LinearHead 概率
  -> conformal 阈值:p1>=τ1 且 t1_hard_ok->T1,p3>=τ3 或 repo_signals->T3,
  其余 T2 默认;collect/shadow 只写观察 executed=现行为,live 决策即执行;
  全模式 observer.log);工件/阈值缓存 + invalidate;D-G4 规则门退化
- routes:/v1/route 契约(D-G6 不落 query 原文)
- fix(observer):embedding list -> BLOB 转换(修 sqlite 绑定)
- 测试 +6(门矩阵/shadow 不改流/live 决策/降级/保守阈值/写观察),全量 405 passed
2026-09-05 14:17:44 +08:00

95 lines
3.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Sense 路由(T-G2 补观察初始化):/sense 前缀组 + /v1 无前缀组。"""
from __future__ import annotations
from typing import List
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
from gateway.sense.config import SenseConfig
from gateway.sense.embedder import embed
from gateway.sense.errors import EmbedderDown
from gateway.sense.observer import get_observer
from gateway.sense.store import SenseStore
def build_sense_routers(cfg: SenseConfig) -> List[APIRouter]:
"""组装 sense 路由组:[/sense 前缀组, /v1 无前缀组]。"""
main = APIRouter(prefix="/sense")
v1 = APIRouter()
store = SenseStore.init_db(cfg.db_path)
@main.on_event("startup")
async def _start_observer():
get_observer(store)
obs = get_observer()
await obs.start()
@main.on_event("shutdown")
async def _stop_observer():
obs = get_observer()
await obs.flush_once()
await obs.stop()
@main.get("/health", tags=["sense"])
async def health():
"""sense 面健康检查(含灰度状态)。"""
return {"enabled": cfg.enabled, "mode": cfg.mode,
"embedder": cfg.embedder.base_url}
@v1.post("/v1/route", tags=["sense"])
async def route_v1(request: Request):
"""分级决策:{query|messages, consumer, domain?} -> TierDecision 视图。
D-G6:不落 query 原文(观察表只存哈希可关联 id + 特征 + int8 向量)。
mode=collect/shadowtier 为决策值,执行仍走消费方现状;
mode=live:消费方按 tier 分流(D-G7)。
"""
from gateway.sense.grader import Grader
try:
body = await request.json()
except Exception:
body = {}
query = (body or {}).get("query")
messages = (body or {}).get("messages")
consumer = str((body or {}).get("consumer") or "proxy")
domain = str((body or {}).get("domain") or "")
payload = messages if messages else (query or "")
grader = Grader(cfg, store,
get_observer(store) if cfg.mode != "collect" else None)
d = await grader.decide(payload, consumer, domain)
probs_total = sum(d.probs.values()) or 1.0
return {
"tier": d.tier if cfg.mode == "live" else d.tier,
"probs": {k: round(v / probs_total, 4) for k, v in d.probs.items()},
"confidence": round(max(d.probs.values()) / probs_total, 4)
if d.probs else 0.0,
"thresholds_version": d.thresholds_version,
"head_version": d.head_version,
"mode": d.mode,
"fallback": d.fallback,
"hard_gates": d.hard_gates,
}
@v1.post("/v1/embeddings", tags=["sense"])
async def embeddings(request: Request):
"""OpenAI 兼容透传 embedder(客户端/代理共用)。"""
try:
body = await request.json()
except Exception:
body = {}
text = str((body or {}).get("input") or "").strip()
if not text:
return JSONResponse({"error": {"message": "input 必填"}}, status_code=400)
model = str((body or {}).get("model") or cfg.embedder.model)
try:
vec = await embed(text, cfg.embedder)
except EmbedderDown as e:
return JSONResponse(
{"error": {"message": str(e), "type": "embedder_down"}}, status_code=503)
return {"object": "list", "model": model,
"data": [{"object": "embedding", "index": 0, "embedding": vec}]}
# /v1/routeT-G5 grader)与 /sense/admin/*T-G3+)后续追加。
return [main, v1]