fix: ensure agent never returns empty result to user
This commit is contained in:
parent
87c59bb3e2
commit
9caf332e9e
|
|
@ -596,6 +596,14 @@ class ReActEngine:
|
||||||
else:
|
else:
|
||||||
output = response.content or ""
|
output = response.content or ""
|
||||||
|
|
||||||
|
# 兜底:确保 output 永远不为空字符串
|
||||||
|
if not output or not output.strip():
|
||||||
|
output = (
|
||||||
|
"抱歉,我暂时无法生成有效的回复。请尝试换一种方式描述你的需求,"
|
||||||
|
"或者稍后再试。"
|
||||||
|
)
|
||||||
|
trace_outcome = "empty_fallback"
|
||||||
|
|
||||||
# 结束轨迹记录
|
# 结束轨迹记录
|
||||||
if trace_recorder is not None:
|
if trace_recorder is not None:
|
||||||
trace_recorder.end_trace(outcome=trace_outcome)
|
trace_recorder.end_trace(outcome=trace_outcome)
|
||||||
|
|
@ -1116,6 +1124,24 @@ class ReActEngine:
|
||||||
"max_steps_reached": True,
|
"max_steps_reached": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 兜底:确保 output 永远不为空字符串
|
||||||
|
if not output or not output.strip():
|
||||||
|
output = (
|
||||||
|
"抱歉,我暂时无法生成有效的回复。请尝试换一种方式描述你的需求,"
|
||||||
|
"或者稍后再试。"
|
||||||
|
)
|
||||||
|
trace_outcome = "empty_fallback"
|
||||||
|
yield ReActEvent(
|
||||||
|
event_type="final_answer",
|
||||||
|
step=step,
|
||||||
|
data={
|
||||||
|
"output": output,
|
||||||
|
"total_steps": len(trajectory),
|
||||||
|
"total_tokens": total_tokens,
|
||||||
|
"empty_fallback": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
# 结束轨迹记录 — always runs even if consumer doesn't fully iterate
|
# 结束轨迹记录 — always runs even if consumer doesn't fully iterate
|
||||||
if trace_recorder is not None:
|
if trace_recorder is not None:
|
||||||
|
|
|
||||||
|
|
@ -514,18 +514,19 @@ async def _handle_chat_message(
|
||||||
task_type="chat",
|
task_type="chat",
|
||||||
)
|
)
|
||||||
final_content = response.content or ""
|
final_content = response.content or ""
|
||||||
if final_content:
|
if not final_content or not final_content.strip():
|
||||||
await websocket.send_json({
|
final_content = "抱歉,我暂时无法生成有效的回复。请尝试换一种方式描述你的需求,或者稍后再试。"
|
||||||
"type": "final_answer",
|
await websocket.send_json({
|
||||||
"content": final_content,
|
"type": "final_answer",
|
||||||
"is_final": True,
|
"content": final_content,
|
||||||
})
|
"is_final": True,
|
||||||
await sm.append_message(
|
})
|
||||||
session_id=session_id,
|
await sm.append_message(
|
||||||
role=MessageRole.ASSISTANT,
|
session_id=session_id,
|
||||||
content=final_content,
|
role=MessageRole.ASSISTANT,
|
||||||
agent_name=agent.name,
|
content=final_content,
|
||||||
)
|
agent_name=agent.name,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Chat DIRECT_CHAT error for session {session_id}: {e}")
|
logger.error(f"Chat DIRECT_CHAT error for session {session_id}: {e}")
|
||||||
await websocket.send_json({"type": "error", "data": {"message": str(e)[:200]}})
|
await websocket.send_json({"type": "error", "data": {"message": str(e)[:200]}})
|
||||||
|
|
@ -611,6 +612,8 @@ async def _handle_chat_message(
|
||||||
token_buffer.clear()
|
token_buffer.clear()
|
||||||
# Then send final answer
|
# Then send final answer
|
||||||
final_content = event.data.get("output", "")
|
final_content = event.data.get("output", "")
|
||||||
|
if not final_content or not final_content.strip():
|
||||||
|
final_content = "抱歉,我暂时无法生成有效的回复。请尝试换一种方式描述你的需求,或者稍后再试。"
|
||||||
await websocket.send_json({
|
await websocket.send_json({
|
||||||
"type": "final_answer",
|
"type": "final_answer",
|
||||||
"content": final_content,
|
"content": final_content,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,17 @@ router = APIRouter(tags=["portal"])
|
||||||
_api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
_api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
||||||
_api_key_query = APIKeyQuery(name="api_key", auto_error=False)
|
_api_key_query = APIKeyQuery(name="api_key", auto_error=False)
|
||||||
|
|
||||||
|
_EMPTY_RESPONSE_FALLBACK = (
|
||||||
|
"抱歉,我暂时无法生成有效的回复。请尝试换一种方式描述你的需求,或者稍后再试。"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_non_empty(text: str | None) -> str:
|
||||||
|
"""Ensure response text is never empty or whitespace-only."""
|
||||||
|
if text and text.strip():
|
||||||
|
return text
|
||||||
|
return _EMPTY_RESPONSE_FALLBACK
|
||||||
|
|
||||||
|
|
||||||
async def _verify_api_key(
|
async def _verify_api_key(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
@ -390,7 +401,7 @@ async def chat(request: ChatRequest, req: Request, _auth: None = Depends(_verify
|
||||||
agent_name="default",
|
agent_name="default",
|
||||||
task_type="chat",
|
task_type="chat",
|
||||||
)
|
)
|
||||||
response_text = response.content or ""
|
response_text = _ensure_non_empty(response.content)
|
||||||
else:
|
else:
|
||||||
# REACT / SKILL_REACT / REWOO / REFLEXION / PLAN_EXEC / TEAM_COLLAB
|
# REACT / SKILL_REACT / REWOO / REFLEXION / PLAN_EXEC / TEAM_COLLAB
|
||||||
# Advanced modes (REWOO, REFLEXION, PLAN_EXEC, TEAM_COLLAB) currently
|
# Advanced modes (REWOO, REFLEXION, PLAN_EXEC, TEAM_COLLAB) currently
|
||||||
|
|
@ -437,9 +448,9 @@ async def chat(request: ChatRequest, req: Request, _auth: None = Depends(_verify
|
||||||
if event.event_type == "final_answer":
|
if event.event_type == "final_answer":
|
||||||
collected_output.append(event.data.get("output", ""))
|
collected_output.append(event.data.get("output", ""))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
response_text = f"Error: {e}"
|
response_text = f"执行出错: {e}"
|
||||||
else:
|
else:
|
||||||
response_text = "".join(collected_output) if collected_output else ""
|
response_text = _ensure_non_empty("".join(collected_output) if collected_output else None)
|
||||||
|
|
||||||
await _conversation_store.add_message(conv.id, "assistant", response_text)
|
await _conversation_store.add_message(conv.id, "assistant", response_text)
|
||||||
|
|
||||||
|
|
@ -495,9 +506,8 @@ async def chat_stream(request: ChatRequest, req: Request, _auth: None = Depends(
|
||||||
agent_name="default",
|
agent_name="default",
|
||||||
task_type="chat",
|
task_type="chat",
|
||||||
)
|
)
|
||||||
response_text = response.content or ""
|
response_text = _ensure_non_empty(response.content)
|
||||||
if response_text:
|
await _conversation_store.add_message(conv.id, "assistant", response_text)
|
||||||
await _conversation_store.add_message(conv.id, "assistant", response_text)
|
|
||||||
yield {
|
yield {
|
||||||
"event": "final_answer",
|
"event": "final_answer",
|
||||||
"data": json.dumps(
|
"data": json.dumps(
|
||||||
|
|
@ -561,9 +571,10 @@ async def chat_stream(request: ChatRequest, req: Request, _auth: None = Depends(
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
||||||
response_text = "".join(collected_output) if collected_output else ""
|
response_text = _ensure_non_empty(
|
||||||
if response_text:
|
"".join(collected_output) if collected_output else None
|
||||||
await _conversation_store.add_message(conv.id, "assistant", response_text)
|
)
|
||||||
|
await _conversation_store.add_message(conv.id, "assistant", response_text)
|
||||||
|
|
||||||
return EventSourceResponse(event_generator())
|
return EventSourceResponse(event_generator())
|
||||||
|
|
||||||
|
|
@ -823,14 +834,14 @@ async def portal_websocket(websocket: WebSocket):
|
||||||
task_type="chat",
|
task_type="chat",
|
||||||
)
|
)
|
||||||
# Store assistant reply for multi-turn context continuity
|
# Store assistant reply for multi-turn context continuity
|
||||||
if response.content:
|
response_content = _ensure_non_empty(response.content)
|
||||||
await _conversation_store.add_message(conv.id, "assistant", response.content)
|
await _conversation_store.add_message(conv.id, "assistant", response_content)
|
||||||
await websocket.send_json(
|
await websocket.send_json(
|
||||||
{
|
{
|
||||||
"type": "result",
|
"type": "result",
|
||||||
"data": {
|
"data": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"content": response.content,
|
"content": response_content,
|
||||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -887,14 +898,14 @@ async def portal_websocket(websocket: WebSocket):
|
||||||
task_type="chat",
|
task_type="chat",
|
||||||
)
|
)
|
||||||
# Store assistant reply for multi-turn context continuity
|
# Store assistant reply for multi-turn context continuity
|
||||||
if response.content:
|
response_content = _ensure_non_empty(response.content)
|
||||||
await _conversation_store.add_message(conv.id, "assistant", response.content)
|
await _conversation_store.add_message(conv.id, "assistant", response_content)
|
||||||
await websocket.send_json(
|
await websocket.send_json(
|
||||||
{
|
{
|
||||||
"type": "result",
|
"type": "result",
|
||||||
"data": {
|
"data": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"content": response.content,
|
"content": response_content,
|
||||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -959,11 +970,12 @@ async def portal_websocket(websocket: WebSocket):
|
||||||
await websocket.send_json({"type": "error", "data": {"message": str(e)}})
|
await websocket.send_json({"type": "error", "data": {"message": str(e)}})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
response_text = "".join(collected_output) if collected_output else ""
|
response_text = _ensure_non_empty(
|
||||||
if response_text:
|
"".join(collected_output) if collected_output else None
|
||||||
await _conversation_store.add_message(conv.id, "assistant", response_text)
|
)
|
||||||
|
await _conversation_store.add_message(conv.id, "assistant", response_text)
|
||||||
|
|
||||||
outcome = "success" if response_text else "failure"
|
outcome = "success" if response_text != _EMPTY_RESPONSE_FALLBACK else "failure"
|
||||||
await websocket.send_json(
|
await websocket.send_json(
|
||||||
{
|
{
|
||||||
"type": "result",
|
"type": "result",
|
||||||
|
|
|
||||||
|
|
@ -404,6 +404,9 @@ class ShellTool(Tool):
|
||||||
else:
|
else:
|
||||||
output = stdout.decode("utf-8", errors="replace") if stdout else ""
|
output = stdout.decode("utf-8", errors="replace") if stdout else ""
|
||||||
exit_code = proc.returncode if proc.returncode is not None else 0
|
exit_code = proc.returncode if proc.returncode is not None else 0
|
||||||
|
# Ensure non-empty output for successful commands
|
||||||
|
if exit_code == 0 and not output.strip():
|
||||||
|
output = "[命令执行成功,无输出内容]"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
output = str(e)
|
output = str(e)
|
||||||
exit_code = -1
|
exit_code = -1
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,10 @@ class WebCrawlTool(Tool):
|
||||||
else:
|
else:
|
||||||
content = result.markdown or ""
|
content = result.markdown or ""
|
||||||
|
|
||||||
|
# Ensure non-empty content with helpful message
|
||||||
|
if not content.strip():
|
||||||
|
content = f"[页面抓取成功但内容为空。URL: {url}。可能原因:1) 页面需要 JavaScript 渲染;2) 页面内容为动态加载;3) 页面被反爬虫拦截]"
|
||||||
|
|
||||||
# 提取链接
|
# 提取链接
|
||||||
links: list[str] = []
|
links: list[str] = []
|
||||||
if hasattr(result, "links") and result.links:
|
if hasattr(result, "links") and result.links:
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,12 @@ class WebSearchTool(Tool):
|
||||||
if result.get("success") and result.get("total", 0) > 0:
|
if result.get("success") and result.get("total", 0) > 0:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Return whatever we have (may be empty)
|
# Return whatever we have, with helpful fallback if empty
|
||||||
|
if not result.get("success") or result.get("total", 0) == 0:
|
||||||
|
result["fallback_message"] = (
|
||||||
|
f"未找到与 '{query}' 相关的搜索结果。建议:1) 尝试不同的关键词;"
|
||||||
|
"2) 检查网络连接;3) 稍后重试"
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue