Create a tool with the @tool decorator:
from reminix_runtime import tool, serve
@tool
async def get_weather(location: str, units: str = "celsius") -> dict:
"""Get the current weather for a city."""
return {
"location": location,
"temperature": 22,
"units": units,
"condition": "sunny"
}
serve(tools=[get_weather], port=8080)
The decorator automatically extracts:
- Name: From the function name (
get_weather)
- Description: From the docstring (first paragraph)
- Parameters: From type hints and default values
- Parameter descriptions: From docstring
Args: section (Google, NumPy, or Sphinx style)
- Output schema: From the return type hint (supports Pydantic, TypedDict, and basic types)
Custom Name and Description
Override the defaults:
@tool(name="weather", description="Fetch current weather data for any city")
async def get_weather(location: str) -> dict:
"""This docstring is ignored when description is provided."""
return {"location": location, "temp": 22}
Sync vs Async
Both sync and async functions work:
# Async tool
@tool
async def fetch_data(url: str) -> dict:
"""Fetch data from a URL."""
# Use async HTTP client
return {"data": "..."}
# Sync tool
@tool
def calculate(a: float, b: float, operation: str) -> dict:
"""Perform basic math operations."""
if operation == "add":
return {"result": a + b}
elif operation == "multiply":
return {"result": a * b}
return {"error": f"Unknown operation: {operation}"}
Type Hints and Schema
Type hints are converted to JSON Schema:
@tool
def process(
text: str, # type: "string", required
count: int, # type: "integer", required
score: float, # type: "number", required
enabled: bool, # type: "boolean", required
tags: list, # type: "array", required
metadata: dict, # type: "object", required
limit: int = 10, # type: "integer", optional (has default)
) -> dict:
"""Process data with various types."""
return {"processed": True}
Generated schema:
{
"properties": {
"text": { "type": "string" },
"count": { "type": "integer" },
"score": { "type": "number" },
"enabled": { "type": "boolean" },
"tags": { "type": "array" },
"metadata": { "type": "object" },
"limit": { "type": "integer", "default": 10 }
},
"required": ["text", "count", "score", "enabled", "tags", "metadata"]
}
Parameter Descriptions from Docstrings
Parameter descriptions are automatically extracted from docstrings (Google, NumPy, or Sphinx style):
@tool
def search_products(
query: str,
category: str = "all",
limit: int = 10
) -> dict:
"""Search for products in the catalog.
Args:
query: Search keywords or product name
category: Product category to filter by
limit: Maximum number of results to return
"""
return {"results": [...]}
Generated input schema:
{
"properties": {
"query": {
"type": "string",
"description": "Search keywords or product name"
},
"category": {
"type": "string",
"description": "Product category to filter by",
"default": "all"
},
"limit": {
"type": "integer",
"description": "Maximum number of results to return",
"default": 10
}
},
"required": ["query"]
}
Supported docstring formats: Google style (Args:), NumPy style (Parameters), and Sphinx style (:param name:).
Generic Types
Use parameterized generics for more specific schemas:
@tool
def search(
query: str,
filters: dict[str, str], # type: "object"
results: list[str], # type: "array" with items: "string"
) -> list[dict]:
"""Search with typed parameters."""
return [{"match": query}]
Error Handling
Return errors as part of the output or raise exceptions:
@tool
def divide(a: float, b: float) -> dict:
"""Divide two numbers."""
if b == 0:
# Return error in output
return {"error": "Cannot divide by zero"}
return {"result": a / b}
@tool
def validate(data: dict) -> dict:
"""Validate data with exception."""
if "required_field" not in data:
# Raise exception (caught and returned as error)
raise ValueError("Missing required_field")
return {"valid": True}
When an exception is raised, the response includes an error field:
{
"output": null,
"error": "Missing required_field"
}
Output Schema
Return type hints become the output schema in metadata. For rich schemas with property details, use Pydantic models or TypedDict.
Pydantic Models (Recommended)
Pydantic models provide full schema generation with descriptions and validation:
from pydantic import BaseModel, Field
from reminix_runtime import tool
class UserOutput(BaseModel):
"""User information."""
id: str = Field(description="Unique user identifier")
name: str = Field(description="Display name")
email: str = Field(description="Email address")
@tool
def get_user(user_id: str) -> UserOutput:
"""Get user information."""
return UserOutput(id=user_id, name="John", email="john@example.com")
Generated output schema:
{
"output": {
"type": "object",
"properties": {
"id": { "type": "string", "description": "Unique user identifier" },
"name": { "type": "string", "description": "Display name" },
"email": { "type": "string", "description": "Email address" }
},
"required": ["id", "name", "email"]
}
}
TypedDict (Simpler Alternative)
For simpler cases without validation, use TypedDict:
from typing import TypedDict
from reminix_runtime import tool
class WeatherOutput(TypedDict):
location: str
temperature: int
condition: str
@tool
def get_weather(location: str) -> WeatherOutput:
"""Get weather for a location."""
return {"location": location, "temperature": 72, "condition": "sunny"}
Generated output schema:
{
"output": {
"type": "object",
"properties": {
"location": { "type": "string" },
"temperature": { "type": "integer" },
"condition": { "type": "string" }
},
"required": ["location", "temperature", "condition"]
}
}
Basic Types
For simple returns, basic types work fine:
@tool
def echo(text: str) -> str:
"""Echo the input."""
return text
@tool
def count_words(text: str) -> int:
"""Count words in text."""
return len(text.split())
Using -> dict without Pydantic or TypedDict results in {"type": "object"} with no property details. Use Pydantic or TypedDict for rich schemas.
Serve one or more tools:
from reminix_runtime import tool, serve
@tool
def tool_a(x: str) -> dict:
"""Tool A."""
return {"a": x}
@tool
def tool_b(y: int) -> dict:
"""Tool B."""
return {"b": y}
serve(tools=[tool_a, tool_b], port=8080)
API Endpoints
When tools are served, these endpoints are available:
| Endpoint | Method | Description |
|---|
/health | GET | Health check |
/info | GET | Discovery (lists all tools with schemas) |
/tools/{name}/call | POST | Call a tool |
curl -X POST http://localhost:8080/tools/get_weather/call \
-H "Content-Type: application/json" \
-d '{"input": {"location": "San Francisco", "units": "fahrenheit"}}'
Response:
{
"output": {
"location": "San Francisco",
"temperature": 65,
"units": "fahrenheit",
"condition": "foggy"
}
}
Discovery
curl http://localhost:8080/info
Response:
{
"tools": [
{
"name": "get_weather",
"type": "tool",
"description": "Get the current weather for a city.",
"input": {
"type": "object",
"properties": {
"location": { "type": "string" },
"units": { "type": "string", "default": "fahrenheit" }
},
"required": ["location"]
},
"output": { "type": "object" }
}
]
}
With Context
Access request context in tool execution:
@tool
async def secure_action(resource_id: str) -> dict:
"""Perform a secure action."""
# Context is passed via the execute request
# Access it through the request object in custom implementations
return {"resource": resource_id, "status": "completed"}
Complete Example
"""
Runtime Tools Example
Usage:
uv run python main.py
Test endpoints:
curl http://localhost:8080/health
curl http://localhost:8080/info
curl -X POST http://localhost:8080/tools/get_weather/call \
-H "Content-Type: application/json" \
-d '{"input": {"location": "San Francisco"}}'
"""
from pydantic import BaseModel, Field
from reminix_runtime import serve, tool
# Define output schemas with Pydantic
class WeatherOutput(BaseModel):
"""Weather information for a location."""
location: str = Field(description="City name")
temperature: int = Field(description="Current temperature")
units: str = Field(description="Temperature units")
condition: str = Field(description="Weather condition")
class CalculationOutput(BaseModel):
"""Result of a math calculation."""
a: float = Field(description="First operand")
b: float = Field(description="Second operand")
operation: str = Field(description="Operation performed")
result: float = Field(description="Calculation result")
WEATHER_DATA = {
"san francisco": {"temp": 65, "condition": "foggy"},
"new york": {"temp": 45, "condition": "cloudy"},
"los angeles": {"temp": 75, "condition": "sunny"},
}
@tool
async def get_weather(location: str, units: str = "fahrenheit") -> WeatherOutput:
"""Get the current weather for a city.
Args:
location: City name (e.g., "San Francisco", "New York")
units: Temperature units - "celsius" or "fahrenheit"
"""
weather = WEATHER_DATA.get(location.lower())
if not weather:
raise ValueError(f'Weather data not available for "{location}"')
temp = weather["temp"]
if units == "celsius":
temp = round((temp - 32) * 5 / 9)
return WeatherOutput(
location=location,
temperature=temp,
units=units,
condition=weather["condition"],
)
@tool
def calculate(a: float, b: float, operation: str) -> CalculationOutput:
"""Perform basic math operations.
Args:
a: First operand
b: Second operand
operation: Math operation - "add", "subtract", "multiply", or "divide"
"""
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
elif operation == "multiply":
result = a * b
elif operation == "divide":
if b == 0:
raise ValueError("Cannot divide by zero")
result = a / b
else:
raise ValueError(f"Unknown operation: {operation}")
return CalculationOutput(a=a, b=b, operation=operation, result=result)
if __name__ == "__main__":
serve(tools=[get_weather, calculate], port=8080)
Next Steps