| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- import json
- import uuid
- from pathlib import Path
- from typing import List, Optional, Tuple
- from hello_agents import HelloAgentsLLM, SimpleAgent, ToolRegistry
- from config import *
- from model import UserMemory, Task, Plan, ScheduleItem
- from prompt import *
- from tools import get_Tools
- class MainAgent:
- def __init__(self):
- self.llm = HelloAgentsLLM(model=LLM_MODEL, api_key=LLM_API_KEY, base_url=LLM_BASE_URL)
- tool = get_Tools()
- self.respostry = ToolRegistry()
- self.respostry.register_tool(tool)
- self.main_agent = SimpleAgent(
- name="总控MAIN_AGENT",
- llm=self.llm,
- system_prompt=MAIN_AGENT_PROMPT,
- tool_registry=self.respostry
- )
- self.np_parser_agent = SimpleAgent(
- name="子Agent:自然语言解析,用户输入 → Task列表 + 意图",
- llm=self.llm,
- system_prompt=NL_PARSE_PROMPT,
- tool_registry=self.respostry
- )
- # self.np_parser_agent.add_tool(tool)
- self.plan_Agent = SimpleAgent(
- name="子Agent:你是日程规划Agent,用于规划用户日程",
- llm=self.llm,
- system_prompt=PLAN_REASON_PROMPT,
- tool_registry=self.respostry
- )
- # self.plan_Agent.add_tool(tool)
- self.validator_agent = SimpleAgent(
- name="子Agent:你是计划校验Agent,检查生成的日程计划是否违反用户记忆、是否存在时间冲突。",
- llm=self.llm,
- system_prompt=VALIDATE_PROMPT,
- tool_registry=self.respostry
- )
- # self.validator_agent.add_tool(tool)
- self.dialog_history: List[dict] = []
- def _build_context_str(self, user_input: str, memory: UserMemory, current_plan: Optional[Plan], mem: Optional[UserMemory], plan: Optional[Plan]) -> str:
- memory_json = json.dumps(memory.model_dump(), ensure_ascii=False, indent=2)
- cp_json = json.dumps(current_plan.model_dump(), ensure_ascii=False, indent=2) if current_plan else 'null'
- history_text = []
- history_text.append(f"====当前系统时间====\n{datetime.now().strftime('%Y‑%m‑%d %H:%M:%S')}")
- history_text.append("====对话历史====")
- for msg in self.dialog_history:
- role = msg['role']
- cnt = msg["content"]
- history_text.append(f"{role}: {cnt}")
- history_text.append("====用户记忆====")
- history_text.append(memory_json)
- history_text.append(mem)
- history_text.append("====当前已存在计划current_plan====")
- history_text.append(cp_json)
- history_text.append(plan)
- history_text.append("====用户最新输入====")
- history_text.append(user_input)
- return "\n".join(history_text)
- def _build_subagent_payload(self, pass_param: dict, memory: UserMemory, current_plan: Optional[Plan]) -> dict:
- """构造子Agent输入:强制注入公共上下文,不依赖总控pass_to_sub"""
- import datetime
- payload = {**pass_param}
- # 强制注入
- payload["current_system_time"] = datetime.datetime.now().strftime("%Y‑%m‑%d %H:%M:%S")
- payload["user_memory"] = memory.model_dump()
- payload["current_plan"] = current_plan.model_dump() if current_plan else None
- payload["dialog_history"] = self.dialog_history.copy()
- return payload
- def run(self, user_input: str, memory: UserMemory, current_plan: Optional[Plan], mem: Optional[UserMemory] = 'null', plan: Optional[Plan] = 'null') -> Tuple[Optional[Plan], str]:
- try:
- self.dialog_history.append({'role':'user','content':user_input})
- max_loop = 10
- loop_count = 0
- final_plan: Optional[Plan] = None
- final_output: str=""
- while loop_count < max_loop:
- loop_count += 1
- context = self._build_context_str(user_input, memory, current_plan, mem, plan)
- main_raw = self.main_agent.run(context)
- main_raw = main_raw.strip().removeprefix("```json").removesuffix("```").strip()
- try:
- main_desicion = json.loads(main_raw)
- except:
- err_msg = f'总控Agent决策解析失败,原始输出:{main_raw[:300]}'
- final_output = err_msg
- break
- call_sub = main_desicion.get("call_sub_agent", 'finish')
- pass_param = main_desicion.get("pass_to_sub", {})
- if call_sub == 'finish':
- # 总控通知结束
- fp_json = pass_param.get("final_plan_json")
- final_output = pass_param.get("output_text", "处理完成")
- if fp_json and fp_json != "null":
- try:
- final_plan = Plan.model_validate_json(fp_json)
- except Exception:
- final_plan = None
- break
- if call_sub == 'nl_parser':
- sub_payload = self._build_subagent_payload(pass_param, memory, current_plan)
- sub_query = json.dumps(sub_payload, ensure_ascii=False, indent=2)
- sub_out = self.np_parser_agent.run(sub_query)
- self.dialog_history.append({'role': 'tool:nl_parser', "content": sub_out})
- elif call_sub == 'planner':
- sub_payload = self._build_subagent_payload(pass_param, memory, current_plan)
- plan_query = json.dumps(sub_payload, ensure_ascii=False, indent=2)
- sub_out = self.plan_Agent.run(plan_query)
- self.dialog_history.append({'role': 'tool:planner', "content": sub_out})
- elif call_sub == 'validator':
- sub_payload = self._build_subagent_payload(pass_param, memory, current_plan)
- sub_query = json.dumps(sub_payload, ensure_ascii=False, indent=2)
- sub_out = self.validator_agent.run(sub_query)
- self.dialog_history.append({'role': 'tool:validator', "content": sub_out})
- else:
- final_output = f"总控调用未知子Agent:{call_sub}"
- break
- else:
- final_output = "循环已满,处理过程终止"
- self.dialog_history.append({'role':'assistant', "content":final_output})
- return final_plan, final_output
- except Exception as e:
- print(f"总控智能体出现错误:{str(e)},即将使用默认模式")
- return self.run_bak(user_input, memory, current_plan)
- def run_bak(self, user_input: str, memory: UserMemory, current_plan: Optional[Plan]) -> Tuple[Optional[Plan], str]:
- """
- :param user_input: 用户原始输入
- :param memory: 用户记忆对象
- :param current_plan: 当前已存在计划(修改场景使用)
- :return: (Plan对象 or None, 对外输出文本/需要澄清的问题)
- """
- # self.dialog_history.append(AgentMessage(role="user", content=user_input))
- memory_json = json.dumps(memory.model_dump(), ensure_ascii=False, indent=2)
- # 把用户输入 + 用户记忆拼为子Agent的user query
- nl_user_query = f"""
- 用户记忆:
- {memory_json}
- 用户输入:
- {user_input}
-
- 请输出JSON格式结果,包含
- intent_type, target_date(YYYY‑MM‑DD,解析不出返回null), tasks, modify_operation。
- tasks 为任务数组。
- """
- ### Agent 第一次调用,解析用户问题,然后生成任务列表
- nl_result_raw = self.np_parser_agent.run(nl_user_query)
- nl_result_raw = nl_result_raw.strip().removeprefix("```json").removesuffix("```").strip()
- try:
- nl_out = json.loads(nl_result_raw)
- except json.JSONDecodeError:
- return None, "解析用户需求失败,大模型返回格式异常,请重新描述你的需求。"
- intent_type = nl_out.get("intent_type", "")
- task_dicts = nl_out.get("tasks", [])
- target_date: str | None = nl_out.get("target_date")
- plan_name: str | None = nl_out.get("name_summary")
- modify_operation = nl_out.get("modify_operation", {})
- long_term_meta = nl_out.get("long_term_meta", {})
- if not target_date:
- # 兜底:取系统当前日期
- target_date = datetime.now().strftime("%Y‑%m‑%d %H:%M:%S")
- try:
- task_list: List[Task] = [Task.model_validate(d) for d in task_dicts]
- except Exception:
- return None, "任务数据解析失败,请简化你的描述。"
- ### 识别意图,然后决定怎么做这个任务
- if intent_type == "new_plan":
- task_list_json = json.dumps([t.model_dump() for t in task_list], ensure_ascii=False, indent=2)
- plan_user_query = f"""
- 用户记忆:
- {memory_json}
- 需要规划的目标日期:
- {target_date}
- 待规划任务列表:
- {task_list_json}
- 请输出JSON,包含 schedule_items(日程条目数组)、plan_summary。
- 日程的时间需要基于目标日期生成,时间格式 YYYY‑MM‑DD HH:MM。
- """
- plan_result_raw = self.plan_Agent.run(plan_user_query)
- plan_result_raw = plan_result_raw.strip().removeprefix("```json").removesuffix("```").strip()
- try:
- plan_out = json.loads(plan_result_raw)
- except json.JSONDecodeError:
- return None, "生成日程失败,格式错误。"
- schedule_dicts = plan_out.get("schedule_items", [])
- try:
- schedule_items: List[ScheduleItem] = [ScheduleItem.model_validate(s) for s in schedule_dicts]
- except Exception:
- return None, "日程条目数据解析失败。"
- new_plan = Plan(
- plan_id=str(uuid.uuid4()),
- plan_name=plan_name,
- create_time=target_date if target_date else datetime.now().strftime("%Y‑%m‑%d %H:%M"),
- source_user_input=user_input,
- tasks=task_list,
- schedule_items=schedule_items,
- plan_summary=plan_out.get("plan_summary", ""),
- conflict_check_result="pending"
- )
- plan_json = json.dumps(new_plan.model_dump(), ensure_ascii=False, indent=2)
- validate_user_query = f"""
- 用户记忆:
- {memory_json}
- 待校验计划:
- {plan_json}
- 请输出JSON:
- {{
- "has_error": bool,
- "need_user_clarify": bool,
- "clarify_question": str,
- "error_msg": str
- }}
- """
- validate_raw = self.validator_agent.run(validate_user_query)
- validate_raw = validate_raw.strip().removeprefix("```json").removesuffix("```").strip()
- try:
- validate_out = json.loads(validate_raw)
- except json.JSONDecodeError:
- new_plan.conflict_check_result = 'unknown'
- return new_plan, new_plan.plan_summary + "\n 校验新建环节解析异常"
- has_error = validate_out.get("has_error", False)
- need_user_clarify = validate_out.get("need_user_clarify", False)
- clarify_question = validate_out.get("clarify_question", "")
- if need_user_clarify:
- return None, clarify_question
- if has_error:
- new_plan.conflict_check_result = "has_error"
- else:
- new_plan.conflict_check_result = "ok"
- return new_plan, new_plan.plan_summary
- elif intent_type == "modify_plan":
- if current_plan is None:
- return None, "当前没有已生成的计划,请先生成一份日程计划之后再执行修改。"
- old_plan_json = json.dumps(current_plan.model_dump(), ensure_ascii=False, indent=2)
- op_json = json.dumps(modify_operation, ensure_ascii=False, indent=2)
- plan_user_query = f"""
- 用户记忆:
- {memory_json}
- 目标日期:{target_date}
- 原始旧计划:
- {old_plan_json}
- 用户修改操作:
- {op_json}
- 请基于旧计划,按照修改操作重新生成完整的schedule_items。
- 输出JSON:{{"schedule_items":[...],"plan_summary":"描述本次修改后的计划"}}
- 时间格式 YYYY‑MM‑DD HH:MM。
- """
- plan_result_raw = self.plan_Agent.run(plan_user_query)
- plan_result_raw = plan_result_raw.strip().removeprefix("```json").removesuffix("```").strip()
- try:
- plan_out = json.loads(plan_result_raw)
- except json.JSONDecodeError:
- return None, "修改计划失败,返回格式异常。"
- schedule_dicts = plan_out.get("schedule_items", [])
- try:
- schedule_items: List[ScheduleItem] = [ScheduleItem.model_validate(s) for s in schedule_dicts]
- except Exception:
- return None, "修改后日程数据解析失败。"
- merged_tasks = current_plan.tasks.copy()
- merged_tasks.extend(task_list)
- modified_plan = Plan(
- plan_id=str(uuid.uuid4()),
- plan_name=plan_name,
- create_time=target_date if target_date else datetime.now().strftime("%Y‑%m‑%d %H:%M"),
- source_user_input=f"[修改] {user_input}",
- tasks=task_list,
- schedule_items=schedule_items,
- plan_summary=plan_out.get("plan_summary", ""),
- conflict_check_result="pending"
- )
- plan_json = json.dumps(modified_plan.model_dump(), ensure_ascii=False, indent=2)
- validate_user_query = f"""
- 用户记忆:
- {memory_json}
- 待校验计划:
- {plan_json}
- 请输出JSON:
- {{
- "has_error": bool,
- "need_user_clarify": bool,
- "clarify_question": str,
- "error_msg": str
- }}
- """
- validate_raw = self.validator_agent.run(validate_user_query)
- validate_raw = validate_raw.strip().removeprefix("```json").removesuffix("```").strip()
- try:
- validate_out = json.loads(validate_raw)
- except json.JSONDecodeError:
- modified_plan.conflict_check_result = 'unknown'
- return modified_plan, modified_plan.plan_summary + "\n 校验修改环节解析异常"
- has_error = validate_out.get("has_error", False)
- need_user_clarify = validate_out.get("need_user_clarify", False)
- clarify_question = validate_out.get("clarify_question", "")
- if need_user_clarify:
- return None, clarify_question
- if has_error:
- modified_plan.conflict_check_result = "has_error"
- else:
- modified_plan.conflict_check_result = "ok"
- return modified_plan, modified_plan.plan_summary
- elif intent_type == "long_term_goal":
- """
- 1.读取long_term_meta,调用 subtask_split_helper_tool 拆出多个子任务描述
- 2.把拆分得到的子任务组装成Task列表
- 3.复用new_plan整套规划+校验逻辑
- """
- try:
- total_days = int(long_term_meta.get("total_days", 0))
- daily_duration = int(long_term_meta.get("daily_duration_min", 0))
- goal_desc = long_term_meta.get("goal_desc", "")
- except (ValueError, TypeError):
- return None, "长期目标参数解析失败,请明确说明总天数、每日耗时。"
- if total_days <= 0 or daily_duration < 0:
- return None, "总天数或者每日时长必须大于0"
- try:
- tool = self.respostry.get_tool("subtask_split_helper_tool")
- sub_task_info = tool.run({"total_days": total_days, "goal_desc": goal_desc})
- except:
- print("仓库工具调用失败,进行本地调用")
- sub_task_info = get_Tools.subtask_split_helper_tool(total_days, main_desc=goal_desc)
- # 把拆分结果转为Task模型
- long_tasks: List[Task] = []
- for idx, info in enumerate(sub_task_info):
- t = Task(
- task_id=f"lt_{uuid.uuid4()}",
- task_name=info["sub_desc"],
- description=f"长期目标:{goal_desc}",
- estimated_duration_min=daily_duration,
- priority="medium",
- deadline=None,
- fixed_time=None,
- allowed_time=None
- )
- long_tasks.append(t)
- # 合并用户输入附带的任务 + 拆分出来长期子任务
- full_task_list = task_list + long_tasks
- task_list_json = json.dumps([t.model_dump() for t in full_task_list], ensure_ascii=False, indent=2)
- plan_user_query = f"""
- 用户记忆:
- {memory_json}
- 规划起始目标日期:{target_date}
- 长期任务列表:
- {task_list_json}
- 请跨多天生成schedule_items,输出JSON,包含schedule_items、plan_summary。
- 时间格式 YYYY‑MM‑DD HH:MM。
- """
- plan_result_raw = self.plan_Agent.run(plan_user_query)
- plan_result_raw = plan_result_raw.strip().removeprefix("```json").removesuffix("```").strip()
- try:
- plan_out = json.loads(plan_result_raw)
- except json.JSONDecodeError:
- return None, "修改计划失败,返回格式异常。"
- schedule_dicts = plan_out.get("schedule_items", [])
- try:
- schedule_items: List[ScheduleItem] = [ScheduleItem.model_validate(s) for s in schedule_dicts]
- except Exception:
- return None, "长期任务日程数据解析失败。"
- long_term_plan = Plan(
- plan_id=str(uuid.uuid4()),
- plan_name=plan_name,
- create_time=target_date if target_date else datetime.now().strftime("%Y‑%m‑%d %H:%M"),
- source_user_input=f"[长期任务] {user_input}",
- tasks=task_list,
- schedule_items=schedule_items,
- plan_summary=plan_out.get("plan_summary", ""),
- conflict_check_result="pending"
- )
- plan_json = json.dumps(long_term_plan.model_dump(), ensure_ascii=False, indent=2)
- validate_user_query = f"""
- 用户记忆:
- {memory_json}
- 待校验计划:
- {plan_json}
- 请输出JSON:
- {{
- "has_error": bool,
- "need_user_clarify": bool,
- "clarify_question": str,
- "error_msg": str
- }}
- """
- validate_raw = self.validator_agent.run(validate_user_query)
- validate_raw = validate_raw.strip().removeprefix("```json").removesuffix("```").strip()
- try:
- validate_out = json.loads(validate_raw)
- except json.JSONDecodeError:
- long_term_plan.conflict_check_result = 'unknown'
- return long_term_plan, long_term_plan.plan_summary + "\n 校验长任务环节解析异常"
- has_error = validate_out.get("has_error", False)
- need_user_clarify = validate_out.get("need_user_clarify", False)
- clarify_question = validate_out.get("clarify_question", "")
- if need_user_clarify:
- return None, clarify_question
- if has_error:
- long_term_plan.conflict_check_result = "has_error"
- else:
- long_term_plan.conflict_check_result = "ok"
- return long_term_plan, long_term_plan.plan_summary
- else:
- return None, f"无法识别意图:{intent_type}, 请重新描述你的需求"
- def clear_memory(self):
- """清空本轮对话记忆,保留agent实例,适合开启新会话"""
- self.dialog_history.clear()
- def save_session(self, save_path: Path | str, memory: UserMemory, current_plan: Optional[Plan]):
- """将会话保存到JSON文件"""
- data = {
- "dialog_history": self.dialog_history,
- "user_memory": memory.model_dump(),
- "last_plan": current_plan.model_dump() if current_plan else None
- }
- with open(save_path, "w", encoding="utf‑8") as f:
- json.dump(data, f, ensure_ascii=False, indent=2)
- def load_session(self, load_path: Path | str) -> tuple[UserMemory | None, Plan | None]:
- """从JSON恢复会话,返回(memory, current_plan),同时填充self.dialog_history"""
- if not Path(load_path).exists():
- return None, None
- with open(load_path, "r", encoding="utf‑8") as f:
- data = json.load(f)
- self.dialog_history = data.get("dialog_history", [])
- mem_data = data.get("user_memory")
- plan_data = data.get("last_plan")
- mem = UserMemory(**mem_data) if mem_data else None
- plan = Plan(**plan_data) if plan_data else None
- return mem, plan
- def demo_memory() -> UserMemory:
- return UserMemory(
- user_id="u_001",
- work_start="09:00",
- work_end="18:00",
- rest_days=["Saturday", "Sunday"],
- avoid_time=["12:00‑13:30"],
- preference={"priority_rule": "工作事务优先"},
- hobbies=["慢跑", "看书"],
- dislike=["早起高强度运动"]
- )
- def main():
- agent = MainAgent()
- memory = demo_memory()
- current_plan = None
- user_input = "帮我规划今天的日程,我需要复习90分钟,慢跑40分钟。"
- plan, output_text = agent.run(user_input, memory, current_plan)
- if plan is None:
- print(f"输出:{output_text}")
- else:
- print(f"\n计划摘要:{plan.plan_summary}")
- for item in plan.schedule_items:
- print(f"{item.start_time} ~ {item.end_time} | {item.task_name}")
- if __name__ == "__main__":
- main()
|