Files

116 lines
5.6 KiB
Python
Raw Permalink Normal View History

"""Scoped configuration and read-only trajectory quality API (BR72–78)."""
from typing import Literal
from fastapi import APIRouter,Depends,Request,HTTPException
from pydantic import BaseModel,ConfigDict,Field
from api.risk_agent_access import authorize
from api.project_assistant import enabled
from api.lease_library import service,guard,project,card,translate as library_translate
from fastapi.responses import JSONResponse
from services import assistant_store as store,catalog_configuration as catalog,trajectory_quality as quality
router=APIRouter(prefix='/api/xingbang/risk-agent/project-assistant/library',dependencies=[Depends(authorize),Depends(enabled)])
def translate(call):
def checked():
try:return call()
except ValueError as exc:
text=str(exc)
if 'COMPONENT_HIDDEN' in text:return JSONResponse(status_code=409,content={'error':{'code':'COMPONENT_HIDDEN','message':'评审方案存在隐藏组件,请更新配置后重试'}})
return JSONResponse(status_code=422,content={'error':{'code':'CONFIGURATION_INVALID','message':'配置不合法,请检查必填项与受信模型地址'}})
return library_translate(checked)
class Input(BaseModel):model_config=ConfigDict(extra='forbid')
class Visibility(Input):hidden:bool
class CardInput(Input):
project_id:str=Field(min_length=1,max_length=180)
kind:Literal['data_card','template']
name:str=Field(min_length=1,max_length=120)
content:dict
class Query(Input):
project_id:str=Field(min_length=1,max_length=180)
message:str=Field(min_length=1,max_length=1000)
class Settings(Input):
provider:Literal['disabled','local','jev']
endpoint:str=Field(default='',max_length=500)
model:str=Field(default='',max_length=160)
api_key:str|None=Field(default=None,max_length=4000)
allow_external:bool=False
class RunInput(Input):run_id:str=Field(min_length=1,max_length=180)
@router.patch('/cards/{card_id}/visibility')
def visibility(card_id:str,payload:Visibility,request:Request):
actor=guard(request,True)
def action():
with service().connect() as db:return catalog.set_visibility(db,card(db,request,card_id),actor,payload.hidden)
return translate(action)
@router.post('/configuration/cards',status_code=201)
def create_card(payload:CardInput,request:Request):
actor=guard(request,True)
def action():
with service().connect() as db:return catalog.create(db,project(db,request,payload.project_id),actor,payload.kind,payload.name,payload.content)
return translate(action)
@router.post('/inventory')
def inventory(payload:Query,request:Request):
guard(request)
def action():
with service().connect() as db:
context=project(db,request,payload.project_id)
return catalog.answer(db,context['id'],context['data_domain'],request.state.identity['role'],payload.message)
return translate(action)
@router.get('/quality/settings')
def settings(request:Request):
guard(request,True);return quality.public()
@router.put('/quality/settings')
def save_settings(payload:Settings,request:Request):
actor=guard(request,True)
def action():
result=quality.save(**payload.model_dump())
with service().connect() as db:store.event(db,'','quality.settings_updated',actor,{'provider':result['provider'],'revision':result['revision']})
return result
return translate(action)
@router.post('/quality/probe')
def probe(request:Request):
guard(request,True);return quality.probe()
@router.get('/projects/{project_id}/quality')
def history(project_id:str,request:Request):
guard(request)
def action():
with service().connect() as db:
project(db,request,project_id)
return {'items':store.listing(db,'quality_review',project_id)}
return translate(action)
@router.post('/projects/{project_id}/quality',status_code=201)
def inspect(project_id:str,payload:RunInput,request:Request):
actor=guard(request)
def action():
svc=service()
with svc.connect() as db:
project(db,request,project_id);run=store.get(db,'run',payload.run_id)
if run['project_id']!=project_id:raise KeyError('运行不可访问')
result=quality.inspect(run)
with svc.connect() as db:
store.put(db,'quality_review',result,immutable=True)
store.event(db,project_id,'trajectory.quality',actor,{'review_id':result['id'],'run_id':payload.run_id,'status':result['judgment']['status']})
return result
return translate(action)
@router.get('/configuration/sources')
def sources(project_id:str,request:Request):
guard(request,True)
def action():
from services import component_registry as registry
with service().connect() as db:
context=project(db,request,project_id)
rows=[registry.resolve(db,p['id']) for p in store.listing(db,'release')]
return {'items':[{'id':r['id'],'name':r['name'],'type':r['type']} for r in rows if r.get('kind')=='data_source' and r.get('scope')==context['data_domain'] and r.get('project_id') in (None,'',project_id) and not catalog.hidden(db,r['id'])]}
return translate(action)
@router.get('/projects/{project_id}/runs/{run_id}/influences')
def influences(project_id:str,run_id:str,request:Request):
guard(request)
def action():
from services.prescreen_explanation import explain
with service().connect() as db:
project(db,request,project_id);run=store.get(db,'run',run_id)
if run.get('project_id')!=project_id:raise KeyError('运行不可访问')
view=store.get(db,'prescreen_run',run_id)
return explain(run,view)
return translate(action)