OpenSpace/openspace/skill_engine/evolution/authoring_contract.py
2026-07-17 11:43:42 +08:00

330 lines
12 KiB
Python

"""Authoring contract for skill-creator style evolution.
The contract turns the textual discipline from skill-creator into structured
runtime data: every drafted skill must carry intent, trigger boundaries, and an
eval plan before it can be behavior-gated and committed.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any, Mapping
_ACTIONS = {"FIX", "DERIVED", "CAPTURED"}
_JUDGE_POLICIES = {"deterministic", "llm", "gdpval", "hybrid", "manual"}
@dataclass(frozen=True, slots=True)
class AuthoringIntentSpec:
capability: str
trigger_contexts: list[str] = field(default_factory=list)
non_trigger_contexts: list[str] = field(default_factory=list)
expected_artifacts: list[str] = field(default_factory=list)
success_criteria: list[str] = field(default_factory=list)
tool_dependencies: list[str] = field(default_factory=list)
resource_plan: dict[str, Any] = field(default_factory=dict)
parent_difference: str = ""
observed_pattern: str = ""
generalization_boundary: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@classmethod
def from_mapping(cls, data: Mapping[str, Any] | None) -> "AuthoringIntentSpec":
mapping = data if isinstance(data, Mapping) else {}
return cls(
capability=str(mapping.get("capability") or "").strip(),
trigger_contexts=_str_list(
mapping.get("trigger_contexts")
or mapping.get("triggers")
or mapping.get("when_to_use")
),
non_trigger_contexts=_str_list(
mapping.get("non_trigger_contexts")
or mapping.get("non_triggers")
or mapping.get("when_not_to_use")
),
expected_artifacts=_str_list(mapping.get("expected_artifacts")),
success_criteria=_str_list(mapping.get("success_criteria")),
tool_dependencies=_str_list(mapping.get("tool_dependencies")),
resource_plan=_dict_or_empty(mapping.get("resource_plan")),
parent_difference=str(mapping.get("parent_difference") or "").strip(),
observed_pattern=str(mapping.get("observed_pattern") or "").strip(),
generalization_boundary=str(
mapping.get("generalization_boundary") or ""
).strip(),
)
def validation_failures(self, action_type: str) -> list[str]:
action = _action(action_type)
failures: list[str] = []
if not self.capability:
failures.append("missing_intent_capability")
if not self.trigger_contexts:
failures.append("missing_intent_trigger_contexts")
if not self.non_trigger_contexts:
failures.append("missing_intent_non_trigger_contexts")
if not self.success_criteria:
failures.append("missing_intent_success_criteria")
if action == "DERIVED" and not self.parent_difference:
failures.append("missing_intent_parent_difference")
if action == "CAPTURED":
if not self.observed_pattern:
failures.append("missing_intent_observed_pattern")
if not self.generalization_boundary:
failures.append("missing_intent_generalization_boundary")
return failures
@dataclass(frozen=True, slots=True)
class SkillReplayTask:
prompt: str
task_id: str = ""
judge_policy: str = "hybrid"
source: str = "generated"
expected_outcome: str = ""
artifacts: list[str] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@classmethod
def from_mapping(cls, data: Mapping[str, Any] | str) -> "SkillReplayTask":
if isinstance(data, str):
return cls(prompt=data.strip())
mapping = data if isinstance(data, Mapping) else {}
return cls(
prompt=str(mapping.get("prompt") or "").strip(),
task_id=str(mapping.get("task_id") or "").strip(),
judge_policy=_judge_policy(mapping.get("judge_policy") or "hybrid"),
source=str(mapping.get("source") or "generated").strip() or "generated",
expected_outcome=str(mapping.get("expected_outcome") or "").strip(),
artifacts=_str_list(mapping.get("artifacts")),
)
def validation_failures(self, index: int) -> list[str]:
return [f"replay_task_{index}_missing_prompt"] if not self.prompt else []
@dataclass(frozen=True, slots=True)
class SkillAssertion:
assertion_type: str
target: str
expected: Any = True
description: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@classmethod
def from_mapping(cls, data: Mapping[str, Any] | str) -> "SkillAssertion":
if isinstance(data, str):
return cls(assertion_type="manual", target=data.strip(), description=data.strip())
mapping = data if isinstance(data, Mapping) else {}
return cls(
assertion_type=str(mapping.get("type") or mapping.get("assertion_type") or "").strip(),
target=str(mapping.get("target") or "").strip(),
expected=mapping.get("expected", True),
description=str(mapping.get("description") or "").strip(),
)
def validation_failures(self, index: int) -> list[str]:
failures: list[str] = []
if not self.assertion_type:
failures.append(f"assertion_{index}_missing_type")
if not self.target and not self.description:
failures.append(f"assertion_{index}_missing_target")
return failures
@dataclass(frozen=True, slots=True)
class SkillEvalPlan:
positive_trigger_queries: list[str] = field(default_factory=list)
negative_trigger_queries: list[str] = field(default_factory=list)
replay_tasks: list[SkillReplayTask] = field(default_factory=list)
deterministic_assertions: list[SkillAssertion] = field(default_factory=list)
judge_policy: str = "hybrid"
success_criteria: list[str] = field(default_factory=list)
baseline: str = "active"
notes: str = ""
def to_dict(self) -> dict[str, Any]:
return {
"positive_trigger_queries": list(self.positive_trigger_queries),
"negative_trigger_queries": list(self.negative_trigger_queries),
"replay_tasks": [task.to_dict() for task in self.replay_tasks],
"deterministic_assertions": [
assertion.to_dict() for assertion in self.deterministic_assertions
],
"judge_policy": self.judge_policy,
"success_criteria": list(self.success_criteria),
"baseline": self.baseline,
"notes": self.notes,
}
@classmethod
def from_mapping(cls, data: Mapping[str, Any] | None) -> "SkillEvalPlan":
mapping = data if isinstance(data, Mapping) else {}
replay_input = mapping.get("replay_tasks") or mapping.get("test_prompts") or []
assertion_input = (
mapping.get("deterministic_assertions")
or mapping.get("assertions")
or []
)
return cls(
positive_trigger_queries=_str_list(
mapping.get("positive_trigger_queries")
or mapping.get("should_trigger")
),
negative_trigger_queries=_str_list(
mapping.get("negative_trigger_queries")
or mapping.get("should_not_trigger")
),
replay_tasks=[
SkillReplayTask.from_mapping(item)
for item in _sequence(replay_input)
],
deterministic_assertions=[
SkillAssertion.from_mapping(item)
for item in _sequence(assertion_input)
],
judge_policy=_judge_policy(mapping.get("judge_policy") or "hybrid"),
success_criteria=_str_list(mapping.get("success_criteria")),
baseline=str(mapping.get("baseline") or "active").strip() or "active",
notes=str(mapping.get("notes") or "").strip(),
)
def validation_failures(self, action_type: str) -> list[str]:
action = _action(action_type)
failures: list[str] = []
if not self.positive_trigger_queries:
failures.append("missing_eval_positive_trigger_queries")
if not self.negative_trigger_queries:
failures.append("missing_eval_negative_trigger_queries")
if not self.success_criteria:
failures.append("missing_eval_success_criteria")
if not self.replay_tasks and action in {"FIX", "DERIVED"}:
failures.append("missing_eval_replay_tasks")
for index, task in enumerate(self.replay_tasks):
failures.extend(task.validation_failures(index))
for index, assertion in enumerate(self.deterministic_assertions):
failures.extend(assertion.validation_failures(index))
if self.judge_policy not in _JUDGE_POLICIES:
failures.append(f"unsupported_eval_judge_policy:{self.judge_policy}")
return failures
@dataclass(frozen=True, slots=True)
class SkillAuthoringContract:
intent: AuthoringIntentSpec
eval_plan: SkillEvalPlan
def to_dict(self) -> dict[str, Any]:
return {
"intent_spec": self.intent.to_dict(),
"eval_plan": self.eval_plan.to_dict(),
}
@classmethod
def from_mappings(
cls,
intent_spec: Mapping[str, Any] | None,
eval_plan: Mapping[str, Any] | None,
) -> "SkillAuthoringContract":
return cls(
intent=AuthoringIntentSpec.from_mapping(intent_spec),
eval_plan=SkillEvalPlan.from_mapping(eval_plan),
)
def validation_failures(self, action_type: str) -> list[str]:
return [
*self.intent.validation_failures(action_type),
*self.eval_plan.validation_failures(action_type),
*self._cross_field_failures(),
]
def _cross_field_failures(self) -> list[str]:
positive = {_normalize_query(item) for item in self.eval_plan.positive_trigger_queries}
negative = {_normalize_query(item) for item in self.eval_plan.negative_trigger_queries}
overlap = sorted(item for item in positive.intersection(negative) if item)
return [f"eval_trigger_query_in_both_sets:{item[:80]}" for item in overlap]
def contract_from_staged(staged: Any) -> SkillAuthoringContract:
return SkillAuthoringContract.from_mappings(
_mapping_or_none(_attr(staged, "intent_spec")),
_mapping_or_none(_attr(staged, "eval_plan")),
)
def contract_validation_failures(staged: Any, action_type: str) -> list[str]:
return contract_from_staged(staged).validation_failures(action_type)
def _action(value: str) -> str:
action = str(value or "").strip().upper()
return action if action in _ACTIONS else action
def _judge_policy(value: Any) -> str:
policy = str(value or "hybrid").strip().lower()
return policy if policy in _JUDGE_POLICIES else policy
def _str_list(value: Any) -> list[str]:
if value is None:
return []
if isinstance(value, str):
text = value.strip()
return [text] if text else []
if isinstance(value, Mapping):
return [
f"{key}: {val}".strip()
for key, val in value.items()
if str(key).strip() or str(val).strip()
]
if isinstance(value, (list, tuple, set)):
result: list[str] = []
for item in value:
if isinstance(item, Mapping):
text = str(
item.get("query")
or item.get("prompt")
or item.get("description")
or item
).strip()
else:
text = str(item).strip()
if text:
result.append(text)
return list(dict.fromkeys(result))
text = str(value).strip()
return [text] if text else []
def _dict_or_empty(value: Any) -> dict[str, Any]:
return dict(value) if isinstance(value, Mapping) else {}
def _mapping_or_none(value: Any) -> Mapping[str, Any] | None:
return value if isinstance(value, Mapping) else None
def _sequence(value: Any) -> list[Any]:
if value is None:
return []
if isinstance(value, (list, tuple, set)):
return list(value)
return [value]
def _normalize_query(value: str) -> str:
return " ".join(str(value or "").strip().lower().split())
def _attr(item: Any, name: str) -> Any:
if isinstance(item, Mapping):
return item.get(name)
return getattr(item, name, None)