
Dspy Tools
- 6 installs
- 11 repo stars
- Updated June 28, 2026
- lebsral/dspy-programming-not-prompting-lms-skills
Helps with ai & agent building tasks.
About
dspy-tools is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- dspy-tools
- AI & Agent Building
- AI-coding skill
Dspy Tools by the numbers
- 6 all-time installs (skills.sh)
- Ranked #12,825 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/lebsral/dspy-programming-not-prompting-lms-skills --skill dspy-toolsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6 |
|---|---|
| repo stars | ★ 11 |
| Last updated | June 28, 2026 |
| Repository | lebsral/dspy-programming-not-prompting-lms-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Give Agents Tool-Calling Abilities with dspy.Tool
Guide the user through wrapping functions as DSPy tools, using dspy.PythonInterpreter for sandboxed code execution, and wiring tools into agents with dspy.ReAct and dspy.CodeAct.
Step 1: What kind of tool integration?
Ask the user before diving in:
1. What does your tool do? Search, calculate, call an API, query a database, execute code? 2. Which agent will use it? ReAct (tool calling) or CodeAct (code generation)? If unsure, start with ReAct. 3. Do you need sandboxed execution? If the agent generates and runs arbitrary code, use PythonInterpreter. Otherwise, plain tool functions are simpler.
Then jump to the relevant section below.
What is dspy.Tool
dspy.Tool wraps a Python function so DSPy agents can call it. It automatically extracts the function's name, docstring, parameter types, and descriptions to build the tool schema that the LM sees.
You can pass plain functions directly to dspy.ReAct or dspy.CodeAct and DSPy wraps them for you. Use dspy.Tool explicitly when you need to override the inferred metadata, convert tools from LangChain or MCP, or inspect the generated schema.
import dspy
# Implicit -- pass a function directly (DSPy wraps it automatically)
agent = dspy.ReAct("question -> answer", tools=[my_search_function])
# Explicit -- wrap it yourself for control over name, description, etc.
tool = dspy.Tool(my_search_function, name="search", desc="Search the knowledge base")
agent = dspy.ReAct("question -> answer", tools=[tool])dspy.Tool constructor
dspy.Tool(
func, # Callable -- the function to wrap
name=None, # str | None -- tool name (inferred from func.__name__ if omitted)
desc=None, # str | None -- description (inferred from docstring if omitted)
args=None, # dict | None -- argument JSON schemas (inferred from type hints)
arg_types=None, # dict | None -- argument type mappings (inferred from type hints)
arg_desc=None, # dict | None -- per-argument descriptions
)All parameters except func are optional. DSPy infers them from the function signature and docstring. Override them when the inferred values are wrong or when you want a different name or description.
def foo(x: int, y: str = "hello"):
"""Combine a number and a string."""
return str(x) + y
tool = dspy.Tool(foo)
print(tool.name) # "foo"
print(tool.desc) # "Combine a number and a string."
print(tool.args) # {'x': {'type': 'integer'}, 'y': {'type': 'string', 'default': 'hello'}}Wrapping functions as tools
The quality of your tools depends on three things: type hints, docstrings, and focused scope.
Type hints tell the agent what to pass
DSPy reads type hints to build the JSON schema the LM uses for tool calling. Always annotate every parameter and the return type.
# Good -- fully typed
def search(query: str, max_results: int = 5) -> str:
"""Search the knowledge base for documents matching the query."""
...
# Bad -- no type hints, agent won't know what to pass
def search(query, max_results=5):
"""Search the knowledge base."""
...Docstrings tell the agent when to use it
The docstring becomes the tool description. Write it from the perspective of someone deciding whether to call this tool.
# Good -- explains what the tool does and when to use it
def lookup_user(email: str) -> str:
"""Look up a user account by email address. Returns name, plan, and join date."""
...
# Bad -- vague, doesn't help the agent decide
def lookup_user(email: str) -> str:
"""Get user info."""
...One tool, one job
Keep tools focused. A tool that searches and summarizes is harder for the agent to use than two separate tools.
# Good -- single responsibility
def search(query: str) -> str:
"""Search for documents matching the query."""
...
def summarize(text: str) -> str:
"""Summarize a long piece of text into key points."""
...
# Bad -- does two things
def search_and_summarize(query: str) -> str:
"""Search for documents and summarize the results."""
...Return strings
Tool return values become the Observation the agent sees. Return a string (or something that converts to string cleanly).
import json
def check_order(order_id: str) -> str:
"""Check the status of an order by its ID."""
order = db.get_order(order_id)
if order:
return json.dumps(order)
return f"No order found with ID {order_id}."Per-argument descriptions
For complex tools, add per-argument descriptions using arg_desc:
tool = dspy.Tool(
search,
arg_desc={
"query": "The search query -- use keywords, not full sentences",
"max_results": "Maximum number of results to return (1-20)",
},
)dspy.PythonInterpreter
dspy.PythonInterpreter runs Python code in a sandboxed Deno + Pyodide environment. By default, the sandbox has no filesystem, network, or environment access. You selectively enable what you need.
Constructor
dspy.PythonInterpreter(
deno_command=None, # list[str] | None -- custom Deno launch command
enable_read_paths=None, # list[str] | None -- paths the sandbox can read
enable_write_paths=None, # list[str] | None -- paths the sandbox can write
enable_env_vars=None, # list[str] | None -- environment variables to expose
enable_network_access=None, # list[str] | None -- allowed network domains
sync_files=True, # bool -- sync file changes back to host
tools=None, # dict[str, Callable] | None -- host-side tool functions
output_fields=None, # list[dict] | None -- output field definitions
)Prerequisites: Deno must be installed. See https://docs.deno.com/runtime/getting_started/installation/
Basic execution
from dspy import PythonInterpreter
with PythonInterpreter() as interp:
result = interp("print(1 + 2)") # Returns "3"With host-side tools
Tools passed to PythonInterpreter run in your normal Python process (not the sandbox). The sandbox calls them via JSON-RPC. This lets tools access databases, APIs, and libraries that aren't available inside the sandbox.
def fetch_price(ticker: str) -> str:
"""Fetch the current stock price for a ticker symbol."""
import requests
resp = requests.get(f"https://api.example.com/price/{ticker}")
return resp.json()["price"]
with PythonInterpreter(tools={"fetch_price": fetch_price}) as interp:
result = interp("price = fetch_price(ticker='AAPL')\nprint(f'Price: {price}')")Selective permissions
# Allow reading from a data directory and accessing one API
interp = PythonInterpreter(
enable_read_paths=["./data"],
enable_network_access=["api.example.com"],
)Using PythonInterpreter with CodeAct
dspy.CodeAct creates a PythonInterpreter automatically if you don't pass one. Pass your own when you need custom permissions:
import dspy
interp = dspy.PythonInterpreter(
enable_read_paths=["./data"],
enable_network_access=["api.example.com"],
)
agent = dspy.CodeAct(
"question -> answer",
tools=[search, calculate],
interpreter=interp,
max_iters=5,
)ToolCalls type
dspy.ToolCalls is a structured type representing tool-calling information -- tool names and their arguments in JSON format. Use it in signatures when you want the LM to output tool calls directly (without the ReAct loop).
import dspy
class PlanActions(dspy.Signature):
"""Given a user request, plan which tools to call."""
request: str = dspy.InputField()
actions: dspy.ToolCalls = dspy.OutputField()
planner = dspy.Predict(PlanActions)
result = planner(request="Look up the weather in Paris and convert to Celsius")
print(result.actions) # ToolCalls with name and args for each tool callCreating ToolCalls from dicts
from dspy import ToolCalls
tool_calls = ToolCalls.from_dict_list([
{"name": "search", "args": {"query": "weather in Paris"}},
{"name": "convert_temp", "args": {"value": 72, "from_unit": "F", "to_unit": "C"}},
])ToolCalls with native LM tool calling
When the configured LM supports native tool calling (most modern LMs do), ToolCalls automatically adapts to use the LM's native function-calling API rather than generating JSON as text. This improves reliability.
Using tools with ReAct
dspy.ReAct is the standard choice for tool-using agents. Pass tools as a list of functions or dspy.Tool objects:
import dspy
def search(query: str) -> str:
"""Search for information about a topic."""
return "DSPy is a framework for programming language models."
def calculate(expression: str) -> float:
"""Evaluate a math expression and return the result."""
return eval(expression)
agent = dspy.ReAct(
"question -> answer",
tools=[search, calculate],
max_iters=5,
)
result = agent(question="What is 2^10 plus the year DSPy was released?")
print(result.answer)The agent decides which tools to call, in what order, and when to stop. See /dspy-react for the full guide.
Using tools with CodeAct
dspy.CodeAct agents write Python code that calls your tools. Tools must be pure functions (not callable objects). The agent can chain calls, use loops, and manipulate data in code:
import dspy
def factorial(n: int) -> int:
"""Calculate the factorial of n."""
if n <= 1:
return 1
return n * factorial(n - 1)
agent = dspy.CodeAct(
"question -> answer",
tools=[factorial],
max_iters=5,
)
result = agent(question="What is factorial(10) + factorial(5)?")
print(result.answer)CodeAct tools have stricter requirements than ReAct tools: they must be plain functions, cannot import external libraries, and cannot reference global state. See /dspy-codeact for the full guide.
Converting LangChain tools
dspy.Tool.from_langchain() converts any LangChain tool to a DSPy tool:
import dspy
from langchain_community.tools import DuckDuckGoSearchRun, WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
search = dspy.Tool.from_langchain(DuckDuckGoSearchRun())
wikipedia = dspy.Tool.from_langchain(
WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper())
)
agent = dspy.ReAct("question -> answer", tools=[search, wikipedia])Install LangChain tools with pip install langchain-community.
Converting MCP tools
dspy.Tool.from_mcp_tool() converts Model Context Protocol tools into DSPy tools. It preserves the tool's name, description, and input schema, and creates an async callable that invokes the tool through the MCP session.
Install the MCP extra:
pip install -U "dspy[mcp]"Remote server (Streamable HTTP)
import dspy
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async with streamablehttp_client("https://mcp.example.com/sse") as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
mcp_tools = await session.list_tools()
tools = [dspy.Tool.from_mcp_tool(session, t) for t in mcp_tools.tools]
agent = dspy.ReAct("question -> answer", tools=tools)
result = await agent.aforward(question="What files are in the repo?")Local server (stdio)
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
server_params = StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", "./data"],
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
mcp_tools = await session.list_tools()
tools = [dspy.Tool.from_mcp_tool(session, t) for t in mcp_tools.tools]
agent = dspy.ReAct("question -> answer", tools=tools)
result = await agent.aforward(question="List the files")Key details
- DSPy doesn't manage the server connection — you set up and tear down the
ClientSessionyourself using themcplibrary. - Tools are async —
from_mcp_toolcreates an async callable, so useawait agent.aforward()or run inside an async context. - Schema is preserved — the tool's name, description, and JSON schema for arguments are carried over from the MCP server.
Tool type hints and docstrings checklist
Good tools make good agents. Before passing a tool to an agent, check:
| Check | Why it matters |
|---|---|
| All parameters have type hints | DSPy generates the JSON schema from them |
| Return type is annotated | Helps the agent know what to expect |
| Docstring explains what the tool does | The agent reads this to decide when to call it |
| Docstring mentions required input format | e.g., "Pass repo as 'owner/name'" |
| Parameters have sensible defaults | Reduces the number of decisions the agent makes |
| Errors return useful strings, not exceptions | The agent sees the error as an Observation and can retry |
# A well-documented tool
def get_github_repo(repo: str) -> str:
"""Get information about a GitHub repository.
Pass the full repository name like 'stanfordnlp/dspy'.
Returns name, description, stars, and language.
"""
try:
response = requests.get(f"https://api.github.com/repos/{repo}", timeout=10)
response.raise_for_status()
data = response.json()
return f"Name: {data['full_name']}, Stars: {data['stargazers_count']}"
except requests.RequestException as e:
return f"Error: {str(e)}"Error handling in tools
Tools should catch exceptions and return error strings. When a tool returns an error string, the agent sees it as an Observation and can retry with different arguments or try a different tool.
def search(query: str) -> str:
"""Search for information."""
try:
response = requests.get("https://api.example.com/search", params={"q": query}, timeout=5)
response.raise_for_status()
return response.json()["results"]
except requests.Timeout:
return "Error: Search timed out. Try a shorter or simpler query."
except requests.HTTPError as e:
return f"Error: Search failed with status {e.response.status_code}."
except Exception as e:
return f"Error: {str(e)}"When NOT to use explicit tool wrapping
- Simple functions passed to ReAct/CodeAct -- DSPy wraps them automatically. Use
dspy.Toolonly when you need to override the inferred name, description, or arg schema. - PythonInterpreter for everything -- if your agent only needs to call a few well-defined functions, plain tools with ReAct are simpler and more predictable than sandboxed code execution.
- Converting tools from other frameworks unnecessarily -- if you can rewrite the tool as a plain Python function, that is simpler than using
from_langchain()orfrom_mcp_tool().
Gotchas
1. Missing type hints cause empty tool schemas. Claude often writes tool functions without type annotations. DSPy infers the JSON schema from type hints — without them, the agent gets no parameter info and passes wrong types or missing arguments. 2. Returning complex objects instead of strings. Claude returns dicts, dataclasses, or ORM objects from tools. The agent sees the repr() which is often unhelpful. Always return a formatted string or json.dumps() output. 3. Tools that import heavy libraries at call time. Claude puts import pandas inside the tool function body. This works but adds latency on every call. Move imports to the top of the file — they run once, not per tool invocation. 4. Forgetting `await agent.aforward()` with MCP tools. MCP tools are async, so the agent must be called with aforward(). Claude defaults to agent() which blocks or fails silently in async contexts. 5. CodeAct tools referencing global state. Claude writes CodeAct tools that read from module-level variables or closures. CodeAct tools run in a sandbox and cannot access your process globals — they must be self-contained pure functions.
Additional resources
- dspy.Tool API docs
- For API details, see reference.md
- For worked examples, see examples.md
Cross-references
Install any skill: npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill <name>- ReAct agents -- see
/dspy-react - CodeAct agents -- see
/dspy-codeact - Action-taking AI from a problem-first perspective -- see
/ai-taking-actions - Signatures for defining agent inputs/outputs -- see
/dspy-signatures - Modules for composing agents -- see
/dspy-modules - Install `/ai-do` if you do not have it — it routes any AI problem to the right skill and is the fastest way to work:
npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill ai-do
[
{
"id": "dspy-tools-wrap-function",
"prompt": "I have a Python function that searches our Postgres database. How do I make it available as a tool for a DSPy ReAct agent?",
"should_contain": ["dspy.Tool", "type hints", "docstring", "ReAct"],
"should_not_contain": [],
"notes": "Should show wrapping with type hints and docstring, passing to ReAct. Should mention returning strings. May mention implicit wrapping vs explicit dspy.Tool."
},
{
"id": "dspy-tools-sandbox-execution",
"prompt": "I need my DSPy agent to run arbitrary Python code safely in a sandbox. How do I set up PythonInterpreter?",
"should_contain": ["PythonInterpreter", "Deno", "CodeAct"],
"should_not_contain": [],
"notes": "Should show PythonInterpreter with context manager, mention Deno prerequisite, selective permissions. Should mention CodeAct integration."
},
{
"id": "dspy-tools-mcp-integration",
"prompt": "I have an MCP server running and want to use its tools in a DSPy agent. How do I convert MCP tools to DSPy tools?",
"should_contain": ["from_mcp_tool", "ClientSession", "aforward"],
"should_not_contain": [],
"notes": "Should show from_mcp_tool pattern with session setup, mention tools are async so use aforward."
}
]
dspy-tools -- Worked Examples
Example 1: Wrapping custom functions as DSPy tools
Demonstrates wrapping functions with dspy.Tool, inspecting the inferred schema, overriding metadata, and using arg_desc for richer descriptions.
import dspy
# --- Define functions ---
def search_docs(query: str, max_results: int = 5) -> str:
"""Search the documentation for articles matching the query."""
# Simulated search
docs = {
"setup": "Install DSPy with pip install -U dspy. Configure an LM with dspy.configure(lm=...).",
"signatures": "Signatures declare input/output behavior: 'question -> answer' or class-based.",
"modules": "Modules wrap signatures with inference strategies: Predict, ChainOfThought, ReAct.",
"tools": "Tools are Python functions with type hints and docstrings that agents can call.",
}
results = []
for key, value in docs.items():
if key in query.lower() or any(w in value.lower() for w in query.lower().split()):
results.append(value)
return "\n".join(results[:max_results]) if results else "No results found."
def get_user(user_id: int) -> str:
"""Look up a user by their numeric ID. Returns name and role."""
users = {
1: {"name": "Alice", "role": "admin"},
2: {"name": "Bob", "role": "viewer"},
3: {"name": "Carol", "role": "editor"},
}
user = users.get(user_id)
if user:
return f"Name: {user['name']}, Role: {user['role']}"
return f"No user found with ID {user_id}."
# --- Implicit wrapping (pass functions directly) ---
lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-sonnet-4-5-20250929", etc.
dspy.configure(lm=lm)
# DSPy wraps these automatically when passed to an agent
agent = dspy.ReAct("question -> answer", tools=[search_docs, get_user])
result = agent(question="What are DSPy signatures?")
print(result.answer)
# --- Explicit wrapping with dspy.Tool ---
# Inspect the auto-inferred schema
tool = dspy.Tool(search_docs)
print(f"Name: {tool.name}") # "search_docs"
print(f"Desc: {tool.desc}") # "Search the documentation for articles matching the query."
print(f"Args: {tool.args}") # {'query': {'type': 'string'}, 'max_results': {'type': 'integer', 'default': 5}}
# Override name and description
tool = dspy.Tool(
search_docs,
name="docs_search",
desc="Search DSPy documentation. Use this for any question about how DSPy works.",
)
print(f"Name: {tool.name}") # "docs_search"
# Add per-argument descriptions
tool = dspy.Tool(
search_docs,
arg_desc={
"query": "Keywords to search for -- use short phrases, not full questions",
"max_results": "How many results to return (1-10)",
},
)
# Use the explicitly wrapped tool in an agent
agent = dspy.ReAct("question -> answer", tools=[tool, get_user])
result = agent(question="Look up user 1 and tell me their role")
print(result.answer)
# --- Format as OpenAI-compatible function call schema ---
schema = tool.format_as_litellm_function_call()
print(schema)
# {
# 'type': 'function',
# 'function': {
# 'name': 'search_docs',
# 'description': '...',
# 'parameters': {'type': 'object', 'properties': {...}, 'required': ['query']}
# }
# }Key points:
- Pass functions directly to agents for the common case --
dspy.Toolis needed only when you want to override metadata dspy.Toolauto-infers name, description, and argument schemas from the function- Use
arg_descto add richer per-argument descriptions for complex tools format_as_litellm_function_call()returns the OpenAI-compatible schema if you need to inspect it
Example 2: Building a multi-tool agent
A research agent with three tools: web search, database lookup, and a calculator. Demonstrates tool selection, chaining, and wrapping in a custom module.
import dspy
import json
from typing import Literal
# --- Tools ---
PRODUCTS_DB = {
"P-100": {"name": "Widget Pro", "price": 29.99, "stock": 150, "category": "hardware"},
"P-200": {"name": "Gadget Plus", "price": 49.99, "stock": 0, "category": "hardware"},
"P-300": {"name": "SaaS Starter", "price": 9.99, "stock": None, "category": "software"},
"P-400": {"name": "Enterprise Suite", "price": 199.99, "stock": None, "category": "software"},
}
def search_products(query: str) -> str:
"""Search the product catalog by name or category. Returns matching products with IDs."""
query_lower = query.lower()
matches = []
for pid, product in PRODUCTS_DB.items():
if query_lower in product["name"].lower() or query_lower in product["category"]:
matches.append(f"{pid}: {product['name']} (${product['price']}, category: {product['category']})")
if matches:
return "\n".join(matches)
return f"No products found matching '{query}'."
def get_product_details(product_id: str) -> str:
"""Get full details for a product by its ID (e.g., P-100). Returns price, stock, and category."""
product = PRODUCTS_DB.get(product_id.upper())
if product:
stock_info = f"{product['stock']} units" if product["stock"] is not None else "unlimited (digital)"
return json.dumps({
"id": product_id.upper(),
"name": product["name"],
"price": product["price"],
"stock": stock_info,
"category": product["category"],
})
return f"No product found with ID {product_id}."
def calculate(expression: str) -> str:
"""Evaluate a mathematical expression. Supports +, -, *, /, **, and parentheses."""
try:
# Only allow safe math operations
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return "Error: Expression contains invalid characters. Use only numbers and +, -, *, /, **."
result = eval(expression)
return str(result)
except Exception as e:
return f"Error evaluating expression: {str(e)}"
# --- Signature ---
class ProductAnswer(dspy.Signature):
"""Answer questions about products using the available tools."""
question: str = dspy.InputField(desc="A question about products, pricing, or availability")
answer: str = dspy.OutputField(desc="A helpful answer with specific product details")
has_data: bool = dspy.OutputField(desc="Whether the answer is based on real data from tools")
# --- Agent module ---
class ProductAgent(dspy.Module):
def __init__(self):
self.agent = dspy.ReAct(
ProductAnswer,
tools=[search_products, get_product_details, calculate],
max_iters=5,
)
def forward(self, question: str):
return self.agent(question=question)
def product_data_reward(args, pred):
"""Reward answers grounded in real tool data."""
if not pred.has_data:
return 0.5
return 1.0
# --- Usage ---
lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-sonnet-4-5-20250929", etc.
dspy.configure(lm=lm)
agent = dspy.Refine(module=ProductAgent(), N=3, reward_fn=product_data_reward, threshold=1.0)
questions = [
"What hardware products do you have and which is cheaper?",
"Is the Gadget Plus in stock?",
"If I buy 3 Widget Pros, how much will it cost?",
"Compare the prices of all software products.",
]
for q in questions:
print(f"\nQ: {q}")
result = agent(question=q)
print(f"A: {result.answer}")
print(f"Based on data: {result.has_data}")
# --- Optimization ---
def product_metric(example, prediction, trace=None):
has_data = prediction.has_data
has_detail = len(prediction.answer.strip()) > 30
return has_data + 0.5 * has_detail
# trainset = [
# dspy.Example(question="What hardware products are available?").with_inputs("question"),
# dspy.Example(question="How much does Widget Pro cost?").with_inputs("question"),
# dspy.Example(question="Is Gadget Plus in stock?").with_inputs("question"),
# ]
# optimizer = dspy.BootstrapFewShot(metric=product_metric, max_bootstrapped_demos=3)
# optimized = optimizer.compile(agent, trainset=trainset)
# optimized.save("product_agent.json")Key points:
- Three tools with different purposes -- the agent picks the right one per question
- The agent chains tools: search for products, then get details, then calculate totals
has_dataoutput field lets the module check whether the agent actually used toolsdspy.Refineretries whenhas_datais false, nudging the agent to look up data rather than guessing- The calculator uses a character allowlist for safety
Example 3: PythonInterpreter for safe code execution
Demonstrates using dspy.PythonInterpreter directly and as part of a CodeAct agent, with host-side tools and selective sandbox permissions.
import dspy
from dspy import PythonInterpreter
# --- Basic execution ---
# Run simple code in the sandbox
with PythonInterpreter() as interp:
result = interp("print(sum(range(1, 101)))")
print(f"Sum 1-100: {result}") # "5050"
# Variables persist across calls within the same session
interp("data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]")
result = interp("print(sorted(data))")
print(f"Sorted: {result}")
# Multi-line code works
result = interp("""
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
print(fibonacci(20))
""")
print(f"Fib(20): {result}")
# --- With host-side tools ---
# Tools run in your normal Python process, not the sandbox.
# This lets tools use libraries, access databases, and call APIs.
def query_database(sql: str) -> str:
"""Execute a SQL query and return the results as a formatted string."""
# Simulated database
if "users" in sql.lower():
return "id,name,role\n1,Alice,admin\n2,Bob,viewer\n3,Carol,editor"
if "orders" in sql.lower():
return "id,user_id,total\n101,1,29.99\n102,1,49.99\n103,2,9.99"
return "No results."
def send_notification(user_id: int, message: str) -> str:
"""Send a notification to a user by ID."""
print(f"[Notification] User {user_id}: {message}")
return f"Notification sent to user {user_id}."
with PythonInterpreter(tools={
"query_database": query_database,
"send_notification": send_notification,
}) as interp:
# The sandbox code calls host-side tools
result = interp("""
users = query_database(sql="SELECT * FROM users")
print(users)
""")
print(f"Query result:\n{result}")
result = interp("""
response = send_notification(user_id=1, message="Your order shipped!")
print(response)
""")
print(f"Notification: {result}")
# --- With selective permissions ---
# Allow reading local files and accessing a specific API
interp = PythonInterpreter(
enable_read_paths=["./reports"],
enable_network_access=["api.example.com"],
)
# Use in a CodeAct agent with custom permissions
with interp:
result = interp("""
# This code runs in the sandbox but can read from ./reports
# and make HTTP requests to api.example.com
print("Sandbox with custom permissions")
""")
# --- CodeAct agent with PythonInterpreter ---
lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-sonnet-4-5-20250929", etc.
dspy.configure(lm=lm)
def fetch_stats(dataset: str) -> str:
"""Fetch summary statistics for a named dataset."""
datasets = {
"sales": "count=1000, mean=45.50, median=38.00, std=22.10, min=5.00, max=199.99",
"users": "count=500, mean_age=34, median_age=31, active=420, inactive=80",
}
return datasets.get(dataset, f"Dataset '{dataset}' not found. Available: {list(datasets.keys())}")
# Let CodeAct create its own interpreter (default)
agent = dspy.CodeAct(
"question -> answer",
tools=[fetch_stats],
max_iters=5,
)
result = agent(question="Get the sales stats and calculate what percentage of the max the mean represents")
print(f"Answer: {result.answer}")
# Or pass a custom interpreter with specific permissions
custom_interp = PythonInterpreter(
enable_read_paths=["./data"],
)
agent_with_perms = dspy.CodeAct(
"question -> answer",
tools=[fetch_stats],
interpreter=custom_interp,
max_iters=5,
)
result = agent_with_perms(question="Fetch user stats and summarize them")
print(f"Answer: {result.answer}")Key points:
PythonInterpreterruns code in a sandboxed Deno + Pyodide environment -- no filesystem or network by default- Use the context manager (
with) to start and shut down the sandbox cleanly - Variables persist across calls within a single session -- the agent can build up state
- Host-side tools run in your normal Python process so they can access databases, APIs, and libraries
- Use
enable_read_paths,enable_write_paths,enable_network_access, andenable_env_varsto grant selective permissions - Pass a custom
PythonInterpretertodspy.CodeActwhen you need specific sandbox permissions - Deno must be installed for
PythonInterpreterto work
Tool API Reference
Condensed from dspy.ai/api/primitives/Tool. Verify against upstream for latest.
dspy.Tool
dspy.Tool(
func, # Callable -- required
name=None, # str | None -- inferred from func.__name__
desc=None, # str | None -- inferred from docstring
args=None, # dict | None -- JSON schemas, inferred from type hints
arg_types=None, # dict | None -- type mappings, inferred from type hints
arg_desc=None, # dict | None -- per-argument descriptions
)| Parameter | Type | Default | Description |
|---|---|---|---|
func | Callable | required | Function to wrap as a tool |
name | `str \ | None` | None |
desc | `str \ | None` | None |
args | `dict \ | None` | None |
arg_types | `dict \ | None` | None |
arg_desc | `dict \ | None` | None |
Class Methods
# Convert a LangChain tool
dspy.Tool.from_langchain(tool: BaseTool) -> Tool
# Convert an MCP tool (async)
dspy.Tool.from_mcp_tool(session: mcp.ClientSession, tool: mcp.types.Tool) -> Tooldspy.PythonInterpreter
dspy.PythonInterpreter(
deno_command=None, # list[str] | None -- custom Deno launch command
enable_read_paths=None, # list[str] | None -- readable paths
enable_write_paths=None, # list[str] | None -- writable paths
enable_env_vars=None, # list[str] | None -- exposed env vars
enable_network_access=None, # list[str] | None -- allowed network domains
sync_files=True, # sync file changes back to host
tools=None, # dict[str, Callable] | None -- host-side tools
output_fields=None, # list[dict] | None -- output field definitions
)Use as a context manager: with PythonInterpreter() as interp:
Requires Deno.
dspy.ToolCalls
Structured type for tool-calling output. Use as an OutputField type.
# Create from dicts
ToolCalls.from_dict_list([{"name": "search", "args": {"query": "..."}}])Adapts to native LM function-calling API when supported.