Parcourir la source

Merge branch 'main' of https://git.yishihui.com/howard/Agent

guantao il y a 7 heures
Parent
commit
55d318b9cd

+ 47 - 26
examples/process_pipeline/prompts/apply_to_grounding.prompt

@@ -1,39 +1,56 @@
-你是内容树映射助手。现在是 Stage 2:把 apply_to_draft 映射为精确 apply_to,并为每个 capability/query 生成一个 suggest_apply_to。
+你是内容树映射助手。你的任务是把 capability.apply_to_draft 映射为精确 apply_to,并在发现内容树不足时给出 suggest_apply_to。
+
+# 输入定义
+
+本 prompt 会提供三类输入:
+- capability 数组:每项包含 capability_id、body、apply_to_draft。
+  - capability_id 是回填结果的唯一标识,必须逐字照抄。
+  - body 是该 capability 在原帖中的做法描述;当需要填写 body_excerpt 时,body 是唯一来源。
+  - apply_to_draft 是上一阶段抽取的自然语言适用范围线索,用来辅助选择路径候选。
+- 路径候选:紧凑 JSON 数组,每个节点包含 id、path、source_type、description,以及可选 elements。
+  - apply_to 只能从这些候选节点中选择。
+  - category_id 对应候选节点 id,category_path 对应候选节点 path。
+- 邻近路径参考:只用于 suggest_apply_to 的命名风格参考,不能作为 apply_to 的 category_id/category_path 来源。
+
+# 输出定义
+
+输出必须是严格 JSON,顶层只包含 capability 数组。每个输出 capability 只包含:
+- capability_id:逐字照抄输入 capability_id。
+- apply_to:真实内容树节点映射,按 实质 / 形式 分组;每条必须来自路径候选。
+- suggest_apply_to:建议补充或调整的理想路径数组;最多 3 条。
 
 # 绝对规则
 
-- 不要调用任何工具,不要查树;只能使用本 prompt 中给出的完整内容树。
-- 只处理:capability 数组中的每一条 capability。
-- **只输出 apply_to 和 suggest_apply_to 字段**,不要回显 inputs、outputs、action、body、effects、stage、tools、criterion、unstructured_what 等字段。
+- 只能使用本 prompt 中给出的路径候选,category_id 和 category_path 必须逐字来自路径候选里的 id/path
+- 处理 capability 数组中的每一条 capability。
+- 只输出 apply_to 和 suggest_apply_to 字段;不要回显 inputs、outputs、action、body、effects、stage、tools、criterion、unstructured_what 等字段。
 - apply_to.实质 只能选择 source_type=实质 的节点;apply_to.形式 只能选择 source_type=形式 的节点。
-- category_id 和 category_path 必须逐字来自内容树里的 id/path。
-- element 只有在该节点 elements 中逐字存在时才能填写;否则省略 element 或填 null。
-- 每侧 1-3 项即可。优先选择最贴近 apply_to_draft 的节点;不确定时选较粗分类,不要编造。
-- rationale 用一句话说明 draft 短语为何落在该节点。
-- 每个 apply_to 条目必须输出 body_excerpt,用来说明该节点与 body 的哪段做法直接相关。
-- body_excerpt 必须逐字摘自输入 capability.body 的连续原文片段,不允许改写、概括、拼接、补词或翻译;如果 body 中没有直接证据,不要选择该节点。
-- **每个 capability/query 必须包含且只能包含一个 suggest_apply_to**,不得在 apply_to 条目内输出 suggest_apply_to。
+- element 只有在该节点 elements 中存在时才能填写;否则省略 element 或填 null
+- 只选择你有信心认为与 apply_to_draft 和 capability.body 直接相关的节点;一般每侧不超过 3 项
+- 不确定时选较粗分类;如果仍然没有信心或 body 中没有直接证据,可以置空数组,不要编造。
+- rationale 用一句话说明为什么该 draft/body 证据落在该节点。
+- 每个 apply_to 条目和 suggest_apply_to 条目都必须包含 body_excerpt 和 body_excerpt_note 字段;这两个字段可以为空字符串
+- 如果填写 body_excerpt,它必须逐字摘自输入 capability.body 的连续原文片段,不允许改写、概括、拼接、补词或翻译。
+- body_excerpt_note 可以自由描述 body_excerpt 的含义;如果 body_excerpt 为空,body_excerpt_note 也应为空
 
 # suggest_apply_to 规则
 
-每个 capability/query 必须输出一个 suggest_apply_to,表示该 query 的整体描述"理想上应该挂在哪个路径"(即使树上不存在):
-- 先完成 apply_to.实质 和 apply_to.形式 的真实树节点映射。
-- 综合 apply_to_draft 的语义,以及你选出的真实 category_path,判断这个 query 最理想的单一路径
-- 优先以最能代表 query 核心语义的真实 category_path 为基准;如果实质和形式都存在,通常优先以实质路径为基准,形式路径只作为命名和细化参考
-- 从根往下逐层检查基准 category_path 的每一层,判断该层是否仍然准确描述了 query 的语义
-- 找到第一个"不够精确或有偏差"的层级,从该层级开始续写(替换该层及其后的所有层)。续写的风格应当与路径中的其他层级相符合,至少词性应当一致
+suggest_apply_to 用来指出:当前候选路径或已有内容树无法很好覆盖 body 中的某个具体做法时,理想上应该补充或调整到什么路径。
+
+- 每个 capability 最多输出 3 个 suggest_apply_to 条目
+- suggest_apply_to 的 body_excerpt 和 body_excerpt_note 字段可以为空;如果填写 body_excerpt,必须来自某段 capability.body 的直接证据
+- suggest_apply_to.path 可以基于真实 category_path 续写,也可以提出树上不存在的新路径
+- suggest_apply_to.source_type 必须是 实质 或 形式
 - suggest_apply_to 的所有层级必须保持同一种内容类型,不能在一条路径里混入另一套语义体系。
-- suggest_apply_to 必须满足根节点已决定的 source_type 分类:实质路径中不能出现形式类词汇,形式路径中不能出现实质类词汇;实质和形式各自有不同的命名规则、用词习惯和层级粒度,生成时必须仔细观察同 source_type 的真实路径后再续写。
-- 如果基准 category_path 所有层级都准确,且 query 没有更细的信息,则 suggest_apply_to = 基准 category_path。
-- 如果基准 category_path 所有层级都准确,但 query 还有更细的信息未体现,则在末尾续写 1-3 个层级。
-- 续写部分命名风格参考下方"邻近路径参考"(两字名词、层级粒度保持一致)。
-- 续写部分可以是树上不存在的节点,这正是 suggest_apply_to 的意义。
+- suggest_apply_to 必须满足 source_type 分类:实质路径中不能出现形式类词汇,形式路径中不能出现实质类词汇。
+- 实质和形式各自有不同的命名规则、用词习惯和层级粒度,生成时必须观察同 source_type 的真实路径后再续写。
+- 如果只是已有路径已经能准确覆盖,不要为了输出而输出 suggest_apply_to。
 
 # 邻近路径参考(向量搜索得到,仅供 suggest_apply_to 命名风格参考,不可用于 category_id/category_path)
 
 {reference_paths}
 
-# 完整内容树(紧凑 JSON)
+# 路径候选(紧凑 JSON)
 
 {compact_tree}
 
@@ -43,13 +60,17 @@
 
 # 输出格式
 
-输出 `{ "capability": [ { "capability_id": "c_w1_s1_0", "apply_to": {...}, "suggest_apply_to": "..." } ] }`
+输出 `{ "capability": [ { "capability_id": "c_w1_s1_0", "apply_to": {...}, "suggest_apply_to": [] } ] }`
 
 - capability 数组长度和输入 capability 保持一致。
 - 每个输出项必须带原输入的 capability_id,逐字照抄。
 - 输入中的 body 只用于抽取 body_excerpt 证据,输出中不要回显完整 body。
 
-每个 apply_to 条目格式:`{ "category_id": 123, "category_path": "...", "element": null, "rationale": "...", "body_excerpt": "逐字来自 body 的连续片段" }`
+每个 apply_to 条目格式:
+`{ "category_id": 123, "category_path": "...", "element": null, "rationale": "...", "body_excerpt": "可为空;非空时逐字来自 body 的连续片段", "body_excerpt_note": "可为空;自由描述该片段含义" }`
+
+每个 suggest_apply_to 条目格式:
+`{ "source_type": "实质", "path": "...", "rationale": "...", "body_excerpt": "可为空;非空时逐字来自 body 的连续片段", "body_excerpt_note": "可为空;自由描述该片段含义" }`
 
 # 输出硬规则
 
@@ -57,4 +78,4 @@
 - 不要任何前言、解释、标题。
 - 数字 id 用整数,不要加引号。
 - 字符串值内禁止出现 ASCII 双引号;需要引号请用中文书名号「」或《》。
-- body_excerpt 必须能在对应 capability.body 中用字符串包含关系直接找到。
+- 所有非空 body_excerpt 必须能在对应 capability.body 中用字符串包含关系直接找到;空字符串允许存在

+ 1 - 1
examples/process_pipeline/prompts/apply_to_grounding.schema.json

@@ -99,4 +99,4 @@
   "required": [
     "fragments-boundary"
   ]
-}
+}

+ 52 - 26
examples/process_pipeline/prompts/apply_to_grounding_capability.schema.json

@@ -4,6 +4,45 @@
   "type": "object",
   "required": ["capability"],
   "additionalProperties": false,
+  "definitions": {
+    "apply_to_item": {
+      "type": "object",
+      "required": [
+        "category_id",
+        "category_path",
+        "rationale",
+        "body_excerpt",
+        "body_excerpt_note"
+      ],
+      "additionalProperties": false,
+      "properties": {
+        "category_id": { "type": "integer" },
+        "category_path": { "type": "string", "minLength": 1 },
+        "element": { "type": ["string", "null"] },
+        "rationale": { "type": "string", "minLength": 1 },
+        "body_excerpt": { "type": "string" },
+        "body_excerpt_note": { "type": "string" }
+      }
+    },
+    "suggest_apply_to_item": {
+      "type": "object",
+      "required": [
+        "source_type",
+        "path",
+        "rationale",
+        "body_excerpt",
+        "body_excerpt_note"
+      ],
+      "additionalProperties": false,
+      "properties": {
+        "source_type": { "type": "string", "enum": ["实质", "形式"] },
+        "path": { "type": "string", "minLength": 1 },
+        "rationale": { "type": "string", "minLength": 1 },
+        "body_excerpt": { "type": "string" },
+        "body_excerpt_note": { "type": "string" }
+      }
+    }
+  },
   "properties": {
     "capability": {
       "type": "array",
@@ -12,8 +51,15 @@
         "required": ["capability_id", "apply_to", "suggest_apply_to"],
         "additionalProperties": false,
         "properties": {
-          "capability_id": { "type": "string", "pattern": "^c_w[0-9]+_(s[0-9]+_[0-9]+|standalone_[0-9]+)$" },
-          "suggest_apply_to": { "type": "string", "minLength": 1 },
+          "capability_id": {
+            "type": "string",
+            "pattern": "^c_w[0-9]+_(s[0-9]+_[0-9]+|standalone_[0-9]+)$"
+          },
+          "suggest_apply_to": {
+            "type": "array",
+            "maxItems": 3,
+            "items": { "$ref": "#/definitions/suggest_apply_to_item" }
+          },
           "apply_to": {
             "type": "object",
             "required": ["实质", "形式"],
@@ -21,33 +67,13 @@
             "properties": {
               "实质": {
                 "type": "array",
-                "items": {
-                  "type": "object",
-                  "required": ["category_id", "category_path", "rationale", "body_excerpt"],
-                  "additionalProperties": false,
-                  "properties": {
-                    "category_id": { "type": "integer" },
-                    "category_path": { "type": "string", "minLength": 1 },
-                    "element": { "type": ["string", "null"] },
-                    "rationale": { "type": "string", "minLength": 1 },
-                    "body_excerpt": { "type": "string", "minLength": 1 }
-                  }
-                }
+                "maxItems": 3,
+                "items": { "$ref": "#/definitions/apply_to_item" }
               },
               "形式": {
                 "type": "array",
-                "items": {
-                  "type": "object",
-                  "required": ["category_id", "category_path", "rationale", "body_excerpt"],
-                  "additionalProperties": false,
-                  "properties": {
-                    "category_id": { "type": "integer" },
-                    "category_path": { "type": "string", "minLength": 1 },
-                    "element": { "type": ["string", "null"] },
-                    "rationale": { "type": "string", "minLength": 1 },
-                    "body_excerpt": { "type": "string", "minLength": 1 }
-                  }
-                }
+                "maxItems": 3,
+                "items": { "$ref": "#/definitions/apply_to_item" }
               }
             }
           }

+ 17 - 9
examples/process_pipeline/script/apply_to_grounding.py

@@ -211,9 +211,9 @@ def build_capability_grounding_input(capability: Dict[str, Any]) -> Dict[str, An
     }
 
 
-def apply_to_body_excerpts_are_verbatim(apply_to: Any, body: str) -> bool:
-    """确认每个 apply_to 条目的 body_excerpt 都逐字来自 capability.body。"""
-    if not isinstance(apply_to, dict) or not isinstance(body, str) or not body.strip():
+def body_excerpts_are_verbatim(apply_to: Any, suggest_apply_to: Any, body: str) -> bool:
+    """确认非空 body_excerpt 都逐字来自 capability.body;空字符串允许存在。"""
+    if not isinstance(apply_to, dict) or not isinstance(suggest_apply_to, list) or not isinstance(body, str):
         return False
     for source_type in ("实质", "形式"):
         items = apply_to.get(source_type, [])
@@ -223,10 +223,18 @@ def apply_to_body_excerpts_are_verbatim(apply_to: Any, body: str) -> bool:
             if not isinstance(item, dict):
                 return False
             excerpt = item.get("body_excerpt")
-            if not isinstance(excerpt, str) or not excerpt.strip():
+            if not isinstance(excerpt, str):
                 return False
-            if excerpt.strip() not in body:
+            if excerpt.strip() and excerpt.strip() not in body:
                 return False
+    for item in suggest_apply_to:
+        if not isinstance(item, dict):
+            return False
+        excerpt = item.get("body_excerpt")
+        if not isinstance(excerpt, str):
+            return False
+        if excerpt.strip() and excerpt.strip() not in body:
+            return False
     return True
 
 
@@ -365,13 +373,13 @@ async def ground_single_case(
                 body = updated_capabilities[capability_idx].get("body", "")
                 if (
                     apply_to is not None
-                    and isinstance(suggest_apply_to, str)
-                    and suggest_apply_to.strip()
+                    and isinstance(suggest_apply_to, list)
+                    and len(suggest_apply_to) <= 3
                     and isinstance(updated_capabilities[capability_idx], dict)
-                    and apply_to_body_excerpts_are_verbatim(apply_to, body)
+                    and body_excerpts_are_verbatim(apply_to, suggest_apply_to, body)
                 ):
                     updated_capabilities[capability_idx]["apply_to"] = apply_to
-                    updated_capabilities[capability_idx]["suggest_apply_to"] = suggest_apply_to.strip()
+                    updated_capabilities[capability_idx]["suggest_apply_to"] = suggest_apply_to
                     updated_capabilities[capability_idx].pop("apply_to_draft", None)
                     used_indices.add(capability_idx)
                 else:

+ 32 - 20
examples/process_pipeline/ui/app.js

@@ -2591,6 +2591,23 @@ window.renderStructuredData = function (items, type, parentItem = null) {
                     <span class="apply-to-path-leaf" style="${leafStyle}">${escapeApplyToText(leaf)}</span>
                 `;
             };
+            const renderTooltip = (pathObj) => {
+                if (
+                    typeof pathObj !== 'object'
+                    || pathObj === null
+                    || !(pathObj.rationale || pathObj.body_excerpt || pathObj.body_excerpt_note || pathObj.category_id)
+                ) {
+                    return '';
+                }
+                return `
+                    <div class="apply-to-tooltip">
+                        ${pathObj.category_id ? `<span class="tooltip-id">id: ${pathObj.category_id}</span>` : ''}
+                        ${pathObj.rationale ? `<span class="tooltip-rationale">${escapeApplyToText(pathObj.rationale)}</span>` : ''}
+                        ${pathObj.body_excerpt ? `<span class="tooltip-body-excerpt">${escapeApplyToText(pathObj.body_excerpt)}</span>` : ''}
+                        ${pathObj.body_excerpt_note ? `<span class="tooltip-body-note">${escapeApplyToText(pathObj.body_excerpt_note)}</span>` : ''}
+                    </div>
+                `;
+            };
             let res = '<div style="display:flex; flex-direction:column; gap:6px;">';
             let hasRows = false;
             Object.entries(valObj).forEach(([k, v]) => {
@@ -2610,20 +2627,7 @@ window.renderStructuredData = function (items, type, parentItem = null) {
                             pathStr = String(pathObj);
                         }
 
-                        let tooltipHtml = '';
-                        if (
-                            typeof pathObj === 'object'
-                            && pathObj !== null
-                            && (pathObj.rationale || pathObj.body_excerpt || pathObj.category_id)
-                        ) {
-                            tooltipHtml = `
-                                <div class="apply-to-tooltip">
-                                    ${pathObj.category_id ? `<span class="tooltip-id">id: ${pathObj.category_id}</span>` : ''}
-                                    ${pathObj.rationale ? `<span class="tooltip-rationale">${escapeApplyToText(pathObj.rationale)}</span>` : ''}
-                                    ${pathObj.body_excerpt ? `<span class="tooltip-body-excerpt">${escapeApplyToText(pathObj.body_excerpt)}</span>` : ''}
-                                </div>
-                            `;
-                        }
+                        let tooltipHtml = renderTooltip(pathObj);
 
                         let htmlParts = '';
                         if (pathStr && elementStr) {
@@ -2641,14 +2645,22 @@ window.renderStructuredData = function (items, type, parentItem = null) {
                 }
             });
 
-            if (typeof suggestApplyTo === 'string' && suggestApplyTo.trim()) {
+            const suggestItems = Array.isArray(suggestApplyTo)
+                ? suggestApplyTo
+                : (typeof suggestApplyTo === 'string' && suggestApplyTo.trim() ? [{ path: suggestApplyTo }] : []);
+            if (suggestItems.length > 0) {
                 hasRows = true;
                 res += `<div class="apply-to-subrow">
-                    <span class="apply-to-key-badge" style="background:#eff6ff; color:#2563eb; border-color:#bfdbfe;">最优</span>
-                    <div class="apply-to-values" style="display:flex; flex-wrap:wrap; gap:4px;">
-                        <span class="apply-to-path-item" style="border: 2px dashed #94a3b8; background: transparent;">${renderPathParts(suggestApplyTo, true)}</span>
-                    </div>
-                </div>`;
+                    <span class="apply-to-key-badge" style="background:#eff6ff; color:#2563eb; border-color:#bfdbfe;">建议</span>
+                    <div class="apply-to-values" style="display:flex; flex-wrap:wrap; gap:4px;">`;
+                suggestItems.forEach(item => {
+                    const pathStr = typeof item === 'object' && item !== null ? item.path : String(item || '');
+                    const tooltipHtml = renderTooltip(item);
+                    if (pathStr) {
+                        res += `<span class="apply-to-path-item ${tooltipHtml ? 'has-tooltip' : ''}" style="border: 2px dashed #94a3b8; background: transparent;">${renderPathParts(pathStr, true)}${tooltipHtml}</span>`;
+                    }
+                });
+                res += `</div></div>`;
             }
 
             res += `</div>`;

+ 6 - 3
examples/process_pipeline/ui/scratchpad.js

@@ -54,10 +54,13 @@ function renderStructuredData(items, type) {
                     html += `</div></div>`;
                 }
             });
-            if (typeof suggestApplyTo === 'string' && suggestApplyTo.trim()) {
+            const suggestItems = Array.isArray(suggestApplyTo)
+                ? suggestApplyTo
+                : (typeof suggestApplyTo === 'string' && suggestApplyTo.trim() ? [{ path: suggestApplyTo }] : []);
+            if (suggestItems.length > 0) {
                 html += `<div class="apply-to-subrow">
-                    <span class="apply-to-key-badge">最优</span>
-                    <div class="apply-to-values">${renderPathBadge(suggestApplyTo, true)}</div>
+                    <span class="apply-to-key-badge">建议</span>
+                    <div class="apply-to-values">${suggestItems.map(item => renderPathBadge(item.path || item, true)).join('')}</div>
                 </div>`;
             }
             html += `</div></div>`;

+ 7 - 0
examples/process_pipeline/ui/style.css

@@ -1254,6 +1254,13 @@ body {
     font-size: 0.82rem;
 }
 
+.tooltip-body-note {
+    display: block;
+    margin-top: 6px;
+    color: #cbd5e1;
+    font-size: 0.82rem;
+}
+
 .apply-to-path-prefix {
     color: #9ca3af;
 }