agent.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. import json
  2. import uuid
  3. from pathlib import Path
  4. from typing import List, Optional, Tuple
  5. from hello_agents import HelloAgentsLLM, SimpleAgent, ToolRegistry
  6. from config import *
  7. from model import UserMemory, Task, Plan, ScheduleItem
  8. from prompt import *
  9. from tools import get_Tools
  10. class MainAgent:
  11. def __init__(self):
  12. self.llm = HelloAgentsLLM(model=LLM_MODEL, api_key=LLM_API_KEY, base_url=LLM_BASE_URL)
  13. tool = get_Tools()
  14. self.respostry = ToolRegistry()
  15. self.respostry.register_tool(tool)
  16. self.main_agent = SimpleAgent(
  17. name="总控MAIN_AGENT",
  18. llm=self.llm,
  19. system_prompt=MAIN_AGENT_PROMPT,
  20. tool_registry=self.respostry
  21. )
  22. self.np_parser_agent = SimpleAgent(
  23. name="子Agent:自然语言解析,用户输入 → Task列表 + 意图",
  24. llm=self.llm,
  25. system_prompt=NL_PARSE_PROMPT,
  26. tool_registry=self.respostry
  27. )
  28. # self.np_parser_agent.add_tool(tool)
  29. self.plan_Agent = SimpleAgent(
  30. name="子Agent:你是日程规划Agent,用于规划用户日程",
  31. llm=self.llm,
  32. system_prompt=PLAN_REASON_PROMPT,
  33. tool_registry=self.respostry
  34. )
  35. # self.plan_Agent.add_tool(tool)
  36. self.validator_agent = SimpleAgent(
  37. name="子Agent:你是计划校验Agent,检查生成的日程计划是否违反用户记忆、是否存在时间冲突。",
  38. llm=self.llm,
  39. system_prompt=VALIDATE_PROMPT,
  40. tool_registry=self.respostry
  41. )
  42. # self.validator_agent.add_tool(tool)
  43. self.dialog_history: List[dict] = []
  44. def _build_context_str(self, user_input: str, memory: UserMemory, current_plan: Optional[Plan], mem: Optional[UserMemory], plan: Optional[Plan]) -> str:
  45. memory_json = json.dumps(memory.model_dump(), ensure_ascii=False, indent=2)
  46. cp_json = json.dumps(current_plan.model_dump(), ensure_ascii=False, indent=2) if current_plan else 'null'
  47. history_text = []
  48. history_text.append(f"====当前系统时间====\n{datetime.now().strftime('%Y‑%m‑%d %H:%M:%S')}")
  49. history_text.append("====对话历史====")
  50. for msg in self.dialog_history:
  51. role = msg['role']
  52. cnt = msg["content"]
  53. history_text.append(f"{role}: {cnt}")
  54. history_text.append("====用户记忆====")
  55. history_text.append(memory_json)
  56. history_text.append(mem)
  57. history_text.append("====当前已存在计划current_plan====")
  58. history_text.append(cp_json)
  59. history_text.append(plan)
  60. history_text.append("====用户最新输入====")
  61. history_text.append(user_input)
  62. return "\n".join(history_text)
  63. def _build_subagent_payload(self, pass_param: dict, memory: UserMemory, current_plan: Optional[Plan]) -> dict:
  64. """构造子Agent输入:强制注入公共上下文,不依赖总控pass_to_sub"""
  65. import datetime
  66. payload = {**pass_param}
  67. # 强制注入
  68. payload["current_system_time"] = datetime.datetime.now().strftime("%Y‑%m‑%d %H:%M:%S")
  69. payload["user_memory"] = memory.model_dump()
  70. payload["current_plan"] = current_plan.model_dump() if current_plan else None
  71. payload["dialog_history"] = self.dialog_history.copy()
  72. return payload
  73. def run(self, user_input: str, memory: UserMemory, current_plan: Optional[Plan], mem: Optional[UserMemory] = 'null', plan: Optional[Plan] = 'null') -> Tuple[Optional[Plan], str]:
  74. try:
  75. self.dialog_history.append({'role':'user','content':user_input})
  76. max_loop = 10
  77. loop_count = 0
  78. final_plan: Optional[Plan] = None
  79. final_output: str=""
  80. while loop_count < max_loop:
  81. loop_count += 1
  82. context = self._build_context_str(user_input, memory, current_plan, mem, plan)
  83. main_raw = self.main_agent.run(context)
  84. main_raw = main_raw.strip().removeprefix("```json").removesuffix("```").strip()
  85. try:
  86. main_desicion = json.loads(main_raw)
  87. except:
  88. err_msg = f'总控Agent决策解析失败,原始输出:{main_raw[:300]}'
  89. final_output = err_msg
  90. break
  91. call_sub = main_desicion.get("call_sub_agent", 'finish')
  92. pass_param = main_desicion.get("pass_to_sub", {})
  93. if call_sub == 'finish':
  94. # 总控通知结束
  95. fp_json = pass_param.get("final_plan_json")
  96. final_output = pass_param.get("output_text", "处理完成")
  97. if fp_json and fp_json != "null":
  98. try:
  99. final_plan = Plan.model_validate_json(fp_json)
  100. except Exception:
  101. final_plan = None
  102. break
  103. if call_sub == 'nl_parser':
  104. sub_payload = self._build_subagent_payload(pass_param, memory, current_plan)
  105. sub_query = json.dumps(sub_payload, ensure_ascii=False, indent=2)
  106. sub_out = self.np_parser_agent.run(sub_query)
  107. self.dialog_history.append({'role': 'tool:nl_parser', "content": sub_out})
  108. elif call_sub == 'planner':
  109. sub_payload = self._build_subagent_payload(pass_param, memory, current_plan)
  110. plan_query = json.dumps(sub_payload, ensure_ascii=False, indent=2)
  111. sub_out = self.plan_Agent.run(plan_query)
  112. self.dialog_history.append({'role': 'tool:planner', "content": sub_out})
  113. elif call_sub == 'validator':
  114. sub_payload = self._build_subagent_payload(pass_param, memory, current_plan)
  115. sub_query = json.dumps(sub_payload, ensure_ascii=False, indent=2)
  116. sub_out = self.validator_agent.run(sub_query)
  117. self.dialog_history.append({'role': 'tool:validator', "content": sub_out})
  118. else:
  119. final_output = f"总控调用未知子Agent:{call_sub}"
  120. break
  121. else:
  122. final_output = "循环已满,处理过程终止"
  123. self.dialog_history.append({'role':'assistant', "content":final_output})
  124. return final_plan, final_output
  125. except Exception as e:
  126. print(f"总控智能体出现错误:{str(e)},即将使用默认模式")
  127. return self.run_bak(user_input, memory, current_plan)
  128. def run_bak(self, user_input: str, memory: UserMemory, current_plan: Optional[Plan]) -> Tuple[Optional[Plan], str]:
  129. """
  130. :param user_input: 用户原始输入
  131. :param memory: 用户记忆对象
  132. :param current_plan: 当前已存在计划(修改场景使用)
  133. :return: (Plan对象 or None, 对外输出文本/需要澄清的问题)
  134. """
  135. # self.dialog_history.append(AgentMessage(role="user", content=user_input))
  136. memory_json = json.dumps(memory.model_dump(), ensure_ascii=False, indent=2)
  137. # 把用户输入 + 用户记忆拼为子Agent的user query
  138. nl_user_query = f"""
  139. 用户记忆:
  140. {memory_json}
  141. 用户输入:
  142. {user_input}
  143. 请输出JSON格式结果,包含
  144. intent_type, target_date(YYYY‑MM‑DD,解析不出返回null), tasks, modify_operation。
  145. tasks 为任务数组。
  146. """
  147. ### Agent 第一次调用,解析用户问题,然后生成任务列表
  148. nl_result_raw = self.np_parser_agent.run(nl_user_query)
  149. nl_result_raw = nl_result_raw.strip().removeprefix("```json").removesuffix("```").strip()
  150. try:
  151. nl_out = json.loads(nl_result_raw)
  152. except json.JSONDecodeError:
  153. return None, "解析用户需求失败,大模型返回格式异常,请重新描述你的需求。"
  154. intent_type = nl_out.get("intent_type", "")
  155. task_dicts = nl_out.get("tasks", [])
  156. target_date: str | None = nl_out.get("target_date")
  157. plan_name: str | None = nl_out.get("name_summary")
  158. modify_operation = nl_out.get("modify_operation", {})
  159. long_term_meta = nl_out.get("long_term_meta", {})
  160. if not target_date:
  161. # 兜底:取系统当前日期
  162. target_date = datetime.now().strftime("%Y‑%m‑%d %H:%M:%S")
  163. try:
  164. task_list: List[Task] = [Task.model_validate(d) for d in task_dicts]
  165. except Exception:
  166. return None, "任务数据解析失败,请简化你的描述。"
  167. ### 识别意图,然后决定怎么做这个任务
  168. if intent_type == "new_plan":
  169. task_list_json = json.dumps([t.model_dump() for t in task_list], ensure_ascii=False, indent=2)
  170. plan_user_query = f"""
  171. 用户记忆:
  172. {memory_json}
  173. 需要规划的目标日期:
  174. {target_date}
  175. 待规划任务列表:
  176. {task_list_json}
  177. 请输出JSON,包含 schedule_items(日程条目数组)、plan_summary。
  178. 日程的时间需要基于目标日期生成,时间格式 YYYY‑MM‑DD HH:MM。
  179. """
  180. plan_result_raw = self.plan_Agent.run(plan_user_query)
  181. plan_result_raw = plan_result_raw.strip().removeprefix("```json").removesuffix("```").strip()
  182. try:
  183. plan_out = json.loads(plan_result_raw)
  184. except json.JSONDecodeError:
  185. return None, "生成日程失败,格式错误。"
  186. schedule_dicts = plan_out.get("schedule_items", [])
  187. try:
  188. schedule_items: List[ScheduleItem] = [ScheduleItem.model_validate(s) for s in schedule_dicts]
  189. except Exception:
  190. return None, "日程条目数据解析失败。"
  191. new_plan = Plan(
  192. plan_id=str(uuid.uuid4()),
  193. plan_name=plan_name,
  194. create_time=target_date if target_date else datetime.now().strftime("%Y‑%m‑%d %H:%M"),
  195. source_user_input=user_input,
  196. tasks=task_list,
  197. schedule_items=schedule_items,
  198. plan_summary=plan_out.get("plan_summary", ""),
  199. conflict_check_result="pending"
  200. )
  201. plan_json = json.dumps(new_plan.model_dump(), ensure_ascii=False, indent=2)
  202. validate_user_query = f"""
  203. 用户记忆:
  204. {memory_json}
  205. 待校验计划:
  206. {plan_json}
  207. 请输出JSON:
  208. {{
  209. "has_error": bool,
  210. "need_user_clarify": bool,
  211. "clarify_question": str,
  212. "error_msg": str
  213. }}
  214. """
  215. validate_raw = self.validator_agent.run(validate_user_query)
  216. validate_raw = validate_raw.strip().removeprefix("```json").removesuffix("```").strip()
  217. try:
  218. validate_out = json.loads(validate_raw)
  219. except json.JSONDecodeError:
  220. new_plan.conflict_check_result = 'unknown'
  221. return new_plan, new_plan.plan_summary + "\n 校验新建环节解析异常"
  222. has_error = validate_out.get("has_error", False)
  223. need_user_clarify = validate_out.get("need_user_clarify", False)
  224. clarify_question = validate_out.get("clarify_question", "")
  225. if need_user_clarify:
  226. return None, clarify_question
  227. if has_error:
  228. new_plan.conflict_check_result = "has_error"
  229. else:
  230. new_plan.conflict_check_result = "ok"
  231. return new_plan, new_plan.plan_summary
  232. elif intent_type == "modify_plan":
  233. if current_plan is None:
  234. return None, "当前没有已生成的计划,请先生成一份日程计划之后再执行修改。"
  235. old_plan_json = json.dumps(current_plan.model_dump(), ensure_ascii=False, indent=2)
  236. op_json = json.dumps(modify_operation, ensure_ascii=False, indent=2)
  237. plan_user_query = f"""
  238. 用户记忆:
  239. {memory_json}
  240. 目标日期:{target_date}
  241. 原始旧计划:
  242. {old_plan_json}
  243. 用户修改操作:
  244. {op_json}
  245. 请基于旧计划,按照修改操作重新生成完整的schedule_items。
  246. 输出JSON:{{"schedule_items":[...],"plan_summary":"描述本次修改后的计划"}}
  247. 时间格式 YYYY‑MM‑DD HH:MM。
  248. """
  249. plan_result_raw = self.plan_Agent.run(plan_user_query)
  250. plan_result_raw = plan_result_raw.strip().removeprefix("```json").removesuffix("```").strip()
  251. try:
  252. plan_out = json.loads(plan_result_raw)
  253. except json.JSONDecodeError:
  254. return None, "修改计划失败,返回格式异常。"
  255. schedule_dicts = plan_out.get("schedule_items", [])
  256. try:
  257. schedule_items: List[ScheduleItem] = [ScheduleItem.model_validate(s) for s in schedule_dicts]
  258. except Exception:
  259. return None, "修改后日程数据解析失败。"
  260. merged_tasks = current_plan.tasks.copy()
  261. merged_tasks.extend(task_list)
  262. modified_plan = Plan(
  263. plan_id=str(uuid.uuid4()),
  264. plan_name=plan_name,
  265. create_time=target_date if target_date else datetime.now().strftime("%Y‑%m‑%d %H:%M"),
  266. source_user_input=f"[修改] {user_input}",
  267. tasks=task_list,
  268. schedule_items=schedule_items,
  269. plan_summary=plan_out.get("plan_summary", ""),
  270. conflict_check_result="pending"
  271. )
  272. plan_json = json.dumps(modified_plan.model_dump(), ensure_ascii=False, indent=2)
  273. validate_user_query = f"""
  274. 用户记忆:
  275. {memory_json}
  276. 待校验计划:
  277. {plan_json}
  278. 请输出JSON:
  279. {{
  280. "has_error": bool,
  281. "need_user_clarify": bool,
  282. "clarify_question": str,
  283. "error_msg": str
  284. }}
  285. """
  286. validate_raw = self.validator_agent.run(validate_user_query)
  287. validate_raw = validate_raw.strip().removeprefix("```json").removesuffix("```").strip()
  288. try:
  289. validate_out = json.loads(validate_raw)
  290. except json.JSONDecodeError:
  291. modified_plan.conflict_check_result = 'unknown'
  292. return modified_plan, modified_plan.plan_summary + "\n 校验修改环节解析异常"
  293. has_error = validate_out.get("has_error", False)
  294. need_user_clarify = validate_out.get("need_user_clarify", False)
  295. clarify_question = validate_out.get("clarify_question", "")
  296. if need_user_clarify:
  297. return None, clarify_question
  298. if has_error:
  299. modified_plan.conflict_check_result = "has_error"
  300. else:
  301. modified_plan.conflict_check_result = "ok"
  302. return modified_plan, modified_plan.plan_summary
  303. elif intent_type == "long_term_goal":
  304. """
  305. 1.读取long_term_meta,调用 subtask_split_helper_tool 拆出多个子任务描述
  306. 2.把拆分得到的子任务组装成Task列表
  307. 3.复用new_plan整套规划+校验逻辑
  308. """
  309. try:
  310. total_days = int(long_term_meta.get("total_days", 0))
  311. daily_duration = int(long_term_meta.get("daily_duration_min", 0))
  312. goal_desc = long_term_meta.get("goal_desc", "")
  313. except (ValueError, TypeError):
  314. return None, "长期目标参数解析失败,请明确说明总天数、每日耗时。"
  315. if total_days <= 0 or daily_duration < 0:
  316. return None, "总天数或者每日时长必须大于0"
  317. try:
  318. tool = self.respostry.get_tool("subtask_split_helper_tool")
  319. sub_task_info = tool.run({"total_days": total_days, "goal_desc": goal_desc})
  320. except:
  321. print("仓库工具调用失败,进行本地调用")
  322. sub_task_info = get_Tools.subtask_split_helper_tool(total_days, main_desc=goal_desc)
  323. # 把拆分结果转为Task模型
  324. long_tasks: List[Task] = []
  325. for idx, info in enumerate(sub_task_info):
  326. t = Task(
  327. task_id=f"lt_{uuid.uuid4()}",
  328. task_name=info["sub_desc"],
  329. description=f"长期目标:{goal_desc}",
  330. estimated_duration_min=daily_duration,
  331. priority="medium",
  332. deadline=None,
  333. fixed_time=None,
  334. allowed_time=None
  335. )
  336. long_tasks.append(t)
  337. # 合并用户输入附带的任务 + 拆分出来长期子任务
  338. full_task_list = task_list + long_tasks
  339. task_list_json = json.dumps([t.model_dump() for t in full_task_list], ensure_ascii=False, indent=2)
  340. plan_user_query = f"""
  341. 用户记忆:
  342. {memory_json}
  343. 规划起始目标日期:{target_date}
  344. 长期任务列表:
  345. {task_list_json}
  346. 请跨多天生成schedule_items,输出JSON,包含schedule_items、plan_summary。
  347. 时间格式 YYYY‑MM‑DD HH:MM。
  348. """
  349. plan_result_raw = self.plan_Agent.run(plan_user_query)
  350. plan_result_raw = plan_result_raw.strip().removeprefix("```json").removesuffix("```").strip()
  351. try:
  352. plan_out = json.loads(plan_result_raw)
  353. except json.JSONDecodeError:
  354. return None, "修改计划失败,返回格式异常。"
  355. schedule_dicts = plan_out.get("schedule_items", [])
  356. try:
  357. schedule_items: List[ScheduleItem] = [ScheduleItem.model_validate(s) for s in schedule_dicts]
  358. except Exception:
  359. return None, "长期任务日程数据解析失败。"
  360. long_term_plan = Plan(
  361. plan_id=str(uuid.uuid4()),
  362. plan_name=plan_name,
  363. create_time=target_date if target_date else datetime.now().strftime("%Y‑%m‑%d %H:%M"),
  364. source_user_input=f"[长期任务] {user_input}",
  365. tasks=task_list,
  366. schedule_items=schedule_items,
  367. plan_summary=plan_out.get("plan_summary", ""),
  368. conflict_check_result="pending"
  369. )
  370. plan_json = json.dumps(long_term_plan.model_dump(), ensure_ascii=False, indent=2)
  371. validate_user_query = f"""
  372. 用户记忆:
  373. {memory_json}
  374. 待校验计划:
  375. {plan_json}
  376. 请输出JSON:
  377. {{
  378. "has_error": bool,
  379. "need_user_clarify": bool,
  380. "clarify_question": str,
  381. "error_msg": str
  382. }}
  383. """
  384. validate_raw = self.validator_agent.run(validate_user_query)
  385. validate_raw = validate_raw.strip().removeprefix("```json").removesuffix("```").strip()
  386. try:
  387. validate_out = json.loads(validate_raw)
  388. except json.JSONDecodeError:
  389. long_term_plan.conflict_check_result = 'unknown'
  390. return long_term_plan, long_term_plan.plan_summary + "\n 校验长任务环节解析异常"
  391. has_error = validate_out.get("has_error", False)
  392. need_user_clarify = validate_out.get("need_user_clarify", False)
  393. clarify_question = validate_out.get("clarify_question", "")
  394. if need_user_clarify:
  395. return None, clarify_question
  396. if has_error:
  397. long_term_plan.conflict_check_result = "has_error"
  398. else:
  399. long_term_plan.conflict_check_result = "ok"
  400. return long_term_plan, long_term_plan.plan_summary
  401. else:
  402. return None, f"无法识别意图:{intent_type}, 请重新描述你的需求"
  403. def clear_memory(self):
  404. """清空本轮对话记忆,保留agent实例,适合开启新会话"""
  405. self.dialog_history.clear()
  406. def save_session(self, save_path: Path | str, memory: UserMemory, current_plan: Optional[Plan]):
  407. """将会话保存到JSON文件"""
  408. data = {
  409. "dialog_history": self.dialog_history,
  410. "user_memory": memory.model_dump(),
  411. "last_plan": current_plan.model_dump() if current_plan else None
  412. }
  413. with open(save_path, "w", encoding="utf‑8") as f:
  414. json.dump(data, f, ensure_ascii=False, indent=2)
  415. def load_session(self, load_path: Path | str) -> tuple[UserMemory | None, Plan | None]:
  416. """从JSON恢复会话,返回(memory, current_plan),同时填充self.dialog_history"""
  417. if not Path(load_path).exists():
  418. return None, None
  419. with open(load_path, "r", encoding="utf‑8") as f:
  420. data = json.load(f)
  421. self.dialog_history = data.get("dialog_history", [])
  422. mem_data = data.get("user_memory")
  423. plan_data = data.get("last_plan")
  424. mem = UserMemory(**mem_data) if mem_data else None
  425. plan = Plan(**plan_data) if plan_data else None
  426. return mem, plan
  427. def demo_memory() -> UserMemory:
  428. return UserMemory(
  429. user_id="u_001",
  430. work_start="09:00",
  431. work_end="18:00",
  432. rest_days=["Saturday", "Sunday"],
  433. avoid_time=["12:00‑13:30"],
  434. preference={"priority_rule": "工作事务优先"},
  435. hobbies=["慢跑", "看书"],
  436. dislike=["早起高强度运动"]
  437. )
  438. def main():
  439. agent = MainAgent()
  440. memory = demo_memory()
  441. current_plan = None
  442. user_input = "帮我规划今天的日程,我需要复习90分钟,慢跑40分钟。"
  443. plan, output_text = agent.run(user_input, memory, current_plan)
  444. if plan is None:
  445. print(f"输出:{output_text}")
  446. else:
  447. print(f"\n计划摘要:{plan.plan_summary}")
  448. for item in plan.schedule_items:
  449. print(f"{item.start_time} ~ {item.end_time} | {item.task_name}")
  450. if __name__ == "__main__":
  451. main()