| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- """Skill contract loading for the formal decode-content workflow."""
- from __future__ import annotations
- import hashlib
- import json
- from dataclasses import dataclass
- from pathlib import Path
- from typing import Any, Iterable
- from core.prompts import PROMPTS_DIR
- from decode_content.models import ContractSnapshot
- ROOT = Path(__file__).resolve().parent.parent
- DEFAULT_SKILL_DIR = ROOT / "创作知识提取-skill"
- SCOPE_TREE_DIR = ROOT / "scope_trees"
- KNOWLEDGE_TYPES = ("how", "what", "why")
- BUSINESS_STAGES = ("灵感", "选题", "脚本", "视觉呈现", "拍摄表达")
- CREATION_STAGES = ("定向", "构思", "结构", "成文", "打磨")
- WHAT_KINDS = ("子集", "多维关系", "序列")
- SCOPE_TYPES = ("substance", "form", "feeling", "effect", "intent")
- REUSE_THRESHOLD = 0.90
- SCOPE_TYPE_CN = {
- "substance": "实质",
- "form": "形式",
- "feeling": "感受",
- "effect": "作用",
- "intent": "意图",
- }
- DIM_CREATIONS = ("创作",)
- DIM_ATTRIBUTE_BY_TYPE = {"how": "how", "what": "what", "why": "why"}
- CUSTOM_EXT_KEYS_BY_TYPE = {
- "how": ("业务阶段", "创作阶段", "动作", "出自"),
- "what": ("业务阶段", "概要", "出自"),
- "why": ("业务阶段", "出自"),
- }
- PROMPT_NAMES = (
- "gate_admit",
- "gate_refute",
- "gate_tiebreak",
- "normalize_scope",
- "gate_how_admit",
- "gate_how_refute",
- "gate_how_tiebreak",
- "gate_why_refute",
- )
- SCOPE_TREE_FILES = ("trees.json", "trees_index.json", "trees_embeddings.npy")
- DRIFT_NOTES = (
- {
- "key": "content_shape_drift",
- "note": "formal ingest boundary uses string content; what/why objects are serialized before payload draft.",
- },
- {
- "key": "how_step_wording_drift",
- "note": "formal code uses input/directive/output even when older phase text mentions 目的/指引/产出.",
- },
- )
- @dataclass(frozen=True)
- class ContractArtifact:
- name: str
- contract_type: str
- source_path: Path
- content: str
- content_hash: str
- def snapshot(self) -> dict[str, Any]:
- return {
- "name": self.name,
- "contract_type": self.contract_type,
- "relative_path": str(self.source_path.relative_to(ROOT)),
- }
- @dataclass(frozen=True)
- class SkillContract:
- """Immutable view of the extraction skill docs used by one decode run."""
- skill_dir: Path
- artifacts: tuple[ContractArtifact, ...]
- scope_tree_cache: tuple[dict[str, Any], ...] = ()
- @property
- def content_hash(self) -> str:
- h = hashlib.sha256()
- for artifact in self.artifacts:
- h.update(artifact.name.encode("utf-8"))
- h.update(artifact.content_hash.encode("ascii"))
- for item in self.scope_tree_cache:
- h.update(str(item.get("path", "")).encode("utf-8"))
- h.update(str(item.get("content_hash", "")).encode("ascii"))
- return h.hexdigest()
- def get(self, name: str) -> ContractArtifact:
- for artifact in self.artifacts:
- if artifact.name == name:
- return artifact
- raise KeyError(name)
- @property
- def phase1(self) -> str:
- return self.get("extraction/phase1-frame.md").content
- @property
- def phase2(self) -> str:
- return self.get("extraction/phase2-scope.md").content
- @property
- def phase3(self) -> str:
- return self.get("extraction/phase3-assemble.md").content
- def schema_for_particle_type(self, particle_type: str) -> dict[str, Any]:
- artifact = self.get(f"format/ingest-payload-{particle_type}.schema.json")
- return json.loads(artifact.content)
- def prompt_sources(self) -> list[dict[str, Any]]:
- return [a.snapshot() | {"content_hash": a.content_hash} for a in self.artifacts if a.contract_type == "prompt"]
- def scope_tree_sources(self) -> list[dict[str, Any]]:
- return list(self.scope_tree_cache)
- def constants(self) -> dict[str, Any]:
- return {
- "knowledge_types": list(KNOWLEDGE_TYPES),
- "business_stages": list(BUSINESS_STAGES),
- "creation_stages": list(CREATION_STAGES),
- "what_kinds": list(WHAT_KINDS),
- "scope_types": list(SCOPE_TYPES),
- "scope_type_cn": SCOPE_TYPE_CN,
- "dim_creations": list(DIM_CREATIONS),
- "dim_attribute_by_type": DIM_ATTRIBUTE_BY_TYPE,
- "custom_ext_keys_by_type": {k: list(v) for k, v in CUSTOM_EXT_KEYS_BY_TYPE.items()},
- }
- def snapshots(self, *, version_label: str | None = None) -> list[ContractSnapshot]:
- snapshots = [
- ContractSnapshot(
- contract_name="创作知识提取-skill",
- contract_type="skill",
- version_label=version_label,
- content_hash=self.content_hash,
- source_path=str(self.skill_dir.relative_to(ROOT)),
- snapshot={
- "artifact_count": len(self.artifacts),
- "artifacts": [a.snapshot() for a in self.artifacts],
- "scope_tree_cache": self.scope_tree_sources(),
- "constants": self.constants(),
- "drift_notes": list(DRIFT_NOTES),
- },
- )
- ]
- snapshots.extend(
- ContractSnapshot(
- contract_name=artifact.name,
- contract_type=artifact.contract_type,
- version_label=version_label,
- content_hash=artifact.content_hash,
- source_path=str(artifact.source_path.relative_to(ROOT)),
- snapshot=artifact.snapshot(),
- )
- for artifact in self.artifacts
- )
- return snapshots
- def _hash_text(text: str) -> str:
- return hashlib.sha256(text.encode("utf-8")).hexdigest()
- def _hash_bytes(data: bytes) -> str:
- return hashlib.sha256(data).hexdigest()
- def _artifact_type(path: Path) -> str:
- parts = path.parts
- if "extraction" in parts:
- return "prompt_phase"
- if "format" in parts:
- return "schema"
- if "taxonomy" in parts:
- return "taxonomy"
- if path.name == "README.md":
- return "readme"
- if "tools" in parts:
- return "tool_contract"
- return "contract"
- def _scope_tree_cache(scope_tree_dir: Path = SCOPE_TREE_DIR) -> tuple[dict[str, Any], ...]:
- out: list[dict[str, Any]] = []
- for name in SCOPE_TREE_FILES:
- path = scope_tree_dir / name
- if not path.exists():
- continue
- stat = path.stat()
- out.append(
- {
- "path": str(path.relative_to(ROOT)),
- "size": stat.st_size,
- "mtime": int(stat.st_mtime),
- "content_hash": _hash_bytes(path.read_bytes()),
- }
- )
- return tuple(out)
- def _iter_contract_files(skill_dir: Path) -> Iterable[Path]:
- preferred = [
- skill_dir / "README.md",
- skill_dir / "extraction" / "phase1-frame.md",
- skill_dir / "extraction" / "phase2-scope.md",
- skill_dir / "extraction" / "phase3-assemble.md",
- skill_dir / "format" / "ingest-payload-how.schema.json",
- skill_dir / "format" / "ingest-payload-what.schema.json",
- skill_dir / "format" / "ingest-payload-why.schema.json",
- skill_dir / "taxonomy" / "业务阶段.json",
- skill_dir / "taxonomy" / "作用域.md",
- skill_dir / "taxonomy" / "创作阶段.json",
- skill_dir / "taxonomy" / "知识类型.json",
- ]
- seen: set[Path] = set()
- for path in preferred:
- if path.exists():
- seen.add(path)
- yield path
- for path in sorted(skill_dir.rglob("*")):
- if path.is_file() and path.suffix in {".md", ".json"} and path not in seen:
- yield path
- def _iter_prompt_files() -> Iterable[Path]:
- for name in PROMPT_NAMES:
- path = PROMPTS_DIR / f"{name}.txt"
- if path.exists():
- yield path
- def load_contract(skill_dir: Path | str = DEFAULT_SKILL_DIR) -> SkillContract:
- root = Path(skill_dir)
- if not root.exists():
- raise FileNotFoundError(f"skill contract directory not found: {root}")
- artifacts: list[ContractArtifact] = []
- for path in _iter_contract_files(root):
- text = path.read_text(encoding="utf-8")
- artifacts.append(
- ContractArtifact(
- name=str(path.relative_to(root)),
- contract_type=_artifact_type(path.relative_to(root)),
- source_path=path,
- content=text,
- content_hash=_hash_text(text),
- )
- )
- for path in _iter_prompt_files():
- text = path.read_text(encoding="utf-8")
- artifacts.append(
- ContractArtifact(
- name=f"prompts/{path.name}",
- contract_type="prompt",
- source_path=path,
- content=text,
- content_hash=_hash_text(text),
- )
- )
- return SkillContract(skill_dir=root, artifacts=tuple(artifacts), scope_tree_cache=_scope_tree_cache())
- def load_decode_contracts(skill_dir: Path | str = DEFAULT_SKILL_DIR) -> SkillContract:
- return load_contract(skill_dir)
- def compute_contract_hash(contract: SkillContract | None = None) -> str:
- return (contract or load_contract()).content_hash
- def iter_contract_artifacts(
- contract: SkillContract | None = None,
- *,
- version_label: str | None = None,
- ) -> list[ContractSnapshot]:
- return (contract or load_contract()).snapshots(version_label=version_label)
- def iter_contract_snapshots(
- contract: SkillContract | None = None,
- *,
- version_label: str | None = None,
- ) -> list[ContractSnapshot]:
- """Backward-compatible alias for tests and callers that need artifact payloads."""
- return iter_contract_artifacts(contract, version_label=version_label)
|