Spaces:
Sleeping
Sleeping
File size: 8,884 Bytes
116524e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | """Agentic SkillManager — mutates the skillbook directly via tool calls.
The SkillManager runs as a :class:`RecursiveAgent` with atomic mutation
tools (``add_skill``, ``update_skill``, ``remove_skill``, ``tag_skill``)
and read-only inspection tools (``search_skills``, ``read_skill``). Each
tool applies its effect to the real :class:`Skillbook` immediately.
The ``SkillManagerOutput`` returned by :meth:`SkillManager.update_skills`
is a post-hoc **audit log**: the ``reasoning`` comes from the agent's
structured output, and ``operations`` is the sequence of mutations the
tools recorded during the run. ``UpdateStep`` is the sole invocation
point; there is no downstream ``ApplyStep`` — the skillbook has already
been mutated when ``update_skills`` returns.
"""
from __future__ import annotations
import json
import logging
from typing import Any, Optional
from pydantic import BaseModel, ConfigDict, Field
from pydantic_ai.models import Model as PydanticModel
from pydantic_ai.settings import ModelSettings
from ..core.insight_source import InsightSource
from ..core.outputs import SkillManagerOutput
from ..core.recursive_agent import AgenticConfig, BudgetExhausted, RecursiveAgent
from ..core.skillbook import Skillbook, UpdateBatch
from .prompts import SKILL_MANAGER_PROMPT, SKILL_MANAGER_SYSTEM
from .sm_tools import (
SMDeps,
register_add_skill,
register_remove_skill,
register_sm_read_skill,
register_sm_search_skills,
register_tag_skill,
register_update_skill,
)
logger = logging.getLogger(__name__)
class SkillManagerReport(BaseModel):
"""Structured output the SkillManager emits when it finishes.
Only ``reasoning`` is produced by the LLM. The audit trail of
executed mutations is collected by the tools on ``SMDeps.operations``
and spliced into the final :class:`SkillManagerOutput` by
:meth:`SkillManager.update_skills`.
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
reasoning: str = Field(..., description="Summary of the actions you took and why.")
class SkillManager(RecursiveAgent):
"""Transforms reflections into skillbook mutations via atomic tool calls.
Subclass of :class:`RecursiveAgent` — inherits compaction, recursion,
and budget management.
The SkillManager is the third ACE role. Tools mutate the real
:class:`Skillbook` directly: there is no staging, and no
``ApplyStep`` follows ``UpdateStep``. The returned
:class:`SkillManagerOutput` is an audit log of what the tools
already executed.
Args:
model: Model identifier string (LiteLLM / PydanticAI) or a
pre-built ``Model`` instance.
config: ``AgenticConfig`` — controls request/token budget,
compaction, recursion depth. ``max_requests=1`` approximates
the old one-shot behavior (a single tool turn).
prompt_template: User prompt template (defaults to
:data:`SKILL_MANAGER_PROMPT`).
system_prompt: System prompt (defaults to
:data:`SKILL_MANAGER_SYSTEM`).
model_settings: Optional PydanticAI ``ModelSettings``.
Example::
sm = SkillManager("gpt-4o-mini", config=AgenticConfig(max_requests=20))
output = sm.update_skills(
reflections=(reflection_output,),
skillbook=skillbook, # real Skillbook, not a view
question_context="Math problem solving",
progress="5/10 correct",
injected_skill_ids=ctx.injected_skill_ids,
)
# skillbook has already been mutated; output is the audit log
"""
def __init__(
self,
model: str | PydanticModel,
*,
config: Optional[AgenticConfig] = None,
prompt_template: str = SKILL_MANAGER_PROMPT,
system_prompt: str = SKILL_MANAGER_SYSTEM,
model_settings: ModelSettings | None = None,
) -> None:
self._prompt_template = prompt_template
if model_settings is None:
from pydantic_ai.models.bedrock import BedrockModelSettings
model_settings = BedrockModelSettings(
bedrock_cache_instructions=True,
bedrock_cache_tool_definitions=True,
bedrock_cache_messages=True,
)
super().__init__(
model,
output_type=SkillManagerReport,
system_prompt=system_prompt,
config=config or AgenticConfig(),
model_settings=model_settings,
tools=[
register_sm_search_skills,
register_sm_read_skill,
register_add_skill,
register_update_skill,
register_remove_skill,
register_tag_skill,
],
tool_names_to_compact=(
"search_skills",
"read_skill",
),
span_label="sm",
)
def update_skills(
self,
*,
reflections: tuple,
skillbook: Skillbook,
question_context: str,
progress: str,
source: InsightSource | None = None,
injected_skill_ids: tuple[str, ...] = (),
**kwargs: Any,
) -> SkillManagerOutput:
"""Run the agent; tools mutate ``skillbook`` as they fire.
This method signature matches :class:`SkillManagerLike`.
Args:
reflections: Tuple of Reflector analyses (1-tuple for single,
N-tuple for batch).
skillbook: Real :class:`Skillbook` — mutated in place by the
agent's tools.
question_context: Description of the task domain.
progress: Current progress summary (e.g. ``"5/10 correct"``).
source: Base provenance record for the current learning trace.
If ``None``, mutations are recorded without provenance.
injected_skill_ids: Skills rendered into the Agent's prompt
this run — the tagging scope surfaced to the agent.
**kwargs: Accepted for protocol compatibility but not
forwarded.
Returns:
:class:`SkillManagerOutput` audit log. The mutations are
already applied; the caller does NOT need to call
``skillbook.apply_update()``.
"""
reflections_data = [
{
"reasoning": r.reasoning,
"error_identification": r.error_identification,
"root_cause_analysis": r.root_cause_analysis,
"correct_approach": r.correct_approach,
"key_insight": r.key_insight,
}
for r in reflections
]
prompt = self._prompt_template.format(
progress=progress,
stats=json.dumps(skillbook.stats()),
injected_skill_ids=(
json.dumps(list(injected_skill_ids)) if injected_skill_ids else "[]"
),
reflections=json.dumps(reflections_data, ensure_ascii=False, indent=2),
question_context=question_context,
)
deps = SMDeps(
config=self.config,
depth=0,
max_depth=self.config.max_depth,
skillbook=skillbook,
current_source=source,
)
from pydantic_ai.messages import CachePoint
prompt_payload: Any = [prompt, CachePoint(ttl="5m")]
try:
report, metadata = self.run(deps=deps, prompt=prompt_payload)
reasoning = report.reasoning if report is not None else ""
raw = {
**metadata,
"sm_trace": {
"total_iterations": deps.iteration,
"compactions": metadata.get("compactions", 0),
},
}
except BudgetExhausted as exc:
logger.warning(
"SkillManager budget exhausted after %d compactions; returning partial audit",
exc.compaction_count,
)
reasoning = "SkillManager budget exhausted before completing."
raw = {
"timeout": True,
"sm_trace": {
"total_iterations": deps.iteration,
"compactions": exc.compaction_count,
},
}
except Exception as e:
logger.error("SkillManager failed: %s", e, exc_info=True)
reasoning = f"SkillManager failed: {e}"
raw = {"error": str(e)}
return SkillManagerOutput(
update=UpdateBatch(reasoning=reasoning, operations=list(deps.operations)),
raw=raw,
)
|