feat:修改投顾agent和nl2sql的功能

This commit is contained in:
2026-09-13 23:46:15 +08:00
parent 4dcfcbeb3a
commit 958b785b04
11 changed files with 436 additions and 61 deletions
+168 -45
View File
@@ -7,12 +7,13 @@ from typing import Literal
from fastapi import APIRouter, BackgroundTasks, Depends, Query, Request
from fastapi.responses import StreamingResponse
from pydantic import ValidationError
from sqlalchemy.ext.asyncio import AsyncSession
from agent.advisor_agent.auth import ensure_customer_access
from agent.advisor_agent.data_query import execute_advisor_data_query
from agent.advisor_agent.intent.fund_analysis import build_fund_analysis
from agent.advisor_agent.intent.recognizer import recognize_advisor_intent
from agent.advisor_agent.intent.classifier import classify_advisor_intent
from agent.advisor_agent.intent.talk_script import build_talk_script
from agent.advisor_agent.llm import generate_text
from agent.advisor_agent.intent.generation_flow import (
@@ -21,7 +22,11 @@ from agent.advisor_agent.intent.generation_flow import (
)
from agent.advisor_agent.protocol import agent_failure, agent_success
from common.common_const import (
AGENT_INTENT_CASUAL_CHAT,
AGENT_INTENT_DATA_QUERY,
AGENT_INTENT_DIALOGUE_SCRIPT,
AGENT_INTENT_FUND_ANALYSIS,
AGENT_INTENT_REBALANCE,
AGENT_INTENT_RECOMMEND,
CUSTOMER_REL_STATUS_SIGNED,
ERR_CODE_DRAFT_NOT_FOUND,
@@ -34,6 +39,10 @@ from common.common_const import (
SSE_EVENT_TYPE_ERROR,
SSE_EVENT_TYPE_META,
SSE_EVENT_TYPE_TEXT,
TALK_SCENE_CUSTOMER_COMPLAINT,
TALK_SCENE_MARKET_FLUCTUATION,
TALK_SCENE_PORTFOLIO_DIVERGENCE,
TALK_SCENE_RISK_BLOCK_ORDER,
)
from api.deps import audited_advisor
from config.deps import get_db
@@ -196,9 +205,51 @@ async def chat_stream(
db: AsyncSession = Depends(get_db),
):
trace_id = _trace_id(request)
chat_request = body
try:
chat_request = (
body
if isinstance(body, AdvisorChatReq)
else AdvisorChatReq.model_validate(body)
)
except ValidationError:
payload = agent_failure(
ERR_CODE_FORBIDDEN_CUSTOMER,
"对话请求缺少有效参数",
trace_id=trace_id,
)
async def validation_error_events():
yield f"data: {json.dumps({'type': SSE_EVENT_TYPE_ERROR, **payload}, ensure_ascii=False)}\n\n"
return StreamingResponse(
validation_error_events(),
media_type="text/event-stream",
headers={"X-Trace-Id": trace_id},
)
runtime = _advisor_runtime(request)
classification = await classify_advisor_intent(
chat_request.query,
getattr(runtime, "llm_client", None),
explicit_intent=chat_request.intent,
)
inferred_intent = classification.intent
customer_id = chat_request.customer_id
inferred_intent = chat_request.intent or _infer_chat_intent(chat_request.query)
if not chat_request.query and not chat_request.intent:
code = ERR_CODE_LLM_ERROR if customer_id is not None else ERR_CODE_FORBIDDEN_CUSTOMER
message = _NOT_READY_MESSAGE if customer_id is not None else "对话请求缺少有效参数"
payload = agent_failure(code, message, trace_id=trace_id)
async def empty_query_events():
yield f"data: {json.dumps({'type': SSE_EVENT_TYPE_ERROR, **payload}, ensure_ascii=False)}\n\n"
return StreamingResponse(
empty_query_events(),
media_type="text/event-stream",
headers={"X-Trace-Id": trace_id},
)
payload = None
# 请求体只传问题时,从问题中解析客户;解析结果仍必须经过投顾关系授权校验。
if customer_id is None:
@@ -228,9 +279,10 @@ async def chat_stream(
if customer_id is None:
if inferred_intent in {
AGENT_INTENT_RECOMMEND,
"rebalance",
"fund_analysis",
"dialogue-script",
AGENT_INTENT_REBALANCE,
AGENT_INTENT_FUND_ANALYSIS,
AGENT_INTENT_DIALOGUE_SCRIPT,
AGENT_INTENT_DATA_QUERY,
}:
if payload is None:
payload = agent_failure(
@@ -239,9 +291,10 @@ async def chat_stream(
trace_id=trace_id,
)
else:
runtime = _advisor_runtime(request)
llm_client = getattr(runtime, "llm_client", None)
if llm_client is None:
if inferred_intent == AGENT_INTENT_CASUAL_CHAT:
answer = "您好,我是投顾助手,请选择客户后使用个性化分析。"
elif llm_client is None:
answer = "已收到问题。当前未配置通用投顾模型,请选择客户后使用个性化分析,或联系管理员配置 Agent 服务。"
else:
answer = await generate_text(
@@ -254,7 +307,7 @@ async def chat_stream(
async def events():
for event in (
{"type": SSE_EVENT_TYPE_META, "intent": "general_question"},
{"type": SSE_EVENT_TYPE_META, "intent": inferred_intent or "general_question"},
{"type": SSE_EVENT_TYPE_TEXT, "content": answer},
{"type": SSE_EVENT_TYPE_DONE},
):
@@ -267,15 +320,10 @@ async def chat_stream(
)
else:
customer_id = chat_request.customer_id
resolved_intent = recognize_advisor_intent(
chat_request.query,
chat_request.intent,
)
relation = await ensure_customer_access(
db, advisor_id=user.id, customer_id=int(customer_id)
)
if inferred_intent == AGENT_INTENT_RECOMMEND:
if resolved_intent == AGENT_INTENT_RECOMMEND:
memories = await _recall_advisor_memories(
request,
customer_id=int(customer_id),
@@ -286,35 +334,43 @@ async def chat_stream(
db, customer_id=int(customer_id)
)
if context is not None:
draft = await generate_recommendation_draft(
draft_repo=AdvisorDraftRepo(db),
advisor_id=user.id,
relation_status=relation.status,
trace_id=trace_id,
memories=memories,
llm_client=getattr(runtime, "llm_client", None),
**context,
)
try:
draft = await generate_recommendation_draft(
draft_repo=AdvisorDraftRepo(db),
advisor_id=user.id,
relation_status=relation.status,
trace_id=trace_id,
memories=memories,
llm_client=getattr(runtime, "llm_client", None),
**context,
)
except Exception:
logger.warning("advisor recommendation generation failed", exc_info=True)
payload = agent_failure(
ERR_CODE_LLM_ERROR,
"推荐方案生成失败,请稍后重试",
trace_id=trace_id,
)
else:
async def events():
for event in (
{
"type": SSE_EVENT_TYPE_META,
"draft_id": draft.draft_id,
"intent": draft.intent,
"status": draft.status,
},
{"type": SSE_EVENT_TYPE_TEXT, "content": draft.content},
{"type": SSE_EVENT_TYPE_DONE, "draft_id": draft.draft_id},
):
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
async def events():
for event in (
{
"type": SSE_EVENT_TYPE_META,
"draft_id": draft.draft_id,
"intent": draft.intent,
"status": draft.status,
},
{"type": SSE_EVENT_TYPE_TEXT, "content": draft.content},
{"type": SSE_EVENT_TYPE_DONE, "draft_id": draft.draft_id},
):
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
return StreamingResponse(
events(),
media_type="text/event-stream",
headers={"X-Trace-Id": trace_id},
)
if resolved_intent == AGENT_INTENT_DATA_QUERY:
return StreamingResponse(
events(),
media_type="text/event-stream",
headers={"X-Trace-Id": trace_id},
)
if inferred_intent == AGENT_INTENT_DATA_QUERY:
if not chat_request.query or not chat_request.query.strip():
payload = agent_failure(
ERR_CODE_FORBIDDEN_CUSTOMER,
@@ -340,8 +396,9 @@ async def chat_stream(
else:
async def events():
yield f"data: {json.dumps({'type': SSE_EVENT_TYPE_META, 'intent': AGENT_INTENT_DATA_QUERY, 'query_id': result.get('query_id'), 'trace_id': trace_id}, ensure_ascii=False)}\n\n"
if result.get("summary"):
yield f"data: {json.dumps({'type': SSE_EVENT_TYPE_TEXT, 'content': result['summary']}, ensure_ascii=False)}\n\n"
answer = result.get("answer") or result.get("summary")
if answer:
yield f"data: {json.dumps({'type': SSE_EVENT_TYPE_TEXT, 'content': answer}, ensure_ascii=False)}\n\n"
yield f"data: {json.dumps({'type': SSE_EVENT_TYPE_DONE, 'query_id': result.get('query_id')}, ensure_ascii=False)}\n\n"
return StreamingResponse(
@@ -349,7 +406,73 @@ async def chat_stream(
media_type="text/event-stream",
headers={"X-Trace-Id": trace_id},
)
payload = agent_failure(_NOT_READY_CODE, _NOT_READY_MESSAGE, trace_id=trace_id)
if inferred_intent == AGENT_INTENT_FUND_ANALYSIS:
fund_codes = re.findall(r"[A-Za-z]{1,6}\d{3,8}", chat_request.query.upper())
contexts = await load_fund_analysis_context(db, fund_codes=fund_codes)
if not contexts:
payload = agent_failure(
ERR_CODE_LLM_ERROR,
"未找到可分析的基金数据",
trace_id=trace_id,
)
else:
result = build_fund_analysis(
contexts[0]["fund"], contexts[0]["performance"]
)
async def events():
for event in (
{"type": SSE_EVENT_TYPE_META, "intent": AGENT_INTENT_FUND_ANALYSIS},
{"type": SSE_EVENT_TYPE_TEXT, "content": result["analysis_text"]},
{"type": SSE_EVENT_TYPE_DONE},
):
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
return StreamingResponse(
events(),
media_type="text/event-stream",
headers={"X-Trace-Id": trace_id},
)
if inferred_intent == AGENT_INTENT_DIALOGUE_SCRIPT:
if "市场" in chat_request.query or "波动" in chat_request.query:
scene_type = TALK_SCENE_MARKET_FLUCTUATION
elif "投诉" in chat_request.query:
scene_type = TALK_SCENE_CUSTOMER_COMPLAINT
elif "拦截" in chat_request.query or "风控" in chat_request.query:
scene_type = TALK_SCENE_RISK_BLOCK_ORDER
else:
scene_type = TALK_SCENE_PORTFOLIO_DIVERGENCE
result = build_talk_script(scene_type)
async def events():
for event in (
{"type": SSE_EVENT_TYPE_META, "intent": AGENT_INTENT_DIALOGUE_SCRIPT},
{"type": SSE_EVENT_TYPE_TEXT, "content": result["content"]},
{"type": SSE_EVENT_TYPE_DONE},
):
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
return StreamingResponse(
events(),
media_type="text/event-stream",
headers={"X-Trace-Id": trace_id},
)
if inferred_intent == AGENT_INTENT_CASUAL_CHAT:
async def events():
for event in (
{"type": SSE_EVENT_TYPE_META, "intent": AGENT_INTENT_CASUAL_CHAT},
{"type": SSE_EVENT_TYPE_TEXT, "content": "您好,我是投顾助手,可以协助您进行基金分析和投资组合管理。"},
{"type": SSE_EVENT_TYPE_DONE},
):
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
return StreamingResponse(
events(),
media_type="text/event-stream",
headers={"X-Trace-Id": trace_id},
)
if payload is None:
payload = agent_failure(_NOT_READY_CODE, _NOT_READY_MESSAGE, trace_id=trace_id)
async def events():
yield f"data: {json.dumps({'type': SSE_EVENT_TYPE_ERROR, **payload}, ensure_ascii=False)}\n\n"