|
|
@@ -0,0 +1,513 @@
|
|
|
+# core/main_agent.py
|
|
|
+"""主 Agent - 协调层,负责意图识别和路由"""
|
|
|
+
|
|
|
+from hello_agents import SimpleAgent, HelloAgentsLLM
|
|
|
+from core.file_manager import FileManager
|
|
|
+
|
|
|
+
|
|
|
+class MainAgent(SimpleAgent):
|
|
|
+ """
|
|
|
+ 系统协调者,负责意图识别和路由
|
|
|
+
|
|
|
+ 职责:
|
|
|
+ - 接收用户输入
|
|
|
+ - 识别用户意图(create/add/vibe/summary/help/exit)
|
|
|
+ - 路由到相应的子 Agent 或处理器
|
|
|
+ - 管理基本命令(help, list, exit)
|
|
|
+ """
|
|
|
+
|
|
|
+ # 意图关键词映射(按优先级排序,更具体的在前)
|
|
|
+ INTENT_KEYWORDS = {
|
|
|
+ "create": [
|
|
|
+ "/create",
|
|
|
+ "创建计划",
|
|
|
+ "制定学习路径",
|
|
|
+ "我想学",
|
|
|
+ "我想学习",
|
|
|
+ "学习计划", # 移除单独的"学习"以避免冲突
|
|
|
+ ],
|
|
|
+ "add": ["/add", "添加笔记", "记录知识", "添加知识"],
|
|
|
+ "vibe": ["/vibe", "开始学习", "互动学习", "练习", "考察"],
|
|
|
+ "summary": ["/summary", "学习进度", "总结", "评估"],
|
|
|
+ "help": ["/help", "帮助", "help"],
|
|
|
+ "list": ["/list", "列出所有", "所有领域", "列表"],
|
|
|
+ "exit": ["/exit", "退出", "quit", "exit"],
|
|
|
+ }
|
|
|
+
|
|
|
+ def __init__(self, llm: HelloAgentsLLM, file_manager: FileManager, streaming: bool = None):
|
|
|
+ """
|
|
|
+ 初始化主 Agent
|
|
|
+
|
|
|
+ Args:
|
|
|
+ llm: HelloAgentsLLM 实例
|
|
|
+ file_manager: FileManager 实例
|
|
|
+ streaming: 是否启用流式输出(None = 自动检测)
|
|
|
+ """
|
|
|
+ system_prompt = """
|
|
|
+ 你是 LearningAgent 学习助手的主界面。
|
|
|
+
|
|
|
+ 支持的功能:
|
|
|
+ 1. 创建学习计划 (/create, "我想学习")
|
|
|
+ 2. 添加知识笔记 (/add, "添加笔记")
|
|
|
+ 3. 开始互动学习 (/vibe, "开始学习")
|
|
|
+ 4. 查看学习总结 (/summary, "总结")
|
|
|
+ 5. 显示帮助 (/help, "帮助")
|
|
|
+ 6. 列出所有领域 (/list)
|
|
|
+ 7. 退出程序 (/exit, "退出")
|
|
|
+
|
|
|
+ 识别用户意图后,调用相应的功能。
|
|
|
+ 如果意图模糊,询问用户确认。
|
|
|
+ """
|
|
|
+
|
|
|
+ self.llm = llm
|
|
|
+ self.file_manager = file_manager
|
|
|
+
|
|
|
+ # 添加流式输出支持
|
|
|
+ from utils.streaming import should_stream
|
|
|
+ self.streaming = should_stream(streaming)
|
|
|
+
|
|
|
+ # 会话状态管理
|
|
|
+ self.active_session = None # {"domain": str, "mode": str, "round": int}
|
|
|
+
|
|
|
+ # 使用父类初始化
|
|
|
+ super().__init__("MainAgent", llm, system_prompt)
|
|
|
+
|
|
|
+ def _identify_intent(self, user_input: str) -> str:
|
|
|
+ """
|
|
|
+ 识别用户意图
|
|
|
+
|
|
|
+ Args:
|
|
|
+ user_input: 用户输入
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 意图类型(create/add/vibe/summary/help/list/exit/unknown)
|
|
|
+ """
|
|
|
+ user_input_lower = user_input.lower().strip()
|
|
|
+
|
|
|
+ # 检查每个意图的关键词
|
|
|
+ for intent, keywords in self.INTENT_KEYWORDS.items():
|
|
|
+ for keyword in keywords:
|
|
|
+ if keyword.lower() in user_input_lower:
|
|
|
+ return intent
|
|
|
+
|
|
|
+ return "unknown"
|
|
|
+
|
|
|
+ def process_command(self, user_input: str) -> str:
|
|
|
+ """
|
|
|
+ 处理用户命令
|
|
|
+
|
|
|
+ Args:
|
|
|
+ user_input: 用户输入
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 处理结果
|
|
|
+ """
|
|
|
+ # 检查是否有活跃的 vibe 会话
|
|
|
+ if self.active_session is not None:
|
|
|
+ # 检查是否要退出会话
|
|
|
+ if self._is_exit_command(user_input):
|
|
|
+ return self._end_vibe_session()
|
|
|
+ # 否则继续对话
|
|
|
+ return self._continue_vibe_session(user_input)
|
|
|
+
|
|
|
+ # 正常命令处理
|
|
|
+ intent = self._identify_intent(user_input)
|
|
|
+
|
|
|
+ if intent == "create":
|
|
|
+ return self._route_to_create_plan(user_input)
|
|
|
+ elif intent == "add":
|
|
|
+ return self._route_to_add_knowledge(user_input)
|
|
|
+ elif intent == "vibe":
|
|
|
+ return self._route_to_vibe_learning(user_input)
|
|
|
+ elif intent == "summary":
|
|
|
+ return self._route_to_summary(user_input)
|
|
|
+ elif intent == "help":
|
|
|
+ return self._show_help()
|
|
|
+ elif intent == "list":
|
|
|
+ return self._list_domains()
|
|
|
+ elif intent == "exit":
|
|
|
+ return "EXIT"
|
|
|
+ elif intent == "unknown":
|
|
|
+ return "❓ 未识别的命令。输入 /help 查看帮助。"
|
|
|
+
|
|
|
+ def _route_to_create_plan(self, input_data: str) -> str:
|
|
|
+ """
|
|
|
+ 路由到 CreatePlanAgent
|
|
|
+
|
|
|
+ Args:
|
|
|
+ input_data: 用户输入
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 执行结果
|
|
|
+ """
|
|
|
+ from agents.create_plan_agent import CreatePlanAgent
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 去掉命令前缀,只保留参数部分
|
|
|
+ # 支持: "/create 数学", "创建计划 数学", "我想学习数学"
|
|
|
+ clean_input = input_data
|
|
|
+
|
|
|
+ # 去掉 /create 前缀
|
|
|
+ for prefix in ["/create", "/CREATE"]:
|
|
|
+ if clean_input.startswith(prefix):
|
|
|
+ clean_input = clean_input[len(prefix) :].strip()
|
|
|
+ break
|
|
|
+
|
|
|
+ # 如果是自然语言形式,保留原样
|
|
|
+ # 例如: "我想学习数学", "创建一个学习计划"
|
|
|
+ if not clean_input or clean_input == input_data:
|
|
|
+ clean_input = input_data
|
|
|
+
|
|
|
+ agent = CreatePlanAgent(self.llm, streaming=self.streaming)
|
|
|
+ return agent.run(clean_input)
|
|
|
+ except Exception as e:
|
|
|
+ return f"❌ 创建学习计划失败:{e}"
|
|
|
+
|
|
|
+ def _route_to_add_knowledge(self, input_data: str) -> str:
|
|
|
+ """
|
|
|
+ 路由到 AddKnowledgeProcessor
|
|
|
+
|
|
|
+ Args:
|
|
|
+ input_data: 用户输入
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 执行结果
|
|
|
+ """
|
|
|
+ from processors.add_knowledge import AddKnowledgeProcessor
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 去掉命令前缀,只保留参数部分
|
|
|
+ # 支持: "/add 数学 算法", "添加笔记", "记录知识"
|
|
|
+ clean_input = input_data
|
|
|
+
|
|
|
+ # 去掉 /add 前缀
|
|
|
+ for prefix in ["/add", "/ADD"]:
|
|
|
+ if clean_input.startswith(prefix):
|
|
|
+ clean_input = clean_input[len(prefix) :].strip()
|
|
|
+ break
|
|
|
+
|
|
|
+ # 如果是自然语言形式,需要询问用户输入内容和领域
|
|
|
+ if not clean_input or clean_input == input_data or len(clean_input) < 10:
|
|
|
+ return self._ask_for_knowledge_input()
|
|
|
+
|
|
|
+ # 解析输入(格式:领域 内容)
|
|
|
+ # 例如: "机器学习 决策树算法"
|
|
|
+ parts = clean_input.split(maxsplit=1)
|
|
|
+ if len(parts) == 2:
|
|
|
+ domain, content = parts
|
|
|
+ domain = domain.strip()
|
|
|
+ content = content.strip()
|
|
|
+ else:
|
|
|
+ # 无法解析,询问用户
|
|
|
+ return self._ask_for_knowledge_input()
|
|
|
+
|
|
|
+ processor = AddKnowledgeProcessor(self.llm, self.file_manager)
|
|
|
+ return processor.add(domain, content)
|
|
|
+ except Exception as e:
|
|
|
+ return f"❌ 添加知识失败:{e}"
|
|
|
+
|
|
|
+ def _ask_for_knowledge_input(self) -> str:
|
|
|
+ """
|
|
|
+ 询问用户知识内容和领域
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 提示信息
|
|
|
+ """
|
|
|
+ return """📝 添加知识笔记
|
|
|
+
|
|
|
+请按以下格式输入:
|
|
|
+
|
|
|
+> /add <领域> <知识内容>
|
|
|
+
|
|
|
+例如:
|
|
|
+> /add 机器学习 # 决策树算法简介
|
|
|
+
|
|
|
+或者直接输入内容(会询问领域):
|
|
|
+> /add 决策树是一种监督学习算法,用于分类和回归...
|
|
|
+
|
|
|
+💡 提示:长内容可以先在编辑器中准备好,然后一次性粘贴。
|
|
|
+"""
|
|
|
+
|
|
|
+ def _route_to_vibe_learning(self, input_data: str) -> str:
|
|
|
+ """
|
|
|
+ 路由到 VibeLearningAgent
|
|
|
+
|
|
|
+ Args:
|
|
|
+ input_data: 用户输入
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 执行结果
|
|
|
+ """
|
|
|
+ from agents.vibe_learning_agent import VibeLearningAgent
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 去掉命令前缀,只保留参数部分
|
|
|
+ # 支持: "/vibe Python", "/vibe Python --mode quiz"
|
|
|
+ clean_input = input_data
|
|
|
+
|
|
|
+ # 去掉 /vibe 前缀
|
|
|
+ for prefix in ["/vibe", "/VIBE", "/Vibe"]:
|
|
|
+ if clean_input.startswith(prefix):
|
|
|
+ clean_input = clean_input[len(prefix):].strip()
|
|
|
+ break
|
|
|
+
|
|
|
+ # 如果是自然语言形式,询问用户
|
|
|
+ if not clean_input or len(clean_input.split()) < 1:
|
|
|
+ return self._ask_for_vibe_input()
|
|
|
+
|
|
|
+ # 解析输入
|
|
|
+ # 格式: <领域> [--mode <mode>]
|
|
|
+ parts = clean_input.split()
|
|
|
+ domain = parts[0].strip()
|
|
|
+
|
|
|
+ # 检查是否有模式选项
|
|
|
+ mode = "free" # 默认模式
|
|
|
+ if "--mode" in parts:
|
|
|
+ mode_idx = parts.index("--mode")
|
|
|
+ if mode_idx + 1 < len(parts):
|
|
|
+ mode = parts[mode_idx + 1].strip().lower()
|
|
|
+ if mode not in ["free", "quiz"]:
|
|
|
+ return "❌ 无效的模式。请使用 --mode free 或 --mode quiz"
|
|
|
+
|
|
|
+ # 启动学习会话
|
|
|
+ agent = VibeLearningAgent(self.llm, self.file_manager,
|
|
|
+ streaming=self.streaming)
|
|
|
+ result = agent.start_session(domain, mode=mode)
|
|
|
+
|
|
|
+ # 设置活跃会话
|
|
|
+ self.active_session = {
|
|
|
+ "domain": domain,
|
|
|
+ "mode": mode,
|
|
|
+ "round": 1,
|
|
|
+ "agent": agent,
|
|
|
+ "streaming": self.streaming # 保存 streaming 设置
|
|
|
+ }
|
|
|
+
|
|
|
+ return result
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ return f"❌ 启动互动学习失败:{e}"
|
|
|
+
|
|
|
+ def _route_to_summary(self, input_data: str) -> str:
|
|
|
+ """
|
|
|
+ 路由到 SummaryAgent
|
|
|
+
|
|
|
+ Args:
|
|
|
+ input_data: 用户输入
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 执行结果
|
|
|
+ """
|
|
|
+ from agents.summary_agent import SummaryAgent
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 去掉命令前缀,只保留参数部分
|
|
|
+ # 支持: "/summary Python", "总结学习进度"
|
|
|
+ clean_input = input_data
|
|
|
+
|
|
|
+ # 去掉 /summary 前缀
|
|
|
+ for prefix in ["/summary", "/SUMMARY", "/Summary"]:
|
|
|
+ if clean_input.startswith(prefix):
|
|
|
+ clean_input = clean_input[len(prefix):].strip()
|
|
|
+ break
|
|
|
+
|
|
|
+ # 如果是自然语言形式,询问用户
|
|
|
+ if not clean_input or len(clean_input.split()) < 1:
|
|
|
+ return self._ask_for_summary_input()
|
|
|
+
|
|
|
+ # 解析输入
|
|
|
+ # 格式: <领域>
|
|
|
+ domain = clean_input.split()[0].strip()
|
|
|
+
|
|
|
+ # 生成学习总结
|
|
|
+ agent = SummaryAgent(self.llm, self.file_manager,
|
|
|
+ streaming=self.streaming)
|
|
|
+ return agent.run(domain)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ return f"❌ 生成学习总结失败:{e}"
|
|
|
+
|
|
|
+ def _is_exit_command(self, user_input: str) -> bool:
|
|
|
+ """
|
|
|
+ 检查是否是退出命令
|
|
|
+
|
|
|
+ Args:
|
|
|
+ user_input: 用户输入
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 是否是退出命令
|
|
|
+ """
|
|
|
+ exit_keywords = ["/exit", "exit", "退出", "quit", "/quit", "结束", "完成"]
|
|
|
+ return user_input.strip().lower() in exit_keywords
|
|
|
+
|
|
|
+ def _end_vibe_session(self) -> str:
|
|
|
+ """
|
|
|
+ 结束 vibe 会话
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 结束消息
|
|
|
+ """
|
|
|
+ domain = self.active_session["domain"]
|
|
|
+ mode = self.active_session["mode"]
|
|
|
+ rounds = self.active_session["round"]
|
|
|
+
|
|
|
+ # 清除会话状态
|
|
|
+ self.active_session = None
|
|
|
+
|
|
|
+ return f"""✅ 会话已结束
|
|
|
+
|
|
|
+📁 领域: {domain}
|
|
|
+📝 模式: {mode}
|
|
|
+💬 对话轮数: {rounds}
|
|
|
+
|
|
|
+💡 上下文已保存。输入 /help 查看可用命令。
|
|
|
+"""
|
|
|
+
|
|
|
+ def _continue_vibe_session(self, user_input: str) -> str:
|
|
|
+ """
|
|
|
+ 继续 vibe 会话
|
|
|
+
|
|
|
+ Args:
|
|
|
+ user_input: 用户回答
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 反馈和下一个问题
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ agent = self.active_session["agent"]
|
|
|
+ domain = self.active_session["domain"]
|
|
|
+ mode = self.active_session["mode"]
|
|
|
+
|
|
|
+ # 确保 streaming 设置一致
|
|
|
+ agent.streaming = self.active_session.get("streaming", agent.streaming)
|
|
|
+
|
|
|
+ # 继续对话(获取反馈和下一个问题)
|
|
|
+ result = agent.continue_session(domain, user_input, mode)
|
|
|
+
|
|
|
+ # 增加轮次计数
|
|
|
+ self.active_session["round"] += 1
|
|
|
+
|
|
|
+ return result
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ # 发生错误时清除会话状态
|
|
|
+ self.active_session = None
|
|
|
+ return f"❌ 对话过程中发生错误:{e}\n\n会话已结束。"
|
|
|
+
|
|
|
+ def _ask_for_vibe_input(self) -> str:
|
|
|
+ """
|
|
|
+ 询问用户互动学习参数
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 提示信息
|
|
|
+ """
|
|
|
+ return """📝 互动学习
|
|
|
+
|
|
|
+请按以下格式输入:
|
|
|
+
|
|
|
+> /vibe <领域> [--mode <模式>]
|
|
|
+
|
|
|
+例如:
|
|
|
+> /vibe Python
|
|
|
+> /vibe Python --mode quiz
|
|
|
+
|
|
|
+模式说明:
|
|
|
+- free: 自由对话模式(默认)
|
|
|
+- quiz: 测验模式
|
|
|
+
|
|
|
+💡 提示:需要先使用 /create 创建学习计划。
|
|
|
+"""
|
|
|
+
|
|
|
+ def _ask_for_summary_input(self) -> str:
|
|
|
+ """
|
|
|
+ 询问用户学习总结参数
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 提示信息
|
|
|
+ """
|
|
|
+ return """📝 学习进度总结
|
|
|
+
|
|
|
+请按以下格式输入:
|
|
|
+
|
|
|
+> /summary <领域>
|
|
|
+
|
|
|
+例如:
|
|
|
+> /summary Python
|
|
|
+> /summary 机器学习
|
|
|
+
|
|
|
+💡 提示:需要先使用 /create 创建学习计划。
|
|
|
+"""
|
|
|
+
|
|
|
+ def _show_help(self) -> str:
|
|
|
+ """显示帮助信息"""
|
|
|
+ return """
|
|
|
+# 🤖 LearningAgent 帮助
|
|
|
+
|
|
|
+## 命令列表
|
|
|
+
|
|
|
+### 创建学习计划
|
|
|
+- `/create <领域>` - 创建学习计划
|
|
|
+ 例:`/create 数学`
|
|
|
+ 例:`/create https://github.com/user/project`
|
|
|
+ 例:`/create ~/paper.pdf`
|
|
|
+
|
|
|
+- 自然语言:`我想学习数学`
|
|
|
+
|
|
|
+### 添加知识笔记 ✨ 新功能
|
|
|
+- `/add <领域> <内容>` - 添加知识笔记
|
|
|
+ 例:`/add 机器学习 # 决策树算法简介`
|
|
|
+ 例:`/add Python 这是一个列表推导式的例子...`
|
|
|
+
|
|
|
+- 文件输入:`/add ~/notes.md`
|
|
|
+
|
|
|
+- 自然语言:`添加笔记` `记录知识`
|
|
|
+
|
|
|
+### 开始互动学习 ✨ 新功能
|
|
|
+- `/vibe <领域>` - 开始互动学习
|
|
|
+ 例:`/vibe Python`
|
|
|
+ 例:`/vibe Python --mode quiz`
|
|
|
+
|
|
|
+- 模式说明:
|
|
|
+ - `free`: 自由对话模式(默认)
|
|
|
+ - `quiz`: 测验模式
|
|
|
+
|
|
|
+- 退出会话:输入"退出"、"exit"或"/exit"即可随时结束
|
|
|
+
|
|
|
+- 自然语言:`开始学习数学` `练习一下`
|
|
|
+
|
|
|
+### 查看学习总结 ✨ 新功能
|
|
|
+- `/summary <领域>` - 查看学习总结
|
|
|
+ 例:`/summary Python`
|
|
|
+ 例:`/summary 机器学习`
|
|
|
+
|
|
|
+- 自然语言:`总结学习进度` `评估我的水平`
|
|
|
+
|
|
|
+### 其他命令
|
|
|
+- `/list` - 列出所有学习领域
|
|
|
+- `/help` - 显示帮助
|
|
|
+- `/exit` 或 `exit` - 退出程序
|
|
|
+
|
|
|
+## 提示
|
|
|
+- 支持命令前缀(如 `/create`)和自然语言(如"我想学习")
|
|
|
+- 添加知识时,内容会自动分析、分类并打标签
|
|
|
+- 随时输入 `/help` 查看帮助
|
|
|
+"""
|
|
|
+
|
|
|
+ def _list_domains(self) -> str:
|
|
|
+ """列出所有学习领域"""
|
|
|
+ domains = self.file_manager.list_domains()
|
|
|
+
|
|
|
+ if not domains:
|
|
|
+ return "📭 还没有创建任何学习领域。\n使用 `/create` 创建第一个学习计划。"
|
|
|
+
|
|
|
+ domain_list = "\n".join([f"- {domain}" for domain in domains])
|
|
|
+ return f"# 📚 学习领域\n\n{domain_list}\n\n共 {len(domains)} 个领域"
|
|
|
+
|
|
|
+ def list_domains(self) -> list:
|
|
|
+ """
|
|
|
+ 获取所有领域列表
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 领域名称列表
|
|
|
+ """
|
|
|
+ return self.file_manager.list_domains()
|