Skip to content

Commit b714f44

Browse files
authored
LangGraph console agent with instrumentation sample (#404)
* LangGraph console based instrumentation sample This sample has region tags to go in the public docs. It is a CLI based LangGraph agent that is manually instrumented for Cloud Obs. * Add README for LangGraph sample * Small readme tweaks
1 parent 5f53472 commit b714f44

File tree

9 files changed

+3822
-0
lines changed

9 files changed

+3822
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

samples/langgraph-sql-agent/README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# OpenTelemetry LangGraph instrumentation example
2+
3+
<!-- TODO: link to devsite doc once it is published -->
4+
5+
This sample is a LangGraph agent instrumented with OpenTelemetry to send traces and logs with
6+
GenAI prompts and responses, and metrics to Google Cloud Observability.
7+
8+
The Agent is a SQL expert that has full access to an ephemeral SQLite database. The database is
9+
initially empty. It is built with the the LangGraph prebuilt [ReAct
10+
Agent](https://langchain-ai.github.io/langgraph/agents/agents/#basic-configuration#code) and the
11+
[SQLDatabaseToolkit](https://python.langchain.com/docs/integrations/tools/sql_database/).
12+
13+
## APIs and Permissions
14+
15+
Enable the relevant Cloud Observability APIs if they aren't already enabled.
16+
```sh
17+
gcloud services enable telemetry.googleapis.com logging.googleapis.com monitoring.googleapis.com cloudtrace.googleapis.com
18+
```
19+
20+
This sample writes to Cloud Logging, Cloud Monitoring, and Cloud Trace. Grant yourself the
21+
following roles to run the example:
22+
- `roles/logging.logWriter` – see https://cloud.google.com/logging/docs/access-control#permissions_and_roles
23+
- `roles/monitoring.metricWriter` – see https://cloud.google.com/monitoring/access-control#predefined_roles
24+
- `roles/telemetry.writer` – see https://cloud.google.com/trace/docs/iam#telemetry-roles
25+
26+
## Running the example
27+
28+
The sample can easily be run in Cloud Shell. You can also use
29+
[Application Default Credentials][ADC] locally. Clone and set environment variables:
30+
```sh
31+
git clone https://github.com/GoogleCloudPlatform/opentelemetry-operations-python.git
32+
cd opentelemetry-operations-python/samples/langgraph-sql-agent
33+
34+
# Capture GenAI prompts and responses
35+
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
36+
# Capture application logs automatically
37+
export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true
38+
```
39+
40+
Create a virtual environment and run the sample:
41+
```sh
42+
python -m venv venv/
43+
source venv/bin/activate
44+
pip install -r requirements.txt
45+
python main.py
46+
```
47+
48+
Alternatively if you have [`uv`](https://docs.astral.sh/uv/) installed:
49+
50+
```sh
51+
uv run main.py
52+
```
53+
54+
## Viewing the results
55+
56+
To view the generated traces with [Generative AI
57+
events](https://cloud.google.com/trace/docs/finding-traces#view_generative_ai_events) in the
58+
GCP console, use the [Trace Explorer](https://cloud.google.com/trace/docs/finding-traces). Filter for spans named `invoke agent`.
59+
60+
[ADC]: https://cloud.google.com/docs/authentication/application-default-credentials

samples/langgraph-sql-agent/agent.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import sqlite3
16+
import tempfile
17+
18+
from langchain_community.agent_toolkits.sql.toolkit import SQLDatabaseToolkit
19+
from langchain_community.utilities.sql_database import SQLDatabase
20+
from langchain_core.messages import HumanMessage, SystemMessage
21+
from langchain_core.runnables.config import (
22+
RunnableConfig,
23+
)
24+
from langgraph.checkpoint.memory import InMemorySaver
25+
from langgraph.prebuilt import create_react_agent
26+
from opentelemetry import trace
27+
from sqlalchemy import create_engine
28+
29+
from patched_vertexai import PatchedChatVertexAI
30+
from utils import ask_prompt, console, print_markdown, render_messages
31+
32+
SYSTEM_PROMPT = SystemMessage(
33+
content=f"""\
34+
You are a helpful AI assistant with a mastery of database design and querying. You have access
35+
to an ephemeral sqlite3 database that you can query and modify through some tools. Help answer
36+
questions and perform actions. Follow these rules:
37+
38+
- Make sure you always use sql_db_query_checker to validate SQL statements **before** running
39+
them. In pseudocode: `checked_query = sql_db_query_checker(query);
40+
sql_db_query(checked_query)`.
41+
- Be creative and don't ask for permission! The database is ephemeral so it's OK to make some mistakes.
42+
- The sqlite version is {sqlite3.sqlite_version} which supports multiple row inserts.
43+
- Always prefer to insert multiple rows in a single call to the sql_db_query tool, if possible.
44+
- You may request to execute multiple sql_db_query tool calls which will be run in parallel.
45+
46+
If you make a mistake, try to recover."""
47+
)
48+
49+
INTRO_TEXT = """\
50+
Starting agent using ephemeral SQLite DB {dbpath}. This demo allows you to chat with an Agent
51+
that has full access to an ephemeral SQLite database. The database is initially empty. It is
52+
built with the the LangGraph prebuilt **ReAct Agent** and the **SQLDatabaseToolkit**. Here are some samples you can try:
53+
54+
**Weather**
55+
- Create a new table to hold weather data.
56+
- Populate the weather database with 20 example rows.
57+
- Add a new column for weather observer notes
58+
59+
**Pets**
60+
- Create a database table for pets including an `owner_id` column.
61+
- Add 20 example rows please.
62+
- Create an owner table.
63+
- Link the two tables together, adding new columns, values, and rows as needed.
64+
- Write a query to join these tables and give the result of owners and their pets.
65+
- Show me the query, then the output as a table
66+
67+
---
68+
"""
69+
70+
tracer = trace.get_tracer(__name__)
71+
72+
73+
def run_agent(*, model_name: str, recursion_limit: int = 50) -> None:
74+
model = PatchedChatVertexAI(model=model_name)
75+
checkpointer = InMemorySaver()
76+
77+
# Ephemeral sqlite database per run
78+
_, dbpath = tempfile.mkstemp(suffix=".db")
79+
engine = create_engine(
80+
f"sqlite:///{dbpath}",
81+
isolation_level="AUTOCOMMIT",
82+
)
83+
84+
# The agent has access to the SQL database through these tools
85+
db = SQLDatabase(engine)
86+
toolkit = SQLDatabaseToolkit(db=db, llm=model)
87+
# Filter out sql_db_list_tables since it only lists the initial tables
88+
tools = [tool for tool in toolkit.get_tools() if tool.name != "sql_db_list_tables"]
89+
90+
# Use the prebuilt ReAct agent graph
91+
# https://langchain-ai.github.io/langgraph/agents/agents/
92+
agent = create_react_agent(
93+
model, tools, checkpointer=checkpointer, prompt=SYSTEM_PROMPT
94+
)
95+
config: RunnableConfig = {
96+
"configurable": {"thread_id": "default"},
97+
"recursion_limit": recursion_limit,
98+
}
99+
100+
print_markdown(INTRO_TEXT.format(dbpath=dbpath))
101+
102+
while True:
103+
# Accept input from the user
104+
try:
105+
prompt_txt = ask_prompt()
106+
except (EOFError, KeyboardInterrupt):
107+
print_markdown("Exiting...")
108+
break
109+
110+
if not prompt_txt:
111+
continue
112+
prompt = HumanMessage(prompt_txt)
113+
114+
with console.status("Agent is thinking"):
115+
# [START opentelemetry_langgraph_agent_span]
116+
# Invoke the agent within a span
117+
with tracer.start_as_current_span("invoke agent"):
118+
result = agent.invoke({"messages": [prompt]}, config=config)
119+
# [END opentelemetry_langgraph_agent_span]
120+
121+
# Print history
122+
render_messages(result["messages"])

samples/langgraph-sql-agent/main.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import google.auth
16+
import google.auth.transport.requests
17+
import grpc
18+
from google.auth.transport.grpc import AuthMetadataPlugin
19+
from opentelemetry import _events as events
20+
from opentelemetry import _logs as logs
21+
from opentelemetry import metrics, trace
22+
from opentelemetry.exporter.cloud_logging import CloudLoggingExporter
23+
from opentelemetry.exporter.cloud_monitoring import CloudMonitoringMetricsExporter
24+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
25+
OTLPSpanExporter,
26+
)
27+
from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor
28+
from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor
29+
from opentelemetry.sdk._events import EventLoggerProvider
30+
from opentelemetry.sdk._logs import LoggerProvider
31+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
32+
from opentelemetry.sdk.metrics import MeterProvider
33+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
34+
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
35+
from opentelemetry.sdk.trace import TracerProvider
36+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
37+
38+
from agent import run_agent
39+
40+
41+
# [START opentelemetry_langgraph_otel_setup]
42+
def setup_opentelemetry() -> None:
43+
credentials, project_id = google.auth.default()
44+
resource = Resource.create(
45+
attributes={
46+
SERVICE_NAME: "langgraph-sql-agent",
47+
# The project to send spans to
48+
"gcp.project_id": project_id,
49+
}
50+
)
51+
52+
# Set up OTLP auth
53+
request = google.auth.transport.requests.Request()
54+
auth_metadata_plugin = AuthMetadataPlugin(credentials=credentials, request=request)
55+
channel_creds = grpc.composite_channel_credentials(
56+
grpc.ssl_channel_credentials(),
57+
grpc.metadata_call_credentials(auth_metadata_plugin),
58+
)
59+
60+
# Set up OpenTelemetry Python SDK
61+
tracer_provider = TracerProvider(resource=resource)
62+
tracer_provider.add_span_processor(
63+
BatchSpanProcessor(
64+
OTLPSpanExporter(
65+
credentials=channel_creds,
66+
endpoint="https://telemetry.googleapis.com:443/v1/traces",
67+
)
68+
)
69+
)
70+
trace.set_tracer_provider(tracer_provider)
71+
72+
logger_provider = LoggerProvider(resource=resource)
73+
logger_provider.add_log_record_processor(
74+
BatchLogRecordProcessor(CloudLoggingExporter())
75+
)
76+
logs.set_logger_provider(logger_provider)
77+
78+
event_logger_provider = EventLoggerProvider(logger_provider)
79+
events.set_event_logger_provider(event_logger_provider)
80+
81+
reader = PeriodicExportingMetricReader(CloudMonitoringMetricsExporter())
82+
meter_provider = MeterProvider(metric_readers=[reader], resource=resource)
83+
metrics.set_meter_provider(meter_provider)
84+
85+
# Load instrumentors
86+
SQLite3Instrumentor().instrument()
87+
VertexAIInstrumentor().instrument()
88+
89+
90+
# [END opentelemetry_langgraph_otel_setup]
91+
92+
# Make sure to set:
93+
# OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true
94+
# OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
95+
# in order to full prompts and responses and logs messages.
96+
setup_opentelemetry()
97+
run_agent(
98+
model_name="gemini-2.0-flash",
99+
# You can increase this, but it may incur more token usage
100+
# https://langchain-ai.github.io/langgraph/troubleshooting/errors/GRAPH_RECURSION_LIMIT/#troubleshooting
101+
recursion_limit=50,
102+
)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from typing import Any
18+
19+
from google.cloud.aiplatform_v1.types import (
20+
GenerateContentRequest as v1GenerateContentRequest,
21+
)
22+
from google.cloud.aiplatform_v1beta1.types import (
23+
GenerateContentRequest,
24+
)
25+
from langchain_core.messages import (
26+
BaseMessage,
27+
)
28+
from langchain_google_vertexai import ChatVertexAI
29+
30+
31+
class PatchedChatVertexAI(ChatVertexAI):
32+
def _prepare_request_gemini(
33+
self, messages: list[BaseMessage], *args: Any, **kwargs: Any
34+
) -> v1GenerateContentRequest | GenerateContentRequest:
35+
# See https://github.com/langchain-ai/langchain-google/issues/886
36+
#
37+
# Filter out any blocked messages with no content which can appear if you have a blocked
38+
# message from finish_reason SAFETY:
39+
#
40+
# AIMessage(
41+
# content="",
42+
# additional_kwargs={},
43+
# response_metadata={
44+
# "is_blocked": True,
45+
# "safety_ratings": [ ... ],
46+
# "finish_reason": "SAFETY",
47+
# },
48+
# ...
49+
# )
50+
#
51+
# These cause `google.api_core.exceptions.InvalidArgument: 400 Unable to submit request
52+
# because it must include at least one parts field`
53+
54+
messages = [
55+
message
56+
for message in messages
57+
if not message.response_metadata.get("is_blocked", False)
58+
]
59+
return super()._prepare_request_gemini(messages, *args, **kwargs)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
[project]
16+
name = "langgraph-sql-agent"
17+
version = "0.1.0"
18+
description = "A LangGraph ReAct agent that can run queries on an ephemeral SQLite database"
19+
readme = "README.md"
20+
requires-python = ">=3.12"
21+
dependencies = [
22+
"langchain-community>=0.3.16",
23+
"langchain-google-vertexai>=2.0.7",
24+
"langgraph>=0.4.3",
25+
"opentelemetry-exporter-gcp-logging>=1.9.0a0",
26+
"opentelemetry-exporter-gcp-monitoring>=1.9.0a0",
27+
"opentelemetry-exporter-otlp-proto-grpc>=1.33.0",
28+
"opentelemetry-instrumentation-httpx>=0.54b0",
29+
"opentelemetry-instrumentation-requests>=0.54b0",
30+
"opentelemetry-instrumentation-sqlite3>=0.54b0",
31+
"opentelemetry-instrumentation-vertexai>=2.0b0",
32+
"rich>=14.0.0",
33+
]

0 commit comments

Comments
 (0)