Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions samples/deepagent-serverless/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# DeepAgent Serverless Sandbox

A DeepAgent that delegates code execution to a serverless sandbox pod via `interrupt(InvokeProcess(...))`.

## What It Demonstrates

- **Serverless code execution**: The agent writes Python scripts and executes them in UiPath Orchestrator, without needing a local runtime.
- **Operation batching**: Writes are buffered and flushed together with execute commands in a single `InvokeProcess` call..
- **Subagent delegation**: A `research_specialist` subagent uses Tavily web search to gather information before the main agent writes code.
- **`SandboxBackendProtocol`**: Extends the deepagents `BackendProtocol` with an `execute` tool, giving the LLM the ability to run shell commands.

## Architecture

```
User prompt
┌─────────────────────────────────────────┐
│ agent (DeepAgent + LLM) │
│ │
│ Tools: write_file, execute, read, ... │
│ Subagents: research_specialist │
│ │
│ ServerlessBackend buffers writes, │
│ then flushes them + execute as a │
│ single interrupt(InvokeProcess(...)) │
└───────────────┬─────────────────────────┘
│ InvokeProcess
┌─────────────────────────────────────────┐
│ sandbox (non-LLM StateGraph) │
│ Runs in UiPath Orchestrator │
│ │
│ Receives batched operations: │
│ [write, write, ..., execute] │
│ Executes them sequentially via │
│ FilesystemBackend + subprocess │
└─────────────────────────────────────────┘
```

## Entry Points

Defined in `langgraph.json`:

| Entry Point | Graph | Description |
|-------------|-------|-------------|
| `agent` | `graph.py:deep_agent` | LLM-powered DeepAgent with sandbox backend |
| `sandbox` | `sandbox.py:graph` | Non-LLM graph that executes batched file/shell operations |

## Requirements

- Python 3.11+
- Tavily API key (for web search)

## Setup

```bash
uv venv -p 3.11 .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
uv sync
```

Run `uv run uipath auth` to authenticate to UiPath.


Add your Tavily API key in `.env`.
```bash
TAVILY_API_KEY=your_tavily_api_key
```

## Usage

```bash
uv run uipath run agent --file input.json
```

## Example Prompt

```
Research the best practices for benchmarking sorting algorithms in Python.
Then write a script that compares bubble sort, merge sort, and Python's
built-in timsort on random lists of 10000 elements, and execute it.
```

This exercises the full flow: research (Tavily) → write code (buffered) → execute → return results.
14 changes: 14 additions & 0 deletions samples/deepagent-serverless/agent.mermaid
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
flowchart TB
__start__(__start__)
model(model)
tools(tools)
TodoListMiddleware.after_model(TodoListMiddleware.after_model)
PatchToolCallsMiddleware.before_agent(PatchToolCallsMiddleware.before_agent)
__end__(__end__)
PatchToolCallsMiddleware.before_agent --> model
TodoListMiddleware.after_model --> __end__
TodoListMiddleware.after_model --> model
TodoListMiddleware.after_model --> tools
__start__ --> PatchToolCallsMiddleware.before_agent
model --> TodoListMiddleware.after_model
tools --> model
4 changes: 4 additions & 0 deletions samples/deepagent-serverless/bindings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": "2.0",
"resources": []
}
8 changes: 8 additions & 0 deletions samples/deepagent-serverless/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"messages": [
{
"type": "human",
"content": "Research the best practices for benchmarking sorting algorithms in Python. Then write a script that compares bubble sort, merge sort, and Python's built-in timsort on random lists of 10000 elements, and execute it."
}
]
}
6 changes: 6 additions & 0 deletions samples/deepagent-serverless/langgraph.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"graphs": {
"agent": "./src/deepagent_serverless/graph.py:deep_agent",
"sandbox": "./src/deepagent_serverless/sandbox.py:graph"
}
}
19 changes: 19 additions & 0 deletions samples/deepagent-serverless/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[project]
name = "sandbox-deepagent"
version = "0.0.6"
description = "DeepAgent with serverless sandbox backend via interrupt(InvokeProcess)"
authors = [{ name = "John Doe", email = "john.doe@myemail.com" }]
requires-python = ">=3.11"
dependencies = [
"deepagents>=0.3.9",
"langchain-tavily>=0.2.17",
"langgraph>=1.0.7",
"tavily-python>=0.7.21",
"uipath",
"uipath-langchain",
]

[dependency-groups]
dev = [
"uipath-dev",
]
6 changes: 6 additions & 0 deletions samples/deepagent-serverless/sandbox.mermaid
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
flowchart TB
__start__(__start__)
execute(execute)
__end__(__end__)
__start__ --> execute
execute --> __end__
Empty file.
37 changes: 37 additions & 0 deletions samples/deepagent-serverless/src/deepagent_serverless/graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""DeepAgent that delegates filesystem operations and code execution to a serverless sandbox."""

from langchain_tavily import TavilySearch
from uipath_langchain.chat import UiPathChatOpenAI

from deepagents import create_deep_agent
from deepagent_serverless.serverless_backend import ServerlessBackend, ServerlessConfig

llm = UiPathChatOpenAI()
tavily_tool = TavilySearch(max_results=5)

MAIN_AGENT_PROMPT = """You are a coding assistant that can write and execute code on a remote serverless sandbox.

When given a task:
1. Delegate research to the research_specialist subagent if you need information
2. Write code files using the write_file tool (always under /tmp/workspace/)
3. Execute them using the execute tool (e.g. "python /tmp/workspace/script.py")
4. Use the execution output to verify results"""

RESEARCH_SUBAGENT_PROMPT = """You are a research specialist. Use web search to find relevant information.
Be thorough but concise. Cite your sources."""

research_subagent = {
"name": "research_specialist",
"description": "Specialized agent for gathering information using internet search",
"system_prompt": RESEARCH_SUBAGENT_PROMPT,
"tools": [tavily_tool],
"model": llm,
}

deep_agent = create_deep_agent(
model=llm,
backend=ServerlessBackend(ServerlessConfig(sandbox_process_name="sandbox-deepagent")),
system_prompt=MAIN_AGENT_PROMPT,
tools=[tavily_tool],
subagents=[research_subagent],
)
150 changes: 150 additions & 0 deletions samples/deepagent-serverless/src/deepagent_serverless/sandbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""Serverless sandbox - a non-LLM graph that executes batched operations.

Receives a list of operations (file ops + shell execution), dispatches them
sequentially to FilesystemBackend / subprocess on the same pod, and returns
all results. This ensures writes and executes happen on the same filesystem.
"""

import dataclasses
import subprocess
from enum import Enum
from typing import Any

from deepagents.backends.filesystem import FilesystemBackend
from langgraph.constants import END, START
from langgraph.graph import StateGraph
from pydantic import BaseModel, Field

_DEFAULT_TIMEOUT = 120
_MAX_OUTPUT_BYTES = 100_000
_ROOT_DIR = "/tmp/workspace"


def _serialize(obj: Any) -> Any:
"""Convert protocol objects (dataclasses, lists of dataclasses) to JSON-safe dicts."""
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
return dataclasses.asdict(obj)
if isinstance(obj, list):
return [_serialize(item) for item in obj]
return obj


class Operation(str, Enum):
LS = "ls"
READ = "read"
WRITE = "write"
EDIT = "edit"
GREP = "grep"
GLOB = "glob"
EXECUTE = "execute"


class OperationRequest(BaseModel):
operation: Operation
args: dict[str, Any] = Field(default_factory=dict)


class OperationResult(BaseModel):
result: Any = None
error: str | None = None


class SandboxInput(BaseModel):
operations: list[OperationRequest]


class SandboxOutput(BaseModel):
results: list[OperationResult]


_backend = FilesystemBackend(root_dir=_ROOT_DIR)


def _execute_shell(args: dict[str, Any]) -> dict[str, Any]:
"""Run a shell command and return output, exit_code, truncated."""
command = args["command"]
timeout = args.get("timeout") or _DEFAULT_TIMEOUT
try:
proc = subprocess.run(
command,
shell=True,
capture_output=True,
timeout=timeout,
cwd=_ROOT_DIR,
)
stdout = proc.stdout.decode("utf-8", errors="replace")
stderr = proc.stderr.decode("utf-8", errors="replace")
parts = []
if stdout:
parts.append(stdout)
if stderr:
parts.extend(f"[stderr] {line}" for line in stderr.splitlines())
output = "\n".join(parts)
truncated = len(output.encode()) > _MAX_OUTPUT_BYTES
if truncated:
output = output.encode()[:_MAX_OUTPUT_BYTES].decode("utf-8", errors="replace")
return {"output": output, "exit_code": proc.returncode, "truncated": truncated}
except subprocess.TimeoutExpired:
return {"output": f"Command timed out after {timeout}s", "exit_code": 124, "truncated": False}
except Exception as e:
return {"output": str(e), "exit_code": 1, "truncated": False}


def _dispatch_one(op: OperationRequest) -> OperationResult:
"""Dispatch a single operation."""
try:
match op.operation:
case Operation.LS:
result = _backend.ls_info(op.args["path"])
case Operation.READ:
result = _backend.read(
op.args["file_path"],
offset=op.args.get("offset", 0),
limit=op.args.get("limit", 2000),
)
case Operation.WRITE:
result = _backend.write(
op.args["file_path"],
op.args["content"],
)
case Operation.EDIT:
result = _backend.edit(
op.args["file_path"],
op.args["old_string"],
op.args["new_string"],
replace_all=op.args.get("replace_all", False),
)
case Operation.GREP:
result = _backend.grep_raw(
op.args["pattern"],
path=op.args.get("path"),
glob=op.args.get("glob"),
)
case Operation.GLOB:
result = _backend.glob_info(
op.args["pattern"],
path=op.args.get("path", "/"),
)
case Operation.EXECUTE:
result = _execute_shell(op.args)
return OperationResult(result=_serialize(result))
except Exception as e:
return OperationResult(error=str(e))


async def execute(input: SandboxInput) -> SandboxOutput:
"""Execute all operations sequentially on the same pod."""
results = [_dispatch_one(op) for op in input.operations]
return SandboxOutput(results=results)


builder = StateGraph(
state_schema=SandboxInput,
input=SandboxInput,
output=SandboxOutput,
)
builder.add_node("execute", execute)
builder.add_edge(START, "execute")
builder.add_edge("execute", END)

graph = builder.compile()
Loading