logic-engine / ace /deduplication /operations.py
ghostdrive1's picture
Upload folder using huggingface_hub
116524e verified
Raw
History Blame Contribute Delete
5.03 kB
"""Consolidation operations for skill deduplication."""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import TYPE_CHECKING, List, Literal, Union
if TYPE_CHECKING:
from ..core.skillbook import Skillbook
logger = logging.getLogger(__name__)
class ConsolidationOpType(str, Enum):
"""Valid consolidation operation types from SkillManager responses."""
MERGE = "MERGE"
DELETE = "DELETE"
KEEP = "KEEP"
UPDATE = "UPDATE"
@dataclass
class MergeOp:
"""Merge multiple skills into one.
Combines helpful/harmful counts from all source skills into the kept skill.
Other skills are soft-deleted.
"""
type: Literal["MERGE"] = "MERGE"
source_ids: List[str] = field(default_factory=list)
merged_content: str = ""
keep_id: str = ""
reasoning: str = ""
@dataclass
class DeleteOp:
"""Soft-delete a skill as redundant."""
type: Literal["DELETE"] = "DELETE"
skill_id: str = ""
reasoning: str = ""
@dataclass
class KeepOp:
"""Keep both skills separate (they serve different purposes)."""
type: Literal["KEEP"] = "KEEP"
skill_ids: List[str] = field(default_factory=list)
differentiation: str = ""
reasoning: str = ""
@dataclass
class UpdateOp:
"""Update a skill's content to differentiate it."""
type: Literal["UPDATE"] = "UPDATE"
skill_id: str = ""
new_content: str = ""
reasoning: str = ""
ConsolidationOperation = Union[MergeOp, DeleteOp, KeepOp, UpdateOp]
# ---------------------------------------------------------------------------
# Apply helpers
# ---------------------------------------------------------------------------
def apply_consolidation_operations(
operations: List[ConsolidationOperation],
skillbook: "Skillbook",
) -> None:
"""Apply a list of consolidation operations to a skillbook."""
for op in operations:
if isinstance(op, MergeOp):
_apply_merge(op, skillbook)
elif isinstance(op, DeleteOp):
_apply_delete(op, skillbook)
elif isinstance(op, KeepOp):
_apply_keep(op, skillbook)
elif isinstance(op, UpdateOp):
_apply_update(op, skillbook)
else:
logger.warning("Unknown operation type: %s", type(op))
def _apply_merge(op: MergeOp, skillbook: "Skillbook") -> None:
keep_skill = skillbook.get_skill(op.keep_id)
if keep_skill is None:
logger.warning("MERGE: Keep skill %s not found", op.keep_id)
return
for source_id in op.source_ids:
if source_id == op.keep_id:
continue
source = skillbook.get_skill(source_id)
if source is None:
logger.warning("MERGE: Source skill %s not found", source_id)
continue
skillbook.remove_skill(source_id, soft=True)
logger.info("MERGE: Soft-deleted %s into %s", source_id, op.keep_id)
if op.merged_content:
if keep_skill.section == "context":
keep_skill.insight = op.merged_content
else:
keep_skill.issue = op.merged_content
keep_skill.embedding = None
keep_skill.updated_at = datetime.now(timezone.utc).isoformat()
logger.info("MERGE: Completed merge into %s", op.keep_id)
def _apply_delete(op: DeleteOp, skillbook: "Skillbook") -> None:
skill = skillbook.get_skill(op.skill_id)
if skill is None:
logger.warning("DELETE: Skill %s not found", op.skill_id)
return
skillbook.remove_skill(op.skill_id, soft=True)
logger.info("DELETE: Soft-deleted %s", op.skill_id)
def _apply_keep(op: KeepOp, skillbook: "Skillbook") -> None:
if len(op.skill_ids) < 2:
logger.warning("KEEP: Need at least 2 skill IDs")
return
from ..core.skillbook import SimilarityDecision
for i, id_a in enumerate(op.skill_ids):
for id_b in op.skill_ids[i + 1 :]:
decision = SimilarityDecision(
decision="KEEP",
reasoning=op.reasoning or op.differentiation,
decided_at=datetime.now(timezone.utc).isoformat(),
similarity_at_decision=0.0,
)
skillbook.set_similarity_decision(id_a, id_b, decision)
logger.info("KEEP: Stored decision for (%s, %s)", id_a, id_b)
def _apply_update(op: UpdateOp, skillbook: "Skillbook") -> None:
skill = skillbook.get_skill(op.skill_id)
if skill is None:
logger.warning("UPDATE: Skill %s not found", op.skill_id)
return
if skill.section == "context":
skill.insight = op.new_content
else:
skill.issue = op.new_content
skill.embedding = None
skill.updated_at = datetime.now(timezone.utc).isoformat()
logger.info("UPDATE: Updated content of %s", op.skill_id)