Skip to content
Merged
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
6 changes: 6 additions & 0 deletions openai-agents/examples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.venv
__pycache__/

# Restate
.restate
restate-data
18 changes: 18 additions & 0 deletions openai-agents/examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Restate and OpenAI Agents SDK for Python

This repository contains (advanced) examples of using Restate with OpenAI Agents in Python.

**First, check out the basic examples in the [Tour of AI Agents with Restate](https://github.com/restatedev/ai-examples/tree/main/openai-agents/tour-of-agents).**

To get started, run:

```shell
uv run .
```


Run Restate and register the service, similar to [the AI Quickstart](https://docs.restate.dev/ai-quickstart#python-%2B-openai).

Then invoke the agents that were registered, via the UI playground, using their default messages.

Check the Restate journal to see how the agents executed their steps durably.
23 changes: 23 additions & 0 deletions openai-agents/examples/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import hypercorn
import asyncio
import restate

from app.mcp import agent_service as mcp_agent
from app.mcp_with_approval import agent_service as mcp_with_approval_agent
from app.websearch import agent_service as websearch_agent
from app.rollback_agent import agent_service as rollback_agent

app = restate.app(
services=[mcp_agent, mcp_with_approval_agent, websearch_agent, rollback_agent]
)


def main():
"""Entry point for running the app."""
conf = hypercorn.Config()
conf.bind = ["0.0.0.0:9080"]
asyncio.run(hypercorn.asyncio.serve(app, conf))


if __name__ == "__main__":
main()
32 changes: 32 additions & 0 deletions openai-agents/examples/app/mcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import restate

from agents import Agent, HostedMCPTool
from openai.types.responses.tool_param import Mcp
from restate.ext.openai import DurableRunner

from app.utils.models import ChatMessage


agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
tools=[
HostedMCPTool(
tool_config=Mcp(
type="mcp",
server_label="restate_docs",
server_description="A knowledge base about Restate's documentation.",
server_url="https://docs.restate.dev/mcp",
require_approval="never",
),
)
],
)

agent_service = restate.Service("McpChat")


@agent_service.handler()
async def message(_ctx: restate.Context, req: ChatMessage) -> str:
result = await DurableRunner.run(agent, req.message)
return result.final_output
55 changes: 55 additions & 0 deletions openai-agents/examples/app/mcp_with_approval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import restate

from agents import (
Agent,
HostedMCPTool,
MCPToolApprovalRequest,
MCPToolApprovalFunctionResult,
)
from openai.types.responses.tool_param import Mcp
from restate.ext.openai import restate_context, DurableRunner

from app.utils.models import ChatMessage
from app.utils.utils import request_mcp_approval

agent_service = restate.Service("McpWithApprovalsChat")


async def approve_func(req: MCPToolApprovalRequest) -> MCPToolApprovalFunctionResult:
# Request human review
approval_id, approval_promise = restate_context().awakeable(type_hint=bool)
await restate_context().run_typed(
"Approve MCP tool",
request_mcp_approval,
mcp_tool_name=req.data.name,
awakeable_id=approval_id,
)
# Wait for human approval
approved = await approval_promise
if not approved:
return {"approve": approved, "reason": "User denied"}
return {"approve": approved}


agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
tools=[
HostedMCPTool(
tool_config=Mcp(
type="mcp",
server_label="restate_docs",
server_description="A knowledge base about Restate's documentation.",
server_url="https://docs.restate.dev/mcp",
),
on_approval_request=approve_func,
# or use require_approval="never" in the tool_config to disable approvals
)
],
)


@agent_service.handler()
async def message(_ctx: restate.Context, req: ChatMessage) -> str:
result = await DurableRunner.run(agent, req.message)
return result.final_output
83 changes: 83 additions & 0 deletions openai-agents/examples/app/rollback_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import restate

from typing import Callable
from agents import Agent, RunContextWrapper, function_tool
from pydantic import Field, BaseModel, ConfigDict
from restate.ext.openai import restate_context, DurableRunner, raise_terminal_errors

from app.utils.models import HotelBooking, FlightBooking, BookingPrompt, BookingResult
from app.utils.utils import (
reserve_hotel,
reserve_flight,
cancel_hotel,
cancel_flight,
)


# Enrich the agent context with a list to track rollback actions
class BookingContext(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
booking_id: str
on_rollback: list[Callable] = Field(default=[])


# Functions raise terminal errors instead of feeding them back to the agent
@function_tool(failure_error_function=raise_terminal_errors)
async def book_hotel(
wrapper: RunContextWrapper[BookingContext], booking: HotelBooking
) -> BookingResult:
"""Book a hotel"""
ctx = restate_context()
booking_ctx, booking_id = wrapper.context, wrapper.context.booking_id
# Register a rollback action for each step, in case of failures further on in the workflow
booking_ctx.on_rollback.append(
lambda: ctx.run_typed("Cancel hotel", cancel_hotel, id=booking_id)
)

# Execute the workflow step
return await ctx.run_typed(
"Book hotel", reserve_hotel, id=booking_id, booking=booking
)


@function_tool(failure_error_function=raise_terminal_errors)
async def book_flight(
wrapper: RunContextWrapper[BookingContext], booking: FlightBooking
) -> BookingResult:
"""Book a flight"""
ctx = restate_context()
booking_ctx, booking_id = wrapper.context, wrapper.context.booking_id
booking_ctx.on_rollback.append(
lambda: ctx.run_typed("Cancel flight", cancel_flight, id=booking_id)
)
return await ctx.run_typed(
"Book flight", reserve_flight, id=booking_id, booking=booking
)


# ... Do the same for cars ...


agent = Agent[BookingContext](
name="BookingWithRollbackAgent",
instructions="Book a complete travel package with the requirements in the prompt."
"Use tools to first book the hotel, then the flight.",
tools=[book_hotel, book_flight],
)


agent_service = restate.Service("BookingWithRollbackAgent")


@agent_service.handler()
async def book(_ctx: restate.Context, req: BookingPrompt) -> str:
booking_ctx = BookingContext(booking_id=req.booking_id)
try:
result = await DurableRunner.run(agent, req.message, context=booking_ctx)
except restate.TerminalError as e:
# Run all the rollback actions on terminal errors
for compensation in reversed(booking_ctx.on_rollback):
await compensation()
raise e

return result.final_output
Empty file.
44 changes: 44 additions & 0 deletions openai-agents/examples/app/utils/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import Optional, Dict, Any

from pydantic.alias_generators import to_camel as camelize

from pydantic import BaseModel, ConfigDict

# Prompts for AI agents (with default messages)


class ChatMessage(BaseModel):
message: str = "Which use cases does Restate support?"


class HotelBooking(BaseModel):
"""Hotel booking data structure."""

name: str
dates: str
guests: int


class FlightBooking(BaseModel):
"""Flight booking data structure."""

origin: str
destination: str
date: str
passengers: int


class BookingPrompt(BaseModel):
"""Booking request data structure."""

booking_id: str = "booking_123"
message: str = (
"I need to book a business trip to San Francisco from March 15-17. Flying from JFK, need a hotel downtown for 1 guest."
)


class BookingResult(BaseModel):
"""Booking result structure."""

id: str
confirmation: str
49 changes: 49 additions & 0 deletions openai-agents/examples/app/utils/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from restate import TerminalError

from app.utils.models import (
BookingResult,
FlightBooking,
HotelBooking,
)


async def request_mcp_approval(mcp_tool_name: str, awakeable_id: str) -> None:
"""Simulate requesting human review."""
print(f"🔔 Human review requested: {mcp_tool_name}")
print(f" Submit your mcp tool approval via: \n ")
print(
f" curl localhost:8080/restate/awakeables/{awakeable_id}/resolve --json true"
)


async def reserve_hotel(id: str, booking: HotelBooking) -> BookingResult:
"""Reserve a hotel (simulated)."""
print(f"🏨 Reserving hotel in {booking.name} for {booking.guests} guests")
return BookingResult(
id=id,
confirmation=f"Hotel {booking.name} booked for {booking.guests} guests on {booking.dates}",
)


async def reserve_flight(id: str, booking: FlightBooking) -> BookingResult:
"""Reserve a flight (simulated)."""
print(f"✈️ Reserving flight from {booking.origin} to {booking.destination}")
if booking.destination == "San Francisco" or booking.destination == "SFO":
print(f"[👻 SIMULATED] Flight booking failed: No flights to SFO available...")
raise TerminalError(
f"[👻 SIMULATED] Flight booking failed: No flights to SFO available..."
)
return BookingResult(
id=id,
confirmation=f"Flight from {booking.origin} to {booking.destination} on {booking.date} for {booking.passengers} passengers",
)


async def cancel_hotel(id: str) -> None:
"""Cancel hotel booking."""
print(f"❌ Cancelling hotel booking {id}")


async def cancel_flight(id: str) -> None:
"""Cancel flight booking."""
print(f"❌ Cancelling flight booking {id}")
21 changes: 21 additions & 0 deletions openai-agents/examples/app/websearch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import restate

from agents import Agent, WebSearchTool
from restate import ObjectContext
from restate.ext.openai import DurableRunner

from app.utils.models import ChatMessage

agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
tools=[WebSearchTool()],
)

agent_service = restate.Service("WebsearchChat")


@agent_service.handler()
async def message(_ctx: ObjectContext, chat_message: ChatMessage) -> str:
result = await DurableRunner.run(agent, chat_message.message)
return result.final_output
24 changes: 24 additions & 0 deletions openai-agents/examples/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[project]
name = "hello-world"
version = "0.1.0"
description = "Example hello world project"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"hypercorn>=0.17.3",
"pydantic>=2.10.6",
"restate-sdk[serde]>=0.13.2",
"openai-agents>=0.6.2",
]

[tool.hatch.build.targets.wheel]
packages = ["app"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[dependency-groups]
dev = [
"mypy>=1.18.2",
]
Loading
Loading