LlamaIndex code execution tool with PandaStack
Give a LlamaIndex agent a Python sandbox by wiring a code execution tool to a PandaStack CodeContext, so the model can run code instead of guessing.
A LlamaIndex agent that reasons about data but can't run code is guessing. The fix is a tool that executes whatever Python the model writes and hands back the real result. This recipe wires a LlamaIndex FunctionTool to a PandaStack code interpreter CodeContext — a persistent Jupyter-style kernel running in an isolated microVM. The agent gets a sandbox that holds state across calls and captures charts and DataFrames as objects, not stdout scraps.
This is the LlamaIndex sibling of the LangChain code execution recipe — same wedge, different framework.
Install
pip install pandastack llama-index-core llama-index-llms-anthropic
export PANDASTACK_API_KEY=pds_... # your PandaStack key
export ANTHROPIC_API_KEY=sk-ant-... # or use llama-index-llms-openaillama-index-core holds the agent and tool classes; the LLM integration package is separate. Swap llama-index-llms-anthropic for llama-index-llms-openai (or any function-calling LLM) if you prefer.
The tool: a CodeContext-backed run_python
The whole integration is one function. Create a sandbox once, open a CodeContext on it, and let the tool call ctx.run_code for every snippet the agent emits. Because the context is a long-lived kernel, variables, imports, and loaded DataFrames persist across the agent's tool calls — turn 3 can use what turn 1 defined.
from pandastack import Sandbox
from llama_index.core.tools import FunctionTool
# One sandbox + one persistent kernel for the whole agent run.
sandbox = Sandbox.create(template="code-interpreter")
ctx = sandbox.create_code_context(language="python")
def run_python(code: str) -> str:
"""Execute Python code in a stateful sandbox kernel and return its output.
Variables, imports, and data defined in earlier calls persist. Use this for
any calculation, data analysis, or plotting. The last expression's value and
anything printed are returned.
"""
ex = ctx.run_code(code)
if ex.error:
return f"ERROR: {ex.error}"
# ex.text is the repr of the last expression; ex.stdout captures prints.
return ex.text or ex.stdout or "(no output)"
code_tool = FunctionTool.from_defaults(fn=run_python)The docstring is load-bearing: LlamaIndex auto-generates the tool's description and input schema from the signature and docstring, and the LLM relies on that description to decide when to call the tool. The code: str type annotation drives the generated fn_schema — keep it.
What each field gives the agent
Field on Execution | What it is | When the agent sees it |
|---|---|---|
ex.text | repr of the last expression (e.g. a DataFrame, a number) | Default return — the model reads it as the tool result |
ex.stdout | everything print()ed | Fallback when the snippet prints instead of returning |
ex.error | traceback string, or None | Surfaced as ERROR: ... so the model can self-correct |
ex.png | base64 PNG of the last plt.show() / figure | Available if you want to persist or display charts |
ex.results | list of rich Result objects (.html, .png, .json) | For structured/rich-output handling |
The wedge: a plt.show() or a bare DataFrame in the last line is captured automatically by the code interpreter template (it bakes in IPython, matplotlib, plotly, pandas). No savefig, no writing to disk — ex.png and ex.results[].html come back as objects.
The run_python tool above returns only text so the LLM gets a clean string, but the chart object is still on the Execution — read it off ex.png (base64 PNG) when you want to persist or render it:
ex = ctx.run_code(
"import matplotlib.pyplot as plt; plt.plot([1, 2, 3]); plt.show()"
)
chart_b64 = ex.png # base64 PNG of the figure — no savefig, no disk writeWire the tool into a FunctionAgent
FunctionAgent is the current workflow-based agent for function-calling LLMs. Pass the tool (or the bare run_python function — the agent wraps plain callables automatically) and an LLM. agent.run(...) is a coroutine, so await it inside an async entrypoint.
import asyncio
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.llms.anthropic import Anthropic
llm = Anthropic(model="claude-opus-4-8") # any function-calling LLM
agent = FunctionAgent(
tools=[code_tool], # or tools=[run_python]
llm=llm,
system_prompt=(
"You can execute Python in a sandbox to answer questions. "
"Prefer computing the answer with code over reasoning about it. "
"The kernel is stateful — reuse variables across steps."
),
)
async def main():
response = await agent.run(
user_msg="Load a sine wave over 0..4π into a numpy array, "
"report its mean and max, then plot it."
)
print(str(response))
if __name__ == "__main__":
asyncio.run(main())
sandbox.kill() # tear down the microVM when doneThe agent reasons, calls run_python with NumPy code to build and summarize the array, then calls it again to plot — the second call reuses the array from the first because the kernel persisted. Note the keyword is user_msg=, not a positional argument, and the call is agent.run(...), not .chat() or .query().
ReActAgent (from the same llama_index.core.agent.workflow module) takes the identical tools / llm / system_prompt kwargs and works with any LLM, including ones without a function-calling API — just swap the class name. To orchestrate the agent inside a larger graph, wrap it: AgentWorkflow(agents=[agent]), also driven by an async .run(user_msg=...).
Reusing one sandbox per session
Create the sandbox and context once per conversation or user session, not per request. The Sandbox.create snapshot-restore boot path is fast (p50 ~179ms), but a persistent kernel is the point — it keeps the agent's working state alive. Tear it down with sandbox.kill() when the session ends.
For longer-lived agents, you can snapshot a warmed sandbox (data loaded, libraries imported) and fork it per user, so each session starts from the same hydrated state in a few hundred milliseconds.
from pandastack import Sandbox
# Self-contained short-lived run: its own sandbox + context-managed kernel.
session = Sandbox.create(template="code-interpreter")
try:
with session.create_code_context() as ctx:
ex = ctx.run_code("import pandas as pd; pd.__version__")
print(ex.text)
# ctx.close() called automatically on exit
finally:
session.kill() # tear down this session's microVMTypeScript note
This recipe is Python because the LlamaIndex agent classes shown live in llama-index-core. The PandaStack side maps directly to the TypeScript SDK if you wire a tool in a TS-native agent framework: Sandbox.create({ template: "code-interpreter" }), const ctx = await sandbox.createCodeContext("python"), then const ex = await ctx.runCode(code) returning the same Execution shape (ex.text, ex.stdout, ex.error, ex.png).
Next steps
- Code interpreter guide — the pillar reference for
CodeContext, rich outputs, and the captured-chart model. - LangChain code execution — the same tool pattern for LangChain agents.
- Snapshots and forks — warm a sandbox once, fork it per session for sub-second hydrated starts.
- MCP server — expose sandbox code execution to any MCP-compatible agent without writing a tool wrapper.
Secure code execution for AutoGen agents
Give an AutoGen or AG2 agent an autogen code execution sandbox in Python — a custom tool that runs the model's code inside an isolated Firecracker microVM.
Pydantic AI code execution with PandaStack
Give a Pydantic AI agent a run_python tool backed by a PandaStack code execution sandbox in Python — the model writes code, the microVM runs it, and charts come back as objects.