feat: 添加线索引擎、NLQ、场景检测、前端界面等核心功能模块
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""线索看板与处置 API(R7/R17/R18/R20)。
|
||||
|
||||
注意:不提供删除线索的端点(R19 线索不可删,独立性硬约束)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.schemas import (
|
||||
AdjudicateRequest,
|
||||
AssignRequest,
|
||||
ClueOut,
|
||||
DashboardSummary,
|
||||
)
|
||||
from app.clues import service as clue_svc
|
||||
from app.clues.models import Clue, ClueStatus, ConfidenceTier
|
||||
from app.db import get_session
|
||||
|
||||
router = APIRouter(prefix="/clues", tags=["clues"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[ClueOut])
|
||||
def list_clues(
|
||||
status: ClueStatus | None = Query(default=None),
|
||||
scenario_code: str | None = Query(default=None),
|
||||
confidence: ConfidenceTier | None = Query(default=None),
|
||||
session: Session = Depends(get_session),
|
||||
) -> list[Clue]:
|
||||
return clue_svc.list_clues(
|
||||
session, status=status, scenario_code=scenario_code, confidence=confidence
|
||||
)
|
||||
|
||||
|
||||
@router.get("/summary", response_model=DashboardSummary)
|
||||
def summary(session: Session = Depends(get_session)) -> DashboardSummary:
|
||||
"""运营看板汇总(R18/R21 的基础指标)。"""
|
||||
clues = session.query(Clue).all()
|
||||
by_status: dict[str, int] = {}
|
||||
by_conf: dict[str, int] = {}
|
||||
by_scenario: dict[str, int] = {}
|
||||
total_amount = 0.0
|
||||
for c in clues:
|
||||
by_status[c.status.value] = by_status.get(c.status.value, 0) + 1
|
||||
by_conf[c.confidence.value] = by_conf.get(c.confidence.value, 0) + 1
|
||||
by_scenario[c.scenario_code] = by_scenario.get(c.scenario_code, 0) + 1
|
||||
total_amount += c.amount_involved or 0.0
|
||||
return DashboardSummary(
|
||||
total=len(clues),
|
||||
by_status=by_status,
|
||||
by_confidence=by_conf,
|
||||
by_scenario=by_scenario,
|
||||
total_amount_involved=total_amount,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{clue_id}", response_model=ClueOut)
|
||||
def get_clue(clue_id: uuid.UUID, session: Session = Depends(get_session)) -> Clue:
|
||||
clue = session.get(Clue, clue_id)
|
||||
if clue is None:
|
||||
raise HTTPException(status_code=404, detail="线索不存在")
|
||||
return clue
|
||||
|
||||
|
||||
@router.post("/{clue_id}/assign", response_model=ClueOut)
|
||||
def assign_clue(
|
||||
clue_id: uuid.UUID, req: AssignRequest, session: Session = Depends(get_session)
|
||||
) -> Clue:
|
||||
clue = session.get(Clue, clue_id)
|
||||
if clue is None:
|
||||
raise HTTPException(status_code=404, detail="线索不存在")
|
||||
return clue_svc.assign(session, clue, assignee=req.assignee, actor=req.actor)
|
||||
|
||||
|
||||
@router.post("/{clue_id}/adjudicate", response_model=ClueOut)
|
||||
def adjudicate_clue(
|
||||
clue_id: uuid.UUID, req: AdjudicateRequest, session: Session = Depends(get_session)
|
||||
) -> Clue:
|
||||
clue = session.get(Clue, clue_id)
|
||||
if clue is None:
|
||||
raise HTTPException(status_code=404, detail="线索不存在")
|
||||
clue_svc.adjudicate(session, clue, confirmed=req.confirmed, actor=req.actor, note=req.note)
|
||||
return clue
|
||||
@@ -0,0 +1,24 @@
|
||||
"""自然语言查询 API(R4/R20)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.schemas import NLQRequest, NLQResponse
|
||||
from app.db import get_session
|
||||
from app.nlq import service as nlq
|
||||
|
||||
router = APIRouter(prefix="/nlq", tags=["nlq"])
|
||||
|
||||
|
||||
@router.post("", response_model=NLQResponse)
|
||||
def ask(req: NLQRequest, session: Session = Depends(get_session)) -> NLQResponse:
|
||||
ans = nlq.ask(req.question, session=session)
|
||||
return NLQResponse(
|
||||
question=ans.question,
|
||||
answer=ans.answer,
|
||||
provider=ans.provider,
|
||||
model=ans.model,
|
||||
egress=ans.egress,
|
||||
)
|
||||
@@ -34,3 +34,52 @@ class PenetrateResponse(BaseModel):
|
||||
max_depth: int
|
||||
related_count: int
|
||||
related: list[RelatedEntityOut]
|
||||
|
||||
|
||||
class ClueOut(BaseModel):
|
||||
id: uuid.UUID
|
||||
title: str
|
||||
risk_domain: str
|
||||
scenario_code: str
|
||||
confidence: str
|
||||
score: float
|
||||
status: str
|
||||
rationale: str
|
||||
evidence: dict = Field(default_factory=dict)
|
||||
subjects: dict = Field(default_factory=dict)
|
||||
amount_involved: float | None = None
|
||||
assignee: str | None = None
|
||||
feedback: str | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class AssignRequest(BaseModel):
|
||||
assignee: str = Field(min_length=1)
|
||||
actor: str = Field(min_length=1)
|
||||
|
||||
|
||||
class AdjudicateRequest(BaseModel):
|
||||
confirmed: bool
|
||||
actor: str = Field(min_length=1)
|
||||
note: str | None = None
|
||||
|
||||
|
||||
class NLQRequest(BaseModel):
|
||||
question: str = Field(min_length=1)
|
||||
|
||||
|
||||
class NLQResponse(BaseModel):
|
||||
question: str
|
||||
answer: str
|
||||
provider: str
|
||||
model: str
|
||||
egress: bool
|
||||
|
||||
|
||||
class DashboardSummary(BaseModel):
|
||||
total: int
|
||||
by_status: dict[str, int]
|
||||
by_confidence: dict[str, int]
|
||||
by_scenario: dict[str, int]
|
||||
total_amount_involved: float
|
||||
|
||||
Reference in New Issue
Block a user