-
Notifications
You must be signed in to change notification settings - Fork 1
feat: implement basic tool system (registry, invocation, results) (#15) #104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
54e1337
e3edf25
88bc06f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| """Tool system — base abstraction, registry, invoker, and errors.""" | ||
|
|
||
| from .base import BaseTool, ToolExecutionResult | ||
| from .errors import ToolError, ToolExecutionError, ToolNotFoundError, ToolParameterError | ||
| from .examples.echo import EchoTool | ||
| from .invoker import ToolInvoker | ||
| from .registry import ToolRegistry | ||
|
|
||
| __all__ = [ | ||
| "BaseTool", | ||
| "EchoTool", | ||
| "ToolError", | ||
| "ToolExecutionError", | ||
| "ToolExecutionResult", | ||
| "ToolInvoker", | ||
| "ToolNotFoundError", | ||
| "ToolParameterError", | ||
| "ToolRegistry", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| """Base tool abstraction and execution result model. | ||
|
|
||
| Defines the ``BaseTool`` ABC that all concrete tools extend, and the | ||
| ``ToolExecutionResult`` value object returned by tool execution. | ||
| """ | ||
|
|
||
| import copy | ||
| from abc import ABC, abstractmethod | ||
| from typing import Any | ||
|
|
||
| from pydantic import BaseModel, ConfigDict, Field | ||
|
|
||
| from ai_company.observability import get_logger | ||
| from ai_company.observability.events import TOOL_BASE_INVALID_NAME | ||
| from ai_company.providers.models import ToolDefinition | ||
|
|
||
| logger = get_logger(__name__) | ||
|
|
||
|
|
||
| class ToolExecutionResult(BaseModel): | ||
| """Result of executing a tool's business logic. | ||
|
|
||
| This is the internal result type returned by ``BaseTool.execute``. | ||
| The invoker converts it into a ``ToolResult`` for the LLM, carrying | ||
| only ``content`` and ``is_error`` — ``metadata`` is not forwarded | ||
| to the LLM and is available only for programmatic consumers. | ||
|
|
||
| Note: | ||
| The ``metadata`` dict is shallowly immutable under the frozen | ||
| model — reassignment is prevented but contents can still be | ||
| mutated. Callers should treat it as read-only. | ||
|
|
||
| Attributes: | ||
| content: Tool output as a string. | ||
| is_error: Whether the execution failed. | ||
| metadata: Optional structured data for programmatic consumers. | ||
| """ | ||
|
|
||
| model_config = ConfigDict(frozen=True) | ||
|
|
||
| content: str = Field(description="Tool output") | ||
| is_error: bool = Field(default=False, description="Whether tool errored") | ||
| metadata: dict[str, Any] = Field( | ||
| default_factory=dict, | ||
| description="Optional structured metadata", | ||
| ) | ||
|
|
||
|
|
||
| class BaseTool(ABC): | ||
| """Abstract base class for all tools in the system. | ||
|
|
||
| Subclasses must implement ``execute`` to define tool behavior. | ||
| The ``to_definition`` method converts the tool into a | ||
| ``ToolDefinition`` suitable for sending to an LLM provider. | ||
|
|
||
| Attributes: | ||
| name: Non-blank tool name. | ||
| description: Human-readable description of the tool. | ||
| parameters_schema: JSON Schema dict describing expected arguments, | ||
| or ``None`` if the tool accepts any arguments. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| *, | ||
| name: str, | ||
| description: str = "", | ||
| parameters_schema: dict[str, Any] | None = None, | ||
| ) -> None: | ||
| """Initialize a tool with name, description, and schema. | ||
|
|
||
| Args: | ||
| name: Non-blank tool name. | ||
| description: Human-readable description. | ||
| parameters_schema: JSON Schema for tool parameters. | ||
|
|
||
| Raises: | ||
| ValueError: If name is empty or whitespace-only. | ||
| """ | ||
| if not name or not name.strip(): | ||
| logger.warning(TOOL_BASE_INVALID_NAME, name=repr(name)) | ||
| msg = "Tool name must not be empty or whitespace-only" | ||
| raise ValueError(msg) | ||
| self._name = name | ||
| self._description = description | ||
| self._parameters_schema: dict[str, Any] | None = ( | ||
| copy.deepcopy(parameters_schema) if parameters_schema is not None else None | ||
| ) | ||
|
|
||
| @property | ||
| def name(self) -> str: | ||
| """Tool name.""" | ||
| return self._name | ||
|
|
||
| @property | ||
| def description(self) -> str: | ||
| """Tool description.""" | ||
| return self._description | ||
|
|
||
| @property | ||
| def parameters_schema(self) -> dict[str, Any] | None: | ||
| """JSON Schema for tool parameters, or None if unspecified. | ||
|
|
||
| Returns a deep copy to prevent mutation of the internal schema. | ||
| """ | ||
| return copy.deepcopy(self._parameters_schema) | ||
|
|
||
| def to_definition(self) -> ToolDefinition: | ||
| """Convert this tool to a ``ToolDefinition`` for LLM providers. | ||
|
|
||
| Returns: | ||
| A ``ToolDefinition`` with name, description, and schema. | ||
| """ | ||
| return ToolDefinition( | ||
| name=self._name, | ||
| description=self._description, | ||
| parameters_schema=self.parameters_schema or {}, | ||
| ) | ||
|
|
||
| @abstractmethod | ||
| async def execute( | ||
| self, | ||
| *, | ||
| arguments: dict[str, Any], | ||
| ) -> ToolExecutionResult: | ||
| """Execute the tool with the given arguments. | ||
|
|
||
| Arguments are pre-validated against the tool's JSON Schema (if | ||
| one is defined) by the ``ToolInvoker`` before reaching this | ||
| method. Implementations with a schema can assume compliance | ||
| when invoked through the invoker; tools without a schema | ||
| receive unvalidated arguments. | ||
|
|
||
| Args: | ||
| arguments: Parsed arguments matching the parameters schema. | ||
|
|
||
| Returns: | ||
| A ``ToolExecutionResult`` with the tool output. | ||
| """ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| """Tool error hierarchy. | ||
|
|
||
| All tool errors carry an immutable context mapping for structured | ||
| metadata. Unlike provider errors, tool errors have no ``is_retryable`` | ||
| flag — retry decisions are made at higher layers. | ||
| """ | ||
|
|
||
| from types import MappingProxyType | ||
| from typing import Any | ||
|
|
||
|
|
||
| class ToolError(Exception): | ||
| """Base exception for all tool-layer errors. | ||
|
|
||
| Attributes: | ||
| message: Human-readable error description. | ||
| context: Immutable metadata about the error (tool name, etc.). | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| message: str, | ||
| *, | ||
| context: dict[str, Any] | None = None, | ||
| ) -> None: | ||
|
Comment on lines
+20
to
+25
|
||
| """Initialize a tool error. | ||
|
|
||
| Args: | ||
| message: Human-readable error description. | ||
| context: Arbitrary metadata about the error. Stored as an | ||
| immutable mapping; defaults to empty if not provided. | ||
| """ | ||
| self.message = message | ||
| self.context: MappingProxyType[str, Any] = MappingProxyType( | ||
| dict(context) if context else {}, | ||
| ) | ||
| super().__init__(message) | ||
|
|
||
| def __str__(self) -> str: | ||
| """Format error with optional context metadata.""" | ||
| if self.context: | ||
| ctx = ", ".join(f"{k}={v!r}" for k, v in self.context.items()) | ||
| return f"{self.message} ({ctx})" | ||
| return self.message | ||
|
|
||
|
|
||
| class ToolNotFoundError(ToolError): | ||
| """Requested tool is not registered in the registry.""" | ||
|
|
||
|
|
||
| class ToolParameterError(ToolError): | ||
| """Tool parameters failed schema validation.""" | ||
|
|
||
|
|
||
| class ToolExecutionError(ToolError): | ||
| """Tool execution raised an unexpected error.""" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| """Echo tool — returns the input message unchanged. | ||
|
|
||
| A minimal reference implementation of ``BaseTool`` useful for testing | ||
| and as a starting point for new tool implementations. | ||
| """ | ||
|
|
||
| from typing import Any | ||
|
|
||
| from ai_company.tools.base import BaseTool, ToolExecutionResult | ||
|
|
||
|
|
||
| class EchoTool(BaseTool): | ||
| """Echoes the input message back as the tool result. | ||
|
|
||
| Examples: | ||
| Basic usage:: | ||
|
|
||
| tool = EchoTool() | ||
| result = await tool.execute(arguments={"message": "hello"}) | ||
| assert result.content == "hello" | ||
| """ | ||
|
|
||
| def __init__(self) -> None: | ||
| """Initialize the echo tool with a fixed schema.""" | ||
| super().__init__( | ||
| name="echo", | ||
| description="Echoes the input message back", | ||
| parameters_schema={ | ||
| "type": "object", | ||
| "properties": {"message": {"type": "string"}}, | ||
| "required": ["message"], | ||
| "additionalProperties": False, | ||
| }, | ||
| ) | ||
|
|
||
| async def execute( | ||
| self, | ||
| *, | ||
| arguments: dict[str, Any], | ||
| ) -> ToolExecutionResult: | ||
| """Return the ``message`` argument as content. | ||
|
|
||
| Args: | ||
| arguments: Must contain a ``message`` key with a string value. | ||
|
|
||
| Returns: | ||
| A ``ToolExecutionResult`` with the message as content. | ||
| """ | ||
| return ToolExecutionResult(content=arguments["message"]) |
Uh oh!
There was an error while loading. Please reload this page.