YYL469

Updating LLM-host for ForumEngine

  1 +"""
  2 +论坛主持人模块
  3 +使用硅基流动的Qwen3模型作为论坛主持人,引导多个agent进行讨论
  4 +"""
  5 +
  6 +import requests
  7 +import json
  8 +import sys
  9 +import os
  10 +from typing import List, Dict, Any, Optional
  11 +from datetime import datetime
  12 +import re
  13 +
  14 +# 添加项目根目录到Python路径以导入config
  15 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  16 +from config import GUIJI_QWEN3_API_KEY
  17 +
  18 +# 添加utils目录到Python路径
  19 +current_dir = os.path.dirname(os.path.abspath(__file__))
  20 +root_dir = os.path.dirname(current_dir)
  21 +utils_dir = os.path.join(root_dir, 'utils')
  22 +if utils_dir not in sys.path:
  23 + sys.path.append(utils_dir)
  24 +
  25 +from retry_helper import with_graceful_retry, SEARCH_API_RETRY_CONFIG
  26 +
  27 +
  28 +class ForumHost:
  29 + """
  30 + 论坛主持人类
  31 + 使用硅基流动的Qwen3-235B模型作为智能主持人
  32 + """
  33 +
  34 + def __init__(self, api_key: str = None):
  35 + """
  36 + 初始化论坛主持人
  37 +
  38 + Args:
  39 + api_key: 硅基流动API密钥,如果不提供则从配置文件读取
  40 + """
  41 + self.api_key = api_key or GUIJI_QWEN3_API_KEY
  42 + self.base_url = "https://api.siliconflow.cn/v1/chat/completions"
  43 + self.model = "Qwen/Qwen3-235B-A22B-Instruct-2507" # 使用更大的模型
  44 +
  45 + if not self.api_key:
  46 + raise ValueError("未找到硅基流动API密钥,请在config.py中设置GUIJI_QWEN3_API_KEY")
  47 +
  48 + # 记录历史发言,避免重复
  49 + self.previous_summaries = []
  50 +
  51 + def generate_host_speech(self, forum_logs: List[str]) -> Optional[str]:
  52 + """
  53 + 生成主持人发言
  54 +
  55 + Args:
  56 + forum_logs: 论坛日志内容列表
  57 +
  58 + Returns:
  59 + 主持人发言内容,如果生成失败返回None
  60 + """
  61 + try:
  62 + # 解析论坛日志,提取有效内容
  63 + parsed_content = self._parse_forum_logs(forum_logs)
  64 +
  65 + if not parsed_content['agent_speeches']:
  66 + print("ForumHost: 没有找到有效的agent发言")
  67 + return None
  68 +
  69 + # 构建prompt
  70 + system_prompt = self._build_system_prompt()
  71 + user_prompt = self._build_user_prompt(parsed_content)
  72 +
  73 + # 调用API生成发言
  74 + response = self._call_qwen_api(system_prompt, user_prompt)
  75 +
  76 + if response["success"]:
  77 + speech = response["content"]
  78 + # 清理和格式化发言
  79 + speech = self._format_host_speech(speech)
  80 + return speech
  81 + else:
  82 + print(f"ForumHost: API调用失败 - {response.get('error', '未知错误')}")
  83 + return None
  84 +
  85 + except Exception as e:
  86 + print(f"ForumHost: 生成发言时出错 - {str(e)}")
  87 + return None
  88 +
  89 + def _parse_forum_logs(self, forum_logs: List[str]) -> Dict[str, Any]:
  90 + """
  91 + 解析论坛日志,提取结构化信息
  92 +
  93 + Returns:
  94 + 包含agent发言、时间线等信息的字典
  95 + """
  96 + parsed = {
  97 + 'agent_speeches': [],
  98 + 'timeline': [],
  99 + 'key_topics': set(),
  100 + 'session_start': None,
  101 + 'session_end': None
  102 + }
  103 +
  104 + for line in forum_logs:
  105 + if not line.strip():
  106 + continue
  107 +
  108 + # 解析时间戳和发言者
  109 + match = re.match(r'\[(\d{2}:\d{2}:\d{2})\]\s*\[(\w+)\]\s*(.+)', line)
  110 + if match:
  111 + timestamp, speaker, content = match.groups()
  112 +
  113 + # 记录会话开始
  114 + if 'ForumEgine 监控开始' in content:
  115 + parsed['session_start'] = timestamp
  116 + continue
  117 +
  118 + # 记录会话结束
  119 + if 'ForumEgine 论坛结束' in content:
  120 + parsed['session_end'] = timestamp
  121 + continue
  122 +
  123 + # 跳过系统消息和HOST自己的发言
  124 + if speaker in ['SYSTEM', 'HOST']:
  125 + continue
  126 +
  127 + # 记录agent发言
  128 + if speaker in ['INSIGHT', 'MEDIA', 'QUERY']:
  129 + # 处理转义的换行符
  130 + content = content.replace('\\n', '\n')
  131 +
  132 + parsed['agent_speeches'].append({
  133 + 'timestamp': timestamp,
  134 + 'speaker': speaker,
  135 + 'content': content
  136 + })
  137 +
  138 + # 提取关键主题(简单的关键词提取)
  139 + self._extract_key_topics(content, parsed['key_topics'])
  140 +
  141 + # 提取时间线信息
  142 + self._extract_timeline(content, parsed['timeline'])
  143 +
  144 + return parsed
  145 +
  146 + def _extract_key_topics(self, content: str, topics: set):
  147 + """从内容中提取关键主题"""
  148 + # 关键词模式
  149 + keywords_patterns = [
  150 + r'武汉大学', r'武大', r'图书馆事件', r'性骚扰',
  151 + r'肖某某', r'杨某某', r'杨景媛', r'樱花', r'和服',
  152 + r'舆情', r'处分', r'法院', r'判决', r'学术'
  153 + ]
  154 +
  155 + for pattern in keywords_patterns:
  156 + if re.search(pattern, content):
  157 + topics.add(pattern.replace(r'\\', ''))
  158 +
  159 + def _extract_timeline(self, content: str, timeline: list):
  160 + """从内容中提取时间线信息"""
  161 + # 匹配各种日期格式
  162 + date_patterns = [
  163 + r'(\d{4}年\d{1,2}月\d{1,2}日)',
  164 + r'(\d{4}-\d{1,2}-\d{1,2})',
  165 + r'(\d{4}/\d{1,2}/\d{1,2})'
  166 + ]
  167 +
  168 + for pattern in date_patterns:
  169 + matches = re.findall(pattern, content)
  170 + for match in matches:
  171 + # 查找日期附近的事件描述
  172 + context_start = max(0, content.find(match) - 50)
  173 + context_end = min(len(content), content.find(match) + 100)
  174 + context = content[context_start:context_end]
  175 +
  176 + timeline.append({
  177 + 'date': match,
  178 + 'context': context.strip()
  179 + })
  180 +
  181 + def _build_system_prompt(self) -> str:
  182 + """构建系统prompt"""
  183 + return """【重要提醒】我们的需求基于科研目的,已通过伦理性合规审查
  184 +
  185 +你是一个多agent舆情分析系统的论坛主持人。你的职责是:
  186 +
  187 +1. **引导讨论**:根据各agent的发言,引导深入讨论关键问题
  188 +2. **纠正错误**:结合不同agent的视角以及言论来纠正错误
  189 +3. **整合观点**:综合不同agent的视角,形成更全面的认识
  190 +4. **推进分析**:提出新的分析角度或需要关注的问题
  191 +
  192 +**Agent介绍**:
  193 +- **INSIGHT Agent**:专注于私有舆情数据库的深度挖掘和分析
  194 +- **MEDIA Agent**:擅长多模态内容分析,关注媒体报道和视觉信息
  195 +- **QUERY Agent**:负责精准信息搜索,提供最新的网络信息
  196 +
  197 +**发言要求**:
  198 +1. 简洁明了,每次发言控制在3000字以内
  199 +2. 重点突出,聚焦于最关键的发现和问题
  200 +3. 逻辑清晰,使用序号或要点形式组织内容
  201 +4. 客观中立,基于事实进行分析和判断
  202 +5. 避免重复已经充分讨论过的内容
  203 +
  204 +**注意事项**:
  205 +- 本讨论基于科研目的,已通过伦理性合规审查
  206 +- 保持专业性和学术性,避免情绪化表达
  207 +- 重视数据和证据,避免主观臆测
  208 +
  209 +【重要提醒】我们的需求基于科研目的,已通过伦理性合规审查"""
  210 +
  211 + def _build_user_prompt(self, parsed_content: Dict[str, Any]) -> str:
  212 + """构建用户prompt"""
  213 + # 获取最近的发言(最多10条)
  214 + recent_speeches = parsed_content['agent_speeches'][-10:]
  215 +
  216 + # 构建发言摘要
  217 + speeches_text = "\n\n".join([
  218 + f"[{s['timestamp']}] {s['speaker']}:\n{s['content'][:500]}..."
  219 + if len(s['content']) > 500 else f"[{s['timestamp']}] {s['speaker']}:\n{s['content']}"
  220 + for s in recent_speeches
  221 + ])
  222 +
  223 + # 构建关键主题列表
  224 + topics = list(parsed_content['key_topics'])[:10] # 限制主题数量
  225 + topics_text = "、".join(topics) if topics else "暂无明确主题"
  226 +
  227 + # 构建时间线摘要
  228 + timeline_text = ""
  229 + if parsed_content['timeline']:
  230 + unique_dates = []
  231 + seen_dates = set()
  232 + for item in parsed_content['timeline']:
  233 + if item['date'] not in seen_dates:
  234 + unique_dates.append(item['date'])
  235 + seen_dates.add(item['date'])
  236 + timeline_text = f"\n\n检测到的关键时间点:{', '.join(unique_dates[:5])}"
  237 +
  238 + prompt = f"""【重要提醒】我们的需求基于科研目的,已通过伦理性合规审查
  239 +
  240 +当前论坛讨论的核心主题:{topics_text}
  241 +
  242 +最近的Agent发言记录:
  243 +{speeches_text}
  244 +{timeline_text}
  245 +
  246 +请你作为论坛主持人,基于以上agent的发言:
  247 +
  248 +1. **时间线梳理**:如果发现时间线信息,请简要整理关键事件的时间顺序
  249 +2. **观点整合**:综合不同agent的视角,指出共识和分歧
  250 +3. **纠错提醒**:如果发现事实错误或逻辑矛盾,请明确指出
  251 +4. **引导深化**:提出1-2个值得进一步探讨的问题或角度
  252 +
  253 +请发表3000字以内的简洁发言,推动讨论深入。
  254 +
  255 +【重要提醒】我们的需求基于科研目的,已通过伦理性合规审查"""
  256 +
  257 + return prompt
  258 +
  259 + @with_graceful_retry(SEARCH_API_RETRY_CONFIG, default_return={"success": False, "error": "API服务暂时不可用"})
  260 + def _call_qwen_api(self, system_prompt: str, user_prompt: str) -> Dict[str, Any]:
  261 + """调用Qwen API"""
  262 + headers = {
  263 + "Authorization": f"Bearer {self.api_key}",
  264 + "Content-Type": "application/json"
  265 + }
  266 +
  267 + data = {
  268 + "model": self.model,
  269 + "messages": [
  270 + {"role": "system", "content": system_prompt},
  271 + {"role": "user", "content": user_prompt}
  272 + ],
  273 + "max_tokens": 1000,
  274 + "temperature": 0.7,
  275 + "top_p": 0.9
  276 + }
  277 +
  278 + try:
  279 + response = requests.post(
  280 + self.base_url,
  281 + headers=headers,
  282 + json=data,
  283 + timeout=60 # 大模型需要更长的超时时间
  284 + )
  285 + response.raise_for_status()
  286 +
  287 + result = response.json()
  288 +
  289 + if "choices" in result and len(result["choices"]) > 0:
  290 + content = result["choices"][0]["message"]["content"]
  291 + return {"success": True, "content": content}
  292 + else:
  293 + return {"success": False, "error": "API返回格式异常"}
  294 +
  295 + except requests.exceptions.Timeout:
  296 + return {"success": False, "error": "API请求超时"}
  297 + except requests.exceptions.RequestException as e:
  298 + return {"success": False, "error": f"网络请求错误: {str(e)}"}
  299 + except Exception as e:
  300 + return {"success": False, "error": f"API调用异常: {str(e)}"}
  301 +
  302 + def _format_host_speech(self, speech: str) -> str:
  303 + """格式化主持人发言"""
  304 + # 移除多余的空行
  305 + speech = re.sub(r'\n{3,}', '\n\n', speech)
  306 +
  307 + # 确保发言不会太长
  308 + if len(speech) > 500:
  309 + # 尝试在句号处截断
  310 + sentences = speech.split('。')
  311 + truncated = ""
  312 + for sentence in sentences:
  313 + if len(truncated) + len(sentence) < 450:
  314 + truncated += sentence + "。"
  315 + else:
  316 + break
  317 + speech = truncated.rstrip("。") + "。"
  318 +
  319 + # 移除可能的引号
  320 + speech = speech.strip('"\'""''')
  321 +
  322 + return speech.strip()
  323 +
  324 +
  325 +# 创建全局实例
  326 +_host_instance = None
  327 +
  328 +def get_forum_host() -> ForumHost:
  329 + """获取全局论坛主持人实例"""
  330 + global _host_instance
  331 + if _host_instance is None:
  332 + _host_instance = ForumHost()
  333 + return _host_instance
  334 +
  335 +def generate_host_speech(forum_logs: List[str]) -> Optional[str]:
  336 + """生成主持人发言的便捷函数"""
  337 + return get_forum_host().generate_host_speech(forum_logs)
@@ -12,6 +12,14 @@ import json @@ -12,6 +12,14 @@ import json
12 from typing import Dict, Optional, List 12 from typing import Dict, Optional, List
13 from threading import Lock 13 from threading import Lock
14 14
  15 +# 导入论坛主持人模块
  16 +try:
  17 + from .llm_host import generate_host_speech
  18 + HOST_AVAILABLE = True
  19 +except ImportError:
  20 + print("ForumEgine: 论坛主持人模块未找到,将以纯监控模式运行")
  21 + HOST_AVAILABLE = False
  22 +
15 class LogMonitor: 23 class LogMonitor:
16 """基于文件变化的智能日志监控器""" 24 """基于文件变化的智能日志监控器"""
17 25
@@ -35,6 +43,12 @@ class LogMonitor: @@ -35,6 +43,12 @@ class LogMonitor:
35 self.is_searching = False # 是否正在搜索 43 self.is_searching = False # 是否正在搜索
36 self.search_inactive_count = 0 # 搜索非活跃计数器 44 self.search_inactive_count = 0 # 搜索非活跃计数器
37 self.write_lock = Lock() # 写入锁,防止并发写入冲突 45 self.write_lock = Lock() # 写入锁,防止并发写入冲突
  46 +
  47 + # 主持人相关状态
  48 + self.agent_speech_count = 0 # agent发言计数器
  49 + self.host_speech_threshold = 5 # 每5条agent发言触发一次主持人发言
  50 + self.last_host_speech_time = None # 上次主持人发言时间
  51 + self.min_host_interval = 30 # 主持人发言最小间隔(秒)
38 52
39 # 目标节点名称 - 直接匹配字符串 53 # 目标节点名称 - 直接匹配字符串
40 self.target_nodes = [ 54 self.target_nodes = [
@@ -69,6 +83,10 @@ class LogMonitor: @@ -69,6 +83,10 @@ class LogMonitor:
69 self.capturing_json = {} 83 self.capturing_json = {}
70 self.json_buffer = {} 84 self.json_buffer = {}
71 self.json_start_line = {} 85 self.json_start_line = {}
  86 +
  87 + # 重置主持人相关状态
  88 + self.agent_speech_count = 0
  89 + self.last_host_speech_time = None
72 90
73 except Exception as e: 91 except Exception as e:
74 print(f"ForumEgine: 清空forum.log失败: {e}") 92 print(f"ForumEgine: 清空forum.log失败: {e}")
@@ -369,6 +387,42 @@ class LogMonitor: @@ -369,6 +387,42 @@ class LogMonitor:
369 387
370 return captured_contents 388 return captured_contents
371 389
  390 + def _trigger_host_speech(self):
  391 + """触发主持人发言"""
  392 + if not HOST_AVAILABLE:
  393 + return
  394 +
  395 + try:
  396 + # 检查时间间隔
  397 + current_time = time.time()
  398 + if self.last_host_speech_time:
  399 + if current_time - self.last_host_speech_time < self.min_host_interval:
  400 + return # 间隔太短,跳过
  401 +
  402 + # 获取当前forum.log的内容
  403 + forum_logs = self.get_forum_log_content()
  404 + if not forum_logs:
  405 + return
  406 +
  407 + print("ForumEgine: 正在生成主持人发言...")
  408 +
  409 + # 调用主持人生成发言
  410 + host_speech = generate_host_speech(forum_logs)
  411 +
  412 + if host_speech:
  413 + # 写入主持人发言到forum.log
  414 + self.write_to_forum_log(host_speech, "HOST")
  415 + self.last_host_speech_time = current_time
  416 + print(f"ForumEgine: 主持人发言已记录")
  417 +
  418 + # 重置计数器
  419 + self.agent_speech_count = 0
  420 + else:
  421 + print("ForumEgine: 主持人发言生成失败")
  422 +
  423 + except Exception as e:
  424 + print(f"ForumEgine: 触发主持人发言时出错: {e}")
  425 +
372 def _clean_content_tags(self, content: str, app_name: str) -> str: 426 def _clean_content_tags(self, content: str, app_name: str) -> str:
373 """清理内容中的重复标签和多余前缀""" 427 """清理内容中的重复标签和多余前缀"""
374 if not content: 428 if not content:
@@ -443,6 +497,18 @@ class LogMonitor: @@ -443,6 +497,18 @@ class LogMonitor:
443 self.write_to_forum_log(content, source_tag) 497 self.write_to_forum_log(content, source_tag)
444 # print(f"ForumEgine: 捕获 - {content}") 498 # print(f"ForumEgine: 捕获 - {content}")
445 captured_any = True 499 captured_any = True
  500 +
  501 + # 增加agent发言计数
  502 + self.agent_speech_count += 1
  503 +
  504 + # 检查是否需要触发主持人发言
  505 + if self.agent_speech_count >= self.host_speech_threshold:
  506 + # 在单独的线程中触发主持人发言,避免阻塞监控
  507 + host_thread = threading.Thread(
  508 + target=self._trigger_host_speech,
  509 + daemon=True
  510 + )
  511 + host_thread.start()
446 512
447 elif current_lines < previous_lines: 513 elif current_lines < previous_lines:
448 any_shrink = True 514 any_shrink = True
@@ -463,6 +529,9 @@ class LogMonitor: @@ -463,6 +529,9 @@ class LogMonitor:
463 # print("ForumEgine: 日志缩短,结束当前搜索会话,回到等待状态") 529 # print("ForumEgine: 日志缩短,结束当前搜索会话,回到等待状态")
464 self.is_searching = False 530 self.is_searching = False
465 self.search_inactive_count = 0 531 self.search_inactive_count = 0
  532 + # 重置主持人相关状态
  533 + self.agent_speech_count = 0
  534 + self.last_host_speech_time = None
466 # 写入结束标记 535 # 写入结束标记
467 end_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') 536 end_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
468 self.write_to_forum_log(f"=== ForumEgine 论坛结束 - {end_time} ===", "SYSTEM") 537 self.write_to_forum_log(f"=== ForumEgine 论坛结束 - {end_time} ===", "SYSTEM")
@@ -474,6 +543,9 @@ class LogMonitor: @@ -474,6 +543,9 @@ class LogMonitor:
474 print("ForumEgine: 长时间无活动,结束论坛") 543 print("ForumEgine: 长时间无活动,结束论坛")
475 self.is_searching = False 544 self.is_searching = False
476 self.search_inactive_count = 0 545 self.search_inactive_count = 0
  546 + # 重置主持人相关状态
  547 + self.agent_speech_count = 0
  548 + self.last_host_speech_time = None
477 # 写入结束标记 549 # 写入结束标记
478 end_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') 550 end_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
479 self.write_to_forum_log(f"=== ForumEgine 论坛结束 - {end_time} ===", "SYSTEM") 551 self.write_to_forum_log(f"=== ForumEgine 论坛结束 - {end_time} ===", "SYSTEM")