Serve a 10xGraph Agent as a REST and SSE API
In shortDeploy a 10xGraph Python AI agent with the 10xgraph-api server. Configure 10xgraph.json, call /v1/graph/invoke and /v1/graph/stream, and use the TypeScript client.
- 5 min read
- 8 sections
- Updated
- v1.0.0rc4
- Markdown
You do not write server code to put a 10xGraph agent behind HTTP. You compile the graph, point 10xgraph.json at it, and the 10xgraph-api package serves REST and streaming endpoints. This example serves a small travel assistant on Anthropic claude-sonnet-5-5 and calls it with curl and with the TypeScript client.
What you will build
A small travel assistant on Anthropic claude-sonnet-5-5, compiled into a graph, and served by the 10xgraph-api package. You get REST and streaming endpoints, a Dockerfile and a TypeScript client call, without writing server code.
The project is two files: graph/agent.py builds the graph, and 10xgraph.json tells the server where to find it. client.ts is the optional TypeScript caller.
Key concepts
ReactAgent(...).compile(checkpointer=...): builds the agent graph. The compiled graph must be a module-level variable.10xgraph.json: the server config."agent": "graph.agent:app"points at that variable./v1/graph/invoke: runs the graph and returns the final messages./v1/graph/stream: runs the graph and streams chunks as newline-delimited JSON.thread_idinconfig: keeps a conversation across requests, through the checkpointer.10xgraph-client: the npm package that wraps these endpoints.
Run it
Run everything from the example folder, because the graph.agent import path is relative to it.
git clone https://github.com/10xGraph/10xGraph.git
cd 10xGraph/agentflow
pip install "10xgraph[anthropic]" 10xgraph-api python-dotenv
export ANTHROPIC_API_KEY=... # or put it in a .env file next to 10xgraph.json
npm install 10xgraph-client # only for the TypeScript example
cd examples/serving
python graph/agent.py # optional: check the graph without the server
10xgraph dev --no-open # API only, on http://127.0.0.1:8000
10xgraph dev # API plus the hosted playground in your browser
10xgraph build --docker-compose # writes Dockerfile, .dockerignore and docker-compose.ymlDocker is needed only for 10xgraph build output.
Call the running server:
curl -s http://127.0.0.1:8000/ping
curl -s http://127.0.0.1:8000/v1/graph/invoke \
-H 'Content-Type: application/json' \
-d '{
"messages": [{"role": "user", "content": [{"type": "text", "text": "Weather in Lisbon?"}]}],
"config": {"thread_id": "demo-1"}
}'
curl -N http://127.0.0.1:8000/v1/graph/stream \
-H 'Content-Type: application/json' \
-d '{
"messages": [{"role": "user", "content": [{"type": "text", "text": "And the local time there?"}]}],
"config": {"thread_id": "demo-1"}
}'Message content is a list of typed blocks, so a plain string is rejected. Reusing thread_id continues the same conversation.
The TypeScript version is in client.ts:
npx tsx client.tsHow the code works
graph/agent.py
The server imports this module once at startup. Two tools are plain functions, and their docstrings become the tool descriptions the model sees:
def get_weather(location: str) -> str:
"""Get the current weather for a location."""
return f"The weather in {location} is sunny, 24 C"The agent and the compiled graph:
agent = ReactAgent(
model="claude-sonnet-5-5",
system_prompt=[
{
"role": "system",
"content": (
"You are a concise travel assistant. Use tools for weather and local time. "
f"Today is {date.today().isoformat()}."
),
}
],
tools=[get_weather, get_time],
)
app = agent.compile(checkpointer=InMemoryCheckpointer())app must be a module-level variable, because the config refers to it by name. The checkpointer is what lets the server keep a thread between requests. InMemoryCheckpointer is fine for a demo, but it loses threads on restart and is not shared between workers. Switch to PgCheckpointer (the pg_checkpoint extra) before running more than one process.
The main() at the bottom runs the graph once without the server, so you can check the agent before wiring it up.
10xgraph.json
This is the only wiring the server needs:
{
"agent": "graph.agent:app",
"env": ".env",
"auth": null,
"remote_tools": []
}agent is module:attribute. env names the dotenv file loaded before the graph is imported. auth: null leaves the API open for local work. For anything reachable from a network, set "auth": "jwt" and JWT_SECRET_KEY. 10xgraph init scaffolds a project with the same layout, including a production template with auth, rate limiting and tests.
The server wraps results in an envelope. An invoke response looks like {"data": {"messages": [...]}, "metadata": {...}}. The stream endpoint sends one JSON object per line, each with an event (message, state, updates or error), and the message chunks carry the assistant text.
client.ts
The TypeScript client makes the same two calls. invoke waits for the answer:
const result = await client.invoke([Message.text_message('Weather in Lisbon?', 'user')], {
config: { thread_id: 'ts-demo' },
});stream is an async iterator, one chunk per line of the server response:
for await (const chunk of stream) {
if (chunk.event === 'message') {
console.log(chunk.message?.content);
}
}If you set "auth" in 10xgraph.json, pass the token with the commented authToken option.
Full code
graph/agent.py
"""The graph the 10xGraph API server serves.
`10xgraph.json` points at `app` below (`"agent": "graph.agent:app"`). The server
imports this module once at startup and exposes the compiled graph over REST and SSE.
Check it locally without a server: python graph/agent.py (from examples/serving)
"""
import asyncio
from datetime import date
from dotenv import load_dotenv
from tenxgraph.core.state import Message
from tenxgraph.prebuilt.agent import ReactAgent
from tenxgraph.storage.checkpointer import InMemoryCheckpointer
load_dotenv()
def get_weather(location: str) -> str:
"""Get the current weather for a location."""
return f"The weather in {location} is sunny, 24 C"
def get_time(city: str) -> str:
"""Get the local time for a city."""
return f"It is 14:30 in {city}"
agent = ReactAgent(
model="claude-sonnet-5-5",
system_prompt=[
{
"role": "system",
"content": (
"You are a concise travel assistant. Use tools for weather and local time. "
f"Today is {date.today().isoformat()}."
),
}
],
tools=[get_weather, get_time],
)
# The server needs a checkpointer to keep threads between requests. The in-memory one
# loses them on restart; use PgCheckpointer (pg_checkpoint extra) for real deployments.
app = agent.compile(checkpointer=InMemoryCheckpointer())
async def main() -> None:
result = await app.ainvoke(
{"messages": [Message.text_message("Weather in Lisbon?")]},
config={"thread_id": "local-check"},
)
print("assistant:", result["messages"][-1].text())
if __name__ == "__main__":
asyncio.run(main())10xgraph.json
{
"agent": "graph.agent:app",
"env": ".env",
"auth": null,
"remote_tools": []
}client.ts
// Call the served graph from TypeScript with the 10xgraph-client npm package.
// Install: npm install 10xgraph-client
// Run (Node 20+): npx tsx client.ts (with `10xgraph dev --no-open` running on :8000)
import { TenxGraphClient, Message } from '10xgraph-client';
const client = new TenxGraphClient({
baseUrl: 'http://localhost:8000',
// authToken: process.env.TENXGRAPH_TOKEN, // only when "auth" is set in 10xgraph.json
});
// One request, one answer. Reuse the same thread_id to continue a conversation.
const result = await client.invoke([Message.text_message('Weather in Lisbon?', 'user')], {
config: { thread_id: 'ts-demo' },
});
console.log(result.messages);
// Same call, streamed. Each chunk is one line of the server's /v1/graph/stream response.
const stream = client.stream([Message.text_message('And the local time there?', 'user')], {
config: { thread_id: 'ts-demo' },
});
for await (const chunk of stream) {
if (chunk.event === 'message') {
console.log(chunk.message?.content);
}
}Source on GitHub: examples/serving
Expected output
Illustrative; text and ids will differ.
$ curl -s http://127.0.0.1:8000/ping
{"data":"pong", ...}
$ curl -s http://127.0.0.1:8000/v1/graph/invoke ...
{"data":{"messages":[{"role":"assistant","content":[{"type":"text","text":"It is sunny in Lisbon, 24 C."}], ...}]}, ...}Next steps
- Evaluation runs the same kind of agent through
10xgraph eval. - Testing covers offline tests for the graph before you deploy it.
Frequently asked questions
- How does the server find my graph?
- 10xgraph.json has an agent field in module:attribute form, for example graph.agent:app. The server imports that module from the project directory and serves the CompiledGraph it finds there.
- What is the difference between /v1/graph/invoke and /v1/graph/stream?
- invoke waits for the run to finish and returns the final messages in one JSON body. stream returns the same run as newline-delimited JSON chunks as they are produced, which suits chat UIs.
- Do I need a checkpointer to serve an agent?
- Yes for multi-turn threads. The server keeps each conversation under the thread_id you send, and the checkpointer stores it. InMemoryCheckpointer loses threads on restart, so use PgCheckpointer in production.
- Are the old 10xgraph api and 10xgraph play commands still there?
- No. The current CLI has one server command, 10xgraph dev. It starts the API and opens the hosted playground. Add --no-open to run only the API.