254 lines
11 KiB
Python
254 lines
11 KiB
Python
"""SQLite lease corpus / standard Skill packages API (BR-61..65)."""
|
|
import os
|
|
from urllib.parse import urlsplit
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from fastapi.responses import JSONResponse, Response
|
|
from api.risk_agent_access import authorize, check_project
|
|
from api.project_assistant import enabled
|
|
from contracts.lease_library import QueryInput, SelectionInput, ExtractionInput, PackageInput, ModelSettingsInput
|
|
from services.project_assistant import ProjectAssistantService
|
|
from services import assistant_store as store, component_registry as registry
|
|
from services import lease_case_library as library, lease_extraction, skill_packages
|
|
from services.assistant_materials import FIELDS
|
|
from services.assistant_settings import flags
|
|
from services import experience_extraction
|
|
|
|
router = APIRouter(prefix='/api/xingbang/risk-agent/project-assistant/library',
|
|
dependencies=[Depends(authorize), Depends(enabled)], tags=['兴邦对公租赁经验库'])
|
|
|
|
|
|
def service():
|
|
svc = ProjectAssistantService()
|
|
library.initialize(svc.db_path)
|
|
return svc
|
|
|
|
|
|
def guard(request, editing=False):
|
|
identity = request.state.identity
|
|
if identity['role'] not in ('manager', 'admin') or (editing and identity['role'] != 'admin'):
|
|
raise HTTPException(403, '当前身份无权进行经验加工')
|
|
if not identity.get('demo') or not flags()['demo']:
|
|
raise HTTPException(403, '本案例库属于模拟数据域,请使用本机演示身份')
|
|
return identity['id']
|
|
|
|
|
|
def project(db, request, project_id):
|
|
check_project(request, project_id)
|
|
body = store.get(db, 'project', project_id)
|
|
if body['data_domain'] != 'demo':
|
|
raise HTTPException(403, '模拟历史经验不能写入真实数据域')
|
|
return body
|
|
|
|
|
|
def translate(call):
|
|
try:
|
|
return call()
|
|
except library.LibraryError as exc:
|
|
code = 409 if exc.code == 'LIBRARY_SELECTION_INVALID' else 422
|
|
return JSONResponse(status_code=code, content={'error': {'code': exc.code, 'message': str(exc), 'retryable': False}})
|
|
except KeyError:
|
|
raise HTTPException(404, '对象不可访问或不存在') from None
|
|
except store.Conflict as exc:
|
|
raise HTTPException(409, str(exc)) from exc
|
|
except (ValueError, PermissionError) as exc:
|
|
raise HTTPException(422 if isinstance(exc, ValueError) else 403, str(exc)) from exc
|
|
|
|
|
|
@router.get('/cases')
|
|
def cases(request: Request):
|
|
guard(request)
|
|
def action():
|
|
with service().connect() as db:
|
|
rows = library.read_query(db, 'SELECT * FROM lease_cases ORDER BY id')
|
|
return {'items': rows, 'count': len(rows), 'total': len(rows), 'schema': list(library.COLUMNS),
|
|
'source': library.SOURCE, 'model_available': experience_extraction.resolve_model_config(None).get('available', False)}
|
|
return translate(action)
|
|
|
|
|
|
@router.post('/queries')
|
|
def query(payload: QueryInput, request: Request):
|
|
actor = guard(request, True)
|
|
def action():
|
|
with service().connect() as db:
|
|
return library.preview(db, actor, payload.mode, payload.text, payload.filters)
|
|
return translate(action)
|
|
|
|
|
|
@router.post('/selections', status_code=201)
|
|
def selection(payload: SelectionInput, request: Request):
|
|
actor = guard(request, True)
|
|
def action():
|
|
with service().connect() as db:
|
|
return library.confirm(db, actor, payload.preview_id, payload.selected_ids)
|
|
return translate(action)
|
|
|
|
|
|
@router.post('/extractions', status_code=201)
|
|
def extraction(payload: ExtractionInput, request: Request):
|
|
actor = guard(request, True)
|
|
def action():
|
|
svc = service()
|
|
with svc.connect() as db:
|
|
context = project(db, request, payload.project_id)
|
|
confirmed, items = library.selected_cases(db, actor, payload.selection_id)
|
|
proposal, trace, failure = None, None, None
|
|
try:
|
|
proposal, trace = lease_extraction.invoke(context, confirmed, items, payload.kind, payload.requirement)
|
|
except Exception:
|
|
# Do not return model HTTP bodies/credentials to the browser.
|
|
failure = '模型未配置、调用未完成或输出未通过契约校验'
|
|
with svc.connect() as db:
|
|
library.selected_cases(db, actor, payload.selection_id)
|
|
return lease_extraction.save(db, context, confirmed, items, actor, payload.kind,
|
|
payload.requirement, proposal, trace, failure)
|
|
return translate(action)
|
|
|
|
|
|
def card(db, request, card_id):
|
|
try:
|
|
row = store.get(db, 'component_draft' if '@' in card_id else 'release', card_id)
|
|
if '@' not in card_id:
|
|
row = registry.resolve(db, card_id)
|
|
except KeyError:
|
|
row = store.get(db, 'component_version', card_id)
|
|
if row.get('scope') != 'demo':
|
|
raise HTTPException(403, '卡片数据域不可访问')
|
|
if row.get('project_id'):
|
|
check_project(request, row['project_id'])
|
|
return row
|
|
|
|
|
|
@router.get('/cards')
|
|
def cards(project_id: str, request: Request):
|
|
guard(request)
|
|
def action():
|
|
with service().connect() as db:
|
|
project(db, request, project_id)
|
|
released = [registry.resolve(db, p['id']) for p in store.listing(db, 'release')]
|
|
drafts = store.listing(db, 'component_draft', project_id)
|
|
items = [r for r in released + drafts if r.get('scope') == 'demo'
|
|
and r.get('kind') in ('skill', 'rule_set', 'data_card')
|
|
and r.get('project_id') in (None, '', project_id) and r.get('id') != 'review.analysis']
|
|
for row in items:
|
|
if row['kind'] == 'skill':
|
|
row['package'] = skill_packages.standard_package(db, row)
|
|
return {'items': items, 'fields': sorted(set(FIELDS.values()))}
|
|
return translate(action)
|
|
|
|
|
|
@router.get('/cards/{card_id}')
|
|
def card_detail(card_id: str, request: Request):
|
|
guard(request)
|
|
def action():
|
|
with service().connect() as db:
|
|
return card(db, request, card_id)
|
|
return translate(action)
|
|
|
|
|
|
@router.post('/skills/import', status_code=201)
|
|
def import_skill(payload: PackageInput, request: Request):
|
|
actor = guard(request, True)
|
|
def action():
|
|
with service().connect() as db:
|
|
context = project(db, request, payload.project_id)
|
|
return skill_packages.import_package(db, context, actor,
|
|
[f.model_dump() for f in payload.files], payload.required_field)
|
|
return translate(action)
|
|
|
|
|
|
@router.get('/skills/{card_id}/package')
|
|
def package(card_id: str, request: Request):
|
|
guard(request)
|
|
def action():
|
|
with service().connect() as db:
|
|
component = card(db, request, card_id)
|
|
if component['kind'] != 'skill':
|
|
raise ValueError('仅 Skill 卡片具有标准文件包')
|
|
return skill_packages.content(db, component)
|
|
return translate(action)
|
|
|
|
|
|
@router.get('/skills/{card_id}/download')
|
|
def download(card_id: str, request: Request):
|
|
guard(request)
|
|
def action():
|
|
with service().connect() as db:
|
|
component = card(db, request, card_id)
|
|
if component['kind'] != 'skill':
|
|
raise ValueError('仅 Skill 可下载标准包')
|
|
name, binary = skill_packages.archive(db, component)
|
|
return Response(binary, media_type='application/zip', headers={'Content-Disposition': f'attachment; filename="{name}.zip"'})
|
|
return translate(action)
|
|
|
|
|
|
@router.post('/skills/{card_id}/open')
|
|
def open_package(card_id: str, request: Request):
|
|
actor = guard(request, True)
|
|
hostname = urlsplit('//'+request.headers.get('x-xingbang-client-host', request.headers.get('host', ''))).hostname
|
|
local_demo = request.client and request.client.host in ('127.0.0.1', '::1', 'testclient') and hostname in ('127.0.0.1', 'localhost', '::1', 'testserver') and not os.getenv('XINGBANG_AUTH_USERS') and os.getenv('XINGBANG_REQUIRE_AUTH') != '1'
|
|
if not (request.state.identity.get('preview') or local_demo):
|
|
raise HTTPException(403, '打开文件夹仅支持已确认的本机预览;其他部署请下载标准包')
|
|
def action():
|
|
with service().connect() as db:
|
|
result = skill_packages.open_folder(db, card(db, request, card_id))
|
|
store.event(db, '', 'skill.folder_open', actor, {'card_id': card_id})
|
|
return result
|
|
return translate(action)
|
|
|
|
|
|
@router.get('/plans/{plan_id}')
|
|
def plan_components(plan_id: str, project_id: str, request: Request):
|
|
guard(request)
|
|
def action():
|
|
from services import review_plan
|
|
from services.assistant_data import load_catalog
|
|
with service().connect() as db:
|
|
context = project(db, request, project_id)
|
|
frozen = review_plan.assemble(db, plan_id, load_catalog())
|
|
if frozen['plan']['scope'] != context['data_domain']:
|
|
raise HTTPException(403, '方案数据域不匹配')
|
|
if frozen['plan'].get('project_id'):
|
|
check_project(request, frozen['plan']['project_id'])
|
|
return {'plan': frozen['plan'], 'skills': frozen['skills'], 'rules': frozen['rules'],
|
|
'cards': frozen.get('legacy_cards', []), 'content_hash': frozen['content_hash']}
|
|
return translate(action)
|
|
|
|
@router.get('/model-settings')
|
|
def model_status(request: Request):
|
|
guard(request, True)
|
|
from services import model_settings
|
|
return model_settings.status()
|
|
|
|
@router.put('/model-settings')
|
|
def save_model_settings(payload: ModelSettingsInput, request: Request):
|
|
actor=guard(request, True)
|
|
from services import model_settings
|
|
try:
|
|
result=model_settings.save(payload.endpoint,payload.name,payload.api_key)
|
|
except ValueError as exc:
|
|
code=str(exc)
|
|
message='模型由环境配置管理,请联系部署管理员修改' if code=='MODEL_ENV_LOCKED' else '模型设置不合法,请使用受信地址和有效模型名称'
|
|
return JSONResponse(status_code=409 if code=='MODEL_ENV_LOCKED' else 422,content={'error':{'code':code,'message':message}})
|
|
with service().connect() as db:
|
|
store.event(db,'','model.settings_updated',actor,{'managed_by':result['managed_by']})
|
|
return result
|
|
|
|
@router.post('/model-settings/probe')
|
|
def probe_model(request: Request):
|
|
actor=guard(request, True)
|
|
from services import model_settings
|
|
result=model_settings.probe()
|
|
with service().connect() as db:
|
|
store.event(db,'','model.connection_checked',actor,{'ok':result['ok']})
|
|
return result
|
|
|
|
@router.get('/projects/{project_id}/feedback')
|
|
def meeting_feedback(project_id: str,request: Request):
|
|
guard(request)
|
|
def action():
|
|
from services.meeting_feedback import summarize
|
|
with service().connect() as db:
|
|
project(db,request,project_id)
|
|
return summarize(db,project_id)
|
|
return translate(action)
|