Skip to content

Commit 27986cd

Browse files
feat(tools): add YAML tool registry and sample echo tool
1 parent 15561b8 commit 27986cd

7 files changed

Lines changed: 106 additions & 4 deletions

File tree

TASKS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ Starter kit for building test-first, easily deployable AI agents.
1313
- [x] Flesh out `README.md` with badges, quick-start guide, and differentiators
1414
- [x] Add GitHub Actions release workflow to publish to PyPI & NPM on version tags
1515
- [x] Bump working version to `0.1.0.dev0` (Python) and `0.1.0-dev` (NPM)
16+
- [x] YAML tool registry with auto-import & CLI stub generation
1617

1718
## In Progress Tasks
1819

19-
- [ ] Implement core ReAct-style agent loop (`mini_agent_harness.core.loop`) **(initial echo LLM completed; full tool loop pending)**
20+
- [ ] Implement core ReAct-style agent loop (`mini_agent_harness.core.loop`) — partial (tool registry wired, reasoning loop pending)
2021

2122
## Future Tasks
2223

23-
- [ ] YAML tool registry with auto-import & CLI stub generation
2424
- [ ] `mini-agent serve` FastAPI server with streaming SSE endpoint
2525
- [ ] React webapp (`webapp/`) with shadcn/ui chat + playground
2626
- [ ] Testing harness Phase 1: DeepEval metrics and Guardrails schema checks

agents/quickstart.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
name: Quickstart Agent
22
description: "A minimal agent spec for the Mini Agent Harness demo."
3-
tools: []
3+
tools:
4+
- tools/echo.yaml

mini_agent_harness/core/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from dataclasses import dataclass
99

1010
from .llm import get_default_llm, LLM
11+
from ..tools import load_tools_from_manifest
1112

1213

1314
@dataclass
@@ -31,6 +32,7 @@ class Agent:
3132
def __init__(self, manifest: dict, llm: LLM | None = None):
3233
self.manifest = manifest
3334
self.llm: LLM = llm or get_default_llm()
35+
self.tools = load_tools_from_manifest(manifest)
3436

3537
def run(self, input_text: str) -> AgentResult:
3638
"""Execute the agent for a single user prompt.
@@ -40,5 +42,13 @@ def run(self, input_text: str) -> AgentResult:
4042
and tool execution steps.
4143
"""
4244

45+
# Naive tool invocation: if the prompt starts with "<tool>: " call it
46+
if ":" in input_text:
47+
tool_name, _, arg = input_text.partition(":")
48+
tool_name = tool_name.strip()
49+
if tool_name in self.tools:
50+
result = self.tools[tool_name](arg.strip())
51+
return AgentResult(response_text=str(result))
52+
4353
output = self.llm.generate(input_text)
4454
return AgentResult(response_text=output)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Dynamic tool registry.
2+
3+
Tools are defined by YAML spec files and backed by Python callables.
4+
Each tool spec looks like:
5+
6+
```yaml
7+
name: echo
8+
description: Echo the user input
9+
python: mini_agent_harness.tools.echo:echo_tool
10+
```
11+
12+
At runtime we import the callable and expose it via `load_tools`.
13+
"""
14+
from __future__ import annotations
15+
16+
import importlib
17+
import inspect
18+
from dataclasses import dataclass
19+
from pathlib import Path
20+
from typing import Callable, Dict, Iterable, List
21+
22+
import yaml
23+
24+
25+
@dataclass
26+
class ToolSpec:
27+
name: str
28+
description: str
29+
python: str # module:path e.g. path.to.mod:function_name
30+
func: Callable | None = None # populated after import
31+
32+
def import_func(self) -> Callable:
33+
if self.func is not None:
34+
return self.func
35+
module_path, _, attr = self.python.partition(":")
36+
if not module_path or not attr:
37+
raise ValueError(f"Invalid python path in tool spec: {self.python}")
38+
module = importlib.import_module(module_path)
39+
func: Callable = getattr(module, attr)
40+
if not callable(func):
41+
raise TypeError(f"Tool target {self.python} is not callable")
42+
# Basic signature check: first param should be str or none
43+
sig = inspect.signature(func)
44+
if len(sig.parameters) == 0:
45+
raise TypeError("Tool callable must accept at least one argument (input string)")
46+
self.func = func
47+
return func
48+
49+
50+
def _load_tool_yaml(path: Path) -> ToolSpec:
51+
data = yaml.safe_load(path.read_text())
52+
return ToolSpec(**data)
53+
54+
55+
def discover_tools(tool_paths: Iterable[str | Path]) -> Dict[str, ToolSpec]:
56+
"""Load multiple tool YAMLs into a registry keyed by tool name."""
57+
registry: Dict[str, ToolSpec] = {}
58+
for p in tool_paths:
59+
spec = _load_tool_yaml(Path(p))
60+
if spec.name in registry:
61+
raise ValueError(f"Duplicate tool name {spec.name}")
62+
registry[spec.name] = spec
63+
return registry
64+
65+
66+
def load_tools_from_manifest(manifest: dict) -> Dict[str, Callable]:
67+
"""Given the agent manifest dict, import and return callable tools."""
68+
tools_field: List[str] = manifest.get("tools", [])
69+
registry = discover_tools(tools_field)
70+
return {name: spec.import_func() for name, spec in registry.items()}
71+
72+
73+
__all__ = [
74+
"ToolSpec",
75+
"discover_tools",
76+
"load_tools_from_manifest",
77+
]

mini_agent_harness/tools/echo.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Sample tool that echoes the input."""
2+
3+
def echo_tool(text: str) -> str:
4+
"""Return the text unchanged (demo tool)."""
5+
return text

tests/test_quickstart.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,11 @@
55

66

77
def test_quickstart_echo():
8+
# direct llm path
89
result = quickstart_agent.run("Hello")
9-
assert "[ECHO]" in result.response_text
10+
assert "[ECHO]" in result.response_text
11+
12+
13+
def test_tool_invocation():
14+
result = quickstart_agent.run("echo: hi there")
15+
assert result.response_text == "hi there"

tools/echo.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name: echo
2+
description: Echo the input string back.
3+
python: mini_agent_harness.tools.echo:echo_tool

0 commit comments

Comments
 (0)