diff --git a/app/api/v1/conversations.py b/app/api/v1/conversations.py index 862a3fa..0a3ef0c 100644 --- a/app/api/v1/conversations.py +++ b/app/api/v1/conversations.py @@ -14,6 +14,7 @@ from app.schemas.conversation import ( ConversationData, ConversationMessageCreateRequest, ConversationSummary, + DirectQueryRequest, ) from app.services.conversations import ConversationService @@ -39,6 +40,15 @@ async def create_conversation( return ApiResponse(data=await ConversationService(session).create(current_user, payload.title)) +@router.post("/direct-query", response_model=ApiResponse[dict]) +async def direct_query( + payload: DirectQueryRequest, + session: DatabaseSession, + _current_user: Annotated[UserAccount, Depends(require_permission("risk.assess", operate=True))], +) -> ApiResponse[dict]: + return ApiResponse(data=await ConversationService(session).direct_query(payload.kind, payload.query)) + + @router.get("/{conversation_id}", response_model=ApiResponse[ConversationData]) async def get_conversation( conversation_id: int, diff --git a/app/schemas/conversation.py b/app/schemas/conversation.py index 3c60ee4..7dedf78 100644 --- a/app/schemas/conversation.py +++ b/app/schemas/conversation.py @@ -19,6 +19,11 @@ class ConversationMessageCreateRequest(ApiSchema): message_type: Literal["text", "image", "file", "audio"] = "text" +class DirectQueryRequest(ApiSchema): + kind: Literal["supplier", "material", "cases", "solution"] + query: str = Field(min_length=1, max_length=2000) + + class ConversationListQuery(ApiSchema): limit: int = Field(default=50, ge=1, le=100) diff --git a/app/services/conversations.py b/app/services/conversations.py index 1ade4fe..f78d1b6 100644 --- a/app/services/conversations.py +++ b/app/services/conversations.py @@ -128,6 +128,49 @@ class ConversationService: raise AppException("对话创建失败", code=500, status_code=500) return self._data(persisted, messages=[], total=0, offset=0) + async def direct_query(self, kind: str, query: str) -> dict[str, Any]: + """Build a shortcut result without creating a conversation message.""" + text = query.strip() + if not text: + raise AppException("查询内容不能为空", code=422, status_code=422) + if kind == "supplier": + return await self._supplier_card(text, {"supplier": text}) + if kind == "material": + return await self._material_card(text, {"packaging": text}) + if kind == "solution": + semantics = self._normalize_semantics( + text, + await self.llm.extract_semantics(text, history=[]), + ) + return await self._solution_card(text, semantics) + results = await self._similar_cases( + text, + category=None, + material=None, + supplier=None, + factory=None, + stage=None, + anomaly_type=None, + ) + top_cases = list(results[:3]) + # Keep the same similarity signal used by the risk assessment card. + # Vector retrieval populates it in RAG; lexical fallback still gets a + # deterministic score so the shortcut card never loses that field. + for item in top_cases: + if item.case.similarity is None: + item.case.similarity = round(max(0.0, min(1.0, item.score / 100)), 3) + return { + "type": "cases", + "title": "案例检索", + "query": text, + "cases": [item.case.model_dump(mode="json", by_alias=True) for item in top_cases], + "matchedCount": len(top_cases), + "suggestions": [ + "同类问题优先复用历史方案,再按判定标准复测验证", + "如已出货,准备客诉应对预案并对同批次抽检", + ], + } + async def delete(self, conversation_id: int, user: UserAccount) -> None: """Delete one user-owned conversation and retain an audit trail.""" @@ -876,6 +919,12 @@ class ConversationService: "type": "solution", "title": "AI 案例推理生成", "confidence": "中", + "thinkingSummary": [ + f"识别产品场景:{category},提取包装、供应链和风险问题等关键信息", + f"检索历史异常案例,匹配到 {len(similar_cases)} 条相似案例并进行相似度排序", + f"关联判定标准库 {len(standards)} 条规则,完成规则引擎兜底校验", + f"综合历史案例与标准规则,整理 {len(suggestions)} 条可执行建议", + ], "analysisBase": ( f"目标品类「{category}」当前命中历史异常 {direct_count} 条;" "方案结合已导入异常案例和标准规则生成。"