54 lines
1.6 KiB
Python
54 lines
1.6 KiB
Python
"""
|
|
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
|