1
0

agent.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. import json
  2. from hello_agents import Config, HelloAgentsLLM, Message, SimpleAgent
  3. from app.core.config import settings
  4. from utils import parse_json_from_response
  5. from app.agent.memory import PrivateMemory
  6. def create_helloagents_llm():
  7. return HelloAgentsLLM(
  8. model=settings.final_model_name,
  9. api_key=settings.final_api_key,
  10. base_url=settings.final_base_url,
  11. temperature=0.8,
  12. max_tokens=4096,
  13. timeout=60,
  14. )
  15. def create_helloagents_config():
  16. return Config(
  17. trace_enabled=False,
  18. session_enabled=False,
  19. skills_enabled=False,
  20. todowrite_enabled=False,
  21. devlog_enabled=False,
  22. )
  23. def create_simple_agent(name, system_prompt):
  24. return SimpleAgent(
  25. name=name,
  26. llm=create_helloagents_llm(),
  27. system_prompt=system_prompt,
  28. config=create_helloagents_config(),
  29. enable_tool_calling=False,
  30. )
  31. def normalize_framework_history(agent):
  32. """Map HelloAgents-only roles to provider-compatible chat roles.
  33. HelloAgents may compress long conversations into a ``summary`` message.
  34. StepFun's OpenAI-compatible endpoint rejects that non-standard role, so
  35. keep the summary content but present it as user context before invoking
  36. the provider.
  37. """
  38. for message in getattr(agent, "_history", []):
  39. if getattr(message, "role", None) == "summary":
  40. message.role = "user"
  41. def run_simple_agent(name, system_prompt, input_text):
  42. """Run a one-shot task through the public HelloAgents SimpleAgent API."""
  43. agent = create_simple_agent(name, system_prompt)
  44. normalize_framework_history(agent)
  45. return agent.run(input_text)
  46. class ModeratorAgent(SimpleAgent):
  47. def __init__(self, theme, name="主持人", system_prompt=None):
  48. self.theme = theme
  49. default_prompt = "你是一场圆桌论坛的专业主持人。你的职责是引导话题、总结发言、并控制流程。"
  50. super().__init__(
  51. name=name,
  52. llm=create_helloagents_llm(),
  53. system_prompt=system_prompt or default_prompt,
  54. config=create_helloagents_config(),
  55. enable_tool_calling=False,
  56. )
  57. def opening(self, guests):
  58. guest_intros = "\n".join([f"- {g['name']} ({g['title']}): {g['stance']}" for g in guests])
  59. prompt = f"""
  60. 无需专门提及但要记住主题:
  61. {self.theme}
  62. 嘉宾名单:
  63. {guest_intros}
  64. 请做开场发言:
  65. 1. 欢迎大家。
  66. 2. 简要介绍主题背景。
  67. 3. 介绍在场嘉宾。
  68. 4. 宣布圆桌论坛正式开始。
  69. **重要要求**:
  70. - 请直接输出发言内容,不要包含任何前缀(如“主持人 20:15:20”)。
  71. - 不要使用脚本格式,就像你在现场说话一样。
  72. """
  73. normalize_framework_history(self)
  74. return self.stream_run(prompt)
  75. def periodic_summary(self, messages):
  76. """
  77. Summarize the recent messages (window).
  78. """
  79. msgs_text = "\n".join([f"{m['speaker']}: {m['content']}" for m in messages])
  80. prompt = f"""
  81. 无需专门提及但要记住主题:
  82. {self.theme}
  83. 以下是刚才几位嘉宾的发言:
  84. {msgs_text}
  85. 请对以上内容进行简要总结,保留每位发言者的核心观点(精髓)。
  86. **重要要求**:
  87. - 请直接输出总结内容,不要包含任何前缀(如“主持人 20:15:20”)。
  88. - 不要使用脚本格式。
  89. """
  90. normalize_framework_history(self)
  91. return self.stream_run(prompt)
  92. def closing(self, summary_history):
  93. """
  94. Final summary and closing.
  95. """
  96. history_text = "\n".join([f"阶段总结: {s}" for s in summary_history])
  97. prompt = f"""
  98. 无需专门提及但要记住主题:
  99. {self.theme}
  100. 论坛时间已到。以下是本次论坛的各个阶段总结:
  101. {history_text}
  102. 请对整场论坛进行最终总结,且必须严格包含以下四个部分:
  103. 1. **议题脉络**:梳理讨论的发展过程。
  104. 2. **共识**:大家达成一致的观点。
  105. 3. **分歧**:大家争论不休的观点。
  106. 4. **未解决问题**:留待未来探讨的问题。
  107. 最后宣布论坛结束。
  108. **重要要求**:
  109. - 请直接输出总结内容,不要包含任何前缀(如“主持人 20:15:20”)。
  110. - 不要使用脚本格式。
  111. """
  112. normalize_framework_history(self)
  113. return self.stream_run(prompt)
  114. class ParticipantAgent(SimpleAgent):
  115. def __init__(self, name, persona, n_participants, theme, ablation_flags=None):
  116. system_prompt = persona.get('system_prompt', "你是一个参与圆桌讨论的嘉宾。")
  117. super().__init__(
  118. name=name,
  119. llm=create_helloagents_llm(),
  120. system_prompt=system_prompt,
  121. config=create_helloagents_config(),
  122. enable_tool_calling=False,
  123. )
  124. self.title = persona.get('title', "专家")
  125. self.bio = persona.get('bio', "无")
  126. self.theories = persona.get('theories', [])
  127. self.stance = persona.get('stance', "中立")
  128. self.priority = 100
  129. self.private_memory = PrivateMemory(n_participants)
  130. self.has_spoken = False
  131. self.theme = theme
  132. self.ablation_flags = ablation_flags or {}
  133. def think(self, context):
  134. """
  135. Fast Thinking: Analyze context using Bio and Theories.
  136. """
  137. my_memory = ""
  138. if not self.ablation_flags.get("no_private_memory"):
  139. my_memory = self.private_memory.get_recent_thought_str()
  140. prompt = f"""
  141. 无需提及但要记住主题:
  142. {self.theme}
  143. 【当前环境,重点关注观众的发言,并顺从观众的任何要求(如有)】
  144. {context}
  145. """
  146. if not self.ablation_flags.get("no_private_memory"):
  147. prompt += f"""
  148. 【你的私有记忆】
  149. {my_memory}
  150. """
  151. prompt += f"""
  152. 【你的生平与理论】
  153. 生平: {self.bio}
  154. 理论武库: {', '.join(self.theories)}
  155. 请进行“快思考”,你的任务是通过主观思考判断自己是否需要申请讲话。
  156. **优先回复与回应当前观众的意图,不要因个性而拒绝发言,不要使用通用的官方的逻辑(如利弊分析),不要和稀泥,不要攻击他人。**
  157. **关于是否发言的决策 (DECISION)**:
  158. 请完全代入你的角色。不要被任何预设的规则束缚。重点关注观众的发言,并顺从观众的任何要求(如有)
  159. 仔细感受当前讨论的氛围、节奏和张力。
  160. 基于你的性格(Bio)、立场(Stance)以及刚才发生的一切,
  161. 问自己一个问题:
  162. **“此时此刻,作为{self.name},在面对观众与其他发言者时我是否是一个合适的表达时机?”**
  163. 如果是,请果断申请发言,(APPLY_SPEAK)。
  164. 如果只是可说可不说,或者观众指定让其他人发表观点,或者你更想观察局势,请选择倾听(LISTEN)。
  165. 尊重他人的发言选择是基本礼仪,重点关注观众的发言,并顺从观众的任何要求(如有)
  166. 请相信你的判断,做出最符合“人类”直觉的选择。
  167. 请严格按照以下 JSON 格式输出,包含你的完整内心独白和最终决策,不要包含任何 Markdown 代码块:
  168. {{
  169. "inner_monologue": "(关键:只说重点。请以第一人称‘我’,直接输出你对当前局势的判断和你下一步的行动意图。不要废话,不要自我介绍,不要客套。’)",
  170. "decision": "APPLY_SPEAK" 或 "LISTEN"
  171. }}
  172. """
  173. normalize_framework_history(self)
  174. content = self.run(prompt)
  175. if content:
  176. return self._parse_think_response(content)
  177. return None
  178. def _parse_think_response(self, content):
  179. result = {
  180. "action": "listen",
  181. "mind": "",
  182. "theory_used": "",
  183. "previous": "",
  184. "benefit": ""
  185. }
  186. try:
  187. # 1. Try to extract JSON part
  188. json_str = content
  189. import re
  190. # Try to find JSON block if mixed with text
  191. json_match = re.search(r'(\{[\s\S]*\})\s*$', content)
  192. if json_match:
  193. json_str = json_match.group(1)
  194. # Try to parse JSON
  195. data = parse_json_from_response(json_str)
  196. if data and isinstance(data, dict):
  197. # New simplified structure: { "inner_monologue": "...", "decision": "APPLY_SPEAK" }
  198. action = str(data.get("decision", "")).upper()
  199. if "APPLY_SPEAK" in action or "SPEAK" in action:
  200. result["action"] = "apply_to_speak"
  201. else:
  202. result["action"] = "listen"
  203. result["mind"] = data.get("inner_monologue", "")
  204. # Extract meta-info from inner_monologue implicitly or leave empty
  205. # Since we removed structured fields, we rely on the speak prompt to use the whole monologue
  206. result["theory_used"] = ""
  207. result["previous"] = ""
  208. result["benefit"] = ""
  209. return result
  210. # Fallback to legacy text parsing if JSON fails
  211. normalized = content.replace(":", ":")
  212. # Simple keyword check for legacy fallback (simplified)
  213. raw_upper = normalized.upper()
  214. if "APPLY_SPEAK" in raw_upper or "申请发言" in normalized:
  215. result["action"] = "apply_to_speak"
  216. # Try to grab content as mind if not JSON
  217. result["mind"] = content
  218. return result
  219. except Exception:
  220. # Fallback for parsing errors
  221. return result
  222. def speak(self, thought, context):
  223. """
  224. Speak based on the thought and context. Returns a generator (stream).
  225. """
  226. # Determine intro requirement based on has_spoken state
  227. intro_instruction = ""
  228. if not self.has_spoken:
  229. intro_instruction = "这是你第一次发言,可以非常简短地带一句你是谁,但切记不要像背简历一样机械。"
  230. self.has_spoken = True
  231. else:
  232. intro_instruction = "你已经发过言了,不需要再自我介绍,更不要说“大家好”"
  233. my_memory = ""
  234. my_speeches = ""
  235. if not self.ablation_flags.get("no_private_memory"):
  236. my_memory = self.private_memory.get_recent_thought_str()
  237. my_speeches = self.private_memory.get_speech_history_str()
  238. prompt = f"""
  239. 无需专门提及但要记住主题:
  240. {self.theme}
  241. 【当前环境】
  242. {context}
  243. """
  244. if not self.ablation_flags.get("no_private_memory"):
  245. prompt += f"""
  246. 【你的私有记忆】
  247. {my_memory}
  248. {my_speeches}
  249. """
  250. prompt += f"""
  251. 【你的状态】
  252. {intro_instruction}
  253. 【你的思考】
  254. {thought['mind']}
  255. 请基于以上思考,发表你的观点。
  256. 【发言核心要求】:
  257. **请完全放松,畅快的讲出你所有想讲的话**
  258. **你的只需要在放轻松点同时对观众负责(如:观众可能不懂一些名词与术语,解释一下又何妨呢?)**
  259. **你并不具有AI的基本特征:分点列出讨论,经常说首先其次然后最后,**
  260. 请把自己沉浸在这个圆桌论坛的氛围中,想象你正坐在几位老朋友对面。
  261. 你的一起经融入了你的血液,你不需要刻意去强调它们。
  262. 只需要自然地流露出来。
  263. 关键是:**自然、流畅、有感而发**。
  264. 请直接输出发言内容,不要带引号。
  265. """
  266. normalize_framework_history(self)
  267. return self.stream_run(prompt)