|
@@ -0,0 +1,290 @@
|
|
|
|
|
+"""
|
|
|
|
|
+交互式控制器
|
|
|
|
|
+
|
|
|
|
|
+提供暂停/继续、交互式菜单、经验总结等功能。
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+import sys
|
|
|
|
|
+import asyncio
|
|
|
|
|
+from typing import Optional, Dict, Any
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
+
|
|
|
|
|
+from agent.core.runner import AgentRunner
|
|
|
|
|
+from agent.trace import TraceStore
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# ===== 非阻塞 stdin 检测 =====
|
|
|
|
|
+
|
|
|
|
|
+if sys.platform == 'win32':
|
|
|
|
|
+ import msvcrt
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def check_stdin() -> Optional[str]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 跨平台非阻塞检查 stdin 输入。
|
|
|
|
|
+
|
|
|
|
|
+ Windows: 使用 msvcrt.kbhit()
|
|
|
|
|
+ macOS/Linux: 使用 select.select()
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 'pause' | 'quit' | None
|
|
|
|
|
+ """
|
|
|
|
|
+ if sys.platform == 'win32':
|
|
|
|
|
+ # Windows: 检查是否有按键按下
|
|
|
|
|
+ if msvcrt.kbhit():
|
|
|
|
|
+ ch = msvcrt.getwch().lower()
|
|
|
|
|
+ if ch == 'p':
|
|
|
|
|
+ return 'pause'
|
|
|
|
|
+ if ch == 'q':
|
|
|
|
|
+ return 'quit'
|
|
|
|
|
+ return None
|
|
|
|
|
+ else:
|
|
|
|
|
+ # Unix/Mac: 使用 select
|
|
|
|
|
+ import select
|
|
|
|
|
+ ready, _, _ = select.select([sys.stdin], [], [], 0)
|
|
|
|
|
+ if ready:
|
|
|
|
|
+ line = sys.stdin.readline().strip().lower()
|
|
|
|
|
+ if line in ('p', 'pause'):
|
|
|
|
|
+ return 'pause'
|
|
|
|
|
+ if line in ('q', 'quit'):
|
|
|
|
|
+ return 'quit'
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def read_multiline() -> str:
|
|
|
|
|
+ """
|
|
|
|
|
+ 读取多行输入,以连续两次回车(空行)结束。
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 用户输入的多行文本
|
|
|
|
|
+ """
|
|
|
|
|
+ print("\n请输入干预消息(连续输入两次回车结束):")
|
|
|
|
|
+ lines = []
|
|
|
|
|
+ blank_count = 0
|
|
|
|
|
+
|
|
|
|
|
+ while True:
|
|
|
|
|
+ line = input()
|
|
|
|
|
+ if line == "":
|
|
|
|
|
+ blank_count += 1
|
|
|
|
|
+ if blank_count >= 2:
|
|
|
|
|
+ break
|
|
|
|
|
+ lines.append("") # 保留单个空行
|
|
|
|
|
+ else:
|
|
|
|
|
+ blank_count = 0
|
|
|
|
|
+ lines.append(line)
|
|
|
|
|
+
|
|
|
|
|
+ # 去掉尾部多余空行
|
|
|
|
|
+ while lines and lines[-1] == "":
|
|
|
|
|
+ lines.pop()
|
|
|
|
|
+
|
|
|
|
|
+ return "\n".join(lines)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# ===== 交互式控制器 =====
|
|
|
|
|
+
|
|
|
|
|
+class InteractiveController:
|
|
|
|
|
+ """
|
|
|
|
|
+ 交互式控制器
|
|
|
|
|
+
|
|
|
|
|
+ 管理暂停/继续、交互式菜单、经验总结等交互功能。
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ def __init__(
|
|
|
|
|
+ self,
|
|
|
|
|
+ runner: AgentRunner,
|
|
|
|
|
+ store: TraceStore,
|
|
|
|
|
+ enable_stdin_check: bool = True
|
|
|
|
|
+ ):
|
|
|
|
|
+ """
|
|
|
|
|
+ 初始化交互式控制器
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ runner: Agent Runner 实例
|
|
|
|
|
+ store: Trace Store 实例
|
|
|
|
|
+ enable_stdin_check: 是否启用 stdin 检查
|
|
|
|
|
+ """
|
|
|
|
|
+ self.runner = runner
|
|
|
|
|
+ self.store = store
|
|
|
|
|
+ self.enable_stdin_check = enable_stdin_check
|
|
|
|
|
+
|
|
|
|
|
+ def check_stdin(self) -> Optional[str]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 检查 stdin 输入
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 'pause' | 'quit' | None
|
|
|
|
|
+ """
|
|
|
|
|
+ if not self.enable_stdin_check:
|
|
|
|
|
+ return None
|
|
|
|
|
+ return check_stdin()
|
|
|
|
|
+
|
|
|
|
|
+ async def show_menu(
|
|
|
|
|
+ self,
|
|
|
|
|
+ trace_id: str,
|
|
|
|
|
+ current_sequence: int
|
|
|
|
|
+ ) -> Dict[str, Any]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 显示交互式菜单
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ trace_id: Trace ID
|
|
|
|
|
+ current_sequence: 当前消息序号
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 用户选择的操作
|
|
|
|
|
+ """
|
|
|
|
|
+ print("\n" + "=" * 60)
|
|
|
|
|
+ print(" 执行已暂停")
|
|
|
|
|
+ print("=" * 60)
|
|
|
|
|
+ print("请选择操作:")
|
|
|
|
|
+ print(" 1. 插入干预消息并继续")
|
|
|
|
|
+ print(" 2. 触发经验总结(reflect)")
|
|
|
|
|
+ print(" 3. 查看当前 GoalTree")
|
|
|
|
|
+ print(" 4. 手动压缩上下文(compact)")
|
|
|
|
|
+ print(" 5. 继续执行")
|
|
|
|
|
+ print(" 6. 停止执行")
|
|
|
|
|
+ print("=" * 60)
|
|
|
|
|
+
|
|
|
|
|
+ while True:
|
|
|
|
|
+ choice = input("请输入选项 (1-6): ").strip()
|
|
|
|
|
+
|
|
|
|
|
+ if choice == "1":
|
|
|
|
|
+ # 插入干预消息
|
|
|
|
|
+ text = read_multiline()
|
|
|
|
|
+ if not text:
|
|
|
|
|
+ print("未输入任何内容,取消操作")
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ print(f"\n将插入干预消息并继续执行...")
|
|
|
|
|
+ # 从 store 读取实际的 last_sequence
|
|
|
|
|
+ live_trace = await self.store.get_trace(trace_id)
|
|
|
|
|
+ actual_sequence = live_trace.last_sequence if live_trace and live_trace.last_sequence else current_sequence
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ "action": "continue",
|
|
|
|
|
+ "messages": [{"role": "user", "content": text}],
|
|
|
|
|
+ "after_sequence": actual_sequence,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ elif choice == "2":
|
|
|
|
|
+ # 触发经验总结
|
|
|
|
|
+ print("\n触发经验总结...")
|
|
|
|
|
+ focus = input("请输入反思重点(可选,直接回车跳过): ").strip()
|
|
|
|
|
+ await self.perform_reflection(trace_id, focus=focus)
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ elif choice == "3":
|
|
|
|
|
+ # 查看 GoalTree
|
|
|
|
|
+ goal_tree = await self.store.get_goal_tree(trace_id)
|
|
|
|
|
+ if goal_tree and goal_tree.goals:
|
|
|
|
|
+ print("\n当前 GoalTree:")
|
|
|
|
|
+ print(goal_tree.to_prompt())
|
|
|
|
|
+ else:
|
|
|
|
|
+ print("\n当前没有 Goal")
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ elif choice == "4":
|
|
|
|
|
+ # 手动压缩上下文
|
|
|
|
|
+ await self.manual_compact(trace_id)
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ elif choice == "5":
|
|
|
|
|
+ # 继续执行
|
|
|
|
|
+ print("\n继续执行...")
|
|
|
|
|
+ return {"action": "continue"}
|
|
|
|
|
+
|
|
|
|
|
+ elif choice == "6":
|
|
|
|
|
+ # 停止执行
|
|
|
|
|
+ print("\n停止执行...")
|
|
|
|
|
+ return {"action": "stop"}
|
|
|
|
|
+
|
|
|
|
|
+ else:
|
|
|
|
|
+ print("无效选项,请重新输入")
|
|
|
|
|
+
|
|
|
|
|
+ async def perform_reflection(
|
|
|
|
|
+ self,
|
|
|
|
|
+ trace_id: str,
|
|
|
|
|
+ focus: str = ""
|
|
|
|
|
+ ):
|
|
|
|
|
+ """
|
|
|
|
|
+ 执行经验总结
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ trace_id: Trace ID
|
|
|
|
|
+ focus: 反思重点(可选)
|
|
|
|
|
+ """
|
|
|
|
|
+ from agent.core.prompts.knowledge import build_reflect_prompt
|
|
|
|
|
+ from agent.core.runner import RunConfig
|
|
|
|
|
+
|
|
|
|
|
+ trace = await self.store.get_trace(trace_id)
|
|
|
|
|
+ if not trace:
|
|
|
|
|
+ print("未找到 Trace")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ saved_head = trace.head_sequence
|
|
|
|
|
+
|
|
|
|
|
+ # 构建反思 prompt
|
|
|
|
|
+ prompt = build_reflect_prompt()
|
|
|
|
|
+ if focus:
|
|
|
|
|
+ prompt += f"\n\n请特别关注:{focus}"
|
|
|
|
|
+
|
|
|
|
|
+ print("正在生成反思...")
|
|
|
|
|
+ reflect_cfg = RunConfig(trace_id=trace_id, max_iterations=1, tools=[])
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ result = await self.runner.run_result(
|
|
|
|
|
+ messages=[{"role": "user", "content": prompt}],
|
|
|
|
|
+ config=reflect_cfg,
|
|
|
|
|
+ )
|
|
|
|
|
+ reflection_text = result.get("summary", "")
|
|
|
|
|
+
|
|
|
|
|
+ if reflection_text:
|
|
|
|
|
+ print("\n--- 反思内容 ---")
|
|
|
|
|
+ print(reflection_text)
|
|
|
|
|
+ print("--- 结束 ---\n")
|
|
|
|
|
+ else:
|
|
|
|
|
+ print("未生成反思内容")
|
|
|
|
|
+
|
|
|
|
|
+ finally:
|
|
|
|
|
+ # 恢复 head_sequence(反思消息成为侧枝,不污染主对话)
|
|
|
|
|
+ await self.store.update_trace(trace_id, head_sequence=saved_head)
|
|
|
|
|
+
|
|
|
|
|
+ async def manual_compact(self, trace_id: str):
|
|
|
|
|
+ """
|
|
|
|
|
+ 手动压缩上下文
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ trace_id: Trace ID
|
|
|
|
|
+ """
|
|
|
|
|
+ from agent.core.runner import RunConfig
|
|
|
|
|
+
|
|
|
|
|
+ print("\n正在执行上下文压缩(compact)...")
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ goal_tree = await self.store.get_goal_tree(trace_id)
|
|
|
|
|
+ trace = await self.store.get_trace(trace_id)
|
|
|
|
|
+
|
|
|
|
|
+ if not trace:
|
|
|
|
|
+ print("未找到 Trace,无法压缩")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # 重建当前 history
|
|
|
|
|
+ main_path = await self.store.get_main_path_messages(trace_id, trace.head_sequence)
|
|
|
|
|
+ history = [msg.to_llm_dict() for msg in main_path]
|
|
|
|
|
+ head_seq = main_path[-1].sequence if main_path else 0
|
|
|
|
|
+ next_seq = head_seq + 1
|
|
|
|
|
+
|
|
|
|
|
+ compact_config = RunConfig(trace_id=trace_id)
|
|
|
|
|
+ new_history, new_head, new_seq = await self.runner._compress_history(
|
|
|
|
|
+ trace_id=trace_id,
|
|
|
|
|
+ history=history,
|
|
|
|
|
+ goal_tree=goal_tree,
|
|
|
|
|
+ config=compact_config,
|
|
|
|
|
+ sequence=next_seq,
|
|
|
|
|
+ head_seq=head_seq,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ print(f"\n✅ 压缩完成: {len(history)} 条消息 → {len(new_history)} 条")
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"\n❌ 压缩失败: {e}")
|