Agent skill
tools
Use when implementing function calling, tool use, or agents with LLMs - unified tool API works across OpenAI, Anthropic, Google, and Ollama with consistent tool definition and execution patterns
Install this agent skill to your Project
npx add-skill https://github.com/juanre/llmring/tree/main/skills/tools
SKILL.md
Function Calling and Tool Use
Installation
# With uv (recommended)
uv add llmring
# With pip
pip install llmring
Provider SDKs (install what you need):
uv add openai>=1.0 # OpenAI
uv add anthropic>=0.67 # Anthropic
uv add google-genai # Google Gemini
uv add ollama>=0.4 # Ollama (prompt-based tools)
API Overview
This skill covers:
- Tool definition format (JSON Schema)
toolsparameter in LLMRequesttool_choiceparameter for controlling tool selectiontool_callsin LLMResponse- Multi-turn tool execution pattern
- Tool result messages
Quick Start
from llmring import LLMRing, LLMRequest, Message
# Define tools
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}
}
}]
async with LLMRing() as service:
request = LLMRequest(
model="tool-user", # Your alias for tool-using tasks
messages=[Message(role="user", content="What's the weather in NYC?")],
tools=tools
)
response = await service.chat(request)
# Check if model wants to call a tool
if response.tool_calls:
tool_call = response.tool_calls[0]
print(f"Tool: {tool_call['function']['name']}")
print(f"Args: {tool_call['function']['arguments']}")
Complete API Documentation
Tool Definition Format
Tools use JSON Schema format for function definitions.
Structure:
tool = {
"type": "function",
"function": {
"name": str, # Function name
"description": str, # What the function does
"parameters": { # JSON Schema for parameters
"type": "object",
"properties": {
"param_name": {
"type": str, # "string", "number", "boolean", "array", "object"
"description": str,
"enum": List[str] # Optional: allowed values
}
},
"required": List[str] # Required parameter names
}
}
}
Example:
get_weather_tool = {
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name, e.g. 'New York' or 'London'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}
}
}
LLMRequest with Tools
Parameters:
tools(list, optional): List of available toolstool_choice(str/dict, optional): Control tool selection"auto": Model decides whether to use tools (default)"none": Force no tool use"required": Force tool use (OpenAI) /"any"(Anthropic equivalent){"type": "function", "function": {"name": "tool_name"}}: Force specific tool
Note: Provider differences - OpenAI uses "required", Anthropic uses "any" for the same behavior. LLMRing handles the translation automatically.
Example:
from llmring import LLMRequest, Message
# Let model decide
request = LLMRequest(
model="tool-user", # Your alias for tool-using tasks
messages=[Message(role="user", content="What's the weather?")],
tools=[get_weather_tool]
)
# Force tool use
request = LLMRequest(
model="tool-user", # Your alias for tool-using tasks
messages=[Message(role="user", content="Check NYC weather")],
tools=[get_weather_tool],
tool_choice="required" # Must use a tool
)
# Force specific tool
request = LLMRequest(
model="tool-user", # Your alias for tool-using tasks
messages=[Message(role="user", content="Get weather")],
tools=[get_weather_tool, search_tool],
tool_choice={
"type": "function",
"function": {"name": "get_weather"}
}
)
# Disable tools
request = LLMRequest(
model="tool-user", # Your alias for tool-using tasks
messages=[Message(role="user", content="Just chat")],
tools=[get_weather_tool],
tool_choice="none" # Don't use tools
)
Tool Calls in Response
When the model wants to call a tool, response.tool_calls is populated.
Structure:
tool_call = {
"id": str, # Unique tool call ID
"type": "function",
"function": {
"name": str, # Function name
"arguments": str # JSON string of arguments
}
}
Example:
response = await service.chat(request)
if response.tool_calls:
for tool_call in response.tool_calls:
tool_name = tool_call["function"]["name"]
tool_args = json.loads(tool_call["function"]["arguments"])
tool_id = tool_call["id"]
print(f"Tool: {tool_name}")
print(f"Arguments: {tool_args}")
Tool Result Messages
After executing a tool, send the result back with role="tool".
Structure:
tool_result = Message(
role="tool",
content: str, # Tool execution result (JSON string)
tool_call_id: str # ID from the tool_call
)
Example:
import json
from llmring import Message
# Execute tool
tool_result = {"temperature": 72, "condition": "sunny"}
# Send result back
result_message = Message(
role="tool",
content=json.dumps(tool_result),
tool_call_id=tool_call["id"]
)
Complete Tool Execution Pattern
import json
from llmring import LLMRing, LLMRequest, Message
def get_weather(location: str, unit: str = "fahrenheit") -> dict:
"""Mock weather function."""
return {
"location": location,
"temperature": 72,
"unit": unit,
"condition": "sunny"
}
# Define tools
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}
}
}]
async with LLMRing() as service:
messages = [
Message(role="user", content="What's the weather in San Francisco?")
]
# First request: Model decides to use tool
request = LLMRequest(model="tool-user", # Your alias for tool-using tasks messages=messages, tools=tools)
response = await service.chat(request)
# Add assistant's tool call to history
messages.append(Message(
role="assistant",
content=response.content or "",
tool_calls=response.tool_calls
))
# Execute tools
if response.tool_calls:
for tool_call in response.tool_calls:
# Parse arguments
args = json.loads(tool_call["function"]["arguments"])
# Execute function
result = get_weather(**args)
# Add tool result to messages
messages.append(Message(
role="tool",
content=json.dumps(result),
tool_call_id=tool_call["id"]
))
# Second request: Model uses tool results to answer
request = LLMRequest(model="tool-user", # Your alias for tool-using tasks messages=messages, tools=tools)
response = await service.chat(request)
print(response.content)
# "The weather in San Francisco is 72°F and sunny."
Common Patterns
Multiple Tools
from llmring import LLMRing, LLMRequest, Message
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
}
}
},
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web for information",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"}
},
"required": ["query"]
}
}
}
]
async with LLMRing() as service:
request = LLMRequest(
model="tool-user", # Your alias for tool-using tasks
messages=[Message(role="user", content="Weather in Paris and latest news")],
tools=tools
)
response = await service.chat(request)
# Model may call multiple tools
if response.tool_calls:
print(f"Model wants to call {len(response.tool_calls)} tools")
Tool Execution Loop
import json
from llmring import LLMRing, LLMRequest, Message
# Available functions
FUNCTIONS = {
"get_weather": lambda location: {"temp": 72, "condition": "sunny"},
"search_web": lambda query: {"results": ["Result 1", "Result 2"]}
}
async with LLMRing() as service:
messages = [
Message(role="user", content="What's the weather in NYC?")
]
tools = [...] # Your tool definitions
# Loop until model stops calling tools
while True:
request = LLMRequest(model="tool-user", # Your alias for tool-using tasks messages=messages, tools=tools)
response = await service.chat(request)
# Add assistant response
messages.append(Message(
role="assistant",
content=response.content or "",
tool_calls=response.tool_calls
))
# If no tool calls, we're done
if not response.tool_calls:
break
# Execute each tool call
for tool_call in response.tool_calls:
func_name = tool_call["function"]["name"]
args = json.loads(tool_call["function"]["arguments"])
# Execute function
result = FUNCTIONS[func_name](**args)
# Add result
messages.append(Message(
role="tool",
content=json.dumps(result),
tool_call_id=tool_call["id"]
))
# Final response
print(response.content)
Parallel Tool Calls
Some models can call multiple tools in parallel:
import json
from llmring import LLMRing, LLMRequest, Message
async with LLMRing() as service:
request = LLMRequest(
model="tool-user", # Your alias for tool-using tasks
messages=[Message(role="user", content="Weather in NYC and Paris")],
tools=tools
)
response = await service.chat(request)
# Model may request multiple tool calls at once
if response.tool_calls:
for tool_call in response.tool_calls:
print(f"Tool: {tool_call['function']['name']}")
print(f"Args: {tool_call['function']['arguments']}")
# Execute all tools, then send all results back
Streaming with Tools
from llmring import LLMRing, LLMRequest, Message
async with LLMRing() as service:
request = LLMRequest(
model="tool-user", # Your alias for tool-using tasks
messages=[Message(role="user", content="What's the weather?")],
tools=tools
)
tool_calls_accumulated = []
async for chunk in service.chat_stream(request):
print(chunk.delta, end="", flush=True)
# Accumulate tool calls (streamed incrementally)
if chunk.tool_calls:
tool_calls_accumulated = chunk.tool_calls
# After streaming, check for tool calls
if tool_calls_accumulated:
print("\nModel wants to call tools")
Error Handling in Tools
import json
from llmring import LLMRing, LLMRequest, Message
def execute_tool_safely(func_name: str, args: dict) -> dict:
"""Execute tool with error handling."""
try:
result = FUNCTIONS[func_name](**args)
return {"success": True, "data": result}
except Exception as e:
return {"success": False, "error": str(e)}
async with LLMRing() as service:
# ... after getting tool_calls ...
for tool_call in response.tool_calls:
func_name = tool_call["function"]["name"]
args = json.loads(tool_call["function"]["arguments"])
# Execute with error handling
result = execute_tool_safely(func_name, args)
# Send result (including errors)
messages.append(Message(
role="tool",
content=json.dumps(result),
tool_call_id=tool_call["id"]
))
Provider Support
| Provider | Tool Support | Notes |
|---|---|---|
| OpenAI | Native | Full support, parallel calls |
| Anthropic | Native | Full support |
| Native | Full support | |
| Ollama | Prompt-based | Tools via prompt engineering |
Note: Ollama uses prompt-based tool calling. LLMRing handles the adaptation automatically.
Common Mistakes
Wrong: Not Including Assistant Message with tool_calls
# DON'T DO THIS - skip assistant message
if response.tool_calls:
for tool_call in response.tool_calls:
result = execute_tool(tool_call)
messages.append(Message(role="tool", content=result))
# Missing assistant message!
Right: Include Assistant Message
# DO THIS - add assistant message with tool_calls
messages.append(Message(
role="assistant",
content=response.content or "",
tool_calls=response.tool_calls # Include this!
))
# Then add tool results
for tool_call in response.tool_calls:
result = execute_tool(tool_call)
messages.append(Message(
role="tool",
content=json.dumps(result),
tool_call_id=tool_call["id"]
))
Wrong: Forgetting tool_call_id
# DON'T DO THIS - no tool_call_id
messages.append(Message(
role="tool",
content=json.dumps(result)
))
Right: Include tool_call_id
# DO THIS - include tool_call_id
messages.append(Message(
role="tool",
content=json.dumps(result),
tool_call_id=tool_call["id"] # Required!
))
Wrong: Tool Result Not JSON String
# DON'T DO THIS - Python dict
result = {"temperature": 72}
messages.append(Message(
role="tool",
content=result, # Should be string!
tool_call_id=tool_id
))
Right: JSON String
# DO THIS - JSON string
result = {"temperature": 72}
messages.append(Message(
role="tool",
content=json.dumps(result), # Convert to string
tool_call_id=tool_id
))
Wrong: Poor Tool Descriptions
# DON'T DO THIS - vague description
{
"name": "get_data",
"description": "Gets data",
"parameters": {...}
}
Right: Clear Descriptions
# DO THIS - specific, actionable description
{
"name": "get_weather",
"description": "Get the current weather conditions for a specific city, including temperature and general conditions",
"parameters": {...}
}
Best Practices
- Clear tool descriptions: Model uses descriptions to decide when to call tools
- Include tool_calls in assistant message: Required for proper conversation flow
- Always include tool_call_id: Links results to requests
- Use JSON strings for results: Tool content must be string, not dict
- Handle errors gracefully: Return error info as JSON to let model respond
- Validate tool arguments: Parse and validate before executing
- Loop until done: Continue conversation until no more tool calls
Related Skills
llmring-chat- Basic chat without toolsllmring-streaming- Streaming tool callsllmring-structured- Combine tools with structured outputllmring-lockfile- Configure models for tool usellmring-providers- Provider-specific tool features
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
providers
Use when switching between LLM providers, accessing provider-specific features (Anthropic caching, OpenAI logprobs), or using raw SDK clients - covers multi-provider patterns and direct SDK access for OpenAI, Anthropic, Google, and Ollama
streaming
Use when building real-time chat interfaces, displaying incremental LLM responses, or streaming output from OpenAI, Anthropic, Google, or Ollama - async iteration with usage tracking works across all providers
lockfile
Use when creating llmring.lock file for new project (REQUIRED for all applications), configuring model aliases with semantic task-based names, managing environment-specific profiles (dev/staging/prod), or setting up fallback models - lockfile creation is mandatory first step, bundled lockfile is only for llmring tools
chat
Use when starting a new project with llmring, building an application using LLMs, making basic chat completions, or sending messages to OpenAI, Anthropic, Google, or Ollama - covers lockfile creation (MANDATORY first step), semantic alias usage, unified interface for all providers with consistent message structure and response handling
structured-output
Use when extracting structured data from LLMs, parsing JSON responses, or enforcing output schemas - unified JSON schema API works across OpenAI, Anthropic, Google, and Ollama with automatic validation and parsing
edit-article
Edit and improve articles by restructuring sections, improving clarity, and tightening prose. Use when user wants to edit, revise, or improve an article draft.
Didn't find tool you were looking for?