Explorar el Código

chore(demand): checkpoint mysql evidence-pack baseline

SamLee hace 1 mes
padre
commit
9a4bbc08e6

+ 47 - 6
examples/demand/demand.md

@@ -78,11 +78,12 @@ $system$
 
 约束:
 
-- `source_kind` 只能按真实来源填写:`element` / `category` / `co_occurrence` / `pattern_itemset`
-- `source_tool` 必须是支撑本条需求的主要工具名
-- `source_post_id` 必须来自 `get_itemset_detail` 返回的 `post_ids` 或 `get_post_elements` 查询过的帖子;没有明确帖子时不要编造
-- `case_ids` 按 `source_kind` 分组,例如 `{"pattern_itemset": ["post_id"]}`;没有 Case seed 时填空对象或对应空数组
-- `itemset_ids/category_ids/seed_terms` 只能来自工具返回或当前 DemandItem 的真实元素/分类词,不允许凭空补 ID
+- **本次 MVP 最终可写入的 DemandItem 只允许 `source_kind="pattern_itemset"`**。不要把 `element` / `category` / `co_occurrence` 作为最终 `evidence_refs.source_kind`,这些只能用于探索,不能用于创建需求。
+- `source_tool` 必须填写 `get_itemset_detail`,因为最终证据必须来自项集详情。
+- `itemset_ids` 必须来自 `get_frequent_itemsets` 返回的真实项集 ID,并且在创建需求前必须用 `get_itemset_detail` 查询过。
+- `source_post_id` 必须从 `get_itemset_detail` 返回的 `post_ids` 中选择一个真实帖子 ID;没有明确帖子时不要创建该 DemandItem。
+- `case_ids` 固定按 pattern 来源表达,例如 `{"pattern_itemset": ["source_post_id"]}`。
+- `seed_terms` 只能来自 `get_itemset_detail` 返回的 items 中的 `element_name` / `category_path` / 分类名称,或当前 DemandItem 的真实元素/分类词,不允许凭空补 ID 或词。
 
 ## 工具概览
 
@@ -112,7 +113,9 @@ $system$
 - category 级 item 必须来自分类树的真实节点(通过 `search_categories` 查到对应的 `category_id`),不允许凭空编造分类
 - 正确的创建顺序:先 element 后 category
 - result 中出现的每一个具体内容,都必须有对应的 DemandItem
-- 每个 DemandItem 都必须包含 `evidence_refs`;如果证据来自 pattern,必须优先用 `get_itemset_detail` 拿到 `itemset_ids`、`post_ids`,并把一个真实 `post_id` 写入 `source_post_id`
+- 每个 DemandItem 都必须包含 `evidence_refs`,且 `evidence_refs.source_kind` 必须是 `pattern_itemset`
+- 创建 DemandItem 前,必须先调用 `get_frequent_itemsets` 找到候选项集,再调用 `get_itemset_detail` 拿到 `itemset_ids`、`items`、`post_ids`,并把一个真实 `post_id` 写入 `source_post_id`
+- 不允许用 `search_elements` / `search_categories` / 共现查询结果直接创建最终 DemandItem;这些工具只能辅助理解和筛选,最终创建时仍必须绑定到一个经过 `get_itemset_detail` 验证的 itemset
 - `evidence_refs` 是候选引用,不要写 `source_certainty=db_validated` 或 `validation_status=passed`,最终校验由代码完成
 - `search_elements` / `search_categories`只能用于查询单元素/单分类,不能用于查询完整树,完整树查询用`get_category_tree`
 
@@ -131,6 +134,44 @@ $user$
 3. 组合需求,分类的起点必须需要先通过`get_frequent_itemsets`工具,搜索频繁出现的分类组合,根据支持度进行移除和保留
 4. 生成的需求,必须要有实际的含义,要能体现出「%merge_level2%」,要从需求中可以了解到「%merge_level2%」,不符合要求的词,给出理由直接过滤掉
 
+## 本地 exact evidence 输出流程(必须遵守)
+
+本次输出要给下游 ContentFindAgent 做证据链回溯,所以最终需求必须能被代码校验成 `evidence_pack`。
+
+请按以下顺序工作:
+
+1. 先用 `get_weight_score_topn` / 分类树 / 搜索工具理解「%merge_level2%」的高价值元素和分类。
+2. 再用 `get_frequent_itemsets` 查找能支撑这些需求的频繁项集。
+3. 对准备采用的每个项集,必须调用 `get_itemset_detail(itemset_ids=[...])`。
+4. 只从 `get_itemset_detail` 返回的详情中选择需求证据:
+   - `itemset_ids`: 当前项集 ID。
+   - `source_post_id`: 从该项集 `post_ids` 中选择一个真实帖子。
+   - `seed_terms`: 从该项集 `items` 里的分类路径、分类名或元素名提取。
+5. 调用 `create_demand_item` 或 `create_demand_items` 时,每条必须使用如下结构:
+
+```json
+{
+  "element_names": ["需求词或短语"],
+  "reason": "为什么这个需求成立,并说明它来自哪个 itemset 的哪些 items",
+  "desc": "用户希望看到什么内容",
+  "type": "pattern",
+  "evidence_refs": {
+    "source_kind": "pattern_itemset",
+    "source_tool": "get_itemset_detail",
+    "itemset_ids": [123],
+    "source_post_id": "55157577",
+    "case_ids": {
+      "pattern_itemset": ["55157577"]
+    },
+    "seed_terms": ["从 itemset items 中提取的真实词"]
+  }
+}
+```
+
+如果某个候选需求没有可绑定的 `pattern_itemset`、`itemset_ids` 或 `source_post_id`,直接丢弃,不要为了凑数量改用 `element` / `category` / `co_occurrence` 证据。
+
+完成 `create_demand_items` 且工具返回成功后,用一句话总结完成情况即可,不要再调用 `write_execution_summary` 或输出长篇执行报告。
+
 ## 要求
 
 1. 共现查询的地点必须来自于高权重分类,不能直接从树上寻找分类

+ 37 - 3
examples/demand/demand_pattern_tools.py

@@ -45,6 +45,39 @@ def _log_tool_output(tool_name: str, result: str) -> str:
     return result
 
 
+def _normalize_itemset_ids(itemset_ids: Any) -> list[int]:
+    """Normalize LLM tool args into a list of integer itemset ids."""
+    if itemset_ids is None:
+        return []
+    if isinstance(itemset_ids, int):
+        return [itemset_ids]
+    if isinstance(itemset_ids, str):
+        raw = itemset_ids.strip()
+        if not raw:
+            return []
+        try:
+            loaded = json.loads(raw)
+        except json.JSONDecodeError:
+            loaded = [part.strip() for part in raw.split(",") if part.strip()]
+        return _normalize_itemset_ids(loaded)
+    if isinstance(itemset_ids, (list, tuple, set)):
+        normalized: list[int] = []
+        seen: set[int] = set()
+        for value in itemset_ids:
+            try:
+                int_value = int(str(value).strip())
+            except (TypeError, ValueError):
+                continue
+            if int_value not in seen:
+                seen.add(int_value)
+                normalized.append(int_value)
+        return normalized
+    try:
+        return [int(itemset_ids)]
+    except (TypeError, ValueError):
+        return []
+
+
 # ============================================================================
 # 执行 & 配置 & 分类树
 # ============================================================================
@@ -163,11 +196,13 @@ def get_itemset_detail(itemset_ids) -> str:
     Returns:
         项集详情列表的JSON字符串,每项含 id, dimension_mode, target_depth, items, post_ids, absolute_support。
     """
-    if isinstance(itemset_ids, int):
-        itemset_ids = [itemset_ids]
+    itemset_ids = _normalize_itemset_ids(itemset_ids)
     params = {"itemset_ids": itemset_ids}
     _log_tool_input("get_itemset_detail", params)
 
+    if not itemset_ids:
+        return _log_tool_output("get_itemset_detail", "错误: itemset_ids 为空或无法解析")
+
     data = pattern_service.get_itemset_posts(itemset_ids)
     if not data:
         return _log_tool_output("get_itemset_detail", f"未找到 itemset_ids={itemset_ids} 的项集")
@@ -489,4 +524,3 @@ def get_element_co_occurrences(element_names: list, top_n: int = 30,
 if __name__ == '__main__':
     TopicBuildAgentContext.set_execution_id(1)
     print(get_category_tree('实质'))
-

+ 6 - 2
examples/demand/evidence_pack_builder.py

@@ -134,7 +134,7 @@ def _build_evidence_pack(
     if binding_reason:
         return _reject(binding_reason)
 
-    seed_terms = _resolve_seed_terms(evidence_refs, itemset_items)
+    seed_terms = _resolve_seed_terms(evidence_refs, itemset_items, element_bindings)
     seed_reason = _validate_seed_terms(seed_terms, itemset_items, element_bindings)
     if seed_reason:
         return _reject(seed_reason)
@@ -432,10 +432,14 @@ def _validate_element_bindings(
 def _resolve_seed_terms(
         evidence_refs: Mapping[str, Any],
         itemset_items: list[dict[str, Any]],
+        element_bindings: list[dict[str, Any]],
 ) -> list[str]:
     provided = _unique_strings(_normalize_list(evidence_refs.get("seed_terms")))
     if provided:
-        return provided
+        covered_terms = _build_covered_terms(itemset_items, element_bindings)
+        verified = [term for term in provided if _normalize_term(term) in covered_terms]
+        if verified:
+            return verified
 
     derived: list[str] = []
     for item in itemset_items:

+ 12 - 2
examples/demand/run.py

@@ -177,6 +177,15 @@ def register_selected_tools(tool_names: list[str]) -> None:
         importlib.import_module(module_path)
 
 
+def _enabled_tools_for_run(configured_tools: list[str]) -> list[str]:
+    tools = configured_tools.copy()
+    if _is_local_json_mode():
+        # 本地批量输出只需要 DemandItem JSON。长摘要会显著放大最后一轮上下文,
+        # 且不参与下游 CFA 验证契约。
+        tools = [tool_name for tool_name in tools if tool_name != "write_execution_summary"]
+    return tools
+
+
 def _join_element_names_to_name(element_names: object) -> str:
     """把 tool 入参 element_names 转成 demand_content.name(逗号分隔)。"""
     if element_names is None:
@@ -824,7 +833,8 @@ async def run_once(
     output_dir.mkdir(parents=True, exist_ok=True)
 
     setup_logging(level=LOG_LEVEL, file=LOG_FILE)
-    register_selected_tools(ENABLED_TOOLS)
+    enabled_tools = _enabled_tools_for_run(ENABLED_TOOLS)
+    register_selected_tools(enabled_tools)
 
     prompt = SimplePrompt(base_dir / "demand.md")
 
@@ -832,7 +842,7 @@ async def run_once(
     model = resolve_model(prompt, run_config)
     run_config.temperature = float(prompt.config.get("temperature", run_config.temperature))
     run_config.max_iterations = int(prompt.config.get("max_iterations", run_config.max_iterations))
-    run_config.tools = ENABLED_TOOLS.copy()
+    run_config.tools = enabled_tools
     # 禁用反思/总结经验相关流程(避免进入 reflection 侧分支)
     run_config.enable_research_flow = False
     run_config.goal_compression = "none"