| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- """Scope candidate generation, normalization, and linking."""
- from __future__ import annotations
- import json
- import re
- from typing import Any, Protocol
- from core.llm import chat_json as default_chat_json
- from core.prompts import load_prompt
- from core.text_limits import DIRECTIVE_CONTEXT_MAX_CHARS, clip_text
- from decode_content.contracts import REUSE_THRESHOLD, SCOPE_TYPE_CN, SkillContract, load_contract
- from decode_content.gates import ChatJsonFn
- NORMALIZE = load_prompt("normalize_scope")
- SRC2CN = SCOPE_TYPE_CN
- STRIP_VERBS = ("寻找", "定位", "推导", "核验", "提取", "挖掘", "捕捉", "识别", "梳理", "归纳", "判断", "验证", "确认", "复盘")
- SCOPE_CONN = re.compile(r"\s*[和与、,,//&]\s*")
- class ScopeLinker(Protocol):
- def link(self, value: str, *, source_type: str, top_k: int = 3) -> list[dict[str, Any]]:
- ...
- def slim_knowledges(knowledges: list[dict[str, Any]]) -> list[dict[str, Any]]:
- slim: list[dict[str, Any]] = []
- for knowledge in knowledges:
- item = {"id": knowledge.get("id"), "type": knowledge.get("type"), "title": knowledge.get("title")}
- if knowledge.get("type") == "how":
- item["steps"] = [
- {
- "id": step.get("id"),
- "input": step.get("input"),
- "directive": clip_text(step.get("directive") or "", DIRECTIVE_CONTEXT_MAX_CHARS),
- "output": step.get("output"),
- }
- for step in knowledge.get("steps", [])
- ]
- else:
- item["内容"] = {
- key: knowledge.get(key)
- for key in ("概要", "维度拆分规则", "body", "阐述")
- if knowledge.get(key)
- }
- slim.append(item)
- return slim
- def scope_candidates(
- knowledges: list[dict[str, Any]],
- *,
- contract: SkillContract | None = None,
- chat_json_fn: ChatJsonFn = default_chat_json,
- ) -> list[dict[str, Any]]:
- contract = contract or load_contract()
- user = (
- "给下面每颗知识标作用域候选(how 逐步:每个 step 一组;what/why 颗级:整颗一组)。\n"
- "每类平铺列出所有相关值(同类可多条、全部对等、不选主次);先脑内摊正交、收敛近义;意图通常 1 个。\n"
- "只输出 JSON:{\"scopes\":[{\"knowledge_id\":\"k1\",\"step_id\":\"s1\",\"items\":["
- "{\"scope_type\":\"substance\",\"value\":\"实质值1\"},{\"scope_type\":\"substance\",\"value\":\"实质值2\"},"
- "{\"scope_type\":\"form\",\"value\":\"形式值1\"},{\"scope_type\":\"intent\",\"value\":\"意图值\"}]},"
- "{\"knowledge_id\":\"k2\",\"step_id\":null,\"items\":[...]}]}\n\n"
- + json.dumps(slim_knowledges(knowledges), ensure_ascii=False)
- )
- return chat_json_fn(contract.phase2, user, timeout=120).get("scopes") or []
- def strip_verb_tail(value: str) -> str:
- if not value or len(value) < 3:
- return value
- original = value
- for verb in STRIP_VERBS:
- if value.startswith(verb) and len(value) - len(verb) >= 2:
- value = value[len(verb):]
- break
- for verb in STRIP_VERBS:
- if value.endswith(verb) and len(value) - len(verb) >= 2:
- value = value[: -len(verb)]
- break
- return value or original
- def nounify_scopes(
- scopes: list[dict[str, Any]],
- *,
- chat_json_fn: ChatJsonFn = default_chat_json,
- ) -> list[dict[str, Any]]:
- vals = sorted(
- {
- item["value"]
- for scope in scopes
- for item in (scope.get("items") or [])
- if item.get("value") and item.get("scope_type") != "intent"
- }
- )
- mapping: dict[str, str] = {}
- if vals:
- try:
- mapping = chat_json_fn(NORMALIZE, json.dumps(vals, ensure_ascii=False), timeout=90).get("映射") or {}
- except Exception:
- mapping = {}
- for scope in scopes:
- for item in scope.get("items") or []:
- value = item.get("value")
- if not value or item.get("scope_type") == "intent":
- continue
- item["value"] = strip_verb_tail(mapping.get(value) or value)
- return scopes
- def link_scope(linker: ScopeLinker, scope_type: str, value: str) -> dict[str, Any]:
- try:
- hits = linker.link(value, source_type=SRC2CN.get(scope_type, scope_type), top_k=3)
- except Exception:
- hits = []
- top = hits[0] if hits else {}
- score = float(top.get("score", 0.0) or 0.0)
- reuse = score >= REUSE_THRESHOLD and top.get("name")
- return {
- "scope_type": scope_type,
- "value": top["name"] if reuse else value,
- "candidate": value,
- "link": "复用" if reuse else "新建",
- "score": round(score, 4),
- "top": [
- {"name": hit["name"], "score": hit["score"], "path": hit.get("path", "")}
- for hit in hits
- if hit.get("name") is not None
- ],
- }
- def split_scope_items(items: list[dict[str, Any]] | None) -> list[dict[str, str]]:
- out: list[dict[str, str]] = []
- for item in items or []:
- scope_type = item.get("scope_type")
- value = item.get("value")
- if not (scope_type and value):
- continue
- parts = [part.strip() for part in SCOPE_CONN.split(value) if part.strip()]
- for part in (parts if len(parts) > 1 else [value]):
- out.append({"scope_type": scope_type, "value": part})
- return out
- def apply_scopes(
- knowledges: list[dict[str, Any]],
- scopes: list[dict[str, Any]],
- linker: ScopeLinker | None = None,
- ) -> None:
- by_k = {knowledge.get("id"): knowledge for knowledge in knowledges}
- for scope in scopes:
- knowledge = by_k.get(scope.get("knowledge_id"))
- if not knowledge:
- continue
- if linker is None:
- linked = [
- {"scope_type": item["scope_type"], "value": item["value"], "candidate": item["value"], "link": "候选"}
- for item in split_scope_items(scope.get("items"))
- ]
- else:
- linked = [
- link_scope(linker, item["scope_type"], item["value"])
- for item in split_scope_items(scope.get("items"))
- ]
- if knowledge.get("type") == "how" and scope.get("step_id"):
- for step in knowledge.get("steps", []):
- if step.get("id") == scope["step_id"]:
- step["作用域"] = linked
- else:
- knowledge["作用域"] = (knowledge.get("作用域") or []) + linked
|