feat:修复客服agent功能
This commit is contained in:
+124
-83
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user