scoping.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. """Scope candidate generation, normalization, and linking."""
  2. from __future__ import annotations
  3. import json
  4. import re
  5. from typing import Any, Protocol
  6. from core.llm import chat_json as default_chat_json
  7. from core.prompts import load_prompt
  8. from decode_content.contracts import REUSE_THRESHOLD, SCOPE_TYPE_CN, SkillContract, load_contract
  9. from decode_content.gates import ChatJsonFn
  10. NORMALIZE = load_prompt("normalize_scope")
  11. SRC2CN = SCOPE_TYPE_CN
  12. STRIP_VERBS = ("寻找", "定位", "推导", "核验", "提取", "挖掘", "捕捉", "识别", "梳理", "归纳", "判断", "验证", "确认", "复盘")
  13. SCOPE_CONN = re.compile(r"\s*[和与、,,//&]\s*")
  14. class ScopeLinker(Protocol):
  15. def link(self, value: str, *, source_type: str, top_k: int = 3) -> list[dict[str, Any]]:
  16. ...
  17. def slim_knowledges(knowledges: list[dict[str, Any]]) -> list[dict[str, Any]]:
  18. slim: list[dict[str, Any]] = []
  19. for knowledge in knowledges:
  20. item = {"id": knowledge.get("id"), "type": knowledge.get("type"), "title": knowledge.get("title")}
  21. if knowledge.get("type") == "how":
  22. item["steps"] = [
  23. {
  24. "id": step.get("id"),
  25. "input": step.get("input"),
  26. "directive": (step.get("directive") or "")[:500],
  27. "output": step.get("output"),
  28. }
  29. for step in knowledge.get("steps", [])
  30. ]
  31. else:
  32. item["内容"] = {
  33. key: knowledge.get(key)
  34. for key in ("概要", "维度拆分规则", "body", "阐述")
  35. if knowledge.get(key)
  36. }
  37. slim.append(item)
  38. return slim
  39. def scope_candidates(
  40. knowledges: list[dict[str, Any]],
  41. *,
  42. contract: SkillContract | None = None,
  43. chat_json_fn: ChatJsonFn = default_chat_json,
  44. ) -> list[dict[str, Any]]:
  45. contract = contract or load_contract()
  46. user = (
  47. "给下面每颗知识标作用域候选(how 逐步:每个 step 一组;what/why 颗级:整颗一组)。\n"
  48. "每类平铺列出所有相关值(同类可多条、全部对等、不选主次);先脑内摊正交、收敛近义;意图通常 1 个。\n"
  49. "只输出 JSON:{\"scopes\":[{\"knowledge_id\":\"k1\",\"step_id\":\"s1\",\"items\":["
  50. "{\"scope_type\":\"substance\",\"value\":\"实质值1\"},{\"scope_type\":\"substance\",\"value\":\"实质值2\"},"
  51. "{\"scope_type\":\"form\",\"value\":\"形式值1\"},{\"scope_type\":\"intent\",\"value\":\"意图值\"}]},"
  52. "{\"knowledge_id\":\"k2\",\"step_id\":null,\"items\":[...]}]}\n\n"
  53. + json.dumps(slim_knowledges(knowledges), ensure_ascii=False)
  54. )
  55. return chat_json_fn(contract.phase2, user, timeout=120).get("scopes") or []
  56. def strip_verb_tail(value: str) -> str:
  57. if not value or len(value) < 3:
  58. return value
  59. original = value
  60. for verb in STRIP_VERBS:
  61. if value.startswith(verb) and len(value) - len(verb) >= 2:
  62. value = value[len(verb):]
  63. break
  64. for verb in STRIP_VERBS:
  65. if value.endswith(verb) and len(value) - len(verb) >= 2:
  66. value = value[: -len(verb)]
  67. break
  68. return value or original
  69. def nounify_scopes(
  70. scopes: list[dict[str, Any]],
  71. *,
  72. chat_json_fn: ChatJsonFn = default_chat_json,
  73. ) -> list[dict[str, Any]]:
  74. vals = sorted(
  75. {
  76. item["value"]
  77. for scope in scopes
  78. for item in (scope.get("items") or [])
  79. if item.get("value") and item.get("scope_type") != "intent"
  80. }
  81. )
  82. mapping: dict[str, str] = {}
  83. if vals:
  84. try:
  85. mapping = chat_json_fn(NORMALIZE, json.dumps(vals, ensure_ascii=False), timeout=90).get("映射") or {}
  86. except Exception:
  87. mapping = {}
  88. for scope in scopes:
  89. for item in scope.get("items") or []:
  90. value = item.get("value")
  91. if not value or item.get("scope_type") == "intent":
  92. continue
  93. item["value"] = strip_verb_tail(mapping.get(value) or value)
  94. return scopes
  95. def link_scope(linker: ScopeLinker, scope_type: str, value: str) -> dict[str, Any]:
  96. try:
  97. hits = linker.link(value, source_type=SRC2CN.get(scope_type, scope_type), top_k=3)
  98. except Exception:
  99. hits = []
  100. top = hits[0] if hits else {}
  101. score = float(top.get("score", 0.0) or 0.0)
  102. reuse = score >= REUSE_THRESHOLD and top.get("name")
  103. return {
  104. "scope_type": scope_type,
  105. "value": top["name"] if reuse else value,
  106. "candidate": value,
  107. "link": "复用" if reuse else "新建",
  108. "score": round(score, 4),
  109. "top": [
  110. {"name": hit["name"], "score": hit["score"], "path": hit.get("path", "")}
  111. for hit in hits
  112. if hit.get("name") is not None
  113. ],
  114. }
  115. def split_scope_items(items: list[dict[str, Any]] | None) -> list[dict[str, str]]:
  116. out: list[dict[str, str]] = []
  117. for item in items or []:
  118. scope_type = item.get("scope_type")
  119. value = item.get("value")
  120. if not (scope_type and value):
  121. continue
  122. parts = [part.strip() for part in SCOPE_CONN.split(value) if part.strip()]
  123. for part in (parts if len(parts) > 1 else [value]):
  124. out.append({"scope_type": scope_type, "value": part})
  125. return out
  126. def apply_scopes(
  127. knowledges: list[dict[str, Any]],
  128. scopes: list[dict[str, Any]],
  129. linker: ScopeLinker | None = None,
  130. ) -> None:
  131. by_k = {knowledge.get("id"): knowledge for knowledge in knowledges}
  132. for scope in scopes:
  133. knowledge = by_k.get(scope.get("knowledge_id"))
  134. if not knowledge:
  135. continue
  136. if linker is None:
  137. linked = [
  138. {"scope_type": item["scope_type"], "value": item["value"], "candidate": item["value"], "link": "候选"}
  139. for item in split_scope_items(scope.get("items"))
  140. ]
  141. else:
  142. linked = [
  143. link_scope(linker, item["scope_type"], item["value"])
  144. for item in split_scope_items(scope.get("items"))
  145. ]
  146. if knowledge.get("type") == "how" and scope.get("step_id"):
  147. for step in knowledge.get("steps", []):
  148. if step.get("id") == scope["step_id"]:
  149. step["作用域"] = linked
  150. else:
  151. knowledge["作用域"] = (knowledge.get("作用域") or []) + linked