Instrument the Application
Attach the Splunk Agent Observability Callback
The agent runs its LangGraph workflow asynchronously, so you’ll attach Splunk Agent Observability’s async callback handler. Because the callback is passed at the graph level, it propagates to every node automatically, with no per-tool instrumentation required.
Add the imports
We’ve already added the following imports to the
~/workshop/healthcare-assistant/2-app-with-instrumentation/agent.py file, which are required
to collect traces:
import os
from galileo import galileo_context
from galileo.handlers.langchain import GalileoAsyncCallbackNote about the SDK
GalileoAsyncCallback.
For new deployments, we recommend using the SplunkAOAsyncCallback package instead.
Refer to the
LangChain and LangGraph
document
for details about this newer SDK.Wrap the graph invocation in a Galileo context
The base version of _process_query_async invokes the graph with no tracing:
async def _process_query_async(self, messages: List[Dict[str, str]]) -> str:
if not self.tools:
self.load_tools()
self.graph = self._build_graph()
langchain_messages: List[BaseMessage] = []
for msg in messages:
if msg["role"] == "user":
langchain_messages.append(HumanMessage(content=msg["content"]))
elif msg["role"] == "assistant":
langchain_messages.append(AIMessage(content=msg["content"]))
result = await self.graph.ainvoke(
{"messages": langchain_messages},
self.langgraph_config,
)
if result["messages"]:
return result["messages"][-1].content
return "No response generated"We’ve updated the ~/workshop/healthcare-assistant/2-app-with-instrumentation/agent.py file
to update this function to open a galileo_context, start a session keyed to the agent’s session_id,
and attach a fresh GalileoAsyncCallback to the run config:
async def _process_query_async(self, messages: List[Dict[str, str]]) -> str:
if not self.tools:
self.load_tools()
self.graph = self._build_graph()
langchain_messages: List[BaseMessage] = []
for msg in messages:
if msg["role"] == "user":
langchain_messages.append(HumanMessage(content=msg["content"]))
elif msg["role"] == "assistant":
langchain_messages.append(AIMessage(content=msg["content"]))
with galileo_context(
project=os.getenv("GALILEO_PROJECT"),
log_stream=os.getenv("GALILEO_LOG_STREAM"),
):
galileo_context.start_session(external_id=self.session_id)
# One callback per request keeps each user turn in its own trace.
callback = GalileoAsyncCallback()
run_config = {**self.langgraph_config, "callbacks": [callback]}
result = await self.graph.ainvoke(
{"messages": langchain_messages},
run_config,
)
if result["messages"]:
return result["messages"][-1].content
return "No response generated"Why a single callback per request?
GalileoAsyncCallback per call to _process_query_async keeps each user turn
in its own trace. Because it’s attached to the LangGraph run config, every node’s LLM and
tool call becomes a nested span under that same trace, giving you the end-to-end view of a
turn instead of a pile of disconnected spans.Why does this app use GalileoAsyncCallback rather than GalileoCallback?
