feat:修改投顾agent和nl2sql的功能
This commit is contained in:
+168
-45
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user