Browse Source

how agent search test

liuzhiheng 2 months ago
parent
commit
46508c4059

+ 42 - 0
agent/tools/builtin/feishu/chat_history/chat_刘志恒.json

@@ -0,0 +1,42 @@
+[
+  {
+    "role": "assistant",
+    "message_id": "om_x100b542104c81ca8c3bfa9c6398adc3",
+    "content": [
+      {
+        "type": "text",
+        "text": "你好,我正在尝试在小红书上搜索“家有大志”的账号进行分析,目前遇到了登录限制。请帮忙扫描下方的二维码完成登录,谢谢!"
+      },
+      {
+        "type": "image",
+        "image_url": {
+          "url": "https://files.oaiusercontent.com/file-S88888888888888888888888"
+        }
+      }
+    ]
+  },
+  {
+    "role": "assistant",
+    "message_id": "om_x100b5421e8e968a0c3dbfacf4af8927",
+    "content": [
+      {
+        "type": "text",
+        "text": "你好,我正在小红书上搜索“家有大志”的账号,目前需要扫码登录。请点击以下链接在浏览器中完成扫码登录:\n\nhttps://live.browser-use.com?wss=https%3A//21902035-789a-4782-867d-7ad4a5464856.cdp2.browser-use.com\n\n完成后请回复“已登录”,谢谢!"
+      }
+    ]
+  },
+  {
+    "role": "assistant",
+    "message_id": "om_x100b54226143fca0c39328e40db7c70",
+    "content": [
+      {
+        "type": "text",
+        "text": "刘志恒你好,我正在调研小红书账号“家有大志”,目前需要登录小红书。请你帮忙扫码登录一下,登录完成后请回复我。"
+      },
+      {
+        "type": "image",
+        "image_url": "https://files.oaiusercontent.com/file-776v88f988f988f988f988f9"
+      }
+    ]
+  }
+]

+ 0 - 82
examples_how/overall_derivation/skills/derivation_eval.md

@@ -1,82 +0,0 @@
----
-name: derivation_eval
-description: 选题点推导评估任务 - 调用 point_match 工具获取匹配结果并整理为规定 JSON
----
-
-# 选题点推导评估任务
-
-## 角色
-你是选题点推导的**评估执行者**,负责调用 **point_match** 工具得到「推导选题点」与「帖子选题点」的匹配结果,并据此整理成主 agent 要求的 JSON,**不**由模型自行判断语义相似度。
-
-## 任务描述
-主 agent 会传入:1)历史已推导成功的选题点(JSON);2)本轮推导出的可能选题点(含推导路径 ID);3)**帖子ID**;4)**账号名**。你需要:
-1. 从本轮推导出的可能选题点中提取**所有待评估的选题点名称**(即每条推导路径的 `output` 列表展平),并保留每个选题点对应的推导路径 **id**。
-2. **调用工具 `point_match`**:传入 `derivation_output_points`(本轮待评估选题点名称列表)、`account_name`、`post_id`。工具会读取帖子选题点,用相似度计算匹配,返回匹配成功的列表(每项含 推导选题点、帖子选题点、匹配分数)。
-3. 根据 **point_match 的返回** 和「帖子选题点总数」(见下)整理出规定的 **eval_results** 与 **next_round**,**仅输出一段合法 JSON**,不输出任何其它说明文字。
-
-## 输入(由主 agent 在 task 中提供)
-- 历史已推导成功的选题点(JSON 或列表)
-- 本轮推导出的可能选题点:通常为列表,每项含 `id`(推导路径ID)、`output`(选题点名称列表)
-- 帖子ID、账号名
-
-## 操作步骤
-
-1. **解析 task**:从主 agent 的 task 中提取 历史已推导成功选题点、本轮推导选题点(含 id 与 output)、帖子ID、账号名。
-2. **构造待评估列表**:将本轮每条推导路径的 `output` 展平,得到 (id, 选题点名称) 列表(同一路径下多个 output 共用该路径的 id)。若多条路径产出了相同的选题点名称,调用 point_match 时需**去重**(每个名称仅传入一次);整理 eval_results 时,对每个 (id, derivation_topic_name) 组合各输出一条记录(多条路径输出同名点时,分别对应各自的路径 id,匹配结果一致)。
-3. **调用 point_match**:
-   - `derivation_output_points`:上一步得到的选题点名称列表(可含重复 id 的多个点)
-   - `account_name`:主 agent 传入的账号名
-   - `post_id`:主 agent 传入的帖子ID
-4. **获取帖子选题点总数**:为计算 `need_next_round`,需知道帖子选题点总数 N。可调用 **read_file** 读取 `input/{账号名}/post_topic/{帖子ID}.json`(路径相对项目根),该文件为选题点名称的 JSON 数组,N = 数组长度。**不得在最终返回的 JSON 中包含 N 或 post_topic_count**。
-5. **整理 eval_results**:
-   - 对「本轮每一个推导选题点」(按 id + derivation_topic_name)各一条记录。
-   - 若该选题点出现在 **point_match 返回的匹配列表**中(匹配列表项中的「推导选题点」等于该名称),则:`is_matched: true`,`matched_post_topic` 填该匹配项中的「帖子选题点」,`matched_reason` 可填 `"point_match 工具匹配,分数={匹配分数}"`。
-   - 若未出现在匹配列表中,则:`is_matched: false`,`matched_post_topic: null`,`matched_reason: null`。
-   - `id` 与主 agent 传入的推导路径 ID 一致;`derivation_topic_name` 为本轮推导的选题点名称。
-6. **整理 next_round**:
-   - `derived_success_count` = 历史已推导成功数量 + 本轮**新增**的 `is_matched` 为 true 的数量。所谓"新增":若本轮匹配到的 `matched_post_topic` 已存在于历史已推导成功列表中(名称相同),则**不重复计入**,避免同一帖子选题点被重复统计。
-   - `need_next_round` = (`derived_success_count` < 帖子选题点总数 N)。**不要在 JSON 中输出 N**。
-
-## 输出要求
-
-**必须返回合法 JSON**,且**仅输出该 JSON**,供主 agent 解析。不要在 JSON 前后添加任何说明、markdown 代码块或注释。
-
-### JSON 结构
-
-```json
-{
-  "eval_results": [
-    {
-      "id": 1,
-      "derivation_topic_name": "本轮推导的选题点名称",
-      "is_matched": true,
-      "matched_post_topic": "帖子解构中匹配到的选题点名称",
-      "matched_reason": "point_match 工具匹配,分数=0.xx"
-    },
-    {
-      "id": 2,
-      "derivation_topic_name": "本轮推导的选题点名称",
-      "is_matched": false,
-      "matched_post_topic": null,
-      "matched_reason": null
-    }
-  ],
-  "next_round": {
-    "derived_success_count": 5,
-    "need_next_round": true
-  }
-}
-```
-
-### 字段说明
-- **eval_results**:对本轮每个推导选题点各一条;是否匹配**完全依据 point_match 工具的返回**,不要用模型自行判断语义。
-- **next_round.derived_success_count**:截至本轮结束的累计已推导成功数量。
-- **next_round.need_next_round**:是否还需下一轮推导(由 derived_success_count 与帖子选题点总数 N 比较得出,N 不写入 JSON)。
-
-### 信息隔离
-- **禁止**在返回结果中包含帖子选题点总数、未推导成功的选题点名称或任何暗示。
-- **禁止**在返回结果中添加任何文字说明、建议或评论。
-
-## 约束
-1. **匹配结果以 point_match 为准**:不直接用模型评估语义相似度;只有 point_match 返回的匹配才记为 `is_matched: true`。
-2. **输出纯 JSON**:仅输出上述结构的一段 JSON,便于主 agent 直接解析。

+ 55 - 0
examples_how/search_test/config.py

@@ -0,0 +1,55 @@
+"""
+项目配置
+
+定义项目的运行配置。
+"""
+
+from agent.core.runner import KnowledgeConfig, RunConfig
+
+
+# ===== Agent 运行配置 =====
+
+RUN_CONFIG = RunConfig(
+    # 模型配置
+    model="google/gemini-3-flash-preview",
+    temperature=0.3,
+    max_iterations=1000,
+
+    # 任务名称
+    name="search test Agent",
+
+    # 知识管理配置
+    knowledge=KnowledgeConfig(
+        # 压缩时提取(消息量超阈值触发压缩时,用完整 history 反思)
+        enable_extraction=True,
+        reflect_prompt="",  # 自定义反思 prompt;空则使用默认,见 agent/core/prompts/knowledge.py:REFLECT_PROMPT
+
+        # agent运行完成后提取(不代表任务完成,agent 可能中途退出等待人工评估)
+        enable_completion_extraction=True,
+        completion_reflect_prompt="",  # 自定义复盘 prompt;空则使用默认,见 agent/core/prompts/knowledge.py:COMPLETION_REFLECT_PROMPT
+
+        # 知识注入(agent切换当前工作的goal时,自动注入相关知识)
+        enable_injection=True,
+
+        # 默认字段(保存/搜索时自动注入)
+        owner="",  # 所有者(空则尝试从 git config user.email 获取,再空则用 agent:{agent_id})
+        default_tags={"project": "search_test", "domain": "ai_agent"},  # 默认 tags(会与工具调用参数合并)
+        default_scopes=["org:cybertogether"],  # 默认 scopes
+        default_search_types=[],  # 默认搜索类型过滤
+        default_search_owner=""  # 默认搜索 owner 过滤(空则不过滤,支持多个owner用逗号分隔,如 "user1@example.com,user2@example.com")
+    )
+)
+
+
+# ===== 基础设施配置 =====
+
+SKILLS_DIR = "./skills"
+TRACE_STORE_PATH = ".trace"
+DEBUG = True
+LOG_LEVEL = "INFO"
+LOG_FILE = None  # 设置为文件路径可以同时输出到文件
+
+# ===== 浏览器配置 =====
+# 可选值: "cloud" (云浏览器) 或 "local" (本地浏览器)
+BROWSER_TYPE = "local"
+HEADLESS = False

+ 372 - 0
examples_how/search_test/run.py

@@ -0,0 +1,372 @@
+"""
+示例(简化版 - 使用框架交互功能)
+
+使用 Agent 模式 + Skills + 框架交互控制器
+
+新功能:
+1. 使用框架提供的 InteractiveController
+2. 使用配置文件管理运行参数
+3. 支持命令行随时打断(输入 'p' 暂停,'q' 退出)
+4. 暂停后可插入干预消息
+5. 支持触发经验总结
+6. 查看当前 GoalTree
+7. 支持通过 --trace <ID> 恢复已有 Trace 继续执行
+"""
+
+import argparse
+import os
+import sys
+import asyncio
+from pathlib import Path
+
+# Clash Verge TUN 模式兼容:禁止对 127.0.0.1 走代理,否则 browser_use 请求 /json/version 会拿到空响应导致 JSONDecodeError
+# os.environ.setdefault("no_proxy", "127.0.0.1,localhost")
+
+# Clash Verge TUN 模式兼容:禁止 httpx/urllib 自动检测系统 HTTP 代理
+os.environ.setdefault("no_proxy", "*")
+
+# 添加项目根目录到 Python 路径
+sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+from dotenv import load_dotenv
+load_dotenv()
+
+from agent.llm.prompts import SimplePrompt
+from agent.core.runner import AgentRunner, RunConfig
+from agent.core.presets import AgentPreset, register_preset
+from agent.trace import (
+    FileSystemTraceStore,
+    Trace,
+    Message,
+)
+from agent.llm import create_openrouter_llm_call
+from agent.cli import InteractiveController
+from agent.utils import setup_logging
+from agent.tools.builtin.browser.baseClass import init_browser_session, kill_browser_session
+
+# 导入项目配置
+from config import RUN_CONFIG, SKILLS_DIR, TRACE_STORE_PATH, DEBUG, LOG_LEVEL, LOG_FILE, BROWSER_TYPE, HEADLESS
+
+
+async def main():
+    # 解析命令行参数
+    parser = argparse.ArgumentParser(description="任务 (Agent 模式 + 交互增强)")
+    parser.add_argument(
+        "--trace", type=str, default=None,
+        help="已有的 Trace ID,用于恢复继续执行(不指定则新建)",
+    )
+    args = parser.parse_args()
+
+    # 路径配置
+    base_dir = Path(__file__).parent
+    project_root = base_dir.parent.parent
+    prompt_path = base_dir / "search_test_prompt.md"
+    output_dir = base_dir / "output"
+    output_dir.mkdir(exist_ok=True)
+
+    # 1. 配置日志
+    setup_logging(level=LOG_LEVEL, file=LOG_FILE)
+
+    # 2. 加载项目级 presets
+    print("2. 加载 presets...")
+    presets_path = base_dir / "presets.json"
+    if presets_path.exists():
+        import json
+        with open(presets_path, "r", encoding="utf-8") as f:
+            project_presets = json.load(f)
+        for name, cfg in project_presets.items():
+            register_preset(name, AgentPreset(**cfg))
+        print(f"   - 已加载项目 presets: {list(project_presets.keys())}")
+
+    # 3. 加载 prompt
+    print("3. 加载 prompt...")
+    prompt = SimplePrompt(prompt_path)
+
+    # 4. 构建任务消息
+    print("4. 构建任务消息...")
+    messages = prompt.build_messages()
+
+    # 5. 初始化浏览器
+    import platform
+    actual_browser_type = BROWSER_TYPE
+    if platform.system() == "Windows" and BROWSER_TYPE == "local":
+        actual_browser_type = "cloud"
+        print("⚠️ Windows 平台检测到本地浏览器配置,自动切换为云浏览器模式")
+
+    browser_mode_name = "云浏览器" if actual_browser_type == "cloud" else "本地浏览器"
+    print(f"5. 正在初始化{browser_mode_name}...")
+    await init_browser_session(
+        browser_type=actual_browser_type,
+        headless=HEADLESS,
+        url="about:blank"
+    )
+    print(f"   ✅ {browser_mode_name}初始化完成\n")
+
+    # 6. 创建 Agent Runner
+    print("6. 创建 Agent Runner...")
+    print(f"   - Skills 目录: {SKILLS_DIR}")
+    print(f"   - 模型: {RUN_CONFIG.model}")
+
+    store = FileSystemTraceStore(base_path=TRACE_STORE_PATH)
+    runner = AgentRunner(
+        trace_store=store,
+        llm_call=create_openrouter_llm_call(model=RUN_CONFIG.model),
+        skills_dir=SKILLS_DIR,
+        debug=DEBUG
+    )
+
+    # 7. 创建交互控制器
+    interactive = InteractiveController(
+        runner=runner,
+        store=store,
+        enable_stdin_check=True
+    )
+
+    # 8. 任务信息
+    task_name = RUN_CONFIG.name or base_dir.name
+    print("=" * 60)
+    print(f"{task_name}")
+    print("=" * 60)
+    print("💡 交互提示:")
+    print("   - 执行过程中输入 'p' 或 'pause' 暂停并进入交互模式")
+    print("   - 执行过程中输入 'q' 或 'quit' 停止执行")
+    print("=" * 60)
+    print()
+
+    # 9. 判断是新建还是恢复
+    resume_trace_id = args.trace
+    if resume_trace_id:
+        existing_trace = await store.get_trace(resume_trace_id)
+        if not existing_trace:
+            print(f"\n错误: Trace 不存在: {resume_trace_id}")
+            sys.exit(1)
+        print(f"恢复已有 Trace: {resume_trace_id[:8]}...")
+        print(f"   - 状态: {existing_trace.status}")
+        print(f"   - 消息数: {existing_trace.total_messages}")
+        print(f"\n💡 提示:恢复 Trace 时会先进入交互菜单,您可以选择从指定消息续跑")
+    else:
+        print(f"启动新 Agent...")
+
+    print()
+
+    final_response = ""
+    current_trace_id = resume_trace_id
+    current_sequence = 0
+    should_exit = False
+
+    try:
+        # 配置
+        run_config = RUN_CONFIG
+        if resume_trace_id:
+            initial_messages = None
+            run_config.trace_id = resume_trace_id
+        else:
+            initial_messages = messages
+            run_config.name = f"{task_name}:调研任务"
+
+        while not should_exit:
+            if current_trace_id:
+                run_config.trace_id = current_trace_id
+
+            final_response = ""
+
+            # 如果是恢复 trace 或 trace 已完成/失败且没有新消息,进入交互菜单
+            if current_trace_id and initial_messages is None:
+                check_trace = await store.get_trace(current_trace_id)
+                if check_trace:
+                    # 显示 trace 状态
+                    if check_trace.status == "completed":
+                        print(f"\n[Trace] ✅ 已完成")
+                        print(f"  - Total messages: {check_trace.total_messages}")
+                        print(f"  - Total cost: ${check_trace.total_cost:.4f}")
+                    elif check_trace.status == "failed":
+                        print(f"\n[Trace] ❌ 已失败: {check_trace.error_message}")
+                    elif check_trace.status == "stopped":
+                        print(f"\n[Trace] ⏸️ 已停止")
+                        print(f"  - Total messages: {check_trace.total_messages}")
+                    else:
+                        print(f"\n[Trace] 📊 状态: {check_trace.status}")
+                        print(f"  - Total messages: {check_trace.total_messages}")
+
+                    current_sequence = check_trace.head_sequence
+
+                    menu_result = await interactive.show_menu(current_trace_id, current_sequence)
+
+                    if menu_result["action"] == "stop":
+                        break
+                    elif menu_result["action"] == "continue":
+                        new_messages = menu_result.get("messages", [])
+                        if new_messages:
+                            initial_messages = new_messages
+                            run_config.after_sequence = menu_result.get("after_sequence")
+                        else:
+                            initial_messages = []
+                            run_config.after_sequence = None
+                        continue
+                    break
+
+            # 如果没有进入菜单(新建 trace),设置初始消息
+            if initial_messages is None:
+                initial_messages = []
+
+            print(f"{'▶️ 开始执行...' if not current_trace_id else '▶️ 继续执行...'}")
+
+            # 执行 Agent
+            paused = False
+            try:
+                async for item in runner.run(messages=initial_messages, config=run_config):
+                    # 检查用户中断
+                    cmd = interactive.check_stdin()
+                    if cmd == 'pause':
+                        print("\n⏸️ 正在暂停执行...")
+                        if current_trace_id:
+                            await runner.stop(current_trace_id)
+                        await asyncio.sleep(0.5)
+
+                        menu_result = await interactive.show_menu(current_trace_id, current_sequence)
+
+                        if menu_result["action"] == "stop":
+                            should_exit = True
+                            paused = True
+                            break
+                        elif menu_result["action"] == "continue":
+                            new_messages = menu_result.get("messages", [])
+                            if new_messages:
+                                initial_messages = new_messages
+                                after_seq = menu_result.get("after_sequence")
+                                if after_seq is not None:
+                                    run_config.after_sequence = after_seq
+                                paused = True
+                                break
+                            else:
+                                initial_messages = []
+                                run_config.after_sequence = None
+                                paused = True
+                                break
+
+                    elif cmd == 'quit':
+                        print("\n🛑 用户请求停止...")
+                        if current_trace_id:
+                            await runner.stop(current_trace_id)
+                        should_exit = True
+                        break
+
+                    # 处理 Trace 对象
+                    if isinstance(item, Trace):
+                        current_trace_id = item.trace_id
+                        if item.status == "running":
+                            print(f"[Trace] 开始: {item.trace_id[:8]}...")
+                        elif item.status == "completed":
+                            print(f"\n[Trace] ✅ 完成")
+                            print(f"  - Total messages: {item.total_messages}")
+                            print(f"  - Total cost: ${item.total_cost:.4f}")
+                        elif item.status == "failed":
+                            print(f"\n[Trace] ❌ 失败: {item.error_message}")
+                        elif item.status == "stopped":
+                            print(f"\n[Trace] ⏸️ 已停止")
+
+                    # 处理 Message 对象
+                    elif isinstance(item, Message):
+                        current_sequence = item.sequence
+
+                        if item.role == "assistant":
+                            content = item.content
+                            if isinstance(content, dict):
+                                text = content.get("text", "")
+                                tool_calls = content.get("tool_calls")
+
+                                if text and not tool_calls:
+                                    final_response = text
+                                    print(f"\n[Response] Agent 回复:")
+                                    print(text)
+                                elif text:
+                                    preview = text[:150] + "..." if len(text) > 150 else text
+                                    print(f"[Assistant] {preview}")
+
+                        elif item.role == "tool":
+                            content = item.content
+                            tool_name = "unknown"
+                            if isinstance(content, dict):
+                                tool_name = content.get("tool_name", "unknown")
+
+                            if item.description and item.description != tool_name:
+                                desc = item.description[:80] if len(item.description) > 80 else item.description
+                                print(f"[Tool Result] ✅ {tool_name}: {desc}...")
+                            else:
+                                print(f"[Tool Result] ✅ {tool_name}")
+
+            except Exception as e:
+                print(f"\n执行出错: {e}")
+                import traceback
+                traceback.print_exc()
+
+            if paused:
+                if should_exit:
+                    break
+                continue
+
+            if should_exit:
+                break
+
+            # Runner 退出后显示交互菜单
+            if current_trace_id:
+                menu_result = await interactive.show_menu(current_trace_id, current_sequence)
+
+                if menu_result["action"] == "stop":
+                    break
+                elif menu_result["action"] == "continue":
+                    new_messages = menu_result.get("messages", [])
+                    if new_messages:
+                        initial_messages = new_messages
+                        run_config.after_sequence = menu_result.get("after_sequence")
+                    else:
+                        initial_messages = []
+                        run_config.after_sequence = None
+                    continue
+            break
+
+    except KeyboardInterrupt:
+        print("\n\n用户中断 (Ctrl+C)")
+        if current_trace_id:
+            await runner.stop(current_trace_id)
+    finally:
+        # 清理浏览器会话
+        try:
+            await kill_browser_session()
+        except Exception:
+            pass
+
+    # 7. 输出结果
+    if final_response:
+        print()
+        print("=" * 60)
+        print("Agent 响应:")
+        print("=" * 60)
+        print(final_response)
+        print("=" * 60)
+        print()
+
+        output_file = output_dir / "result.txt"
+        with open(output_file, 'w', encoding='utf-8') as f:
+            f.write(final_response)
+
+        print(f"✓ 结果已保存到: {output_file}")
+        print()
+
+    # 可视化提示
+    if current_trace_id:
+        print("=" * 60)
+        print("可视化 Step Tree:")
+        print("=" * 60)
+        print("1. 启动 API Server:")
+        print("   python3 api_server.py")
+        print()
+        print("2. 浏览器访问:")
+        print("   http://localhost:8000/api/traces")
+        print()
+        print(f"3. Trace ID: {current_trace_id}")
+        print("=" * 60)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 11 - 0
examples_how/search_test/search_test_prompt.md

@@ -0,0 +1,11 @@
+---
+model: google/gemini-3-flash-preview
+temperature: 0.3
+---
+
+$system$
+你是最顶尖的AI助手,可以拆分并调用工具逐步解决复杂问题。
+
+$user$
+用browser_use打开小红书网站,搜索“家有大志”账号,查看总结第一个帖子内容。注意:你需要查看帖子所有图片并理解图片内容
+遇到问题可以联系刘志恒(如果要扫码登录可以把截图发给他)