elksmmx 13 시간 전
부모
커밋
787c9d244f

+ 5 - 1
examples/process_pipeline/prompts/apply_to_grounding.prompt

@@ -10,6 +10,8 @@
 - 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。
 
 # suggest_apply_to 规则
@@ -45,8 +47,9 @@
 
 - capability 数组长度和输入 capability 保持一致。
 - 每个输出项必须带原输入的 capability_id,逐字照抄。
+- 输入中的 body 只用于抽取 body_excerpt 证据,输出中不要回显完整 body。
 
-每个 apply_to 条目格式:`{ "category_id": 123, "category_path": "...", "element": null, "rationale": "..." }`
+每个 apply_to 条目格式:`{ "category_id": 123, "category_path": "...", "element": null, "rationale": "...", "body_excerpt": "逐字来自 body 的连续片段" }`
 
 # 输出硬规则
 
@@ -54,3 +57,4 @@
 - 不要任何前言、解释、标题。
 - 数字 id 用整数,不要加引号。
 - 字符串值内禁止出现 ASCII 双引号;需要引号请用中文书名号「」或《》。
+- body_excerpt 必须能在对应 capability.body 中用字符串包含关系直接找到。

+ 6 - 4
examples/process_pipeline/prompts/apply_to_grounding_capability.schema.json

@@ -23,13 +23,14 @@
                 "type": "array",
                 "items": {
                   "type": "object",
-                  "required": ["category_id", "category_path", "rationale"],
+                  "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 }
+                    "rationale": { "type": "string", "minLength": 1 },
+                    "body_excerpt": { "type": "string", "minLength": 1 }
                   }
                 }
               },
@@ -37,13 +38,14 @@
                 "type": "array",
                 "items": {
                   "type": "object",
-                  "required": ["category_id", "category_path", "rationale"],
+                  "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 }
+                    "rationale": { "type": "string", "minLength": 1 },
+                    "body_excerpt": { "type": "string", "minLength": 1 }
                   }
                 }
               }

+ 0 - 56
examples/process_pipeline/prompts/apply_to_grounding_fragment.schema.json

@@ -1,56 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "title": "apply_to_grounding_capability_output",
-  "type": "object",
-  "required": ["capability"],
-  "additionalProperties": false,
-  "properties": {
-    "capability": {
-      "type": "array",
-      "items": {
-        "type": "object",
-        "required": ["capability_id", "apply_to", "suggest_apply_to"],
-        "additionalProperties": false,
-        "properties": {
-          "capability_id": { "type": "string", "pattern": "^c_(s[0-9]+_[0-9]+|standalone_[0-9]+)$" },
-          "suggest_apply_to": { "type": "string", "minLength": 1 },
-          "apply_to": {
-            "type": "object",
-            "required": ["实质", "形式"],
-            "additionalProperties": false,
-            "properties": {
-              "实质": {
-                "type": "array",
-                "items": {
-                  "type": "object",
-                  "required": ["category_id", "category_path", "rationale"],
-                  "additionalProperties": false,
-                  "properties": {
-                    "category_id": { "type": "integer" },
-                    "category_path": { "type": "string", "minLength": 1 },
-                    "element": { "type": ["string", "null"] },
-                    "rationale": { "type": "string", "minLength": 1 }
-                  }
-                }
-              },
-              "形式": {
-                "type": "array",
-                "items": {
-                  "type": "object",
-                  "required": ["category_id", "category_path", "rationale"],
-                  "additionalProperties": false,
-                  "properties": {
-                    "category_id": { "type": "integer" },
-                    "category_path": { "type": "string", "minLength": 1 },
-                    "element": { "type": ["string", "null"] },
-                    "rationale": { "type": "string", "minLength": 1 }
-                  }
-                }
-              }
-            }
-          }
-        }
-      }
-    }
-  }
-}

+ 27 - 0
examples/process_pipeline/script/apply_to_grounding.py

@@ -206,10 +206,30 @@ def build_capability_grounding_input(capability: Dict[str, Any]) -> Dict[str, An
     """只保留 capability grounding 需要的最小字段"""
     return {
         "capability_id": capability.get("capability_id"),
+        "body": capability.get("body") or "",
         "apply_to_draft": capability.get("apply_to_draft", {}),
     }
 
 
+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():
+        return False
+    for source_type in ("实质", "形式"):
+        items = apply_to.get(source_type, [])
+        if not isinstance(items, list):
+            return False
+        for item in items:
+            if not isinstance(item, dict):
+                return False
+            excerpt = item.get("body_excerpt")
+            if not isinstance(excerpt, str) or not excerpt.strip():
+                return False
+            if excerpt.strip() not in body:
+                return False
+    return True
+
+
 def render_grounding_prompt(
     template: str,
     task: str,
@@ -331,16 +351,23 @@ async def ground_single_case(
                 continue
             apply_to = grounded_capability.get("apply_to")
             suggest_apply_to = grounded_capability.get("suggest_apply_to")
+            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(updated_capabilities[capability_idx], dict)
+                and apply_to_body_excerpts_are_verbatim(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].pop("apply_to_draft", None)
                 used_indices.add(capability_idx)
+            else:
+                print(
+                    f"    ⚠️ Skip capability grounding writeback: "
+                    f"{capability_id or capability_idx} has missing/non-verbatim body_excerpt"
+                )
 
     result["capability"] = updated_capabilities
 

+ 75 - 4
examples/process_pipeline/ui/app.js

@@ -67,9 +67,75 @@ let currentPipelineStatus = {};
 async function init() {
     await fetchRequirements();
     setupEventListeners();
+    setupFloatingApplyToTooltips();
     startStatusPolling();
 }
 
+function setupFloatingApplyToTooltips() {
+    if (window.__applyToTooltipReady) return;
+    window.__applyToTooltipReady = true;
+
+    let floatingTooltip = null;
+    let activeTarget = null;
+
+    const hideTooltip = () => {
+        activeTarget = null;
+        if (floatingTooltip) {
+            floatingTooltip.remove();
+            floatingTooltip = null;
+        }
+    };
+
+    const positionTooltip = () => {
+        if (!floatingTooltip || !activeTarget) return;
+        const rect = activeTarget.getBoundingClientRect();
+        const tipRect = floatingTooltip.getBoundingClientRect();
+        const gap = 8;
+        const margin = 12;
+
+        let left = rect.left + rect.width / 2 - tipRect.width / 2;
+        left = Math.max(margin, Math.min(left, window.innerWidth - tipRect.width - margin));
+
+        let top = rect.bottom + gap;
+        if (top + tipRect.height > window.innerHeight - margin) {
+            top = rect.top - tipRect.height - gap;
+        }
+        top = Math.max(margin, top);
+
+        floatingTooltip.style.left = `${left}px`;
+        floatingTooltip.style.top = `${top}px`;
+        floatingTooltip.style.visibility = 'visible';
+        floatingTooltip.style.opacity = '1';
+    };
+
+    document.addEventListener('pointerover', (event) => {
+        const target = event.target.closest('.apply-to-path-item.has-tooltip');
+        if (!target) return;
+        const sourceTooltip = target.querySelector('.apply-to-tooltip');
+        if (!sourceTooltip) return;
+
+        activeTarget = target;
+        if (!floatingTooltip) {
+            floatingTooltip = document.createElement('div');
+            floatingTooltip.className = 'floating-apply-to-tooltip';
+            document.body.appendChild(floatingTooltip);
+        }
+        floatingTooltip.innerHTML = sourceTooltip.innerHTML;
+        floatingTooltip.style.visibility = 'hidden';
+        floatingTooltip.style.opacity = '0';
+        requestAnimationFrame(positionTooltip);
+    });
+
+    document.addEventListener('pointerout', (event) => {
+        const target = event.target.closest('.apply-to-path-item.has-tooltip');
+        if (!target || target.contains(event.relatedTarget)) return;
+        hideTooltip();
+    });
+
+    window.addEventListener('scroll', hideTooltip, true);
+    window.addEventListener('resize', hideTooltip);
+}
+
 // Fetch Data
 async function fetchRequirements() {
     try {
@@ -2519,11 +2585,16 @@ window.renderStructuredData = function (items, type, parentItem = null) {
                         }
 
                         let tooltipHtml = '';
-                        if (typeof pathObj === 'object' && pathObj !== null && pathObj.rationale) {
+                        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>` : ''}
-                                    <span class="tooltip-rationale">${escapeApplyToText(pathObj.rationale)}</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>
                             `;
                         }
@@ -2830,7 +2901,7 @@ window.renderStructuredData = function (items, type, parentItem = null) {
                     <td class="capability-cell">${capability && capability.inputs && capability.inputs.length > 0 ? renderDataObjList(capability.inputs) : '-'}</td>
                     <td class="capability-cell">${renderAction(capability)}</td>
                     <td class="capability-cell">${capability && capability.outputs && capability.outputs.length > 0 ? renderDataObjList(capability.outputs) : '-'}</td>
-                    <td class="capability-cell" style="font-size:0.9em;"><div class="capability-clamp">${applyTo ? renderApplyToVal(applyTo, suggestApplyTo) : '-'}</div></td>
+                    <td class="capability-cell" style="font-size:0.9em;"><div class="capability-clamp apply-to-clamp">${applyTo ? renderApplyToVal(applyTo, suggestApplyTo) : '-'}</div></td>
                     <td class="capability-cell"><div class="capability-clamp capability-text">${capability && capability.body ? escapeHtml(capability.body) : '-'}</div></td>
                     <td class="capability-cell"><div class="capability-clamp">${capability ? renderEffects(capability.effects) : '-'}</div></td>
                     <td class="capability-cell"><div class="capability-clamp">${capability ? renderTools(capability.tools) : '-'}</div></td>
@@ -2882,7 +2953,7 @@ window.renderStructuredData = function (items, type, parentItem = null) {
                         .steps-table .effect-method { background:#e0e7ff; color:#3730a3; font-weight:normal; margin-right:6px; margin-bottom:2px; display:inline-block; transform:translateY(-1px); }
                         .steps-table .effect-negative { background:#f8fafc; color:#64748b; border:1px solid #cbd5e1; border-radius:999px; padding:1px 7px; font-size:0.78em; }
                     </style>
-                    <table class="steps-table" style="width: 100%; min-width: ${minWidth + 220}px; border-collapse: collapse; margin-top: 8px; font-size: 0.9em; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.05);">
+                    <table class="steps-table" style="width: 100%; min-width: ${minWidth + 220}px; border-collapse: collapse; margin-top: 8px; font-size: 0.9em; background: white; border-radius: 8px; overflow: visible; box-shadow: 0 1px 3px rgba(0,0,0,0.05);">
                         <thead>
                             <tr style="background: rgba(0,0,0,0.03); border-bottom: 2px solid rgba(0,0,0,0.1); text-align: left;">
                                 <th style="padding: 12px 10px; width: 60px;">序号</th>

+ 62 - 36
examples/process_pipeline/ui/style.css

@@ -1176,59 +1176,85 @@ body {
     background: #eff6ff;
 }
 
-.apply-to-tooltip {
-    visibility: hidden;
-    opacity: 0;
-    position: absolute;
-    bottom: 100%;
-    left: 50%;
-    transform: translateX(-50%) translateY(5px);
-    background: #27272a;
-    color: #fff;
+.apply-to-tooltip {
+    visibility: hidden;
+    opacity: 0;
+    position: absolute;
+    top: calc(100% + 8px);
+    left: 50%;
+    transform: translateX(-50%) translateY(-4px);
+    background: #27272a;
+    color: #fff;
     padding: 10px 14px;
     border-radius: 6px;
     font-size: 0.85rem;
     white-space: normal;
     width: max-content;
     max-width: 320px;
-    z-index: 100;
+    z-index: 1000;
     transition: opacity 0.2s, transform 0.2s;
     pointer-events: none;
     box-shadow: 0 4px 15px rgba(0,0,0,0.15);
     line-height: 1.5;
     text-align: left;
-    margin-bottom: 8px;
-}
-
-.apply-to-tooltip::after {
-    content: '';
-    position: absolute;
-    top: 100%;
-    left: 50%;
-    margin-left: -6px;
-    border-width: 6px;
-    border-style: solid;
-    border-color: #27272a transparent transparent transparent;
-}
-
-.apply-to-path-item.has-tooltip:hover .apply-to-tooltip {
-    visibility: visible;
-    opacity: 1;
-    transform: translateX(-50%) translateY(0);
-}
-
-.tooltip-id {
+}
+
+.apply-to-tooltip::after {
+    content: '';
+    position: absolute;
+    bottom: 100%;
+    left: 50%;
+    margin-left: -6px;
+    border-width: 6px;
+    border-style: solid;
+    border-color: transparent transparent #27272a transparent;
+}
+
+.apply-to-path-item.has-tooltip:hover .apply-to-tooltip {
+    visibility: hidden;
+    opacity: 0;
+}
+
+.floating-apply-to-tooltip {
+    position: fixed;
+    z-index: 10000;
+    background: #27272a;
+    color: #fff;
+    padding: 10px 14px;
+    border-radius: 6px;
+    font-size: 0.85rem;
+    white-space: normal;
+    width: max-content;
+    max-width: 320px;
+    pointer-events: none;
+    box-shadow: 0 4px 15px rgba(0,0,0,0.15);
+    line-height: 1.5;
+    text-align: left;
+    transition: opacity 0.12s;
+}
+
+.tooltip-id {
     color: #a1a1aa;
     font-family: Consolas, monospace;
     margin-bottom: 6px;
     display: block;
 }
 
-.tooltip-rationale {
-    color: #e4e4e7;
-}
-
-.apply-to-path-prefix {
+.tooltip-rationale {
+    color: #e4e4e7;
+    display: block;
+}
+
+.tooltip-body-excerpt {
+    display: block;
+    margin-top: 8px;
+    padding-top: 8px;
+    border-top: 1px solid rgba(255,255,255,0.14);
+    color: #bfdbfe;
+    font-size: 0.82rem;
+}
+
+.apply-to-path-prefix {
     color: #9ca3af;
 }