- 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
95 lines
3.6 KiB
Python
95 lines
3.6 KiB
Python
"""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/shadow:tier 为决策值,执行仍走消费方现状;
|
||
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/route(T-G5 grader)与 /sense/admin/*(T-G3+)后续追加。
|
||
return [main, v1]
|