| layout | default |
|---|---|
| title | Letta Tutorial - Chapter 4: Tool Integration |
| nav_order | 4 |
| has_children | false |
| parent | Letta Tutorial |
Welcome to Chapter 4: Tool Integration. In this part of Letta Tutorial: Stateful LLM Agents, you will build an intuitive mental model first, then move into concrete implementation details and practical production tradeoffs.
Extend agent capabilities with custom tools, functions, and external integrations.
Tools allow agents to interact with the external world - calling APIs, running code, accessing databases, and more. This chapter covers creating and integrating tools with Letta agents.
Letta comes with several built-in tools:
# List available tools
letta list-tools
# Enable tools for an agent
letta update-agent --name researcher --tools web_search,web_scrape,save_file- web_search: Search the web for information
- web_scrape: Extract content from web pages
- save_file: Save content to files
- run_terminal: Execute shell commands
- read_file: Read file contents
- send_email: Send emails (requires SMTP config)
Define custom functions that agents can call:
from letta import create_client
import requests
client = create_client()
# Define a custom tool function
def weather_lookup(city: str) -> str:
"""Get current weather for a city."""
api_key = "your-openweather-api-key"
url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}&units=metric"
response = requests.get(url)
data = response.json()
if response.status_code == 200:
temp = data["main"]["temp"]
description = data["weather"][0]["description"]
return f"Weather in {city}: {temp}°C, {description}"
else:
return f"Could not get weather for {city}"
# Register the tool
client.add_tool(
name="weather_lookup",
func=weather_lookup,
description="Get current weather information for a city"
)Define tool parameters with proper typing:
from typing import Optional
from pydantic import BaseModel
class WeatherParams(BaseModel):
city: str
units: Optional[str] = "metric" # metric, imperial, or kelvin
def weather_lookup(params: WeatherParams) -> str:
"""Get weather with flexible units."""
# Implementation here
pass
# Register with schema
client.add_tool(
name="weather_lookup",
func=weather_lookup,
description="Get current weather for a city",
parameters=WeatherParams
)Enable tools for specific agents:
# Add tools when creating agent
agent = client.create_agent(
name="weather-assistant",
persona="You are a weather assistant who can look up current conditions.",
tools=["weather_lookup"]
)
# Or update existing agent
client.update_agent_tools("weather-assistant", ["weather_lookup", "web_search"])Agents automatically use tools when appropriate:
$ letta chat --name weather-assistant
Human: What's the weather like in Tokyo?
Assistant: I'll check the current weather for Tokyo using the weather lookup tool.
[Using tool: weather_lookup]
Weather in Tokyo: 22°C, clear sky
Assistant: The weather in Tokyo is currently 22°C with clear skies.import sqlite3
from typing import List, Dict, Any
def query_database(query: str) -> List[Dict[str, Any]]:
"""Execute a read-only SQL query."""
conn = sqlite3.connect("company.db")
cursor = conn.cursor()
try:
cursor.execute(query)
columns = [desc[0] for desc in cursor.description]
results = [dict(zip(columns, row)) for row in cursor.fetchall()]
return results
finally:
conn.close()
client.add_tool(
name="query_database",
func=query_database,
description="Execute SQL queries on the company database (read-only)"
)import subprocess
import sys
from typing import Optional
def run_python_code(code: str, timeout: Optional[int] = 30) -> str:
"""Execute Python code safely."""
try:
result = subprocess.run(
[sys.executable, "-c", code],
capture_output=True,
text=True,
timeout=timeout
)
output = result.stdout
if result.stderr:
output += f"\nErrors:\n{result.stderr}"
return output
except subprocess.TimeoutExpired:
return "Code execution timed out"
except Exception as e:
return f"Execution error: {str(e)}"
client.add_tool(
name="run_python_code",
func=run_python_code,
description="Execute Python code and return the output"
)import requests
from typing import Dict, Any, Optional
def call_rest_api(
url: str,
method: str = "GET",
headers: Optional[Dict[str, str]] = None,
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Make HTTP requests to REST APIs."""
try:
response = requests.request(
method=method.upper(),
url=url,
headers=headers or {},
json=data,
timeout=10
)
return {
"status_code": response.status_code,
"headers": dict(response.headers),
"content": response.text,
"json": response.json() if response.headers.get("content-type", "").startswith("application/json") else None
}
except Exception as e:
return {"error": str(e)}
client.add_tool(
name="call_rest_api",
func=call_rest_api,
description="Make HTTP requests to REST APIs"
)Implement safety measures:
def safe_shell_command(command: str) -> str:
"""Execute safe shell commands only."""
# Whitelist of allowed commands
allowed_commands = ["ls", "pwd", "echo", "cat", "head", "tail", "grep"]
cmd_base = command.split()[0]
if cmd_base not in allowed_commands:
return f"Command '{cmd_base}' is not allowed"
# Additional safety checks
if "rm" in command or ">" in command or "|" in command:
return "Potentially dangerous command blocked"
# Execute safely
result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=10)
return result.stdout or result.stderr
client.add_tool(
name="safe_shell",
func=safe_shell_command,
description="Execute safe shell commands from an approved list"
)Help agents understand available tools:
# Get all available tools
tools = client.list_tools()
for tool in tools:
print(f"Tool: {tool.name}")
print(f"Description: {tool.description}")
print(f"Parameters: {tool.parameters}")
print("---")Create complex workflows by combining tools:
def research_topic(topic: str) -> str:
"""Research a topic using multiple tools."""
# Step 1: Search the web
search_results = client.call_tool("web_search", {"query": topic})
# Step 2: Extract key information
key_info = client.call_tool("summarize_text", {"text": search_results})
# Step 3: Save to file
client.call_tool("save_file", {
"filename": f"{topic}_research.txt",
"content": key_info
})
return f"Research completed for {topic}"
client.add_tool(
name="research_topic",
func=research_topic,
description="Complete research workflow: search, summarize, and save"
)Handle tool failures gracefully:
def robust_web_search(query: str) -> str:
"""Web search with fallback."""
try:
result = client.call_tool("web_search", {"query": query})
return result
except Exception as e:
# Fallback to alternative search
try:
return client.call_tool("alternative_search", {"query": query})
except Exception as e2:
return f"Search failed: {str(e)}, fallback also failed: {str(e2)}"
client.add_tool(
name="robust_web_search",
func=robust_web_search,
description="Web search with automatic fallback"
)Create test suites for tool validation:
def test_weather_tool():
"""Test the weather lookup tool."""
result = client.call_tool("weather_lookup", {"city": "London"})
assert "London" in result
assert "°C" in result or "°F" in result
print("Weather tool test passed")
def test_database_tool():
"""Test database query tool."""
result = client.call_tool("query_database", {"query": "SELECT COUNT(*) FROM users"})
assert isinstance(result, list)
assert len(result) > 0
print("Database tool test passed")
# Run tests
test_weather_tool()
test_database_tool()Track tool usage and performance:
import time
def monitored_tool_call(tool_name: str, **kwargs):
"""Monitor tool performance."""
start_time = time.time()
try:
result = client.call_tool(tool_name, kwargs)
duration = time.time() - start_time
# Log performance
client.add_to_archival_memory(
"system",
f"Tool {tool_name} called successfully in {duration:.2f}s"
)
return result
except Exception as e:
duration = time.time() - start_time
client.add_to_archival_memory(
"system",
f"Tool {tool_name} failed after {duration:.2f}s: {str(e)}"
)
raise- Security First: Always validate inputs and restrict dangerous operations
- Error Handling: Implement robust error handling and fallbacks
- Documentation: Provide clear descriptions and parameter documentation
- Testing: Thoroughly test tools before production use
- Monitoring: Track tool performance and usage patterns
- Modularity: Keep tools focused on single responsibilities
- Versioning: Version tools and handle backward compatibility
Next: Manage long-running conversations and context.
Most teams struggle here because the hard part is not writing more code, but deciding clear boundaries for client, result, weather so behavior stays predictable as complexity grows.
In practical terms, this chapter helps you avoid three common failures:
- coupling core logic too tightly to one implementation path
- missing the handoff boundaries between setup, execution, and validation
- shipping changes without clear rollback or observability strategy
After working through this chapter, you should be able to reason about Chapter 4: Tool Integration as an operating subsystem inside Letta Tutorial: Stateful LLM Agents, with explicit contracts for inputs, state transitions, and outputs.
Use the implementation notes around tool, description, name as your checklist when adapting these patterns to your own repository.
Under the hood, Chapter 4: Tool Integration usually follows a repeatable control path:
- Context bootstrap: initialize runtime config and prerequisites for
client. - Input normalization: shape incoming data so
resultreceives stable contracts. - Core execution: run the main logic branch and propagate intermediate state through
weather. - Policy and safety checks: enforce limits, auth scopes, and failure boundaries.
- Output composition: return canonical result payloads for downstream consumers.
- Operational telemetry: emit logs/metrics needed for debugging and performance tuning.
When debugging, walk this sequence in order and confirm each stage has explicit success/failure conditions.
Use the following upstream sources to verify implementation details while reading this chapter:
- View Repo
Why it matters: authoritative reference on
View Repo(github.com). - Awesome Code Docs
Why it matters: authoritative reference on
Awesome Code Docs(github.com).
Suggested trace strategy:
- search upstream code for
clientandresultto map concrete implementation paths - compare docs claims against actual runtime/config code before reusing patterns in production