logic-engine / ace /core /skillbook.py
ghostdrive1's picture
Upload folder using huggingface_hub
116524e verified
Raw
History Blame Contribute Delete
33.8 kB
"""Skill, Skillbook, and update operations for the ACE framework."""
from __future__ import annotations
import json
import re
import threading
from dataclasses import asdict, dataclass, field, fields as dataclass_fields
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, FrozenSet, Iterable, List, Literal, Optional, Union, cast
from .insight_source import InsightSource, coerce_insight_source, coerce_insight_sources
# ---------------------------------------------------------------------------
# Constants / helpers
# ---------------------------------------------------------------------------
OperationType = Literal["ADD", "UPDATE", "TAG", "REMOVE"]
SkillSection = Literal["context", "harness"]
SCHEMA_VERSION = "2"
VALID_SECTIONS: frozenset[str] = frozenset({"context", "harness"})
DEFAULT_LEGACY_SECTION: SkillSection = "context"
InsightSourceInput = Union[
InsightSource,
Dict[str, Any],
List[Union[InsightSource, Dict[str, Any]]],
]
_UNSET = object()
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _normalize_optional_text(value: Any) -> str | None:
if value is None:
return None
text = str(value).strip()
return text or None
def _normalize_required_text(value: Any, field_name: str) -> str:
text = _normalize_optional_text(value)
if text is None:
raise ValueError(f"{field_name} is required and must be non-empty")
return text
def _normalize_keyword(value: Any) -> str | None:
text = _normalize_optional_text(value)
if text is None:
return None
text = re.sub(r"\s+", "_", text.lower())
return text or None
def _normalize_keywords(keywords: Iterable[Any] | None) -> list[str]:
normalized: list[str] = []
seen: set[str] = set()
if keywords is None:
return normalized
for value in keywords:
keyword = _normalize_keyword(value)
if keyword is None or keyword in seen:
continue
normalized.append(keyword)
seen.add(keyword)
return normalized
def _coerce_section_and_keywords(
section: str,
keywords: Iterable[Any] | None,
) -> tuple[SkillSection, list[str]]:
normalized_section = _normalize_required_text(section, "section").lower()
normalized_keywords = _normalize_keywords(keywords)
if normalized_section in VALID_SECTIONS:
return cast(SkillSection, normalized_section), normalized_keywords
# Backward-compatible coercion for legacy free-form sections.
legacy_keywords = _normalize_keywords([normalized_section, *normalized_keywords])
return DEFAULT_LEGACY_SECTION, legacy_keywords
def _embedding_sidecar_path(file_path: Path) -> Path:
if file_path.suffix:
stem = file_path.with_suffix("")
else:
stem = file_path
return stem.parent / f"{stem.name}.embeddings.npz"
def _insight_source_signature(source: InsightSource) -> str:
return json.dumps(source.to_dict(), ensure_ascii=False, sort_keys=True, default=str)
def _append_unique_sources(
existing: List[InsightSource],
incoming: Iterable[InsightSource],
) -> None:
seen = {_insight_source_signature(source) for source in existing}
for source in incoming:
signature = _insight_source_signature(source)
if signature in seen:
continue
existing.append(source)
seen.add(signature)
def _serialize_sources(sources: Iterable[InsightSource]) -> list[dict[str, Any]]:
return [source.to_dict() for source in sources]
def _deserialize_sources(raw_sources: Any) -> list[InsightSource]:
if not isinstance(raw_sources, list):
return []
deduped_sources: list[InsightSource] = []
_append_unique_sources(
deduped_sources,
[
coerce_insight_source(item)
for item in raw_sources
if isinstance(item, (InsightSource, dict))
],
)
return deduped_sources
# ---------------------------------------------------------------------------
# Update operations
# ---------------------------------------------------------------------------
@dataclass
class UpdateOperation:
"""Single mutation to apply to the skillbook."""
type: OperationType
section: str
issue: Optional[str] = None
keywords: List[str] = field(default_factory=list)
insight: Optional[str] = None
skill_id: Optional[str] = None
metadata: Dict[str, int] = field(default_factory=dict)
reason: Optional[str] = None
insight_source: Optional[InsightSourceInput] = None
learning_index: Optional[int] = None
reflection_index: Optional[int] = None
reflection_indices: List[int] = field(default_factory=list)
@classmethod
def from_json(cls, payload: Dict[str, object]) -> "UpdateOperation":
metadata_raw = payload.get("metadata") or {}
metadata: Dict[str, Any] = (
cast(Dict[str, Any], metadata_raw) if isinstance(metadata_raw, dict) else {}
)
op_type = str(payload["type"]).upper()
if op_type not in ("ADD", "UPDATE", "TAG", "REMOVE"):
raise ValueError(f"Invalid operation type: {op_type}")
raw_source = payload.get("insight_source")
insight_source: Optional[InsightSourceInput] = None
if isinstance(raw_source, dict):
insight_source = InsightSource.from_dict(cast(Dict[str, Any], raw_source))
elif isinstance(raw_source, Iterable) and not isinstance(
raw_source, (str, bytes)
):
insight_source = [
InsightSource.from_dict(cast(Dict[str, Any], item))
for item in raw_source
if isinstance(item, dict)
]
raw_learning_index = payload.get("learning_index")
learning_index: Optional[int] = None
if raw_learning_index is not None:
try:
learning_index = int(cast(int, raw_learning_index))
except (TypeError, ValueError):
pass
raw_reflection_index = payload.get("reflection_index")
reflection_index: Optional[int] = None
if raw_reflection_index is not None:
try:
reflection_index = int(cast(int, raw_reflection_index))
except (TypeError, ValueError):
pass
reflection_indices: List[int] = []
raw_reflection_indices = payload.get("reflection_indices")
if isinstance(raw_reflection_indices, Iterable) and not isinstance(
raw_reflection_indices, (str, bytes)
):
for value in raw_reflection_indices:
try:
reflection_indices.append(int(cast(int, value)))
except (TypeError, ValueError):
continue
raw_keywords = payload.get("keywords")
keywords: list[str] = []
if isinstance(raw_keywords, Iterable) and not isinstance(
raw_keywords, (str, bytes)
):
keywords = _normalize_keywords(raw_keywords)
issue = _normalize_optional_text(payload.get("issue"))
insight = _normalize_optional_text(payload.get("insight"))
reason = _normalize_optional_text(payload.get("reason"))
return cls(
type=cast(OperationType, op_type),
section=str(payload.get("section", "")),
issue=issue,
keywords=keywords,
insight=insight,
skill_id=(
str(payload["skill_id"])
if payload.get("skill_id") is not None
else None
),
metadata={str(k): int(v) for k, v in metadata.items()},
reason=reason,
insight_source=insight_source,
learning_index=learning_index,
reflection_index=reflection_index,
reflection_indices=reflection_indices,
)
def to_json(self) -> Dict[str, object]:
data: Dict[str, object] = {"type": self.type, "section": self.section}
if self.issue is not None:
data["issue"] = self.issue
if self.keywords:
data["keywords"] = list(self.keywords)
if self.insight is not None:
data["insight"] = self.insight
if self.skill_id is not None:
data["skill_id"] = self.skill_id
if self.metadata:
data["metadata"] = self.metadata
if self.reason is not None:
data["reason"] = self.reason
if self.insight_source is not None:
sources = coerce_insight_sources(self.insight_source)
if len(sources) == 1:
data["insight_source"] = sources[0].to_dict()
elif sources:
data["insight_source"] = [source.to_dict() for source in sources]
if self.learning_index is not None:
data["learning_index"] = self.learning_index
if self.reflection_index is not None:
data["reflection_index"] = self.reflection_index
if self.reflection_indices:
data["reflection_indices"] = list(self.reflection_indices)
return data
@dataclass
class UpdateBatch:
"""Bundle of skill manager reasoning and operations."""
reasoning: str
operations: List[UpdateOperation] = field(default_factory=list)
@classmethod
def from_json(cls, payload: Dict[str, object]) -> "UpdateBatch":
ops_payload = payload.get("operations")
operations = []
if isinstance(ops_payload, Iterable):
for item in ops_payload:
if isinstance(item, dict):
operations.append(UpdateOperation.from_json(item))
return cls(reasoning=str(payload.get("reasoning", "")), operations=operations)
def to_json(self) -> Dict[str, object]:
return {
"reasoning": self.reasoning,
"operations": [op.to_json() for op in self.operations],
}
# ---------------------------------------------------------------------------
# Skill types
# ---------------------------------------------------------------------------
@dataclass
class SimilarityDecision:
"""Record of a SkillManager decision to KEEP two skills separate."""
decision: Literal["KEEP"]
reasoning: str
decided_at: str
similarity_at_decision: float
@dataclass
class Skill:
"""Single skillbook entry."""
id: str
section: SkillSection
keywords: list[str]
issue: str
insight: str | None = None
occurrences: List[InsightSource] = field(default_factory=list)
active: bool = True
used_count: int = 0
helpful_count: int = 0
harmful_count: int = 0
neutral_count: int = 0
embedding: Optional[List[float]] = None
created_at: str = field(default_factory=_now_iso)
updated_at: str = field(default_factory=_now_iso)
def to_llm_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"section": self.section,
"keywords": list(self.keywords),
"issue": self.issue,
"insight": self.insight,
"active": self.active,
"used_count": self.used_count,
"helpful_count": self.helpful_count,
"harmful_count": self.harmful_count,
"neutral_count": self.neutral_count,
}
def embedding_text(self) -> str:
parts = [self.issue]
if self.insight:
parts.append(self.insight)
if self.keywords:
parts.append(f"Keywords: {', '.join(self.keywords)}")
return "\n\n".join(parts)
# ---------------------------------------------------------------------------
# Skillbook
# ---------------------------------------------------------------------------
class Skillbook:
"""Structured context store as defined by ACE."""
def __init__(self) -> None:
self._skills: Dict[str, Skill] = {}
self._sections: Dict[str, List[str]] = {}
self._next_id = 0
self._similarity_decisions: Dict[FrozenSet[str], SimilarityDecision] = {}
self._lock = threading.RLock()
def __repr__(self) -> str:
return f"Skillbook(skills={len(self._skills)}, sections={list(self._sections.keys())})"
def __str__(self) -> str:
if not self._skills:
return "Skillbook(empty)"
return self.as_prompt()
# ------------------------------------------------------------------ #
# CRUD
# ------------------------------------------------------------------ #
def add_skill(
self,
section: str,
issue: str | None = None,
*,
keywords: Iterable[Any] | None = None,
insight: str | None = None,
skill_id: Optional[str] = None,
insight_source: Optional[InsightSourceInput] = None,
) -> Skill:
with self._lock:
raw_section = _normalize_required_text(section, "section").lower()
normalized_section, normalized_keywords = _coerce_section_and_keywords(
section, keywords
)
issue_text = _normalize_required_text(issue, "issue")
insight_text = _normalize_optional_text(insight)
if not normalized_keywords:
normalized_keywords = _normalize_keywords([raw_section])
if normalized_section == "context" and insight_text is None:
if raw_section not in VALID_SECTIONS:
insight_text = issue_text
else:
raise ValueError("context skills require a non-empty insight")
skill_id = skill_id or self._generate_id(normalized_section)
skill = Skill(
id=skill_id,
section=normalized_section,
keywords=normalized_keywords,
issue=issue_text,
insight=insight_text,
)
_append_unique_sources(
skill.occurrences, coerce_insight_sources(insight_source)
)
self._skills[skill_id] = skill
self._sections.setdefault(normalized_section, []).append(skill_id)
return skill
def update_skill(
self,
skill_id: str,
*,
issue: object = _UNSET,
keywords: object = _UNSET,
insight: object = _UNSET,
insight_source: Optional[InsightSourceInput] = None,
) -> Optional[Skill]:
with self._lock:
skill = self._skills.get(skill_id)
if skill is None:
return None
if issue is not _UNSET and issue is not None:
skill.issue = _normalize_required_text(issue, "issue")
if keywords is not _UNSET and keywords is not None:
normalized_keywords = _normalize_keywords(cast(Iterable[Any], keywords))
if not normalized_keywords:
raise ValueError("keywords are required and must be non-empty")
skill.keywords = normalized_keywords
if insight is not _UNSET and insight is not None:
skill.insight = _normalize_optional_text(insight)
if skill.section == "context" and not skill.insight:
raise ValueError("context skills require a non-empty insight")
if insight_source is not None:
_append_unique_sources(
skill.occurrences,
coerce_insight_sources(insight_source),
)
skill.embedding = None
skill.updated_at = _now_iso()
return skill
def tag_skill(
self,
skill_id: str,
delta: Literal[1, -1, 0],
*,
insight_source: Optional[InsightSourceInput] = None,
) -> Optional[Skill]:
"""Record an effectiveness observation for a skill."""
with self._lock:
skill = self._skills.get(skill_id)
if skill is None:
return None
if delta == 1:
skill.helpful_count += 1
elif delta == -1:
skill.harmful_count += 1
else:
skill.neutral_count += 1
if insight_source is not None:
_append_unique_sources(
skill.occurrences,
coerce_insight_sources(insight_source),
)
skill.updated_at = _now_iso()
return skill
def mark_used(self, skill_ids: Iterable[str]) -> None:
"""Bump ``used_count`` for each active skill ID."""
with self._lock:
for sid in skill_ids:
skill = self._skills.get(sid)
if skill is not None and skill.active:
skill.used_count += 1
skill.updated_at = _now_iso()
def remove_skill(
self,
skill_id: str,
soft: bool = True,
*,
insight_source: Optional[InsightSourceInput] = None,
) -> None:
with self._lock:
skill = self._skills.get(skill_id)
if skill is None:
return
if soft:
skill.active = False
if insight_source is not None:
_append_unique_sources(
skill.occurrences,
coerce_insight_sources(insight_source),
)
skill.updated_at = _now_iso()
else:
self.purge(skill_id)
def purge(self, skill_id: str) -> None:
with self._lock:
skill = self._skills.pop(skill_id, None)
if skill is None:
return
section_list = self._sections.get(skill.section)
if section_list:
self._sections[skill.section] = [
sid for sid in section_list if sid != skill_id
]
if not self._sections[skill.section]:
del self._sections[skill.section]
def get_skill(self, skill_id: str) -> Optional[Skill]:
return self._skills.get(skill_id)
def skills(self, include_invalid: bool = False) -> List[Skill]:
if include_invalid:
return list(self._skills.values())
return [s for s in self._skills.values() if s.active]
# ------------------------------------------------------------------ #
# Similarity decisions
# ------------------------------------------------------------------ #
def get_similarity_decision(
self, skill_id_a: str, skill_id_b: str
) -> Optional[SimilarityDecision]:
pair_key = frozenset([skill_id_a, skill_id_b])
return self._similarity_decisions.get(pair_key)
def set_similarity_decision(
self,
skill_id_a: str,
skill_id_b: str,
decision: SimilarityDecision,
) -> None:
with self._lock:
pair_key = frozenset([skill_id_a, skill_id_b])
self._similarity_decisions[pair_key] = decision
def has_keep_decision(self, skill_id_a: str, skill_id_b: str) -> bool:
decision = self.get_similarity_decision(skill_id_a, skill_id_b)
return decision is not None and decision.decision == "KEEP"
# ------------------------------------------------------------------ #
# Serialization
# ------------------------------------------------------------------ #
def to_dict(self, exclude_embeddings: bool = False) -> Dict[str, object]:
del exclude_embeddings # JSON never carries embeddings in v2.
similarity_decisions_serialized = {
",".join(sorted(pair_ids)): asdict(decision)
for pair_ids, decision in self._similarity_decisions.items()
}
skills_serialized = {}
for skill_id, skill in self._skills.items():
skill_dict = asdict(skill)
skill_dict.pop("embedding", None)
skill_dict["occurrences"] = _serialize_sources(skill.occurrences)
skills_serialized[skill_id] = skill_dict
return {
"schema_version": SCHEMA_VERSION,
"skills": skills_serialized,
"sections": self._sections,
"next_id": self._next_id,
"similarity_decisions": similarity_decisions_serialized,
}
@classmethod
def from_dict(cls, payload: Dict[str, object]) -> "Skillbook":
schema_version = str(payload.get("schema_version", ""))
if schema_version != SCHEMA_VERSION:
raise ValueError("Skillbook format v2 required — regenerate")
instance = cls()
skills_payload = payload.get("skills", {})
if isinstance(skills_payload, dict):
for skill_id, skill_value in skills_payload.items():
if not isinstance(skill_value, dict):
continue
skill_data = dict(skill_value)
skill_data["embedding"] = None
skill_data["active"] = bool(skill_data.get("active", True))
raw_keywords = skill_data.get("keywords")
skill_data["keywords"] = _normalize_keywords(
raw_keywords if isinstance(raw_keywords, list) else []
)
if not skill_data["keywords"]:
legacy_section = skill_data.get("section", DEFAULT_LEGACY_SECTION)
skill_data["keywords"] = _normalize_keywords([legacy_section])
section_value = str(skill_data.get("section", DEFAULT_LEGACY_SECTION))
normalized_section, normalized_keywords = _coerce_section_and_keywords(
section_value,
skill_data["keywords"],
)
skill_data["section"] = normalized_section
skill_data["keywords"] = normalized_keywords
skill_data["issue"] = _normalize_required_text(
skill_data.get("issue"), "issue"
)
skill_data["insight"] = _normalize_optional_text(
skill_data.get("insight")
)
skill_data["occurrences"] = _deserialize_sources(
skill_data.get("occurrences")
)
valid_fields = {f.name for f in dataclass_fields(Skill)}
skill_data = {k: v for k, v in skill_data.items() if k in valid_fields}
instance._skills[str(skill_id)] = Skill(**skill_data)
sections_payload = payload.get("sections", {})
if isinstance(sections_payload, dict):
normalized_sections: dict[str, list[str]] = {}
for section, ids in sections_payload.items():
if not isinstance(ids, Iterable) or isinstance(ids, (str, bytes)):
continue
normalized_sections[str(section)] = [str(item) for item in ids]
instance._sections = normalized_sections
next_id_value = payload.get("next_id", 0)
instance._next_id = (
int(cast(Union[int, str], next_id_value))
if next_id_value is not None
else 0
)
similarity_decisions_payload = payload.get("similarity_decisions", {})
if isinstance(similarity_decisions_payload, dict):
for pair_key_str, decision_value in similarity_decisions_payload.items():
if isinstance(decision_value, dict):
pair_ids = frozenset(str(pair_key_str).split(","))
instance._similarity_decisions[pair_ids] = SimilarityDecision(
**decision_value
)
# Prefer the explicit serialized section ordering, but rebuild if needed.
if not instance._sections:
for skill in instance._skills.values():
instance._sections.setdefault(skill.section, []).append(skill.id)
return instance
def dumps(self, exclude_embeddings: bool = False) -> str:
return json.dumps(
self.to_dict(exclude_embeddings=exclude_embeddings),
ensure_ascii=False,
indent=2,
)
@classmethod
def loads(cls, data: str) -> "Skillbook":
payload = json.loads(data)
if not isinstance(payload, dict):
raise ValueError("Skillbook serialization must be a JSON object.")
return cls.from_dict(payload)
def save_to_file(self, path: str, exclude_embeddings: bool = False) -> None:
file_path = Path(path)
sidecar_path = _embedding_sidecar_path(file_path)
file_path.parent.mkdir(parents=True, exist_ok=True)
with file_path.open("w", encoding="utf-8") as f:
f.write(self.dumps(exclude_embeddings=True))
if exclude_embeddings:
return
embeddings = {
skill.id: skill.embedding
for skill in self._skills.values()
if skill.embedding is not None
}
if not embeddings:
if sidecar_path.exists():
sidecar_path.unlink()
return
import numpy as np
sidecar_path.parent.mkdir(parents=True, exist_ok=True)
arrays = {
skill_id: np.asarray(embedding, dtype="float32")
for skill_id, embedding in embeddings.items()
}
np.savez_compressed(sidecar_path, **arrays) # type: ignore[arg-type]
@classmethod
def load_from_file(cls, path: str) -> "Skillbook":
file_path = Path(path)
if not file_path.exists():
raise FileNotFoundError(f"Skillbook file not found: {path}")
with file_path.open("r", encoding="utf-8") as f:
skillbook = cls.loads(f.read())
sidecar_path = _embedding_sidecar_path(file_path)
if not sidecar_path.exists():
return skillbook
import numpy as np
with np.load(sidecar_path) as embeddings:
for skill_id in embeddings.files:
skill = skillbook.get_skill(skill_id)
if skill is not None:
skill.embedding = embeddings[skill_id].astype("float32").tolist()
return skillbook
# ------------------------------------------------------------------ #
# Update application
# ------------------------------------------------------------------ #
def apply_update(self, update: UpdateBatch) -> None:
with self._lock:
for operation in update.operations:
self._apply_operation(operation)
def _apply_operation(self, operation: UpdateOperation) -> None:
op_type = operation.type.upper()
if op_type == "ADD":
self.add_skill(
section=operation.section,
issue=operation.issue or "",
keywords=operation.keywords,
insight=operation.insight,
skill_id=operation.skill_id,
insight_source=operation.insight_source,
)
elif op_type == "UPDATE":
if operation.skill_id is None:
return
self.update_skill(
operation.skill_id,
issue=operation.issue if operation.issue is not None else _UNSET,
keywords=operation.keywords if operation.keywords else _UNSET,
insight=operation.insight if operation.insight is not None else _UNSET,
insight_source=operation.insight_source,
)
elif op_type == "TAG":
if operation.skill_id is None:
return
delta = int(operation.metadata.get("delta", 0))
if delta > 0:
delta = 1
elif delta < 0:
delta = -1
self.tag_skill(
operation.skill_id,
cast(Literal[1, -1, 0], delta),
insight_source=operation.insight_source,
)
elif op_type == "REMOVE":
if operation.skill_id is None:
return
self.remove_skill(
operation.skill_id, insight_source=operation.insight_source
)
# ------------------------------------------------------------------ #
# Presentation
# ------------------------------------------------------------------ #
def as_prompt(self) -> str:
parts: List[str] = []
for section in ("context", "harness"):
skill_ids = self._sections.get(section, [])
section_skills = [
self._skills[sid] for sid in skill_ids if self._skills[sid].active
]
if not section_skills:
continue
parts.append(f"## {section}")
for skill in section_skills:
parts.append(f"- [{skill.id}]")
parts.append(f" Keywords: {', '.join(skill.keywords)}")
parts.append(f" Issue: {skill.issue}")
if skill.insight:
parts.append(f" Insight: {skill.insight}")
parts.append("")
return "\n".join(parts).rstrip()
def stats(self) -> Dict[str, object]:
active_skills = [skill for skill in self._skills.values() if skill.active]
by_section: dict[str, int] = {section: 0 for section in VALID_SECTIONS}
for skill in active_skills:
by_section[skill.section] = by_section.get(skill.section, 0) + 1
return {
"sections": len(
[section for section, count in by_section.items() if count]
),
"skills": len(self._skills),
"active_skills": len(active_skills),
"by_section": by_section,
}
# ------------------------------------------------------------------ #
# Insight source analysis
# ------------------------------------------------------------------ #
def source_map(self) -> Dict[str, List[Dict[str, Any]]]:
with self._lock:
result: Dict[str, List[Dict[str, Any]]] = {}
for skill_id, skill in self._skills.items():
if skill.occurrences:
result[skill_id] = _serialize_sources(skill.occurrences)
return result
def source_summary(self) -> Dict[str, Any]:
with self._lock:
epochs: Dict[Optional[int], int] = {}
source_systems: Dict[str, int] = {}
trace_uids: Dict[str, int] = {}
sample_questions: Dict[str, int] = {}
total = 0
for skill in self._skills.values():
for src in skill.occurrences:
total += 1
epochs[src.epoch] = epochs.get(src.epoch, 0) + 1
source_systems[src.source_system] = (
source_systems.get(src.source_system, 0) + 1
)
trace_uids[src.trace_uid] = trace_uids.get(src.trace_uid, 0) + 1
sq = src.sample_question or ""
if sq:
sample_questions[sq] = sample_questions.get(sq, 0) + 1
return {
"total_sources": total,
"epochs": epochs,
"source_systems": source_systems,
"trace_uids": trace_uids,
"sample_questions": sample_questions,
}
def source_filter(
self,
*,
epoch: Optional[int] = None,
sample_question: Optional[str] = None,
trace_uid: Optional[str] = None,
trace_id: Optional[str] = None,
source_system: Optional[str] = None,
) -> Dict[str, List[Dict[str, Any]]]:
with self._lock:
result: Dict[str, List[Dict[str, Any]]] = {}
for skill_id, skill in self._skills.items():
matches = []
for src in skill.occurrences:
if epoch is not None and src.epoch != epoch:
continue
if trace_uid is not None and src.trace_uid != trace_uid:
continue
if trace_id is not None and src.trace_id != trace_id:
continue
if source_system is not None and src.source_system != source_system:
continue
if sample_question is not None:
sq = src.sample_question or ""
if sample_question.lower() not in sq.lower():
continue
matches.append(src.to_dict())
if matches:
result[skill_id] = matches
return result
# ------------------------------------------------------------------ #
# Internal
# ------------------------------------------------------------------ #
def _generate_id(self, section: SkillSection) -> str:
self._next_id += 1
section_prefix = section.split()[0].lower()
return f"{section_prefix}-{self._next_id:05d}"