|
|
@@ -0,0 +1,273 @@
|
|
|
+"""Formal single-item decode orchestration."""
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import re
|
|
|
+from dataclasses import dataclass, field
|
|
|
+from typing import Any, Callable
|
|
|
+from uuid import UUID
|
|
|
+
|
|
|
+from core.config import Settings
|
|
|
+from core.llm import chat_json as default_chat_json
|
|
|
+from core.models import Post
|
|
|
+from decode_content.contracts import SkillContract, load_contract
|
|
|
+from decode_content.framing import frame_and_clean
|
|
|
+from decode_content.gates import ChatJsonFn, creation_gate
|
|
|
+from decode_content.models import DecodeResult, GateResult, PayloadDraft, ReadResult
|
|
|
+from decode_content.payloads import build_payloads
|
|
|
+from decode_content.readers.service import read_post
|
|
|
+from decode_content.repository import DecodeRepository
|
|
|
+from decode_content.scoping import ScopeLinker, apply_scopes, nounify_scopes, scope_candidates
|
|
|
+
|
|
|
+
|
|
|
+@dataclass
|
|
|
+class DecodeWorkflowOutput:
|
|
|
+ item_id: UUID
|
|
|
+ read_result: ReadResult
|
|
|
+ gate_result: GateResult
|
|
|
+ knowledges: list[dict[str, Any]] = field(default_factory=list)
|
|
|
+ payloads: list[dict[str, Any]] = field(default_factory=list)
|
|
|
+ decode_result: DecodeResult | None = None
|
|
|
+ payload_drafts: list[PayloadDraft] = field(default_factory=list)
|
|
|
+ status: str = "draft"
|
|
|
+
|
|
|
+
|
|
|
+def slugify(text: str) -> str:
|
|
|
+ s = re.sub(r"[^\w一-鿿]+", "-", text or "").strip("-").lower()
|
|
|
+ return s or "run"
|
|
|
+
|
|
|
+
|
|
|
+class DecodeService:
|
|
|
+ """Decode one creation candidate without depending on web files or SQLite."""
|
|
|
+
|
|
|
+ def __init__(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ settings: Settings | None = None,
|
|
|
+ repository: DecodeRepository | None = None,
|
|
|
+ contract: SkillContract | None = None,
|
|
|
+ scope_linker: ScopeLinker | None = None,
|
|
|
+ chat_json_fn: ChatJsonFn | None = None,
|
|
|
+ reader: Callable[[Post], ReadResult] | None = None,
|
|
|
+ ) -> None:
|
|
|
+ self.settings = settings
|
|
|
+ self.repository = repository
|
|
|
+ self.contract = contract or load_contract()
|
|
|
+ self.scope_linker = scope_linker
|
|
|
+ self.chat_json_fn = chat_json_fn or default_chat_json
|
|
|
+ self.reader = reader
|
|
|
+
|
|
|
+ def _read(self, post: Post) -> ReadResult:
|
|
|
+ if self.reader is not None:
|
|
|
+ return self.reader(post)
|
|
|
+ return read_post(post, settings=self.settings)
|
|
|
+
|
|
|
+ def decode_post(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ item_id: UUID,
|
|
|
+ post: Post,
|
|
|
+ read_result: ReadResult | None = None,
|
|
|
+ ) -> DecodeWorkflowOutput:
|
|
|
+ repo = self.repository
|
|
|
+ job = repo.create_decode_job(item_id=item_id, status="running") if repo else None
|
|
|
+ self._save_contract_snapshots()
|
|
|
+ read = read_result or self._read(post)
|
|
|
+ if read.is_empty:
|
|
|
+ gate = GateResult(passed=False, reason="读懂结果为空", details={"is_empty": True})
|
|
|
+ decode_result = self._save_decode_result(
|
|
|
+ item_id=item_id,
|
|
|
+ job_id=getattr(job, "id", None),
|
|
|
+ read=read,
|
|
|
+ gate=gate,
|
|
|
+ knowledges=[],
|
|
|
+ status="skipped",
|
|
|
+ )
|
|
|
+ return DecodeWorkflowOutput(
|
|
|
+ item_id=item_id,
|
|
|
+ read_result=read,
|
|
|
+ gate_result=gate,
|
|
|
+ decode_result=decode_result,
|
|
|
+ status="skipped",
|
|
|
+ )
|
|
|
+
|
|
|
+ gate = creation_gate(read.text, chat_json_fn=self.chat_json_fn)
|
|
|
+ if not gate.passed:
|
|
|
+ decode_result = self._save_decode_result(
|
|
|
+ item_id=item_id,
|
|
|
+ job_id=getattr(job, "id", None),
|
|
|
+ read=read,
|
|
|
+ gate=gate,
|
|
|
+ knowledges=[],
|
|
|
+ status="rejected",
|
|
|
+ )
|
|
|
+ return DecodeWorkflowOutput(
|
|
|
+ item_id=item_id,
|
|
|
+ read_result=read,
|
|
|
+ gate_result=gate,
|
|
|
+ decode_result=decode_result,
|
|
|
+ status="rejected",
|
|
|
+ )
|
|
|
+
|
|
|
+ knowledges = frame_and_clean(
|
|
|
+ post,
|
|
|
+ read.text,
|
|
|
+ contract=self.contract,
|
|
|
+ chat_json_fn=self.chat_json_fn,
|
|
|
+ )
|
|
|
+ scopes = scope_candidates(knowledges, contract=self.contract, chat_json_fn=self.chat_json_fn)
|
|
|
+ scopes = nounify_scopes(scopes, chat_json_fn=self.chat_json_fn)
|
|
|
+ apply_scopes(knowledges, scopes, self.scope_linker)
|
|
|
+ payloads = build_payloads(post, knowledges)
|
|
|
+ decode_result = self._save_decode_result(
|
|
|
+ item_id=item_id,
|
|
|
+ job_id=getattr(job, "id", None),
|
|
|
+ read=read,
|
|
|
+ gate=gate,
|
|
|
+ knowledges=knowledges,
|
|
|
+ status="decoded",
|
|
|
+ )
|
|
|
+ payload_drafts = self._save_particles_and_payloads(
|
|
|
+ item_id=item_id,
|
|
|
+ decode_result=decode_result,
|
|
|
+ knowledges=knowledges,
|
|
|
+ payloads=payloads,
|
|
|
+ )
|
|
|
+ return DecodeWorkflowOutput(
|
|
|
+ item_id=item_id,
|
|
|
+ read_result=read,
|
|
|
+ gate_result=gate,
|
|
|
+ knowledges=knowledges,
|
|
|
+ payloads=payloads,
|
|
|
+ decode_result=decode_result,
|
|
|
+ payload_drafts=payload_drafts,
|
|
|
+ status="decoded",
|
|
|
+ )
|
|
|
+
|
|
|
+ def _save_contract_snapshots(self) -> None:
|
|
|
+ if self.repository is None:
|
|
|
+ return
|
|
|
+ for snapshot in self.contract.snapshots():
|
|
|
+ self.repository.save_contract_snapshot(
|
|
|
+ contract_name=snapshot.contract_name,
|
|
|
+ contract_type=snapshot.contract_type,
|
|
|
+ version_label=snapshot.version_label,
|
|
|
+ content_hash=snapshot.content_hash,
|
|
|
+ source_path=snapshot.source_path,
|
|
|
+ snapshot=snapshot.snapshot,
|
|
|
+ )
|
|
|
+
|
|
|
+ def _save_decode_result(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ item_id: UUID,
|
|
|
+ job_id: UUID | None,
|
|
|
+ read: ReadResult,
|
|
|
+ gate: GateResult,
|
|
|
+ knowledges: list[dict[str, Any]],
|
|
|
+ status: str,
|
|
|
+ ) -> DecodeResult:
|
|
|
+ framing_result = {"knowledges": knowledges, "contract_hash": self.contract.content_hash}
|
|
|
+ if self.repository is None:
|
|
|
+ return DecodeResult(
|
|
|
+ item_id=item_id,
|
|
|
+ decode_job_id=job_id,
|
|
|
+ read_result=read.model_dump(mode="json"),
|
|
|
+ gate_result=gate.model_dump(mode="json"),
|
|
|
+ framing_result=framing_result,
|
|
|
+ status=status,
|
|
|
+ )
|
|
|
+ return self.repository.save_decode_result(
|
|
|
+ item_id=item_id,
|
|
|
+ decode_job_id=job_id,
|
|
|
+ read_result=read.model_dump(mode="json"),
|
|
|
+ gate_result=gate.model_dump(mode="json"),
|
|
|
+ framing_result=framing_result,
|
|
|
+ status=status,
|
|
|
+ )
|
|
|
+
|
|
|
+ def _save_particles_and_payloads(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ item_id: UUID,
|
|
|
+ decode_result: DecodeResult,
|
|
|
+ knowledges: list[dict[str, Any]],
|
|
|
+ payloads: list[dict[str, Any]],
|
|
|
+ ) -> list[PayloadDraft]:
|
|
|
+ if self.repository is None:
|
|
|
+ return [PayloadDraft(item_id=item_id, payload=payload) for payload in payloads]
|
|
|
+ drafts: list[PayloadDraft] = []
|
|
|
+ for knowledge, payload in zip(knowledges, payloads):
|
|
|
+ particle = self.repository.save_knowledge_particle(
|
|
|
+ item_id=item_id,
|
|
|
+ decode_result_id=decode_result.id,
|
|
|
+ particle_type=knowledge.get("type") or "how",
|
|
|
+ title=knowledge.get("title") or "",
|
|
|
+ content=knowledge,
|
|
|
+ status="draft",
|
|
|
+ )
|
|
|
+ for scope in payload.get("scopes") or []:
|
|
|
+ source_scope = self._scope_source(knowledge, scope)
|
|
|
+ self.repository.save_scope_result(
|
|
|
+ item_id=item_id,
|
|
|
+ particle_id=particle.id,
|
|
|
+ scope_type=scope["scope_type"],
|
|
|
+ scope_value=scope["value"],
|
|
|
+ evidence={k: v for k, v in source_scope.items() if k not in {"scope_type", "value"}},
|
|
|
+ status="draft",
|
|
|
+ )
|
|
|
+ drafts.append(
|
|
|
+ self.repository.save_payload_draft(
|
|
|
+ item_id=item_id,
|
|
|
+ particle_id=particle.id,
|
|
|
+ payload=payload,
|
|
|
+ review_status="pending",
|
|
|
+ ingest_ready=False,
|
|
|
+ status="draft",
|
|
|
+ )
|
|
|
+ )
|
|
|
+ return drafts
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _scope_source(knowledge: dict[str, Any], scope: dict[str, Any]) -> dict[str, Any]:
|
|
|
+ source_scopes = list(knowledge.get("作用域", []))
|
|
|
+ for step in knowledge.get("steps", []):
|
|
|
+ source_scopes.extend(step.get("作用域", []))
|
|
|
+ return next(
|
|
|
+ (
|
|
|
+ item
|
|
|
+ for item in source_scopes
|
|
|
+ if item.get("scope_type") == scope["scope_type"] and item.get("value") == scope["value"]
|
|
|
+ ),
|
|
|
+ scope,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def decode_post(
|
|
|
+ *,
|
|
|
+ item_id: UUID,
|
|
|
+ post: Post,
|
|
|
+ settings: Settings | None = None,
|
|
|
+ repository: DecodeRepository | None = None,
|
|
|
+ scope_linker: ScopeLinker | None = None,
|
|
|
+ chat_json_fn: ChatJsonFn | None = None,
|
|
|
+ reader: Callable[[Post], ReadResult] | None = None,
|
|
|
+ read_result: ReadResult | None = None,
|
|
|
+) -> DecodeWorkflowOutput:
|
|
|
+ service = DecodeService(
|
|
|
+ settings=settings,
|
|
|
+ repository=repository,
|
|
|
+ scope_linker=scope_linker,
|
|
|
+ chat_json_fn=chat_json_fn,
|
|
|
+ reader=reader,
|
|
|
+ )
|
|
|
+ return service.decode_post(item_id=item_id, post=post, read_result=read_result)
|
|
|
+
|
|
|
+
|
|
|
+def decode_item(
|
|
|
+ item_id: UUID,
|
|
|
+ *,
|
|
|
+ load_post: Callable[[UUID], Post],
|
|
|
+ service: DecodeService | None = None,
|
|
|
+) -> DecodeWorkflowOutput:
|
|
|
+ svc = service or DecodeService()
|
|
|
+ return svc.decode_post(item_id=item_id, post=load_post(item_id))
|