Multi-Agent LangGraph
Instrument a LangGraph multi-agent workflow with Neatlogs.
LangGraph is a popular Python library for building agents as a graph of steps. A common pattern is a "supervisor" step that reads the user's request and routes it to the right specialist agent (e.g. a knowledge-base agent or an orders agent). This guide traces such a workflow so you can see which agent the supervisor chose and what each one did.
If you're new to spans and traces, start with Your First Trace.
Setup
import os
import neatlogs
neatlogs.init(
api_key=os.environ["NEATLOGS_API_KEY"],
workflow_name="customer-support",
)
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.graph import END, START, StateGraph
handler = neatlogs.langchain_handler()Define State and Agents
from typing import Annotated, Sequence, TypedDict, Literal
from langchain_core.messages import BaseMessage, add_messages
import neatlogs
from neatlogs import SystemPromptTemplate, UserPromptTemplate
class WorkflowState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
intent: str
llm = ChatOpenAI(model="gpt-4o", temperature=0.3)
supervisor_prompt = SystemPromptTemplate([{
"role": "system",
"content": "Classify the intent of the user's query as either 'knowledge' or 'orders'. Respond with only the intent word.",
}])
supervisor_user_prompt = UserPromptTemplate([{"role": "user", "content": "Query: {{query}}"}])
def supervisor(state: WorkflowState) -> dict:
query = state["messages"][-1].content
with neatlogs.trace("classify_intent", kind="LLM",
system_prompt_template=supervisor_prompt,
user_prompt_template=supervisor_user_prompt):
msgs = supervisor_prompt.compile() + supervisor_user_prompt.compile(query=query)
response = llm.invoke(msgs)
intent = "orders" if "order" in response.content.lower() else "knowledge"
return {"intent": intent, "messages": [response]}
knowledge_prompt = SystemPromptTemplate([{
"role": "system",
"content": "You are a helpful assistant. Answer the user's question about our products and policies.",
}])
knowledge_user_prompt = UserPromptTemplate([{"role": "user", "content": "{{question}}"}])
def knowledge_agent(state: WorkflowState) -> dict:
question = state["messages"][0].content
with neatlogs.trace("answer_question", kind="LLM",
system_prompt_template=knowledge_prompt,
user_prompt_template=knowledge_user_prompt):
msgs = knowledge_prompt.compile() + knowledge_user_prompt.compile(question=question)
response = llm.invoke(msgs)
return {"messages": [response]}
orders_prompt = SystemPromptTemplate([{
"role": "system",
"content": "You are an order management assistant. Help the user with their order inquiry.",
}])
orders_user_prompt = UserPromptTemplate([{"role": "user", "content": "{{question}}"}])
def orders_agent(state: WorkflowState) -> dict:
question = state["messages"][0].content
with neatlogs.trace("handle_order", kind="LLM",
system_prompt_template=orders_prompt,
user_prompt_template=orders_user_prompt):
msgs = orders_prompt.compile() + orders_user_prompt.compile(question=question)
response = llm.invoke(msgs)
return {"messages": [response]}Build and Run the Graph
def route(state: WorkflowState) -> Literal["knowledge_agent", "orders_agent"]:
return "orders_agent" if state["intent"] == "orders" else "knowledge_agent"
graph = StateGraph(WorkflowState)
graph.add_node("supervisor", supervisor)
graph.add_node("knowledge_agent", knowledge_agent)
graph.add_node("orders_agent", orders_agent)
graph.add_edge(START, "supervisor")
graph.add_conditional_edges("supervisor", route)
graph.add_edge("knowledge_agent", END)
graph.add_edge("orders_agent", END)
app = graph.compile()
@neatlogs.span(kind="WORKFLOW", name="support_request")
def run_workflow(query: str) -> str:
# Attach the handler at the graph invocation — every node (and the LLM nested under it) inherits it.
result = app.invoke(
{
"messages": [HumanMessage(content=query)],
"intent": "",
},
config={"callbacks": [handler]},
)
return result["messages"][-1].content
print(run_workflow("What is your return policy?"))
neatlogs.flush()
neatlogs.shutdown()This example is synchronous (app.invoke, llm.invoke), so the handler propagates to every node automatically. If you run the graph async (app.ainvoke / astream) on Python 3.10, nodes must accept and forward config so their LLM calls inherit the handler — see the async note in the LangChain integration. On Python 3.11+ no change is needed.
What You'll See in the Dashboard
A WORKFLOW span containing:
- Spans for each graph node (
supervisor,knowledge_agent,orders_agent) - LLM spans for each provider call with prompt templates attached
