feat:修复客服agent功能

This commit is contained in:
2026-09-14 17:47:25 +08:00
parent b989d78248
commit 4f296eb58c
10 changed files with 556 additions and 91 deletions
+124 -83
View File
@@ -56,7 +56,9 @@ from service.advisor_agent.context import (
load_rebalance_context,
load_recommendation_context,
)
from service.advisor_agent.customer_context import load_customer_context
from service.event_publisher import publish_event
from nl2sql.session_context import SessionContextStore, build_conversation_context
from schemas.advisor_agent import (
AdvisorDraftOperateReq,
AdvisorDraftSaveReq,
@@ -110,6 +112,16 @@ def _advisor_runtime(request: Request):
return getattr(getattr(app, "state", None), "advisor_agent_runtime", None)
@router.get("/session/{session_id}/history")
async def advisor_session_history(
session_id: str,
user: SysUser = Depends(audited_advisor),
):
"""读取当前投顾自己的短期 Agent 会话记录。"""
messages = await SessionContextStore(redis_db.client()).load(user.id, session_id)
return agent_success(messages, trace_id=new_request_id())
def _infer_chat_intent(query: str) -> str | None:
"""从自然语言问题推断投顾意图;无法确定时保留通用问答。"""
if any(word in query for word in ("调仓", "再平衡", "组合偏离")):
@@ -242,7 +254,8 @@ async def chat_stream(
explicit_intent=None,
)
inferred_intent = classification.intent
customer_id = None
scope = chat_request.scope
customer_id = chat_request.customer_id
if not chat_request.query:
code = ERR_CODE_FORBIDDEN_CUSTOMER
message = "对话请求缺少有效参数"
@@ -259,60 +272,30 @@ async def chat_stream(
payload = None
# 单客户范围且未传编号时,兼容从问题中解析客户;投顾范围查询不解析客户。
if inferred_intent in {
AGENT_INTENT_RECOMMEND,
AGENT_INTENT_REBALANCE,
AGENT_INTENT_FUND_ANALYSIS,
AGENT_INTENT_DIALOGUE_SCRIPT,
AGENT_INTENT_DATA_QUERY,
}:
customer_id, resolve_error = await _resolve_customer_from_query(
# 客户 ID 是可选上下文;未选择客户时尝试从自然语言识别姓名,失败不阻断 Agent 判断。
if customer_id is not None:
await ensure_customer_access(db, advisor_id=user.id, customer_id=int(customer_id))
else:
resolved_customer_id, _resolve_error = await _resolve_customer_from_query(
db, advisor_id=user.id, query=chat_request.query
)
if resolve_error:
payload = agent_failure(ERR_CODE_FORBIDDEN_CUSTOMER, resolve_error, trace_id=trace_id)
async def resolve_error_events():
yield f"data: {json.dumps({'type': SSE_EVENT_TYPE_ERROR, **payload}, ensure_ascii=False)}\n\n"
return StreamingResponse(resolve_error_events(), media_type="text/event-stream", headers={"X-Trace-Id": trace_id})
if resolved_customer_id is not None:
customer_id = resolved_customer_id
scope = "customer"
# 不带客户编号时只提供通用基金问答,不读取客户画像,也不生成个性化草稿。
if False:
payload = agent_failure(
ERR_CODE_FORBIDDEN_CUSTOMER,
"投顾范围查询仅支持客户数据查询",
trace_id=trace_id,
)
elif False:
payload = agent_failure(
ERR_CODE_FORBIDDEN_CUSTOMER,
"投顾范围查询不能指定单个客户",
trace_id=trace_id,
)
elif customer_id is None:
if inferred_intent in {
AGENT_INTENT_RECOMMEND,
AGENT_INTENT_REBALANCE,
AGENT_INTENT_FUND_ANALYSIS,
AGENT_INTENT_DIALOGUE_SCRIPT,
AGENT_INTENT_DATA_QUERY,
}:
if payload is None:
payload = agent_failure(
ERR_CODE_FORBIDDEN_CUSTOMER,
"个性化投顾分析需要在问题中明确客户编号或姓名",
trace_id=trace_id,
)
if customer_id is None:
if inferred_intent == AGENT_INTENT_DATA_QUERY:
scope = "advisor"
else:
llm_client = getattr(runtime, "llm_client", None)
if inferred_intent == AGENT_INTENT_CASUAL_CHAT:
answer = "您好,我是投顾助手,请选择客户后使用个性化分析。"
answer = "您好,我是投顾助手,可以协助您查询名下客户数据、分析基金和生成投顾辅助方案。"
elif llm_client is None:
answer = "已收到问题。当前未配置通用投顾模型,请选择客户后使用个性化分析,或联系管理员配置 Agent 服务。"
answer = "已收到问题。当前未配置通用投顾模型,但您可以直接查询名下客户数据。"
else:
answer = await generate_text(
llm_client,
system_prompt="你是基金投顾助手,只回答通用基金知识和产品分析问题,不读取或推断任何客户信息,不承诺收益,不代客交易。",
system_prompt="你是基金投顾助手。投顾未指定单个客户时,你可以回答通用问题或说明需要的客户范围;不得越权读取客户数据,不承诺收益,不代客交易。",
user_prompt=chat_request.query,
fallback=lambda: "当前模型暂时不可用,请稍后重试。",
timeout=5.0,
@@ -331,7 +314,42 @@ async def chat_stream(
media_type="text/event-stream",
headers={"X-Trace-Id": trace_id},
)
else:
if inferred_intent == AGENT_INTENT_DATA_QUERY:
effective_scope = "customer" if customer_id is not None else "advisor"
try:
context_store = SessionContextStore(redis_db.client())
conversation_context = build_conversation_context(
await context_store.load(user.id, chat_request.session_id)
)
result = await execute_advisor_data_query(
db,
advisor_id=user.id,
customer_id=int(customer_id) if customer_id is not None else None,
scope=effective_scope,
question=chat_request.query,
trace_id=trace_id,
session_id=chat_request.session_id,
conversation_context=conversation_context,
llm_client=getattr(_advisor_runtime(request), "llm_client", None),
)
except QueryServiceError as exc:
payload = agent_failure(ERR_CODE_LLM_ERROR, _data_query_error_message(exc), trace_id=trace_id)
else:
await context_store.append(
user.id, chat_request.session_id, chat_request.query,
result.get("answer") or result.get("summary") or "查询完成",
)
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"
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(events(), media_type="text/event-stream", headers={"X-Trace-Id": trace_id})
if customer_id is not None:
relation = await ensure_customer_access(
db, advisor_id=user.id, customer_id=int(customer_id)
)
@@ -345,6 +363,9 @@ async def chat_stream(
context = await load_recommendation_context(
db, customer_id=int(customer_id)
)
customer_context = await load_customer_context(
db, customer_id=int(customer_id), memories=memories
)
if context is not None:
try:
draft = await generate_recommendation_draft(
@@ -354,6 +375,7 @@ async def chat_stream(
trace_id=trace_id,
memories=memories,
llm_client=getattr(runtime, "llm_client", None),
customer_context=customer_context,
**context,
)
except Exception:
@@ -377,43 +399,6 @@ async def chat_stream(
):
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_DATA_QUERY:
if not chat_request.query or not chat_request.query.strip():
payload = agent_failure(
ERR_CODE_FORBIDDEN_CUSTOMER,
"查询问题不能为空",
trace_id=trace_id,
)
else:
try:
result = await execute_advisor_data_query(
db,
advisor_id=user.id,
customer_id=int(customer_id) if customer_id is not None else None,
scope="customer",
question=chat_request.query,
trace_id=trace_id,
llm_client=getattr(_advisor_runtime(request), "llm_client", None),
)
except QueryServiceError as exc:
payload = agent_failure(
ERR_CODE_LLM_ERROR,
_data_query_error_message(exc),
trace_id=trace_id,
)
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"
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(
events(),
media_type="text/event-stream",
@@ -432,6 +417,35 @@ async def chat_stream(
result = build_fund_analysis(
contexts[0]["fund"], contexts[0]["performance"]
)
runtime = _advisor_runtime(request)
if getattr(runtime, "llm_client", None) is not None:
customer_context = await load_customer_context(
db,
customer_id=int(customer_id),
memories=await _recall_advisor_memories(
request,
customer_id=int(customer_id),
query=chat_request.query,
),
)
result["analysis_text"] = await generate_text(
runtime.llm_client,
system_prompt=(
"你是合规的基金投顾分析助手。只能根据基金数据和客户上下文回答,"
"不得承诺收益,不得代客交易;如果信息不足要明确说明。"
),
user_prompt=json.dumps(
{
"question": chat_request.query,
"fund": contexts[0]["fund"],
"performance": contexts[0]["performance"],
"customer_context": customer_context,
},
ensure_ascii=False,
),
fallback=lambda: result["analysis_text"],
timeout=5.0,
)
async def events():
for event in (
@@ -456,6 +470,33 @@ async def chat_stream(
else:
scene_type = TALK_SCENE_PORTFOLIO_DIVERGENCE
result = build_talk_script(scene_type)
runtime = _advisor_runtime(request)
if getattr(runtime, "llm_client", None) is not None:
memories = await _recall_advisor_memories(
request,
customer_id=int(customer_id),
query=chat_request.query,
)
customer_context = await load_customer_context(
db, customer_id=int(customer_id), memories=memories
)
result["content"] = await generate_text(
runtime.llm_client,
system_prompt=(
"你是华夏基金合规投顾助手。请生成简洁、克制、尊重客户的沟通参考话术,"
"结合客户上下文但不要暴露内部字段,不承诺收益,不代客交易。"
),
user_prompt=json.dumps(
{
"scene": scene_type,
"request": chat_request.query,
"customer_context": customer_context,
},
ensure_ascii=False,
),
fallback=lambda: result["content"],
timeout=5.0,
)
async def events():
for event in (