Skip to content

FastAPI + OpenAI Agents SDK

A complete example with a FastAPI backend using the OpenAI Agents SDK and a React frontend consuming the stream with @devscalelabs/react-sse-chat.

import json
from agents import Agent, RawResponsesStreamEvent, RunItemStreamEvent, Runner
from agents.extensions.memory import SQLAlchemySession
from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse
from openai.types.responses import ResponseFunctionToolCall, ResponseTextDeltaEvent
from sqlmodel import Session
from app.models.engine import engine, get_db
chat_router = APIRouter(prefix="/chat")
@chat_router.post("/")
async def generate_answer(request: ChatRequest, db_session: Session = Depends(get_db)):
session = SQLAlchemySession(
session_id=request.session_id,
engine=engine,
create_tables=True,
)
agent = Agent(
"Assistant",
instructions="You are a helpful assistant.",
model="gpt-4o-mini",
tools=[], # add your tools here
)
runner = Runner.run_streamed(agent, input=request.message, session=session)
async def event_generator():
async for event in runner.stream_events():
if event.type == "raw_response_event" and isinstance(
event, RawResponsesStreamEvent
):
if (
isinstance(event.data, ResponseTextDeltaEvent)
and event.data.delta != ""
):
yield f"data: {json.dumps({'type': 'text_delta', 'delta': event.data.delta})}\n\n"
elif (
isinstance(event, RunItemStreamEvent)
and event.name == "tool_called"
):
if isinstance(event.item.raw_item, ResponseFunctionToolCall):
yield f"data: {json.dumps({'type': 'tool_call', 'tool_name': event.item.raw_item.name, 'argument': event.item.raw_item.arguments})}\n\n"
return StreamingResponse(event_generator(), media_type="text/event-stream")

Since this backend emits both text_delta and tool_call events, use onEvent to handle them:

import { useChat } from "@devscalelabs/react-sse-chat";
function Chat() {
const { messages, isLoading, sendMessage, stop } = useChat({
api: "http://localhost:8000/chat/",
body: { session_id: "3" },
onEvent: (event, helpers) => {
switch (event.type) {
case "text_delta":
helpers.appendText(event.delta);
break;
case "tool_call":
helpers.appendPart({
type: "tool_call",
tool_name: event.tool_name,
argument: event.argument,
});
break;
}
},
});
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong>
{msg.parts.map((part, i) => {
switch (part.type) {
case "text":
return <span key={i} style={{ whiteSpace: "pre-wrap" }}>{part.text}</span>;
case "tool_call":
return (
<pre key={i} style={{ display: "block", opacity: 0.7, fontSize: "0.85em" }}>
[Tool: {part.tool_name}]{"\n"}
{JSON.stringify(JSON.parse(part.argument), null, 2)}
</pre>
);
default:
return null;
}
})}
</div>
))}
{isLoading && <button onClick={stop}>Stop</button>}
<form
onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.elements.namedItem("message") as HTMLInputElement;
sendMessage(input.value);
input.value = "";
}}
>
<input name="message" placeholder="Type a message..." />
<button type="submit" disabled={isLoading}>Send</button>
</form>
</div>
);
}

The full runnable example is in the cookbook/fastapi-agents directory:

Terminal window
git clone https://github.com/devscalelabs/react-sse-chat.git
cd react-sse-chat
pnpm install
pnpm --filter cookbook-fastapi-agents dev