Build a graph by hand

In shortBuild a 10xGraph agent with a tool by hand, route between the agent and tool nodes, then compare it with ReactAgent.

  • 6 min read
  • 13 sections
  • Updated
  • v0.10.0
  • Markdown

You build a two-node graph by hand: an Agent node that calls the model and a ToolNode that runs a Python function, joined by a conditional edge that loops between them until the model stops asking for tools. Then you rebuild it with ReactAgent in a few lines and learn when each approach fits.

What you build in this step

You build an agent that can call a get_weather function. The model decides when it needs the tool, the graph runs the tool, and the result goes back to the model so it can write the final answer.

This is the same loop behind most tool-using agents, so building it once by hand makes every prebuilt agent easier to read. The graph looks like this:

flowchart LR
    S([START]) --> M[MAIN: Agent]
    M -->|tool calls| T[TOOL: ToolNode]
    M -->|no tool calls| E([END])
    T --> M

Prerequisites

You need the provider extra for the model you use, plus an API key in your environment. This page uses a Gemini model.

Terminal
pip install "10xgraph[google-genai]"   # or "10xgraph[openai]" or "10xgraph[anthropic]"
export GEMINI_API_KEY="your-key"       # or GOOGLE_API_KEY

The provider is picked from the model name: names starting with gemini- use Google, gpt- and the o1-, o3-, o4- families use OpenAI, and claude- uses Anthropic. For OpenAI, set OPENAI_API_KEY and change the model name. If you skipped the previous step, read the mental model first so the terms node, edge and state are familiar.

Write a tool function

A tool is a plain Python function with type hints and a docstring. 10xGraph reads the signature and docstring to build the schema the model sees, so a clear docstring is how you tell the model when to use the tool.

agent_with_tools.py
def get_weather(location: str) -> str:
    """Get the current weather for a specific location."""
    # A real tool would call a weather API here.
    return f"The weather in {location} is sunny and 22 degrees C."

The model sees the parameter name and type (location: str) and the description from the docstring. Keep both specific: vague docstrings lead to tools being called at the wrong time.

Create the agent and the tool node

ToolNode wraps your functions so the graph can execute them. The Agent receives the same ToolNode so it knows which tools it may request.

agent_with_tools.py
from tenxgraph.core.graph import Agent, StateGraph, ToolNode
from tenxgraph.core.state import AgentState, Message
from tenxgraph.utils import END

# get_weather is defined above in the same file.

# Wrap the function so the graph can run it.
tool_node = ToolNode([get_weather])

# The agent sends the conversation to the model and may request tools.
agent = Agent(
    model="gemini-2.5-flash",
    system_prompt=[
        {
            "role": "system",
            "content": "You are a helpful assistant. Use the get_weather tool when you need weather information.",
        }
    ],
    tool_node=tool_node,
)

Route between the agent and the tools

A routing function reads the state after the agent runs and returns the name of the next node. It returns "TOOL" when the last message is an assistant message with tool calls, and END otherwise.

agent_with_tools.py
def should_use_tools(state: AgentState) -> str:
    """Return "TOOL" if the last assistant message requested tools, else END."""
    if not state.context:
        return END

    last_message = state.context[-1]
    if (
        last_message.role == "assistant"
        and hasattr(last_message, "tools_calls")
        and last_message.tools_calls
    ):
        return "TOOL"

    return END

Assemble and compile the graph

Add the two nodes, connect them, and compile. The conditional edge leaves MAIN, and a plain edge sends every tool result back to MAIN so the model can read it.

agent_with_tools.py
graph = StateGraph(AgentState)
graph.add_node("MAIN", agent)
graph.add_node("TOOL", tool_node)

# From MAIN, the routing function picks TOOL or END.
graph.add_conditional_edges(
    "MAIN",
    should_use_tools,
    {"TOOL": "TOOL", END: END},
)

# After tools run, always return to MAIN so the model can answer.
graph.add_edge("TOOL", "MAIN")

# Start at MAIN.
graph.set_entry_point("MAIN")

# compile() returns the runnable graph.
app = graph.compile()

The loop ends when the model answers without requesting a tool. The run also stops at the recursion limit (25 steps by default, set with recursion_limit in config), which guards against a model that never stops calling tools.

Run it and read the messages

invoke takes a dict with a messages list and returns a dict whose messages list holds the messages produced by the run. A thread_id in config names the conversation.

agent_with_tools.py
if __name__ == "__main__":
    result = app.invoke(
        {"messages": [Message.text_message("What is the weather in London?")]},
        config={"thread_id": "graph-with-tools-demo"},
    )

    for message in result["messages"]:
        print(f"{message.role}: {message.text()}")
Terminal
python agent_with_tools.py

The exact wording varies by model. This is an example of the shape you should see:

Text
assistant: I'll check the weather in London for you.
tool: The weather in London is sunny and 22 degrees C.
assistant: The weather in London is sunny and 22 degrees C.

Read it as the loop ran: the assistant asked for the tool, the ToolNode ran get_weather and produced a tool message, and the assistant used that result for its final answer. If the model needs several tools at once, ToolNode runs them in parallel within the same step.

Build the same agent with ReactAgent

ReactAgent is a prebuilt class that creates this exact graph for you: a MAIN agent node, a TOOL node, the routing function and the edge back to MAIN. You pass the functions directly with tools.

react_agent.py
from tenxgraph.core.state import Message
from tenxgraph.prebuilt.agent import ReactAgent


def get_weather(location: str) -> str:
    """Get the current weather for a specific location."""
    return f"The weather in {location} is sunny and 22 degrees C."


# ReactAgent builds the nodes, routing and edges internally.
app = ReactAgent(
    model="gemini-2.5-flash",
    system_prompt=[
        {
            "role": "system",
            "content": "You are a helpful assistant. Use the get_weather tool when you need weather information.",
        }
    ],
    tools=[get_weather],
).compile()

if __name__ == "__main__":
    result = app.invoke(
        {"messages": [Message.text_message("What is the weather in London?")]},
        config={"thread_id": "react-agent-demo"},
    )

    for message in result["messages"]:
        print(f"{message.role}: {message.text()}")

You get the same kind of conversation with no routing code. See the ReactAgent guide for its options.

Choose between ReactAgent and a hand-built graph

Use ReactAgent for the standard loop and build by hand when the flow is specific to your product. Both produce a compiled graph that you run, serve and test the same way, so you can start with ReactAgent and switch later.

Situation Use
Model calls tools, tools run, model answers ReactAgent
Default AgentState is enough ReactAgent
Custom routing, such as a validation or approval node between agent and tools Hand-built graph
Custom state fields Hand-built graph
Several agents with different roles in one graph Hand-built graph, or a prebuilt multi-agent class

For help picking among all building blocks, see Choosing a building block.

Handle tool errors

If a tool raises an exception, ToolNode catches it and returns a failed tool message instead of crashing the run. The model reads the error text on its next turn and can retry, try something else or tell the user.

Make the tool fail some of the time to watch this happen:

flaky_weather.py
import random


def get_weather(location: str) -> str:
    """Get the current weather for a specific location."""
    if random.random() < 0.3:  # about 30 percent of calls fail
        raise RuntimeError(f"Failed to fetch weather for {location}")
    return f"The weather in {location} is sunny and 22 degrees C."

Swap this function into either version above and run it several times. On a failure the tool message contains the error text, and the next assistant message reacts to it. Do not rely on this for tools with side effects such as payments: make those tools idempotent or have them return a clear error string, because the model may retry.

What you learned

  • A tool is a Python function whose signature and docstring become the schema the model sees.
  • ToolNode runs the functions the model requests, in parallel when there are several.
  • A routing function passed to add_conditional_edges decides between TOOL and END, and a plain edge sends results back to MAIN.
  • ReactAgent builds the same loop for you, and a hand-built graph gives full control over routing and state.
  • Tool exceptions reach the model as failed tool messages, so the model can recover.

Next step

Keep conversation state across calls with Add memory.

Frequently asked questions

When should I use ReactAgent instead of building the graph by hand?
Use ReactAgent when you want the standard loop where the model calls tools and then answers. Build by hand when you need custom routing, extra nodes or custom state fields.
What happens when a tool raises an exception?
ToolNode catches the exception and returns it to the model as a failed tool message. The model can retry, try another approach or explain the failure to the user.
Which API key do I need?
Set the key for the provider your model name resolves to. Gemini models read GEMINI_API_KEY or GOOGLE_API_KEY, and OpenAI models read OPENAI_API_KEY.
Last updated for v0.10.0Edit this page on GitHubReport an issue