ghostdrive1's picture
Upload folder using huggingface_hub
116524e verified
Raw
History Blame Contribute Delete
6.52 kB
"""RR-specific tool registrars and dependency container.
Generic tools (execute_code, recurse) are provided by
:mod:`ace.core.recursive_agent`. This module adds RR-specific
tools and the RR dependency container.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Optional, Union
from pydantic_ai import ModelRetry, RunContext
from ace.core.context import SkillbookView
from ace.core.recursive_agent import AgenticDeps
from ace.core.skillbook import Skillbook
if TYPE_CHECKING:
from pydantic_ai import Agent as PydanticAgent
from .config import RecursiveConfig
# ------------------------------------------------------------------
# Dependency container
# ------------------------------------------------------------------
@dataclass
class RRDeps(AgenticDeps):
"""Dependencies injected into RR tool calls via ``RunContext``.
Extends :class:`AgenticDeps` with RR-specific trace and skillbook fields.
``sandbox`` is inherited from :class:`AgenticDeps``.
``skillbook`` (optional) is the real :class:`Skillbook` — provided so the
read-only ``search_skillbook`` and ``read_skill`` tools can inspect
strategies without the agent having to scan serialized text.
"""
trace_data: dict[str, Any] = field(default_factory=dict)
skillbook_text: str = ""
skillbook: Optional[Union[Skillbook, SkillbookView]] = None
thoughts: list[dict[str, Any]] = field(default_factory=list)
# ------------------------------------------------------------------
# RR-specific tool registrars
# ------------------------------------------------------------------
def register_output_validator(agent: "PydanticAgent[RRDeps, Any]") -> None:
"""Register the standard output validator on any RR agent."""
@agent.output_validator
def validate_output(ctx: RunContext[RRDeps], output: Any) -> Any:
"""Ensure the agent explored data before concluding."""
if ctx.deps.iteration < 1:
raise ModelRetry(
"You haven't explored the data enough. "
"Use execute_code first, then provide your final answer."
)
return output
def register_read_skill(agent: "PydanticAgent[RRDeps, Any]") -> None:
"""Register the ``read_skill`` read-only tool.
Returns the full skill payload (including counters) for a given ID,
or a ``not found`` message. No sandbox, no mutation.
"""
@agent.tool
def read_skill(ctx: RunContext[RRDeps], skill_id: str) -> dict[str, Any]:
"""Look up a skill by ID."""
sb = ctx.deps.skillbook
if sb is None:
return {"error": "skillbook unavailable"}
skill = sb.get_skill(skill_id)
if skill is None:
return {"error": f"skill not found: {skill_id}"}
return {
"id": skill.id,
"section": skill.section,
"keywords": list(skill.keywords),
"issue": skill.issue,
"insight": skill.insight,
"active": skill.active,
"used_count": skill.used_count,
"helpful_count": skill.helpful_count,
"harmful_count": skill.harmful_count,
"neutral_count": skill.neutral_count,
"occurrences": [source.to_dict() for source in skill.occurrences],
}
def register_think(agent: "PydanticAgent[RRDeps, Any]") -> None:
"""Register the ``think`` narration channel.
``think`` is the home for the model's running narration during a
tool-use turn — what it just confirmed, what it is checking next, brief
observations. This keeps prose out of ``execute_code`` stdout, where
Python should only print compact structured evidence. The final
conclusion still belongs in ``ReflectorOutput`` (the only sink that
propagates to the SkillManager); ``think`` notes are surfaced in
``output.raw["thoughts"]`` for inspection only.
"""
@agent.tool
def think(
ctx: RunContext[RRDeps],
thought: str,
evidence_refs: list[str] | None = None,
) -> dict[str, Any]:
"""Narrate your working state during the run.
Use this for mid-run prose: "checking the constraint window next",
"the mismatch is confirmed", "the decisive message is at index 12".
Use it freely — it is the right home for everything you would
naturally say while working. The final conclusion still goes in
``ReflectorOutput``; reusable data still lives in sandbox variables
via ``execute_code``.
"""
normalized = thought.strip()
if not normalized:
raise ModelRetry("Thought must be non-empty.")
refs = [ref.strip() for ref in (evidence_refs or []) if ref.strip()]
entry = {
"thought": normalized,
"evidence_refs": refs,
}
ctx.deps.thoughts.append(entry)
return {
"ok": True,
"thought_count": len(ctx.deps.thoughts),
}
def register_search_skillbook(agent: "PydanticAgent[RRDeps, Any]") -> None:
"""Register the ``search_skillbook`` read-only tool.
Returns the top-k skills most relevant to the query via embedding
similarity. Falls back to the first k active skills if embeddings are
unavailable.
"""
@agent.tool
def search_skillbook(
ctx: RunContext[RRDeps], query: str, top_k: int = 5
) -> list[dict[str, Any]]:
"""Search for skills matching a natural-language query."""
sb = ctx.deps.skillbook
if sb is None:
return [{"error": "skillbook unavailable"}]
from ace.implementations.skill_rendering import retrieve_top_k
actual_sb = sb._sb if isinstance(sb, SkillbookView) else sb
results = retrieve_top_k(actual_sb, query, top_k=top_k)
return [
{
"id": s.id,
"section": s.section,
"keywords": list(s.keywords),
"issue": s.issue,
"insight": s.insight,
"active": s.active,
"used_count": s.used_count,
"helpful_count": s.helpful_count,
"harmful_count": s.harmful_count,
"neutral_count": s.neutral_count,
}
for s in results
]