feat: Implement incremental historical event loading and a centralized state synchronization mechanism, including stream reconnection, for the AgentAudit page.
Build and Push CodeReview / build (push) Waiting to run
Details
Build and Push CodeReview / build (push) Waiting to run
Details
This commit is contained in:
parent
05db656fd1
commit
62c42341c4
|
|
@ -574,6 +574,17 @@ Final Answer: {{"findings": [...], "summary": "..."}}"""
|
||||||
# 重置空响应计数器
|
# 重置空响应计数器
|
||||||
self._empty_retry_count = 0
|
self._empty_retry_count = 0
|
||||||
|
|
||||||
|
# 🔥 检查是否是 API 错误(从 BaseAgent.stream_llm_call 返回)
|
||||||
|
if llm_output.startswith("[API_ERROR:"):
|
||||||
|
# 提取错误类型和消息
|
||||||
|
match = re.match(r"\[API_ERROR:(\w+)\]\s*(.*)", llm_output)
|
||||||
|
if match:
|
||||||
|
error_type = match.group(1)
|
||||||
|
error_message = match.group(2)
|
||||||
|
logger.error(f"[{self.name}] Fatal API error: {error_type} - {error_message}")
|
||||||
|
await self.emit_event("error", f"LLM API 错误 ({error_type}): {error_message}")
|
||||||
|
break
|
||||||
|
|
||||||
# 解析 LLM 响应
|
# 解析 LLM 响应
|
||||||
step = self._parse_llm_response(llm_output)
|
step = self._parse_llm_response(llm_output)
|
||||||
self._steps.append(step)
|
self._steps.append(step)
|
||||||
|
|
|
||||||
|
|
@ -1033,7 +1033,7 @@ class BaseAgent(ABC):
|
||||||
|
|
||||||
# 使用特殊前缀标记 API 错误,让调用方能够识别
|
# 使用特殊前缀标记 API 错误,让调用方能够识别
|
||||||
# 格式:[API_ERROR:error_type] user_message
|
# 格式:[API_ERROR:error_type] user_message
|
||||||
if error_type in ("rate_limit", "quota_exceeded", "authentication", "connection"):
|
if error_type in ("rate_limit", "quota_exceeded", "authentication", "connection", "server_error"):
|
||||||
accumulated = f"[API_ERROR:{error_type}] {user_message}"
|
accumulated = f"[API_ERROR:{error_type}] {user_message}"
|
||||||
elif not accumulated:
|
elif not accumulated:
|
||||||
accumulated = f"[系统错误: {error_msg}] 请重新思考并输出你的决策。"
|
accumulated = f"[系统错误: {error_msg}] 请重新思考并输出你的决策。"
|
||||||
|
|
|
||||||
|
|
@ -331,6 +331,19 @@ Action Input: {{"参数": "值"}}
|
||||||
await asyncio.sleep(5) # 等待 5 秒后重试
|
await asyncio.sleep(5) # 等待 5 秒后重试
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
elif error_type == "server_error":
|
||||||
|
# 服务器错误 (5xx) - 重试
|
||||||
|
api_retry_count = getattr(self, '_api_retry_count', 0) + 1
|
||||||
|
self._api_retry_count = api_retry_count
|
||||||
|
if api_retry_count >= 3:
|
||||||
|
logger.error(f"[{self.name}] Too many server errors, stopping")
|
||||||
|
await self.emit_event("error", f"API 服务端错误重试次数过多: {error_message}")
|
||||||
|
break
|
||||||
|
logger.warning(f"[{self.name}] Server error, retrying ({api_retry_count}/3)")
|
||||||
|
await self.emit_event("warning", f"API 服务端繁忙或异常,重试中 ({api_retry_count}/3)")
|
||||||
|
await asyncio.sleep(10) # 等待 10 秒后重试(服务端错误通常需要更久恢复)
|
||||||
|
continue
|
||||||
|
|
||||||
# 重置 API 重试计数器(成功获取响应后)
|
# 重置 API 重试计数器(成功获取响应后)
|
||||||
self._api_retry_count = 0
|
self._api_retry_count = 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -509,6 +509,17 @@ Final Answer: [JSON格式的结果]"""
|
||||||
# 重置空响应计数器
|
# 重置空响应计数器
|
||||||
self._empty_retry_count = 0
|
self._empty_retry_count = 0
|
||||||
|
|
||||||
|
# 🔥 检查是否是 API 错误(从 BaseAgent.stream_llm_call 返回)
|
||||||
|
if llm_output.startswith("[API_ERROR:"):
|
||||||
|
# 提取错误类型和消息
|
||||||
|
match = re.match(r"\[API_ERROR:(\w+)\]\s*(.*)", llm_output)
|
||||||
|
if match:
|
||||||
|
error_type = match.group(1)
|
||||||
|
error_message = match.group(2)
|
||||||
|
logger.error(f"[{self.name}] Fatal API error: {error_type} - {error_message}")
|
||||||
|
await self.emit_event("error", f"LLM API 错误 ({error_type}): {error_message}")
|
||||||
|
break
|
||||||
|
|
||||||
# 解析 LLM 响应
|
# 解析 LLM 响应
|
||||||
step = self._parse_llm_response(llm_output)
|
step = self._parse_llm_response(llm_output)
|
||||||
self._steps.append(step)
|
self._steps.append(step)
|
||||||
|
|
|
||||||
|
|
@ -718,6 +718,17 @@ class VerificationAgent(BaseAgent):
|
||||||
|
|
||||||
self._total_tokens += tokens_this_round
|
self._total_tokens += tokens_this_round
|
||||||
|
|
||||||
|
# 🔥 检查是否是 API 错误(从 BaseAgent.stream_llm_call 返回)
|
||||||
|
if llm_output.startswith("[API_ERROR:"):
|
||||||
|
# 提取错误类型和消息
|
||||||
|
match = re.match(r"\[API_ERROR:(\w+)\]\s*(.*)", llm_output)
|
||||||
|
if match:
|
||||||
|
error_type = match.group(1)
|
||||||
|
error_message = match.group(2)
|
||||||
|
logger.error(f"[{self.name}] Fatal API error: {error_type} - {error_message}")
|
||||||
|
await self.emit_event("error", f"LLM API 错误 ({error_type}): {error_message}")
|
||||||
|
break
|
||||||
|
|
||||||
# 🔥 Handle empty LLM response to prevent loops
|
# 🔥 Handle empty LLM response to prevent loops
|
||||||
if not llm_output or not llm_output.strip():
|
if not llm_output or not llm_output.strip():
|
||||||
logger.warning(f"[{self.name}] Empty LLM response in iteration {self._iteration}")
|
logger.warning(f"[{self.name}] Empty LLM response in iteration {self._iteration}")
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,9 @@ class LiteLLMAdapter(BaseLLMAdapter):
|
||||||
except litellm.exceptions.APIConnectionError as e:
|
except litellm.exceptions.APIConnectionError as e:
|
||||||
api_response = self._extract_api_response(e)
|
api_response = self._extract_api_response(e)
|
||||||
raise LLMError(f"无法连接到 API 服务", self.config.provider, api_response=api_response)
|
raise LLMError(f"无法连接到 API 服务", self.config.provider, api_response=api_response)
|
||||||
|
except (litellm.exceptions.ServiceUnavailableError, litellm.exceptions.InternalServerError) as e:
|
||||||
|
api_response = self._extract_api_response(e)
|
||||||
|
raise LLMError(f"API 服务暂时不可用 ({type(e).__name__})", self.config.provider, 503, api_response=api_response)
|
||||||
except litellm.exceptions.APIError as e:
|
except litellm.exceptions.APIError as e:
|
||||||
api_response = self._extract_api_response(e)
|
api_response = self._extract_api_response(e)
|
||||||
raise LLMError(f"API 错误", self.config.provider, getattr(e, 'status_code', None), api_response=api_response)
|
raise LLMError(f"API 错误", self.config.provider, getattr(e, 'status_code', None), api_response=api_response)
|
||||||
|
|
@ -469,6 +472,17 @@ class LiteLLMAdapter(BaseLLMAdapter):
|
||||||
"accumulated": accumulated_content,
|
"accumulated": accumulated_content,
|
||||||
"usage": None,
|
"usage": None,
|
||||||
}
|
}
|
||||||
|
except (litellm.exceptions.ServiceUnavailableError, litellm.exceptions.InternalServerError) as e:
|
||||||
|
# 服务不可用 - 服务器端 5xx 错误
|
||||||
|
logger.error(f"Stream server error ({type(e).__name__}): {e}")
|
||||||
|
yield {
|
||||||
|
"type": "error",
|
||||||
|
"error_type": "server_error",
|
||||||
|
"error": str(e),
|
||||||
|
"user_message": f"API 服务暂时不可用 ({type(e).__name__})",
|
||||||
|
"accumulated": accumulated_content,
|
||||||
|
"usage": None,
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 其他错误 - 检查是否是包装的速率限制错误
|
# 其他错误 - 检查是否是包装的速率限制错误
|
||||||
|
|
|
||||||
|
|
@ -146,23 +146,33 @@ function AgentAuditPageContent() {
|
||||||
}, [loadAgentTree]);
|
}, [loadAgentTree]);
|
||||||
|
|
||||||
// 🔥 NEW: 加载历史事件并转换为日志项
|
// 🔥 NEW: 加载历史事件并转换为日志项
|
||||||
const loadHistoricalEvents = useCallback(async () => {
|
const loadHistoricalEvents = useCallback(async (isSync = false) => {
|
||||||
if (!taskId) return 0;
|
if (!taskId) return 0;
|
||||||
|
|
||||||
// 🔥 防止重复加载历史事件
|
// 🔥 只有在非同步模式下才检查是否已加载过(首屏加载)
|
||||||
if (hasLoadedHistoricalEventsRef.current) {
|
if (!isSync && hasLoadedHistoricalEventsRef.current) {
|
||||||
console.log('[AgentAudit] Historical events already loaded, skipping');
|
console.log('[AgentAudit] Historical events already loaded, skipping');
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是首屏加载,标记为已尝试加载
|
||||||
|
if (!isSync) {
|
||||||
hasLoadedHistoricalEventsRef.current = true;
|
hasLoadedHistoricalEventsRef.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[AgentAudit] Fetching historical events for task ${taskId}...`);
|
const currentSeq = lastEventSequenceRef.current;
|
||||||
const events = await getAgentEvents(taskId, { limit: 500 });
|
console.log(`[AgentAudit] Fetching historical events for task ${taskId} (after_sequence=${currentSeq})...`);
|
||||||
|
|
||||||
|
// 🔥 传递当前已知的最后序列号,实现增量加载
|
||||||
|
const events = await getAgentEvents(taskId, {
|
||||||
|
after_sequence: currentSeq,
|
||||||
|
limit: 500
|
||||||
|
});
|
||||||
console.log(`[AgentAudit] Received ${events.length} events from API`);
|
console.log(`[AgentAudit] Received ${events.length} events from API`);
|
||||||
|
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
console.log('[AgentAudit] No historical events found');
|
console.log('[AgentAudit] No new historical events found');
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -629,8 +639,43 @@ function AgentAuditPageContent() {
|
||||||
disconnectStreamRef.current = disconnectStream;
|
disconnectStreamRef.current = disconnectStream;
|
||||||
}, [disconnectStream]);
|
}, [disconnectStream]);
|
||||||
|
|
||||||
|
// 🔥 Centralized Sync State Function
|
||||||
|
const syncState = useCallback(async () => {
|
||||||
|
if (!taskId) return;
|
||||||
|
|
||||||
|
console.log('[AgentAudit] Synchronizing state...');
|
||||||
|
try {
|
||||||
|
// 1. 同步任务基本信息、发现项和树结构
|
||||||
|
await Promise.all([loadTask(), loadFindings(), loadAgentTree()]);
|
||||||
|
|
||||||
|
// 2. 增量同步日志完成状态或缺失日志
|
||||||
|
await loadHistoricalEvents(true);
|
||||||
|
|
||||||
|
// 3. 如果连接断开且任务仍在运行,尝试重新连接流
|
||||||
|
if (!isConnected && isRunning) {
|
||||||
|
console.log('[AgentAudit] Stream disconnected while task running, attempting reconnect...');
|
||||||
|
hasConnectedRef.current = false; // 允许重新连接
|
||||||
|
connectStream();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[AgentAudit] Failed to sync state:', err);
|
||||||
|
}
|
||||||
|
}, [taskId, loadTask, loadFindings, loadAgentTree, loadHistoricalEvents, isConnected, isRunning, connectStream]);
|
||||||
|
|
||||||
// ============ Effects ============
|
// ============ Effects ============
|
||||||
|
|
||||||
|
// 🔥 Visibility Change Listener - 同步状态的主要触发器
|
||||||
|
useEffect(() => {
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
syncState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
}, [syncState]);
|
||||||
|
|
||||||
// Status animation
|
// Status animation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isRunning) return;
|
if (!isRunning) return;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue