elksmmx 4 дней назад
Родитель
Сommit
8b9b2dc5dc

+ 4 - 3
examples/process_pipeline/prompts/apply_to_grounding.prompt

@@ -31,7 +31,8 @@
 - rationale 用一句话说明为什么该 draft/body 证据落在该节点。
 - rationale 用一句话说明为什么该 draft/body 证据落在该节点。
 - 每个 apply_to 条目和 suggest_apply_to 条目都必须包含 body_excerpt 和 body_excerpt_note 字段;这两个字段可以为空字符串。
 - 每个 apply_to 条目和 suggest_apply_to 条目都必须包含 body_excerpt 和 body_excerpt_note 字段;这两个字段可以为空字符串。
 - 如果填写 body_excerpt,它必须逐字摘自输入 capability.body 的连续原文片段,不允许改写、概括、拼接、补词或翻译。
 - 如果填写 body_excerpt,它必须逐字摘自输入 capability.body 的连续原文片段,不允许改写、概括、拼接、补词或翻译。
-- body_excerpt_note 可以自由描述 body_excerpt 的含义;如果 body_excerpt 为空,body_excerpt_note 也应为空。
+- body_excerpt_note 是把原文片段改写成语义完整、能让人看明白的句子:保留 body_excerpt 的核心语义,补足省略的主语/宾语/动作对象,使人不看上下文也能理解这段做法;不是 rationale,也不要解释为什么匹配该节点。
+- 如果 body_excerpt 为空,body_excerpt_note 必须也为空;不允许只填写 body_excerpt_note。
 
 
 # suggest_apply_to 规则
 # suggest_apply_to 规则
 
 
@@ -67,10 +68,10 @@ suggest_apply_to 用来指出:当前候选路径或已有内容树无法很好
 - 输入中的 body 只用于抽取 body_excerpt 证据,输出中不要回显完整 body。
 - 输入中的 body 只用于抽取 body_excerpt 证据,输出中不要回显完整 body。
 
 
 每个 apply_to 条目格式:
 每个 apply_to 条目格式:
-`{ "category_id": 123, "category_path": "...", "element": null, "rationale": "...", "body_excerpt": "可为空;非空时逐字来自 body 的连续片段", "body_excerpt_note": "可为空;自由描述该片段含义" }`
+`{ "category_id": 123, "category_path": "...", "element": null, "rationale": "...", "body_excerpt": "可为空;非空时逐字来自 body 的连续片段", "body_excerpt_note": "可为空;把原文片段改写成语义完整、能让人看明白的句子" }`
 
 
 每个 suggest_apply_to 条目格式:
 每个 suggest_apply_to 条目格式:
-`{ "source_type": "实质", "path": "...", "rationale": "...", "body_excerpt": "可为空;非空时逐字来自 body 的连续片段", "body_excerpt_note": "可为空;自由描述该片段含义" }`
+`{ "source_type": "实质", "path": "...", "rationale": "...", "body_excerpt": "可为空;非空时逐字来自 body 的连续片段", "body_excerpt_note": "可为空;把原文片段改写成语义完整、能让人看明白的句子" }`
 
 
 # 输出硬规则
 # 输出硬规则
 
 

+ 12 - 0
examples/process_pipeline/prompts/apply_to_grounding_capability.schema.json

@@ -15,6 +15,12 @@
         "body_excerpt_note"
         "body_excerpt_note"
       ],
       ],
       "additionalProperties": false,
       "additionalProperties": false,
+      "allOf": [
+        {
+          "if": { "properties": { "body_excerpt": { "const": "" } } },
+          "then": { "properties": { "body_excerpt_note": { "const": "" } } }
+        }
+      ],
       "properties": {
       "properties": {
         "category_id": { "type": "integer" },
         "category_id": { "type": "integer" },
         "category_path": { "type": "string", "minLength": 1 },
         "category_path": { "type": "string", "minLength": 1 },
@@ -34,6 +40,12 @@
         "body_excerpt_note"
         "body_excerpt_note"
       ],
       ],
       "additionalProperties": false,
       "additionalProperties": false,
+      "allOf": [
+        {
+          "if": { "properties": { "body_excerpt": { "const": "" } } },
+          "then": { "properties": { "body_excerpt_note": { "const": "" } } }
+        }
+      ],
       "properties": {
       "properties": {
         "source_type": { "type": "string", "enum": ["实质", "形式"] },
         "source_type": { "type": "string", "enum": ["实质", "形式"] },
         "path": { "type": "string", "minLength": 1 },
         "path": { "type": "string", "minLength": 1 },

+ 15 - 12
examples/process_pipeline/script/apply_to_grounding.py

@@ -213,6 +213,19 @@ def build_capability_grounding_input(capability: Dict[str, Any]) -> Dict[str, An
 
 
 def body_excerpts_are_verbatim(apply_to: Any, suggest_apply_to: Any, body: str) -> bool:
 def body_excerpts_are_verbatim(apply_to: Any, suggest_apply_to: Any, body: str) -> bool:
     """确认非空 body_excerpt 都逐字来自 capability.body;空字符串允许存在。"""
     """确认非空 body_excerpt 都逐字来自 capability.body;空字符串允许存在。"""
+    def _entry_is_valid(item: Any) -> bool:
+        if not isinstance(item, dict):
+            return False
+        excerpt = item.get("body_excerpt")
+        note = item.get("body_excerpt_note")
+        if not isinstance(excerpt, str) or not isinstance(note, str):
+            return False
+        if not excerpt.strip() and note.strip():
+            return False
+        if excerpt.strip() and excerpt.strip() not in body:
+            return False
+        return True
+
     if not isinstance(apply_to, dict) or not isinstance(suggest_apply_to, list) or not isinstance(body, str):
     if not isinstance(apply_to, dict) or not isinstance(suggest_apply_to, list) or not isinstance(body, str):
         return False
         return False
     for source_type in ("实质", "形式"):
     for source_type in ("实质", "形式"):
@@ -220,20 +233,10 @@ def body_excerpts_are_verbatim(apply_to: Any, suggest_apply_to: Any, body: str)
         if not isinstance(items, list):
         if not isinstance(items, list):
             return False
             return False
         for item in items:
         for item in items:
-            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:
+            if not _entry_is_valid(item):
                 return False
                 return False
     for item in suggest_apply_to:
     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:
+        if not _entry_is_valid(item):
             return False
             return False
     return True
     return True
 
 

+ 86 - 3
examples/process_pipeline/ui/app.js

@@ -132,10 +132,40 @@ function setupFloatingApplyToTooltips() {
         hideTooltip();
         hideTooltip();
     });
     });
 
 
+    document.addEventListener('pointerover', (event) => {
+        const target = event.target.closest('.apply-to-evidence-note[data-excerpt-key]');
+        if (!target) return;
+        const row = target.closest('tr');
+        if (!row) return;
+        row.querySelectorAll(`.body-excerpt-highlight[data-excerpt-key="${target.dataset.excerptKey}"]`).forEach(el => {
+            el.classList.add('active');
+        });
+    });
+
+    document.addEventListener('pointerout', (event) => {
+        const target = event.target.closest('.apply-to-evidence-note[data-excerpt-key]');
+        if (!target || target.contains(event.relatedTarget)) return;
+        const row = target.closest('tr');
+        if (!row) return;
+        row.querySelectorAll(`.body-excerpt-highlight[data-excerpt-key="${target.dataset.excerptKey}"]`).forEach(el => {
+            el.classList.remove('active');
+        });
+    });
+
     window.addEventListener('scroll', hideTooltip, true);
     window.addEventListener('scroll', hideTooltip, true);
     window.addEventListener('resize', hideTooltip);
     window.addEventListener('resize', hideTooltip);
 }
 }
 
 
+function makeExcerptKey(text) {
+    let hash = 0;
+    const str = String(text || '');
+    for (let i = 0; i < str.length; i += 1) {
+        hash = ((hash << 5) - hash) + str.charCodeAt(i);
+        hash |= 0;
+    }
+    return `e${Math.abs(hash)}`;
+}
+
 function getWorkflowGroups(item) {
 function getWorkflowGroups(item) {
     if (!item || !Array.isArray(item.workflow_groups)) return [];
     if (!item || !Array.isArray(item.workflow_groups)) return [];
     return item.workflow_groups.filter(group => group && typeof group === 'object');
     return item.workflow_groups.filter(group => group && typeof group === 'object');
@@ -2608,6 +2638,19 @@ window.renderStructuredData = function (items, type, parentItem = null) {
                     </div>
                     </div>
                 `;
                 `;
             };
             };
+            const renderEvidence = (pathObj) => {
+                if (typeof pathObj !== 'object' || pathObj === null) return '';
+                if (!('body_excerpt' in pathObj) && !('body_excerpt_note' in pathObj)) return '';
+                const excerpt = pathObj.body_excerpt || '';
+                const note = pathObj.body_excerpt_note || '';
+                const excerptKey = excerpt ? makeExcerptKey(excerpt) : '';
+                return `<div class="apply-to-evidence">
+                    <div class="apply-to-evidence-row">
+                        <span class="apply-to-evidence-label">关联做法</span>
+                        <span class="apply-to-evidence-value apply-to-evidence-note ${note ? '' : 'empty'}" ${excerptKey ? `data-excerpt-key="${excerptKey}"` : ''}>${note ? escapeApplyToText(note) : '空'}</span>
+                    </div>
+                </div>`;
+            };
             let res = '<div style="display:flex; flex-direction:column; gap:6px;">';
             let res = '<div style="display:flex; flex-direction:column; gap:6px;">';
             let hasRows = false;
             let hasRows = false;
             Object.entries(valObj).forEach(([k, v]) => {
             Object.entries(valObj).forEach(([k, v]) => {
@@ -2637,7 +2680,8 @@ window.renderStructuredData = function (items, type, parentItem = null) {
                         }
                         }
 
 
                         if (htmlParts) {
                         if (htmlParts) {
-                            res += `<span class="apply-to-path-item has-tooltip">${htmlParts}${tooltipHtml}</span>`;
+                            const evidenceHtml = renderEvidence(pathObj);
+                            res += `<span class="apply-to-path-block"><span class="apply-to-path-item ${tooltipHtml ? 'has-tooltip' : ''}">${htmlParts}${tooltipHtml}</span>${evidenceHtml}</span>`;
                         }
                         }
                     });
                     });
 
 
@@ -2657,7 +2701,8 @@ window.renderStructuredData = function (items, type, parentItem = null) {
                     const pathStr = typeof item === 'object' && item !== null ? item.path : String(item || '');
                     const pathStr = typeof item === 'object' && item !== null ? item.path : String(item || '');
                     const tooltipHtml = renderTooltip(item);
                     const tooltipHtml = renderTooltip(item);
                     if (pathStr) {
                     if (pathStr) {
-                        res += `<span class="apply-to-path-item ${tooltipHtml ? 'has-tooltip' : ''}" style="border: 2px dashed #94a3b8; background: transparent;">${renderPathParts(pathStr, true)}${tooltipHtml}</span>`;
+                        const evidenceHtml = renderEvidence(item);
+                        res += `<span class="apply-to-path-block"><span class="apply-to-path-item ${tooltipHtml ? 'has-tooltip' : ''}" style="border: 2px dashed #94a3b8; background: transparent;">${renderPathParts(pathStr, true)}${tooltipHtml}</span>${evidenceHtml}</span>`;
                     }
                     }
                 });
                 });
                 res += `</div></div>`;
                 res += `</div></div>`;
@@ -2929,9 +2974,47 @@ window.renderStructuredData = function (items, type, parentItem = null) {
                 });
                 });
             };
             };
             const matchedCapabilities = new Set();
             const matchedCapabilities = new Set();
+            const collectBodyExcerpts = (applyTo, suggestApplyTo) => {
+                const excerpts = [];
+                const addExcerpt = (entry) => {
+                    if (entry && typeof entry === 'object' && typeof entry.body_excerpt === 'string' && entry.body_excerpt.trim()) {
+                        excerpts.push(entry.body_excerpt.trim());
+                    }
+                };
+                if (applyTo && typeof applyTo === 'object') {
+                    Object.values(applyTo).forEach(items => {
+                        if (Array.isArray(items)) items.forEach(addExcerpt);
+                    });
+                }
+                if (Array.isArray(suggestApplyTo)) {
+                    suggestApplyTo.forEach(addExcerpt);
+                }
+                return [...new Set(excerpts)].sort((a, b) => b.length - a.length);
+            };
+            const renderBodyWithExcerptHighlights = (body, applyTo, suggestApplyTo) => {
+                if (!body) return '-';
+                const text = String(body);
+                const excerpts = collectBodyExcerpts(applyTo, suggestApplyTo);
+                if (excerpts.length === 0) return escapeHtml(text);
+
+                let out = '';
+                let i = 0;
+                while (i < text.length) {
+                    const match = excerpts.find(excerpt => text.startsWith(excerpt, i));
+                    if (match) {
+                        out += `<span class="body-excerpt-highlight" data-excerpt-key="${makeExcerptKey(match)}">${escapeHtml(match)}</span>`;
+                        i += match.length;
+                    } else {
+                        out += escapeHtml(text[i]);
+                        i += 1;
+                    }
+                }
+                return out;
+            };
             const renderCapabilityColumns = (capability) => {
             const renderCapabilityColumns = (capability) => {
                 const applyTo = capability && (capability.apply_to_draft || capability.apply_to_grounding || capability.apply_to);
                 const applyTo = capability && (capability.apply_to_draft || capability.apply_to_grounding || capability.apply_to);
                 const suggestApplyTo = capability && capability.apply_to_draft ? null : capability && capability.suggest_apply_to;
                 const suggestApplyTo = capability && capability.apply_to_draft ? null : capability && capability.suggest_apply_to;
+                const bodyHtml = capability && capability.body ? renderBodyWithExcerptHighlights(capability.body, applyTo, suggestApplyTo) : '-';
                 return `
                 return `
                     <td class="capability-cell" style="font-family: monospace;">
                     <td class="capability-cell" style="font-family: monospace;">
                         <span class="row-expand-icon">▶</span>
                         <span class="row-expand-icon">▶</span>
@@ -2941,7 +3024,7 @@ window.renderStructuredData = function (items, type, parentItem = null) {
                     <td class="capability-cell">${renderAction(capability)}</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">${capability && capability.outputs && capability.outputs.length > 0 ? renderDataObjList(capability.outputs) : '-'}</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" 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-text">${bodyHtml}</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 ? renderEffects(capability.effects) : '-'}</div></td>
                     <td class="capability-cell"><div class="capability-clamp">${capability ? renderTools(capability.tools) : '-'}</div></td>
                     <td class="capability-cell"><div class="capability-clamp">${capability ? renderTools(capability.tools) : '-'}</div></td>
                 `;
                 `;

+ 9 - 1
examples/process_pipeline/ui/scratchpad.js

@@ -37,9 +37,17 @@ function renderStructuredData(items, type) {
                 const leaf = parts.pop();
                 const leaf = parts.pop();
                 const prefix = parts.length > 0 ? parts.join('/') + '/' : '';
                 const prefix = parts.length > 0 ? parts.join('/') + '/' : '';
                 const leafStyle = highlight ? 'background:#eff6ff; color:#2563eb; border:1px solid #bfdbfe;' : '';
                 const leafStyle = highlight ? 'background:#eff6ff; color:#2563eb; border:1px solid #bfdbfe;' : '';
-                return `<span class="apply-to-path-item" ${highlight ? 'style="border: 2px dashed #94a3b8; background: transparent;"' : ''}>
+                const evidence = typeof path === 'object' && path !== null && ('body_excerpt' in path || 'body_excerpt_note' in path)
+                    ? `<div style="display:flex; flex-direction:column; gap:2px; font-size:0.78rem; color:#64748b; margin-top:4px;">
+                        <div style="display:flex; flex-direction:column; gap:2px;"><strong style="color:#94a3b8;">关联做法</strong><span>${path.body_excerpt_note ? String(path.body_excerpt_note).replace(/</g, '&lt;').replace(/>/g, '&gt;') : '<span style="color:#cbd5e1;font-style:italic;">空</span>'}</span></div>
+                    </div>`
+                    : '';
+                return `<span style="display:inline-flex; flex-direction:column; gap:4px;">
+                    <span class="apply-to-path-item" ${highlight ? 'style="border: 2px dashed #94a3b8; background: transparent;"' : ''}>
                     ${prefix ? `<span class="apply-to-path-prefix">${prefix}</span>` : ''}
                     ${prefix ? `<span class="apply-to-path-prefix">${prefix}</span>` : ''}
                     <span class="apply-to-path-leaf" style="${leafStyle}">${leaf}</span>
                     <span class="apply-to-path-leaf" style="${leafStyle}">${leaf}</span>
+                    </span>
+                    ${evidence}
                 </span>`;
                 </span>`;
             };
             };
             
             

+ 58 - 4
examples/process_pipeline/ui/style.css

@@ -1171,10 +1171,64 @@ body {
     word-break: break-all;
     word-break: break-all;
 }
 }
 
 
-.apply-to-path-item.has-tooltip:hover {
-    border-color: var(--accent-primary);
-    background: #eff6ff;
-}
+.apply-to-path-item.has-tooltip:hover {
+    border-color: var(--accent-primary);
+    background: #eff6ff;
+}
+
+.apply-to-path-block {
+    display: inline-flex;
+    flex-direction: column;
+    gap: 4px;
+    max-width: 100%;
+}
+
+.apply-to-evidence {
+    display: flex;
+    flex-direction: column;
+    gap: 2px;
+    padding-left: 2px;
+    font-size: 0.78rem;
+    line-height: 1.35;
+    color: #64748b;
+}
+
+.apply-to-evidence-row {
+    display: flex;
+    flex-direction: column;
+    gap: 2px;
+    align-items: stretch;
+}
+
+.apply-to-evidence-label {
+    color: #94a3b8;
+    font-weight: 600;
+}
+
+.apply-to-evidence-value {
+    color: #475569;
+    word-break: break-word;
+}
+
+.apply-to-evidence-note[data-excerpt-key] {
+    cursor: default;
+}
+
+.apply-to-evidence-value.empty {
+    color: #cbd5e1;
+    font-style: italic;
+}
+
+.body-excerpt-highlight {
+    border-radius: 3px;
+    transition: background 0.12s, color 0.12s;
+}
+
+.body-excerpt-highlight.active {
+    background: #dbeafe;
+    color: #1d4ed8;
+    box-shadow: 0 0 0 1px #bfdbfe inset;
+}
 
 
 .apply-to-tooltip {
 .apply-to-tooltip {
     visibility: hidden;
     visibility: hidden;