فهرست منبع

feat: content_tree_analyst

Talegorithm 12 ساعت پیش
والد
کامیت
7690ca78d0

+ 10 - 0
agent/tools/builtin/feishu/chat_history/chat_谭景玉.json

@@ -8,5 +8,15 @@
         "text": "你好!我需要登录小红书来完成搜索摄影主题的任务,但是没有找到保存的cookie。\n\n请点击以下链接在浏览器中完成小红书登录:\nhttps://live.browser-use.com?wss=wss%3A//4599a061-1830-4cb0-99fc-fffb5503e99a.cdp1.browser-use.com/devtools/browser/f77323a4-3759-4558-85e0-f4eb3eb04368\n\n登录完成后请告诉我,我会保存登录状态。谢谢!"
       }
     ]
+  },
+  {
+    "role": "assistant",
+    "message_id": "om_x100b5488244594a4c4d3c52f961965f",
+    "content": [
+      {
+        "type": "text",
+        "text": "需要协助登录小红书进行调研。\n\n请打开云浏览器链接完成小红书登录:\n(云浏览器链接需要先初始化)\n\n任务:搜索\"AI角色连载\"\"AI虚拟人日常\"\"AI短剧连载\"相关内容,找出持续更新同一角色故事的账号\n\n请登录后回复确认,我将保存cookie继续调研。"
+      }
+    ]
   }
 ]

+ 44 - 0
examples/content_tree_analyst/analyst.prompt

@@ -0,0 +1,44 @@
+---
+model: qwen3.5-plus
+temperature: 0.3
+---
+
+$system$
+
+## 角色
+你是内容制作需求分析专家,擅长从内容树节点结构中归纳出有价值的图文内容制作需求。
+
+## 工作流程
+
+### 第一步:获取节点局部结构
+给定一个内容树节点 id(category 或 element),调用 `search_content_tree` 或 `get_category_tree` 获取:
+- **祖先路径**(`include_ancestors=true`):了解该节点的上下文和所属维度
+- **同级节点**:搜索同名关键词或父节点的直接子节点,了解同类
+- **子孙节点**(`descendant_depth=2`):了解该节点的细分方向
+
+### 第二步:判断与图文制作的相关性
+结合节点的名称、描述、所属维度(实质/形式/意图),判断哪些节点与**图文内容制作**直接相关:
+- **保留**:与视觉呈现、角色设计、场景构图、风格表达、情感传达等制作行为直接相关的节点
+- **过滤**:纯语义/主题分类节点(如"节日"、"品牌"等不涉及制作手法的节点)
+
+### 第四步:归纳制作需求
+对每组相关节点,归纳出若干条制作能力或工具需求:
+- **粒度适中**:不能太细("生成猫咪"),也不能太粗("生成图像")
+- **正确示例**:"需要能够生成保持角色一致性的人物图像的能力"
+- **同批需求不重叠**:不同需求应覆盖不同的制作维度,而且最好是对应到不同的工具
+
+### 第五步:输出结构化需求
+将归纳结果写入 `%output_dir%/requirements.md`,每条需求包含:
+- 需求描述(自然语言)
+- 来源节点 id 列表
+- 相关频繁项集 id(若有)
+- 所属维度(实质/形式/意图)
+
+$user$
+请对以下内容树节点进行制作需求归纳分析:
+
+stable_id:334
+source_type:形式
+
+请按照工作流程,逐步分析该节点及其周边结构,最终将结构化的制作需求列表输出到 %output_dir%/requirements.md。
+注意分析出来的需求不可以彼此之间有显著重叠;最好是有所区分的不同能力、需要不同工具支撑的能力。

+ 34 - 0
examples/content_tree_analyst/config.py

@@ -0,0 +1,34 @@
+"""
+项目配置
+"""
+
+from agent.core.runner import RunConfig
+from agent.tools.builtin.knowledge import KnowledgeConfig
+
+
+RUN_CONFIG = RunConfig(
+    model="qwen3.5-plus",
+    temperature=0.3,
+    max_iterations=500,
+    extra_llm_params={"extra_body": {"enable_thinking": True}},
+    agent_type="analyst",
+    name="内容树需求归纳",
+
+    knowledge=KnowledgeConfig(
+        enable_extraction=False,
+        enable_completion_extraction=False,
+        enable_injection=False,
+        owner="sunlit.howard@gmail.com",
+        default_tags={"project": "gen_query_from_content_tree"},
+        default_scopes=["org:cybertogether"],
+        # default_search_types=["strategy"],
+        default_search_owner="sunlit.howard@gmail.com",
+    ),
+)
+
+OUTPUT_DIR = "examples/content_tree_analyst/output"
+SKILLS_DIR = "./skills"
+TRACE_STORE_PATH = ".trace"
+DEBUG = True
+LOG_LEVEL = "INFO"
+LOG_FILE = None

+ 39 - 0
examples/content_tree_analyst/docs/requirements.md

@@ -0,0 +1,39 @@
+任务1:从内容树节点归纳制作需求                                                                      
+                                                                                               
+  - 输入:内容树中某个节点的 id(category 或 element)
+  - 过程:                                
+    a. 调用搜索 API
+  获取该节点的局部结构:祖先路径(了解上下文)、同级节点(了解同类)、子孙节点(了解细分)             
+    b. 结合节点的名称、描述、所属维度(实质/形式/意图),判断哪些节点与图文内容制作直接相关(过滤掉与制
+  作无关的纯语义/主题类节点)                                                                          
+    c. 对筛选出的重要节点,进一步获取其关联的频繁项集(通过get_frequent_itemsets接口,每次传入一个category_ids;频繁项集中可以看到经常与指定节点在优质内容中共同出现的要素,比如能给“动作姿态”节点扩展到“夸张”“运动”等关联要素,从而能提出更准的需求)
+    d. 对每一组节点,归纳出一个制作能力或工具需求(如"需要能够生成保持角色一致性的人物图像的能力")
+  - 输出:若干条结构化的制作需求,每条包含:  
+    - 需求描述(自然语言)                
+    - 来源节点 id 列表(支撑这条需求的节点)
+    - 相关频繁项集id(若有)
+    - 所属维度(实质/形式/意图)                                                                       
+  - 约束:                                
+    - 需求粒度不能太细(不是"生成猫咪"),也不能太粗(不是"生成图像")                                 
+    - 同一批输出的需求之间应尽量不重叠                                                                 
+                                          
+  ---                                                                                                  
+  任务2:从制作能力/工具关联内容树节点                                                                 
+   
+  - 输入:一条制作能力或工具知识(包含名称、描述、适用场景)                                           
+  - 过程:                                                        
+    a. 从知识的名称和适用场景中提取关键词,调用搜索 API 在内容树中检索相关节点
+    b. 对返回的节点按相关度评分,过滤掉低相关节点
+    c. 对保留的节点,标注关联类型:
+        - 直接关联:该能力/工具直接用于制作包含此节点的内容
+        - 间接关联:该能力/工具是制作此类内容的前置步骤或辅助手段
+    d. 将 节点 id - 知识 id - 关联类型 - 置信度 写入映射表
+  - 输出:一批映射记录,每条包含:
+    - 节点 id + 节点名称                      
+    - 知识 id                             
+    - 关联类型(直接/间接)
+    - 置信度(高/中/低)                                                                               
+  - 约束:
+    - 低置信度的映射需标记,待后续实验验证后升级或删除                                                 
+    - 同一个节点可以关联多条知识,需支持后续按置信度排序检索      
+                                          

+ 740 - 0
examples/content_tree_analyst/docs/内容树API.md

@@ -0,0 +1,740 @@
+# 搜索 API 文档
+
+本文档包含三个搜索相关的 API 接口:
+
+1. **关键词搜索** - 根据关键词搜索分类和元素
+2. **获取分类树** - 获取指定分类的完整路径和子树
+3. **获取全量元素** - 分页浏览元素,支持排序和筛选
+
+---
+
+## 接口 1:关键词搜索
+
+### 接口地址
+
+```
+GET http://8.147.104.190:8001/api/search
+```
+
+### 功能说明
+
+搜索全局分类库中的分类(Category)和元素(Element),支持文本匹配、上下文扩展、平台筛选等功能。
+
+## 请求参数
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| q | string | ✓ | - | 搜索关键词 |
+| source_type | string | ✓ | - | 元素类型:`实质` / `形式` / `意图` |
+| entity_type | string | ✗ | all | 搜索对象类型:`category`(分类)/ `element`(元素)/ `all`(全部) |
+| top_k | integer | ✗ | 20 | 返回结果数量,范围 1-100 |
+| use_description | boolean | ✗ | false | 是否在描述字段中搜索(true=搜索名称+描述,false=仅搜索名称) |
+| mode | string | ✗ | text | 搜索模式:`text`(文本匹配)/ `vector`(向量搜索)/ `hybrid`(混合),当前仅支持 text |
+| include_ancestors | boolean | ✗ | false | 是否返回祖先路径(从根节点到父节点的完整路径) |
+| descendant_depth | integer | ✗ | 0 | 返回 N 代以内的子孙节点,0=不返回,1=直接子节点,2=子节点+孙节点... |
+| platform | string | ✗ | null | 按平台筛选,仅对元素有效(如:`小红书`、`抖音`、`微博` 等) |
+
+## 返回格式
+
+```json
+{
+  "success": true,
+  "query": "搜索关键词",
+  "source_type": "实质",
+  "entity_type": "all",
+  "count": 3,
+  "results": [
+    {
+      "entity_type": "category",
+      "entity_id": 123,
+      "stable_id": 456,
+      "name": "分类名称",
+      "description": "分类描述",
+      "path": "/一级分类/二级分类",
+      "category_nature": "领域层级",
+      "level": 2,
+      "score": 0.95,
+      "scores": {
+        "text": 0.95,
+        "vector": 0.0
+      },
+      "ancestors": [...],
+      "descendants": [...]
+    },
+    {
+      "entity_type": "element",
+      "entity_id": 789,
+      "name": "元素名称",
+      "description": "元素描述",
+      "belong_category_stable_id": 456,
+      "occurrence_count": 25,
+      "score": 0.88,
+      "scores": {
+        "text": 0.88,
+        "vector": 0.0
+      }
+    }
+  ]
+}
+```
+
+### 返回字段说明
+
+#### 通用字段
+- `success`: 请求是否成功
+- `query`: 搜索关键词
+- `source_type`: 元素类型
+- `entity_type`: 搜索对象类型
+- `count`: 返回结果数量
+- `results`: 结果列表
+
+#### 结果对象字段(分类 Category)
+- `entity_type`: 固定为 "category"
+- `entity_id`: 分类数据库 ID
+- `stable_id`: 分类稳定 ID(用于跨版本引用)
+- `name`: 分类名称
+- `description`: 分类描述
+- `path`: 分类路径(如 `/主体/角色类型/人物角色`)
+- `category_nature`: 分类性质(领域层级/元描述层级)
+- `level`: 层级深度(1=根节点)
+- `score`: 综合相似度分数(0-1)
+- `scores`: 各维度分数
+- `ancestors`: 祖先路径(当 `include_ancestors=true` 时返回)
+- `descendants`: 子孙节点(当 `descendant_depth>0` 时返回)
+
+#### 结果对象字段(元素 Element)
+- `entity_type`: 固定为 "element"
+- `entity_id`: 元素数据库 ID
+- `name`: 元素名称
+- `description`: 元素描述
+- `belong_category_stable_id`: 所属分类的 stable_id
+- `occurrence_count`: 出现次数
+- `score`: 综合相似度分数(0-1)
+- `scores`: 各维度分数
+
+#### ancestors 字段结构
+```json
+[
+  {
+    "stable_id": 1,
+    "name": "主体",
+    "level": 1
+  },
+  {
+    "stable_id": 10,
+    "name": "角色类型",
+    "level": 2
+  }
+]
+```
+
+#### descendants 字段结构
+```json
+[
+  {
+    "stable_id": 500,
+    "name": "人物角色",
+    "level": 3,
+    "depth_from_parent": 1,
+    "is_leaf": false
+  },
+  {
+    "stable_id": 501,
+    "name": "动物角色",
+    "level": 3,
+    "depth_from_parent": 1,
+    "is_leaf": true
+  }
+]
+```
+
+---
+
+## 使用示例
+
+### 1. 基础搜索
+
+#### 搜索所有(分类+元素)
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=角色" \
+  --data-urlencode "source_type=实质"
+```
+
+#### 只搜索分类
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=主体" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "entity_type=category"
+```
+
+#### 只搜索元素
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=猫咪" \
+  --data-urlencode "source_type=形式" \
+  --data-urlencode "entity_type=element"
+```
+
+#### 限制返回数量
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=角色" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "top_k=5"
+```
+
+### 2. 扩展搜索范围
+
+#### 搜索名称+描述
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=拟人" \
+  --data-urlencode "source_type=形式" \
+  --data-urlencode "use_description=true"
+```
+
+### 3. 获取上下文信息
+
+#### 返回祖先路径
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=人物角色" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "entity_type=category" \
+  --data-urlencode "include_ancestors=true"
+```
+
+#### 返回1代子孙节点
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=主体" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "entity_type=category" \
+  --data-urlencode "descendant_depth=1"
+```
+
+#### 返回2代子孙节点
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=主体" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "entity_type=category" \
+  --data-urlencode "descendant_depth=2"
+```
+
+#### 同时返回祖先+子孙
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=角色类型" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "entity_type=category" \
+  --data-urlencode "include_ancestors=true" \
+  --data-urlencode "descendant_depth=2"
+```
+
+### 4. 平台筛选
+
+#### 只搜索小红书平台的元素
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=角色" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "entity_type=element" \
+  --data-urlencode "platform=小红书"
+```
+
+#### 搜索抖音平台的元素
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=主体" \
+  --data-urlencode "source_type=形式" \
+  --data-urlencode "platform=抖音"
+```
+
+#### 平台筛选+描述搜索
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=拟人" \
+  --data-urlencode "source_type=形式" \
+  --data-urlencode "platform=小红书" \
+  --data-urlencode "use_description=true"
+```
+
+### 5. 组合查询
+
+#### 全功能组合
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=角色" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "entity_type=all" \
+  --data-urlencode "top_k=10" \
+  --data-urlencode "use_description=true" \
+  --data-urlencode "include_ancestors=true" \
+  --data-urlencode "descendant_depth=1" \
+  --data-urlencode "platform=小红书"
+```
+
+#### 不同 source_type 的搜索
+```bash
+# 搜索意图维度
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=情感" \
+  --data-urlencode "source_type=意图"
+
+# 搜索形式维度
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=视觉" \
+  --data-urlencode "source_type=形式"
+```
+
+### 6. 浏览器直接访问
+
+```
+http://8.147.104.190:8001/api/search?q=角色&source_type=实质
+http://8.147.104.190:8001/api/search?q=主体&source_type=实质&entity_type=category&include_ancestors=true&descendant_depth=2
+http://8.147.104.190:8001/api/search?q=猫咪&source_type=形式&platform=小红书
+```
+
+### 7. FastAPI 交互式文档
+
+访问以下地址可以在网页上直接测试 API:
+
+```
+http://8.147.104.190:8001/docs
+```
+
+在文档页面找到 `/api/search` 接口,点击 "Try it out" 按钮,填写参数后点击 "Execute" 即可测试。
+
+---
+
+## 返回示例
+
+### 示例 1:基础搜索
+
+**请求:**
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=角色" \
+  --data-urlencode "source_type=实质"
+```
+
+**响应:**
+```json
+{
+  "success": true,
+  "query": "角色",
+  "source_type": "实质",
+  "entity_type": "all",
+  "count": 2,
+  "results": [
+    {
+      "entity_type": "category",
+      "entity_id": 123,
+      "stable_id": 456,
+      "name": "角色类型",
+      "description": "内容主体的角色分类",
+      "path": "/主体/角色类型",
+      "category_nature": "领域层级",
+      "level": 2,
+      "score": 0.95,
+      "scores": {
+        "text": 0.95,
+        "vector": 0.0
+      }
+    },
+    {
+      "entity_type": "element",
+      "entity_id": 789,
+      "name": "人物角色",
+      "description": "真实或虚拟的人物形象",
+      "belong_category_stable_id": 456,
+      "occurrence_count": 25,
+      "score": 0.88,
+      "scores": {
+        "text": 0.88,
+        "vector": 0.0
+      }
+    }
+  ]
+}
+```
+
+### 示例 2:带祖先和子孙的搜索
+
+**请求:**
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=角色类型" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "entity_type=category" \
+  --data-urlencode "include_ancestors=true" \
+  --data-urlencode "descendant_depth=2"
+```
+
+**响应:**
+```json
+{
+  "success": true,
+  "query": "角色类型",
+  "source_type": "实质",
+  "entity_type": "category",
+  "count": 1,
+  "results": [
+    {
+      "entity_type": "category",
+      "entity_id": 123,
+      "stable_id": 456,
+      "name": "角色类型",
+      "description": "内容主体的角色分类",
+      "path": "/主体/角色类型",
+      "category_nature": "领域层级",
+      "level": 2,
+      "score": 1.0,
+      "scores": {
+        "text": 1.0,
+        "vector": 0.0
+      },
+      "ancestors": [
+        {
+          "stable_id": 1,
+          "name": "主体",
+          "level": 1
+        }
+      ],
+      "descendants": [
+        {
+          "stable_id": 500,
+          "name": "人物角色",
+          "level": 3,
+          "depth_from_parent": 1,
+          "is_leaf": false
+        },
+        {
+          "stable_id": 501,
+          "name": "动物角色",
+          "level": 3,
+          "depth_from_parent": 1,
+          "is_leaf": false
+        },
+        {
+          "stable_id": 600,
+          "name": "真人角色",
+          "level": 4,
+          "depth_from_parent": 2,
+          "is_leaf": true
+        },
+        {
+          "stable_id": 601,
+          "name": "虚拟角色",
+          "level": 4,
+          "depth_from_parent": 2,
+          "is_leaf": true
+        }
+      ]
+    }
+  ]
+}
+```
+
+### 示例 3:平台筛选
+
+**请求:**
+```bash
+curl -G "http://8.147.104.190:8001/api/search" \
+  --data-urlencode "q=猫咪" \
+  --data-urlencode "source_type=形式" \
+  --data-urlencode "entity_type=element" \
+  --data-urlencode "platform=小红书"
+```
+
+**响应:**
+```json
+{
+  "success": true,
+  "query": "猫咪",
+  "source_type": "形式",
+  "entity_type": "element",
+  "count": 3,
+  "results": [
+    {
+      "entity_type": "element",
+      "entity_id": 1001,
+      "name": "猫咪",
+      "description": "可爱的猫咪形象",
+      "belong_category_stable_id": 200,
+      "occurrence_count": 15,
+      "score": 1.0,
+      "scores": {
+        "text": 1.0,
+        "vector": 0.0
+      }
+    },
+    {
+      "entity_type": "element",
+      "entity_id": 1002,
+      "name": "猫咪拟人",
+      "description": "拟人化的猫咪角色",
+      "belong_category_stable_id": 201,
+      "occurrence_count": 8,
+      "score": 0.85,
+      "scores": {
+        "text": 0.85,
+        "vector": 0.0
+      }
+    }
+  ]
+}
+```
+
+---
+
+## 错误处理
+
+### 错误响应格式
+
+```json
+{
+  "detail": "错误信息描述"
+}
+```
+
+### 常见错误
+
+| HTTP 状态码 | 错误原因 | 解决方法 |
+|------------|---------|---------|
+| 400 | 缺少必填参数(q 或 source_type) | 检查请求参数 |
+| 400 | source_type 值不合法 | 使用 `实质`、`形式` 或 `意图` |
+| 400 | entity_type 值不合法 | 使用 `category`、`element` 或 `all` |
+| 400 | mode 不支持 | 当前仅支持 `text` 模式 |
+| 500 | 服务器内部错误 | 联系技术支持 |
+
+---
+
+## 注意事项
+
+1. **中文参数编码**:使用 curl 时,中文参数需要用 `--data-urlencode` 进行 URL 编码
+2. **平台筛选限制**:`platform` 参数仅对元素(element)有效,对分类(category)无效
+3. **性能考虑**:
+   - `use_description=true` 会增加搜索范围,可能降低精确度
+   - `descendant_depth` 越大,返回数据越多,响应时间越长
+   - 建议 `top_k` 不超过 50
+4. **搜索模式**:当前仅支持 `text` 模式,`vector` 和 `hybrid` 模式将在后续版本中支持
+5. **平台名称**:`platform` 参数值需要与数据库中的平台名称完全一致(区分大小写)
+
+---
+
+## 接口 2:获取分类树
+
+### 接口地址
+
+```
+GET http://8.147.104.190:8001/api/search/category/{stable_id}
+```
+
+### 功能说明
+
+获取指定分类的完整路径和子树结构,用于导航和展示分类层级关系。
+
+### 请求参数
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| stable_id | integer | ✓ | - | 分类的 stable_id(路径参数) |
+| source_type | string | ✓ | - | 元素类型:`实质` / `形式` / `意图` |
+| include_ancestors | boolean | ✗ | true | 是否返回祖先路径(从根节点到当前节点) |
+| descendant_depth | integer | ✗ | -1 | 返回子孙深度,-1=全部,0=仅当前节点,1=子节点,2=子+孙... |
+
+### 返回格式
+
+```json
+{
+  "success": true,
+  "current": {
+    "stable_id": 125,
+    "name": "吉祥用语",
+    "description": "通用的吉祥话语和祝福词句",
+    "path": "/表象/符号/表达符号/祝福语/吉祥用语",
+    "level": 5
+  },
+  "ancestors": [
+    {
+      "stable_id": 38,
+      "name": "表象",
+      "path": "/表象",
+      "level": 1
+    },
+    {
+      "stable_id": 40,
+      "name": "符号",
+      "path": "/表象/符号",
+      "level": 2
+    }
+  ],
+  "descendants": [
+    {
+      "stable_id": 126,
+      "name": "新年祝福",
+      "description": "新年相关的祝福语",
+      "path": "/表象/符号/表达符号/祝福语/吉祥用语/新年祝福",
+      "level": 6,
+      "children": []
+    }
+  ]
+}
+```
+
+### 使用示例
+
+#### 获取分类的完整路径和所有子孙
+```bash
+curl -G "http://8.147.104.190:8001/api/search/category/125" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "include_ancestors=true" \
+  --data-urlencode "descendant_depth=-1"
+```
+
+#### 只获取当前节点和祖先路径
+```bash
+curl -G "http://8.147.104.190:8001/api/search/category/125" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "include_ancestors=true" \
+  --data-urlencode "descendant_depth=0"
+```
+
+#### 获取2代子孙节点
+```bash
+curl -G "http://8.147.104.190:8001/api/search/category/125" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "descendant_depth=2"
+```
+
+---
+
+## 接口 3:获取全量元素数据
+
+### 接口地址
+
+```
+GET http://8.147.104.190:8001/api/search/elements
+```
+
+### 功能说明
+
+获取全量元素数据,支持分页、排序、筛选。主要用于获取高频元素、按分类浏览元素等场景。
+
+### 请求参数
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| source_type | string | ✓ | - | 元素类型:`实质` / `形式` / `意图` |
+| page | integer | ✗ | 1 | 页码(从1开始) |
+| page_size | integer | ✗ | 50 | 每页数量(1-200) |
+| sort_by | string | ✗ | occurrence_count | 排序字段:`occurrence_count` / `name` / `id` |
+| order | string | ✗ | desc | 排序方向:`asc` / `desc` |
+| category_stable_id | integer | ✗ | null | 按分类筛选(可选) |
+| platform | string | ✗ | null | 按平台筛选(可选) |
+| min_occurrence | integer | ✗ | null | 最小出现次数(可选) |
+
+### 返回格式
+
+```json
+{
+  "success": true,
+  "source_type": "实质",
+  "page": 1,
+  "page_size": 50,
+  "total": 4082,
+  "total_pages": 82,
+  "results": [
+    {
+      "id": 45,
+      "name": "祝福语",
+      "description": "'吉祥如意'、'幸福安康'等文字内容",
+      "occurrence_count": 974,
+      "element_sub_type": "具象概念",
+      "category": {
+        "stable_id": 125,
+        "name": "吉祥用语",
+        "path": "/表象/符号/表达符号/祝福语/吉祥用语"
+      }
+    }
+  ]
+}
+```
+
+### 使用示例
+
+#### 获取高频元素前50(默认)
+```bash
+curl -G "http://8.147.104.190:8001/api/search/elements" \
+  --data-urlencode "source_type=实质"
+```
+
+#### 获取高频元素前10
+```bash
+curl -G "http://8.147.104.190:8001/api/search/elements" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "page=1" \
+  --data-urlencode "page_size=10" \
+  --data-urlencode "sort_by=occurrence_count" \
+  --data-urlencode "order=desc"
+```
+
+#### 按名称排序
+```bash
+curl -G "http://8.147.104.190:8001/api/search/elements" \
+  --data-urlencode "source_type=形式" \
+  --data-urlencode "sort_by=name" \
+  --data-urlencode "order=asc"
+```
+
+#### 筛选指定分类下的元素
+```bash
+curl -G "http://8.147.104.190:8001/api/search/elements" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "category_stable_id=125"
+```
+
+#### 筛选出现次数>=10的元素
+```bash
+curl -G "http://8.147.104.190:8001/api/search/elements" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "min_occurrence=10"
+```
+
+#### 按平台筛选元素
+```bash
+curl -G "http://8.147.104.190:8001/api/search/elements" \
+  --data-urlencode "source_type=形式" \
+  --data-urlencode "platform=小红书"
+```
+
+#### 组合筛选:指定分类+最小出现次数
+```bash
+curl -G "http://8.147.104.190:8001/api/search/elements" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "category_stable_id=125" \
+  --data-urlencode "min_occurrence=50" \
+  --data-urlencode "page_size=20"
+```
+
+#### 分页浏览
+```bash
+# 第1页
+curl -G "http://8.147.104.190:8001/api/search/elements" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "page=1" \
+  --data-urlencode "page_size=50"
+
+# 第2页
+curl -G "http://8.147.104.190:8001/api/search/elements" \
+  --data-urlencode "source_type=实质" \
+  --data-urlencode "page=2" \
+  --data-urlencode "page_size=50"
+```
+
+---
+

+ 15 - 0
examples/content_tree_analyst/docs/频繁项集API.md

@@ -0,0 +1,15 @@
+  curl 'https://pattern.aiddit.com/api/pattern/tools/get_frequent_itemsets/execute' \
+  -H 'Accept: */*' \
+  -H 'Accept-Language: zh-CN,zh;q=0.9' \
+  -H 'Connection: keep-alive' \
+  -H 'Content-Type: application/json' \
+  -H 'Origin: https://pattern.aiddit.com' \
+  -H 'Referer: https://pattern.aiddit.com/execution/33' \
+  -H 'Sec-Fetch-Dest: empty' \
+  -H 'Sec-Fetch-Mode: cors' \
+  -H 'Sec-Fetch-Site: same-origin' \
+  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36' \
+  -H 'sec-ch-ua: "Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"' \
+  -H 'sec-ch-ua-mobile: ?0' \
+  -H 'sec-ch-ua-platform: "macOS"' \
+  --data-raw '{"execution_id":33,"args":{"top_n":20,"category_ids":[378],"sort_by":"absolute_support"}}'

+ 9 - 0
examples/content_tree_analyst/presets.json

@@ -0,0 +1,9 @@
+{
+  "analyst": {
+    "system_prompt_file": "analyst.prompt",
+    "max_iterations": 200,
+    "temperature": 0.3,
+    "skills": ["planning"],
+    "description": "内容树需求归纳 Agent"
+  }
+}

+ 221 - 0
examples/content_tree_analyst/run.py

@@ -0,0 +1,221 @@
+"""
+内容树需求归纳 Agent
+
+从内容树节点归纳制作需求(任务1)。
+
+用法:
+  python run.py                    # 直接运行(任务在 analyst.prompt 中配置)
+  python run.py --trace <TRACE_ID> # 恢复已有 trace
+"""
+
+import argparse
+import os
+import sys
+import asyncio
+from pathlib import Path
+
+os.environ.setdefault("no_proxy", "*")
+
+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.trace import FileSystemTraceStore, Trace, Message
+from agent.llm import create_qwen_llm_call
+from agent.cli import InteractiveController
+from agent.utils import setup_logging
+
+# 注册自定义工具
+from tools.content_tree import search_content_tree, get_category_tree
+from tools.frequent_itemsets import get_frequent_itemsets
+
+from config import RUN_CONFIG, SKILLS_DIR, TRACE_STORE_PATH, DEBUG, LOG_LEVEL, LOG_FILE, OUTPUT_DIR
+
+
+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
+    output_dir = project_root / OUTPUT_DIR
+    output_dir.mkdir(parents=True, exist_ok=True)
+
+    setup_logging(level=LOG_LEVEL, file=LOG_FILE)
+
+    # 加载 presets
+    presets_path = base_dir / "presets.json"
+    if presets_path.exists():
+        from agent.core.presets import load_presets_from_json
+        load_presets_from_json(str(presets_path))
+        print("已加载 presets")
+
+    # 构建任务消息(直接从 analyst.prompt 加载,无变量替换)
+    if not args.trace:
+        prompt = SimplePrompt(base_dir / "analyst.prompt")
+        messages = prompt.build_messages(output_dir=OUTPUT_DIR)
+    else:
+        messages = None
+
+    # 创建 Runner
+    store = FileSystemTraceStore(base_path=TRACE_STORE_PATH)
+    runner = AgentRunner(
+        trace_store=store,
+        llm_call=create_qwen_llm_call(model=RUN_CONFIG.model),
+        skills_dir=SKILLS_DIR,
+        debug=DEBUG,
+    )
+
+    interactive = InteractiveController(runner=runner, store=store, enable_stdin_check=True)
+    runner.stdin_check = interactive.check_stdin
+
+    print("=" * 60)
+    print("内容树需求归纳 Agent")
+    print("=" * 60)
+    print("💡 输入 'p' 暂停,'q' 退出")
+    print("=" * 60)
+
+    run_config = RUN_CONFIG
+    resume_trace_id = args.trace
+    current_trace_id = resume_trace_id
+    current_sequence = 0
+    should_exit = False
+    final_response = ""
+
+    try:
+        if resume_trace_id:
+            existing = await store.get_trace(resume_trace_id)
+            if not existing:
+                print(f"错误: Trace 不存在: {resume_trace_id}")
+                sys.exit(1)
+            run_config.trace_id = resume_trace_id
+            print(f"恢复 Trace: {resume_trace_id[:8]}...")
+
+        while not should_exit:
+            if current_trace_id:
+                run_config.trace_id = current_trace_id
+
+            # 恢复模式:先进交互菜单
+            if current_trace_id and messages is None:
+                check_trace = await store.get_trace(current_trace_id)
+                if check_trace:
+                    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_msgs = menu_result.get("messages", [])
+                        messages = new_msgs if new_msgs else []
+                        run_config.after_sequence = menu_result.get("after_sequence")
+                        continue
+                break
+
+            if messages is None:
+                messages = []
+
+            print("▶️ 开始执行...")
+            paused = False
+
+            try:
+                async for item in runner.run(messages=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_msgs = menu_result.get("messages", [])
+                            messages = new_msgs if new_msgs else []
+                            run_config.after_sequence = menu_result.get("after_sequence")
+                            paused = True
+                            break
+                    elif cmd == "quit":
+                        print("\n🛑 停止...")
+                        if current_trace_id:
+                            await runner.stop(current_trace_id)
+                        should_exit = True
+                        break
+
+                    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] ✅ 完成 | messages={item.total_messages} | cost=${item.total_cost:.4f}")
+                        elif item.status == "failed":
+                            print(f"\n[Trace] ❌ 失败: {item.error_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]\n{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 = content.get("tool_name", "unknown") if isinstance(content, dict) else "unknown"
+                            desc = item.description or ""
+                            if desc and desc != tool_name:
+                                print(f"[Tool] ✅ {tool_name}: {desc[:80]}")
+                            else:
+                                print(f"[Tool] ✅ {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
+
+            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_msgs = menu_result.get("messages", [])
+                    messages = new_msgs if new_msgs else []
+                    run_config.after_sequence = menu_result.get("after_sequence")
+                    continue
+            break
+
+    except KeyboardInterrupt:
+        print("\n用户中断 (Ctrl+C)")
+        if current_trace_id:
+            await runner.stop(current_trace_id)
+
+    # 保存最终结果
+    if final_response:
+        result_file = output_dir / "result.txt"
+        result_file.write_text(final_response, encoding="utf-8")
+        print(f"\n✓ 结果已保存: {result_file}")
+
+    if current_trace_id:
+        print(f"\nTrace ID: {current_trace_id}")
+        print("可视化: python3 api_server.py → http://localhost:8000/api/traces")
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 1 - 0
examples/content_tree_analyst/tools/__init__.py

@@ -0,0 +1 @@
+# tools/__init__.py

+ 167 - 0
examples/content_tree_analyst/tools/content_tree.py

@@ -0,0 +1,167 @@
+"""
+内容树 API 工具
+
+封装内容树搜索接口:
+1. search_content_tree - 关键词搜索分类和元素
+2. get_category_tree - 获取指定分类的完整路径和子树
+"""
+
+import logging
+from typing import Optional
+
+import httpx
+
+from agent.tools import tool
+from agent.tools.models import ToolResult
+
+logger = logging.getLogger(__name__)
+
+BASE_URL = "http://8.147.104.190:8001"
+
+
+@tool(description="在内容树中搜索分类(category)和元素(element),支持获取祖先路径和子孙节点")
+async def search_content_tree(
+    q: str,
+    source_type: str,
+    entity_type: str = "all",
+    top_k: int = 20,
+    use_description: bool = False,
+    include_ancestors: bool = False,
+    descendant_depth: int = 0,
+) -> ToolResult:
+    """
+    关键词搜索内容树中的分类和元素。
+
+    Args:
+        q: 搜索关键词
+        source_type: 维度,必须是 "实质" / "形式" / "意图" 之一
+        entity_type: 搜索对象类型,"category" / "element" / "all"(默认)
+        top_k: 返回结果数量,1-100(默认20)
+        use_description: 是否同时搜索描述字段(默认仅搜索名称)
+        include_ancestors: 是否返回祖先路径
+        descendant_depth: 返回子孙节点深度,0=不返回,1=直接子节点,2=子+孙...
+    """
+    params = {
+        "q": q,
+        "source_type": source_type,
+        "entity_type": entity_type,
+        "top_k": top_k,
+        "use_description": str(use_description).lower(),
+        "include_ancestors": str(include_ancestors).lower(),
+        "descendant_depth": descendant_depth,
+    }
+
+    try:
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            resp = await client.get(f"{BASE_URL}/api/search", params=params)
+            resp.raise_for_status()
+            data = resp.json()
+
+        count = data.get("count", 0)
+        results = data.get("results", [])
+
+        # 格式化输出
+        lines = [f"搜索「{q}」({source_type}维度)共找到 {count} 条结果:\n"]
+        for r in results:
+            etype = r.get("entity_type", "")
+            name = r.get("name", "")
+            score = r.get("score", 0)
+            if etype == "category":
+                sid = r.get("stable_id", "")
+                path = r.get("path", "")
+                desc = r.get("description", "")
+                lines.append(f"[分类] stable_id={sid} | {path} | score={score:.2f}")
+                if desc:
+                    lines.append(f"  描述: {desc}")
+                ancestors = r.get("ancestors", [])
+                if ancestors:
+                    anc_names = " > ".join(a["name"] for a in ancestors)
+                    lines.append(f"  祖先: {anc_names}")
+                descendants = r.get("descendants", [])
+                if descendants:
+                    desc_names = ", ".join(d["name"] for d in descendants[:10])
+                    lines.append(f"  子孙({len(descendants)}): {desc_names}")
+            else:
+                eid = r.get("entity_id", "")
+                belong = r.get("belong_category_stable_id", "")
+                occ = r.get("occurrence_count", 0)
+                lines.append(f"[元素] entity_id={eid} | {name} | belong_category={belong} | 出现次数={occ} | score={score:.2f}")
+                edesc = r.get("description", "")
+                if edesc:
+                    lines.append(f"  描述: {edesc}")
+            lines.append("")
+
+        return ToolResult(
+            title=f"内容树搜索: {q} ({source_type}) → {count} 条",
+            output="\n".join(lines),
+        )
+
+    except httpx.HTTPError as e:
+        return ToolResult(title="内容树搜索失败", output=f"HTTP 错误: {e}")
+    except Exception as e:
+        logger.exception("search_content_tree error")
+        return ToolResult(title="内容树搜索失败", output=f"错误: {e}")
+
+
+@tool(description="获取指定分类节点的完整路径、祖先和子孙结构(通过 stable_id 精确查询)")
+async def get_category_tree(
+    stable_id: int,
+    source_type: str,
+    include_ancestors: bool = True,
+    descendant_depth: int = -1,
+) -> ToolResult:
+    """
+    获取指定分类的完整路径和子树结构。
+
+    Args:
+        stable_id: 分类的 stable_id
+        source_type: 维度,"实质" / "形式" / "意图"
+        include_ancestors: 是否返回祖先路径(默认 True)
+        descendant_depth: 子孙深度,-1=全部,0=仅当前,1=子节点,2=子+孙...
+    """
+    params = {
+        "source_type": source_type,
+        "include_ancestors": str(include_ancestors).lower(),
+        "descendant_depth": descendant_depth,
+    }
+
+    try:
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            resp = await client.get(f"{BASE_URL}/api/search/category/{stable_id}", params=params)
+            resp.raise_for_status()
+            data = resp.json()
+
+        current = data.get("current", {})
+        ancestors = data.get("ancestors", [])
+        descendants = data.get("descendants", [])
+
+        lines = []
+        lines.append(f"分类节点: {current.get('name', '')} (stable_id={stable_id})")
+        lines.append(f"路径: {current.get('path', '')}")
+        if current.get("description"):
+            lines.append(f"描述: {current['description']}")
+        lines.append("")
+
+        if ancestors:
+            lines.append("祖先路径:")
+            for a in ancestors:
+                lines.append(f"  L{a.get('level', '?')} {a.get('name', '')} (stable_id={a.get('stable_id', '')})")
+            lines.append("")
+
+        if descendants:
+            lines.append(f"子孙节点 ({len(descendants)} 个):")
+            for d in descendants:
+                indent = "  " * d.get("depth_from_parent", 1)
+                leaf_mark = " [叶]" if d.get("is_leaf") else ""
+                lines.append(f"{indent}L{d.get('level', '?')} {d.get('name', '')} (stable_id={d.get('stable_id', '')}){leaf_mark}")
+
+        return ToolResult(
+            title=f"分类树: {current.get('name', stable_id)} (stable_id={stable_id})",
+            output="\n".join(lines),
+        )
+
+    except httpx.HTTPError as e:
+        return ToolResult(title="获取分类树失败", output=f"HTTP 错误: {e}")
+    except Exception as e:
+        logger.exception("get_category_tree error")
+        return ToolResult(title="获取分类树失败", output=f"错误: {e}")

+ 99 - 0
examples/content_tree_analyst/tools/frequent_itemsets.py

@@ -0,0 +1,99 @@
+"""
+频繁项集 API 工具
+
+封装 pattern.aiddit.com 的频繁项集接口,用于查询与指定分类节点
+在优质内容中共同出现的关联要素。
+"""
+
+import logging
+
+import httpx
+
+from agent.tools import tool
+from agent.tools.models import ToolResult
+
+logger = logging.getLogger(__name__)
+
+ITEMSETS_URL = "https://pattern.aiddit.com/api/pattern/tools/get_frequent_itemsets/execute"
+
+HEADERS = {
+    "Accept": "*/*",
+    "Accept-Language": "zh-CN,zh;q=0.9",
+    "Connection": "keep-alive",
+    "Content-Type": "application/json",
+    "Origin": "https://pattern.aiddit.com",
+    "Referer": "https://pattern.aiddit.com/execution/33",
+    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
+}
+
+
+@tool(description="查询与指定分类节点在优质内容中共同出现的频繁项集(关联要素),用于扩展制作需求的关联维度")
+async def get_frequent_itemsets(
+    entity_ids: list,
+    top_n: int = 20,
+    execution_id: int = 33,
+    sort_by: str = "absolute_support",
+) -> ToolResult:
+    """
+    获取与指定分类节点关联的频繁项集。
+
+    Args:
+        entity_ids: 分类节点的 entity_id 列表(即搜索接口返回的 entity_id 字段,非 stable_id)
+        top_n: 返回前 N 个项集,默认 20
+        execution_id: 执行 ID,默认 33
+        sort_by: 排序字段,默认 "absolute_support"
+    """
+    payload = {
+        "execution_id": execution_id,
+        "args": {
+            "top_n": top_n,
+            "category_ids": entity_ids,
+            "sort_by": sort_by,
+        },
+    }
+
+    try:
+        import json as _json
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            resp = await client.post(ITEMSETS_URL, json=payload, headers=HEADERS)
+            resp.raise_for_status()
+            outer = resp.json()
+
+        # result 字段是 JSON 字符串,需要二次解析
+        data = _json.loads(outer["result"])
+        total = data.get("total", 0)
+        groups = data.get("groups", {})
+
+        # 收集所有 group 下的 itemsets
+        all_itemsets = []
+        for group_key, group in groups.items():
+            for itemset in group.get("itemsets", []):
+                itemset["_group"] = group_key
+                all_itemsets.append(itemset)
+
+        lines = [f"频繁项集查询 entity_ids={entity_ids},共 {total} 条,返回 {len(all_itemsets)} 条:\n"]
+        for i, itemset in enumerate(all_itemsets, 1):
+            itemset_id = itemset.get("id", "")
+            item_count = itemset.get("item_count", "")
+            support = itemset.get("support", 0)
+            abs_support = itemset.get("absolute_support", "")
+            lines.append(f"{i}. 项集ID={itemset_id} | 项数={item_count} | support={support:.4f} | abs={abs_support}")
+            for elem in itemset.get("items", []):
+                dim = elem.get("dimension", "")
+                path = elem.get("category_path", "")
+                ename = elem.get("element_name") or ""
+                label = f"{path}({ename})" if ename else path
+                lines.append(f"   [{dim}] {label}")
+            lines.append("")
+
+        return ToolResult(
+            title=f"频繁项集: entity_ids={entity_ids} → {total} 条",
+            output="\n".join(lines),
+        )
+        return ToolResult(
+            title="频繁项集查询失败",
+            output=f"HTTP {e.response.status_code}: {e.response.text[:200]}",
+        )
+    except Exception as e:
+        logger.exception("get_frequent_itemsets error")
+        return ToolResult(title="频繁项集查询失败", output=f"错误: {e}")