feat(agent): YAML tool specs as the LLM-facing semantic layer
Introduce a first-class semantic layer for tool descriptions, separated
from Python signatures (which stay the source of truth for types and
required-ness).
New
- alfred/agent/tools/spec.py — ToolSpec / ParameterSpec / ReturnsSpec
dataclasses with strict YAML validation (ToolSpecError on malformed
or inconsistent specs). compile_description() builds the rich text
passed to the LLM as Tool.description, with sections for summary,
description, when_to_use, when_not_to_use, next_steps, and returns.
compile_parameter_description() injects the 'why_needed' field next
to each parameter so the LLM sees the *intent* of each argument.
- alfred/agent/tools/spec_loader.py — discovers tools/specs/*.yaml,
enforces filename ↔ spec.name match, rejects duplicates.
- alfred/agent/tools/specs/ — one YAML per tool:
* resolve_season_destination.yaml
* resolve_episode_destination.yaml
* resolve_movie_destination.yaml
* resolve_series_destination.yaml
* move_to_destination.yaml
Refactor
- alfred/agent/registry.py
* _create_tool_from_function now takes an optional ToolSpec.
When provided, the long description + per-parameter descriptions
come from the spec; types and required-ness still come from the
Python signature.
* Cross-validates spec.parameters against the function signature —
crashes on missing or extra entries.
* make_tools() loads all specs at startup and hands the right one
to each tool. Tools without a spec fall back to the old
docstring-only behaviour, so the 14 not-yet-migrated tools keep
working unchanged.
* Adds 'array' and 'object' to the Python→JSON type mapping and
handles Optional[X] / X | None annotations.
- alfred/agent/tools/filesystem.py
* Drops the '_tool' suffix on the 4 resolve_* wrappers (option 1:
alias the use-case imports as _resolve_*). Tool names exposed to
the LLM now match the underlying use case verbatim.
* Wrapper docstrings shrink to a one-liner pointing to the YAML
spec — no more duplicated when_to_use/Args/Returns in Python.
Verified
- make_tools() loads 19 tools (5 with YAML spec, 14 doc-only).
- Compiled descriptions render cleanly with all sections.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
ToolSpecLoader — discover and load all YAML tool specs from a directory.
|
||||
|
||||
Convention: one YAML file per tool, named exactly like the Python function
|
||||
that implements it (e.g. resolve_season_destination.yaml).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from .spec import ToolSpec, ToolSpecError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEFAULT_SPECS_DIR = Path(__file__).parent / "specs"
|
||||
|
||||
|
||||
def load_tool_specs(specs_dir: Path | None = None) -> dict[str, ToolSpec]:
|
||||
"""
|
||||
Load every {tool}.yaml under specs_dir into a {name -> ToolSpec} mapping.
|
||||
|
||||
Args:
|
||||
specs_dir: Directory to scan. Defaults to alfred/agent/tools/specs/.
|
||||
|
||||
Returns:
|
||||
Mapping from tool name to its parsed ToolSpec.
|
||||
|
||||
Raises:
|
||||
ToolSpecError: if a spec is malformed, or if the filename doesn't
|
||||
match the 'name' field inside the YAML.
|
||||
"""
|
||||
root = specs_dir or _DEFAULT_SPECS_DIR
|
||||
if not root.exists():
|
||||
logger.warning(f"Tool specs directory not found: {root}")
|
||||
return {}
|
||||
|
||||
specs: dict[str, ToolSpec] = {}
|
||||
for path in sorted(root.glob("*.yaml")):
|
||||
spec = ToolSpec.from_yaml_path(path)
|
||||
expected_name = path.stem
|
||||
if spec.name != expected_name:
|
||||
raise ToolSpecError(
|
||||
f"{path}: filename stem '{expected_name}' "
|
||||
f"does not match spec.name '{spec.name}'"
|
||||
)
|
||||
if spec.name in specs:
|
||||
raise ToolSpecError(f"duplicate tool spec name: '{spec.name}'")
|
||||
specs[spec.name] = spec
|
||||
|
||||
logger.info(f"Loaded {len(specs)} tool spec(s) from {root}")
|
||||
return specs
|
||||
Reference in New Issue
Block a user