Merge pull request #155 from DoiiarX/fix-agent-communication
日志解析修复、及日志解析修复测试用例
This commit is contained in:
@@ -35,13 +35,13 @@ class ForumHost:
|
||||
初始化论坛主持人
|
||||
|
||||
Args:
|
||||
api_key: 硅基流动API密钥,如果不提供则从配置文件读取
|
||||
base_url: 接口基础地址,默认使用配置文件提供的SiliconFlow地址
|
||||
api_key: 论坛主持人 LLM API 密钥,如果不提供则从配置文件读取
|
||||
base_url: 论坛主持人 LLM API 接口基础地址,默认使用配置文件提供的SiliconFlow地址
|
||||
"""
|
||||
self.api_key = api_key or settings.FORUM_HOST_API_KEY
|
||||
|
||||
if not self.api_key:
|
||||
raise ValueError("未找到硅基流动API密钥,请在config.py中设置FORUM_HOST_API_KEY")
|
||||
raise ValueError("未找到论坛主持人API密钥,请在环境变量文件中设置FORUM_HOST_API_KEY")
|
||||
|
||||
self.base_url = base_url or settings.FORUM_HOST_BASE_URL
|
||||
|
||||
|
||||
+158
-79
@@ -18,7 +18,7 @@ try:
|
||||
from .llm_host import generate_host_speech
|
||||
HOST_AVAILABLE = True
|
||||
except ImportError:
|
||||
logger.warning("ForumEngine: 论坛主持人模块未找到,将以纯监控模式运行")
|
||||
logger.exception("ForumEngine: 论坛主持人模块未找到,将以纯监控模式运行")
|
||||
HOST_AVAILABLE = False
|
||||
|
||||
class LogMonitor:
|
||||
@@ -50,10 +50,20 @@ class LogMonitor:
|
||||
self.host_speech_threshold = 5 # 每5条agent发言触发一次主持人发言
|
||||
self.is_host_generating = False # 主持人是否正在生成发言
|
||||
|
||||
# 目标节点名称 - 直接匹配字符串
|
||||
self.target_nodes = [
|
||||
'FirstSummaryNode',
|
||||
'ReflectionSummaryNode'
|
||||
# 目标节点识别模式
|
||||
# 1. 类名(旧格式可能包含)
|
||||
# 2. 完整模块路径(实际日志格式,包含引擎前缀)
|
||||
# 3. 部分模块路径(兼容性)
|
||||
# 4. 关键标识文本
|
||||
self.target_node_patterns = [
|
||||
'FirstSummaryNode', # 类名
|
||||
'ReflectionSummaryNode', # 类名
|
||||
'InsightEngine.nodes.summary_node', # InsightEngine完整路径
|
||||
'MediaEngine.nodes.summary_node', # MediaEngine完整路径
|
||||
'QueryEngine.nodes.summary_node', # QueryEngine完整路径
|
||||
'nodes.summary_node', # 模块路径(兼容性,用于部分匹配)
|
||||
'正在生成首次段落总结', # FirstSummaryNode的标识
|
||||
'正在生成反思总结', # ReflectionSummaryNode的标识
|
||||
]
|
||||
|
||||
# 多行内容捕获状态
|
||||
@@ -107,12 +117,33 @@ class LogMonitor:
|
||||
f.flush()
|
||||
except Exception as e:
|
||||
logger.exception(f"ForumEngine: 写入forum.log失败: {e}")
|
||||
|
||||
|
||||
def is_target_log_line(self, line: str) -> bool:
|
||||
"""检查是否是目标日志行(SummaryNode)"""
|
||||
# 简单字符串包含检查,更可靠
|
||||
for node_name in self.target_nodes:
|
||||
if node_name in line:
|
||||
"""检查是否是目标日志行(SummaryNode)
|
||||
|
||||
支持多种识别方式:
|
||||
1. 类名:FirstSummaryNode, ReflectionSummaryNode
|
||||
2. 完整模块路径:InsightEngine.nodes.summary_node、MediaEngine.nodes.summary_node、QueryEngine.nodes.summary_node
|
||||
3. 部分模块路径:nodes.summary_node(兼容性)
|
||||
4. 关键标识文本:正在生成首次段落总结、正在生成反思总结
|
||||
|
||||
排除条件:
|
||||
- ERROR 级别的日志(错误日志不应被识别为目标节点)
|
||||
- 包含错误关键词的日志(JSON解析失败、JSON修复失败等)
|
||||
"""
|
||||
# 排除 ERROR 级别的日志
|
||||
if "| ERROR" in line or "| ERROR |" in line:
|
||||
return False
|
||||
|
||||
# 排除包含错误关键词的日志
|
||||
error_keywords = ["JSON解析失败", "JSON修复失败", "Traceback", "File \""]
|
||||
for keyword in error_keywords:
|
||||
if keyword in line:
|
||||
return False
|
||||
|
||||
# 检查是否包含目标节点模式
|
||||
for pattern in self.target_node_patterns:
|
||||
if pattern in line:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -145,7 +176,10 @@ class LogMonitor:
|
||||
return False
|
||||
|
||||
# 如果行长度过短,也认为不是有价值的内容
|
||||
clean_line = re.sub(r'\[\d{2}:\d{2}:\d{2}\]', '', line).strip()
|
||||
# 移除时间戳:支持旧格式和新格式
|
||||
clean_line = re.sub(r'\[\d{2}:\d{2}:\d{2}\]', '', line)
|
||||
clean_line = re.sub(r'\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3}\s*\|\s*[A-Z]+\s*\|\s*[^|]+?\s*-\s*', '', clean_line)
|
||||
clean_line = clean_line.strip()
|
||||
if len(clean_line) < 30: # 阈值可以调整
|
||||
return False
|
||||
|
||||
@@ -156,9 +190,25 @@ class LogMonitor:
|
||||
return "清理后的输出: {" in line
|
||||
|
||||
def is_json_end_line(self, line: str) -> bool:
|
||||
"""判断是否是JSON结束行"""
|
||||
"""判断是否是JSON结束行
|
||||
|
||||
只判断纯粹的结束标记行,不包含任何日志格式信息(时间戳等)。
|
||||
如果行包含时间戳,应该先清理再判断,但这里返回False表示需要进一步处理。
|
||||
"""
|
||||
stripped = line.strip()
|
||||
return stripped == "}" or (stripped.startswith("[") and stripped.endswith("] }"))
|
||||
|
||||
# 如果行包含时间戳(旧格式或新格式),说明不是纯粹的结束行
|
||||
# 旧格式:[HH:MM:SS]
|
||||
if re.match(r'^\[\d{2}:\d{2}:\d{2}\]', stripped):
|
||||
return False
|
||||
# 新格式:YYYY-MM-DD HH:mm:ss.SSS
|
||||
if re.match(r'^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3}', stripped):
|
||||
return False
|
||||
|
||||
# 不包含时间戳的行,检查是否是纯结束标记
|
||||
if stripped == "}" or stripped == "] }":
|
||||
return True
|
||||
return False
|
||||
|
||||
def extract_json_content(self, json_lines: List[str]) -> Optional[str]:
|
||||
"""从多行中提取并解析JSON内容"""
|
||||
@@ -200,8 +250,12 @@ class LogMonitor:
|
||||
# 处理多行JSON
|
||||
json_text = json_part
|
||||
for line in json_lines[json_start_idx + 1:]:
|
||||
# 移除时间戳
|
||||
# 移除时间戳:支持旧格式 [HH:MM:SS] 和新格式 loguru (YYYY-MM-DD HH:mm:ss.SSS | LEVEL | ...)
|
||||
# 旧格式:[HH:MM:SS]
|
||||
clean_line = re.sub(r'^\[\d{2}:\d{2}:\d{2}\]\s*', '', line)
|
||||
# 新格式:移除 loguru 格式的时间戳和级别信息
|
||||
# 格式: YYYY-MM-DD HH:mm:ss.SSS | LEVEL | module:function:line -
|
||||
clean_line = re.sub(r'^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3}\s*\|\s*[A-Z]+\s*\|\s*[^|]+?\s*-\s*', '', clean_line)
|
||||
json_text += clean_line
|
||||
|
||||
# 尝试解析JSON
|
||||
@@ -247,42 +301,51 @@ class LogMonitor:
|
||||
|
||||
def extract_node_content(self, line: str) -> Optional[str]:
|
||||
"""提取节点内容,去除时间戳、节点名称等前缀"""
|
||||
# 移除时间戳部分
|
||||
# 格式: [HH:MM:SS] [NodeName] message
|
||||
match = re.search(r'\[\d{2}:\d{2}:\d{2}\]\s*(.+)', line)
|
||||
if match:
|
||||
content = match.group(1).strip()
|
||||
|
||||
# 移除所有的方括号标签(包括节点名称和应用名称)
|
||||
content = line
|
||||
|
||||
# 移除时间戳部分:支持旧格式和新格式
|
||||
# 旧格式: [HH:MM:SS]
|
||||
match_old = re.search(r'\[\d{2}:\d{2}:\d{2}\]\s*(.+)', content)
|
||||
if match_old:
|
||||
content = match_old.group(1).strip()
|
||||
else:
|
||||
# 新格式: YYYY-MM-DD HH:mm:ss.SSS | LEVEL | module:function:line -
|
||||
match_new = re.search(r'\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3}\s*\|\s*[A-Z]+\s*\|\s*[^|]+?\s*-\s*(.+)', content)
|
||||
if match_new:
|
||||
content = match_new.group(1).strip()
|
||||
|
||||
if not content:
|
||||
return line.strip()
|
||||
|
||||
# 移除所有的方括号标签(包括节点名称和应用名称)
|
||||
content = re.sub(r'^\[.*?\]\s*', '', content)
|
||||
|
||||
# 继续移除可能的多个连续标签
|
||||
while re.match(r'^\[.*?\]\s*', content):
|
||||
content = re.sub(r'^\[.*?\]\s*', '', content)
|
||||
|
||||
# 继续移除可能的多个连续标签
|
||||
while re.match(r'^\[.*?\]\s*', content):
|
||||
content = re.sub(r'^\[.*?\]\s*', '', content)
|
||||
|
||||
# 移除常见前缀(如"首次总结: "、"反思总结: "等)
|
||||
prefixes_to_remove = [
|
||||
"首次总结: ",
|
||||
"反思总结: ",
|
||||
"清理后的输出: "
|
||||
]
|
||||
|
||||
for prefix in prefixes_to_remove:
|
||||
if content.startswith(prefix):
|
||||
content = content[len(prefix):]
|
||||
break
|
||||
|
||||
# 移除可能存在的应用名标签(不在方括号内的)
|
||||
app_names = ['INSIGHT', 'MEDIA', 'QUERY']
|
||||
for app_name in app_names:
|
||||
# 移除单独的APP_NAME(在行首)
|
||||
content = re.sub(rf'^{app_name}\s+', '', content, flags=re.IGNORECASE)
|
||||
|
||||
# 清理多余的空格
|
||||
content = re.sub(r'\s+', ' ', content)
|
||||
|
||||
return content.strip()
|
||||
return line.strip()
|
||||
|
||||
# 移除常见前缀(如"首次总结: "、"反思总结: "等)
|
||||
prefixes_to_remove = [
|
||||
"首次总结: ",
|
||||
"反思总结: ",
|
||||
"清理后的输出: "
|
||||
]
|
||||
|
||||
for prefix in prefixes_to_remove:
|
||||
if content.startswith(prefix):
|
||||
content = content[len(prefix):]
|
||||
break
|
||||
|
||||
# 移除可能存在的应用名标签(不在方括号内的)
|
||||
app_names = ['INSIGHT', 'MEDIA', 'QUERY']
|
||||
for app_name in app_names:
|
||||
# 移除单独的APP_NAME(在行首)
|
||||
content = re.sub(rf'^{app_name}\s+', '', content, flags=re.IGNORECASE)
|
||||
|
||||
# 清理多余的空格
|
||||
content = re.sub(r'\s+', ' ', content)
|
||||
|
||||
return content.strip()
|
||||
|
||||
def get_file_size(self, file_path: Path) -> int:
|
||||
"""获取文件大小"""
|
||||
@@ -349,36 +412,49 @@ class LogMonitor:
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
# 检查是否是目标节点行
|
||||
if self.is_target_log_line(line):
|
||||
if self.is_json_start_line(line):
|
||||
# 开始捕获JSON
|
||||
self.capturing_json[app_name] = True
|
||||
self.json_buffer[app_name] = [line]
|
||||
self.json_start_line[app_name] = line
|
||||
# 检查是否是目标节点行和JSON开始标记
|
||||
is_target = self.is_target_log_line(line)
|
||||
is_json_start = self.is_json_start_line(line)
|
||||
|
||||
# 只有目标节点(SummaryNode)的JSON输出才应该被捕获
|
||||
# 过滤掉SearchNode等其他节点的输出(它们不是目标节点,即使有JSON也不会被捕获)
|
||||
if is_target and is_json_start:
|
||||
# 开始捕获JSON(必须是目标节点且包含"清理后的输出: {")
|
||||
self.capturing_json[app_name] = True
|
||||
self.json_buffer[app_name] = [line]
|
||||
self.json_start_line[app_name] = line
|
||||
|
||||
# 检查是否是单行JSON
|
||||
if line.strip().endswith("}"):
|
||||
# 单行JSON,立即处理
|
||||
content = self.extract_json_content([line])
|
||||
if content: # 只有成功解析的内容才会被记录
|
||||
# 去除重复的标签和格式化
|
||||
clean_content = self._clean_content_tags(content, app_name)
|
||||
captured_contents.append(f"{clean_content}")
|
||||
self.capturing_json[app_name] = False
|
||||
self.json_buffer[app_name] = []
|
||||
|
||||
# 检查是否是单行JSON
|
||||
if line.strip().endswith("}"):
|
||||
# 单行JSON,立即处理
|
||||
content = self.extract_json_content([line])
|
||||
if content: # 只有成功解析的内容才会被记录
|
||||
# 去除重复的标签和格式化
|
||||
clean_content = self._clean_content_tags(content, app_name)
|
||||
captured_contents.append(f"{clean_content}")
|
||||
self.capturing_json[app_name] = False
|
||||
self.json_buffer[app_name] = []
|
||||
|
||||
elif self.is_valuable_content(line):
|
||||
# 其他有价值的SummaryNode内容
|
||||
clean_content = self._clean_content_tags(self.extract_node_content(line), app_name)
|
||||
captured_contents.append(f"{clean_content}")
|
||||
elif is_target and self.is_valuable_content(line):
|
||||
# 其他有价值的SummaryNode内容(必须是目标节点且有价值)
|
||||
clean_content = self._clean_content_tags(self.extract_node_content(line), app_name)
|
||||
captured_contents.append(f"{clean_content}")
|
||||
|
||||
elif self.capturing_json[app_name]:
|
||||
# 正在捕获JSON的后续行
|
||||
self.json_buffer[app_name].append(line)
|
||||
|
||||
# 检查是否是JSON结束
|
||||
if self.is_json_end_line(line):
|
||||
# 先清理时间戳,然后判断清理后的行是否是结束标记
|
||||
cleaned_line = line.strip()
|
||||
# 清理旧格式时间戳:[HH:MM:SS]
|
||||
cleaned_line = re.sub(r'^\[\d{2}:\d{2}:\d{2}\]\s*', '', cleaned_line)
|
||||
# 清理新格式时间戳:YYYY-MM-DD HH:mm:ss.SSS | LEVEL | module:function:line -
|
||||
cleaned_line = re.sub(r'^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3}\s*\|\s*[A-Z]+\s*\|\s*[^|]+?\s*-\s*', '', cleaned_line)
|
||||
cleaned_line = cleaned_line.strip()
|
||||
|
||||
# 清理后判断是否是结束标记
|
||||
if cleaned_line == "}" or cleaned_line == "] }":
|
||||
# JSON结束,处理完整的JSON
|
||||
content = self.extract_json_content(self.json_buffer[app_name])
|
||||
if content: # 只有成功解析的内容才会被记录
|
||||
@@ -484,13 +560,16 @@ class LogMonitor:
|
||||
# 先检查是否需要触发搜索(只触发一次)
|
||||
if not self.is_searching:
|
||||
for line in new_lines:
|
||||
if line.strip() and 'FirstSummaryNode' in line:
|
||||
logger.info(f"ForumEngine: 在{app_name}中检测到第一次论坛发表内容")
|
||||
self.is_searching = True
|
||||
self.search_inactive_count = 0
|
||||
# 清空forum.log开始新会话
|
||||
self.clear_forum_log()
|
||||
break # 找到一个就够了,跳出循环
|
||||
# 检查是否包含目标节点模式(支持多种格式)
|
||||
if line.strip() and self.is_target_log_line(line):
|
||||
# 进一步确认是首次总结节点(FirstSummaryNode或包含"正在生成首次段落总结")
|
||||
if 'FirstSummaryNode' in line or '正在生成首次段落总结' in line:
|
||||
logger.info(f"ForumEngine: 在{app_name}中检测到第一次论坛发表内容")
|
||||
self.is_searching = True
|
||||
self.search_inactive_count = 0
|
||||
# 清空forum.log开始新会话
|
||||
self.clear_forum_log()
|
||||
break # 找到一个就够了,跳出循环
|
||||
|
||||
# 处理所有新增内容(如果正在搜索状态)
|
||||
if self.is_searching:
|
||||
|
||||
Reference in New Issue
Block a user