Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions samples/langgraph-sql-agent/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
60 changes: 60 additions & 0 deletions samples/langgraph-sql-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# OpenTelemetry LangGraph instrumentation example

<!-- TODO: link to devsite doc once it is published -->

This sample is a LangGraph agent instrumented with OpenTelemetry to send traces and logs with
GenAI prompts and responses, and metrics to Google Cloud Observability.

The Agent is a SQL expert that has full access to an ephemeral SQLite database. The database is
initially empty. It is built with the the LangGraph prebuilt [ReAct
Agent](https://langchain-ai.github.io/langgraph/agents/agents/#basic-configuration#code) and the
[SQLDatabaseToolkit](https://python.langchain.com/docs/integrations/tools/sql_database/).

## APIs and Permissions

Enable the relevant Cloud Observability APIs if they aren't already enabled.
```sh
gcloud services enable telemetry.googleapis.com logging.googleapis.com monitoring.googleapis.com cloudtrace.googleapis.com
```

This sample writes to Cloud Logging, Cloud Monitoring, and Cloud Trace. Grant yourself the
following roles to run the example:
- `roles/logging.logWriter` – see https://cloud.google.com/logging/docs/access-control#permissions_and_roles
- `roles/monitoring.metricWriter` – see https://cloud.google.com/monitoring/access-control#predefined_roles
- `roles/telemetry.writer` – see https://cloud.google.com/trace/docs/iam#telemetry-roles

## Running the example

The sample can easily be run in Cloud Shell. You can also use
[Application Default Credentials][ADC] locally. Clone and set environment variables:
```sh
git clone https://github.com/GoogleCloudPlatform/opentelemetry-operations-python.git
cd opentelemetry-operations-python/samples/langgraph-sql-agent

# Capture GenAI prompts and responses
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
# Capture application logs automatically
export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true
```

Create a virtual environment and run the sample:
```sh
python -m venv venv/
source venv/bin/activate
pip install -r requirements.txt
python main.py
```

Alternatively if you have [`uv`](https://docs.astral.sh/uv/) installed:

```sh
uv run main.py
```

## Viewing the results

To view the generated traces with [Generative AI
events](https://cloud.google.com/trace/docs/finding-traces#view_generative_ai_events) in the
GCP console, use the [Trace Explorer](https://cloud.google.com/trace/docs/finding-traces). Filter for spans named `invoke agent`.

[ADC]: https://cloud.google.com/docs/authentication/application-default-credentials
122 changes: 122 additions & 0 deletions samples/langgraph-sql-agent/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import sqlite3
import tempfile

from langchain_community.agent_toolkits.sql.toolkit import SQLDatabaseToolkit
from langchain_community.utilities.sql_database import SQLDatabase
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.runnables.config import (
RunnableConfig,
)
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.prebuilt import create_react_agent
from opentelemetry import trace
from sqlalchemy import create_engine

from patched_vertexai import PatchedChatVertexAI
from utils import ask_prompt, console, print_markdown, render_messages

SYSTEM_PROMPT = SystemMessage(
content=f"""\
You are a helpful AI assistant with a mastery of database design and querying. You have access
to an ephemeral sqlite3 database that you can query and modify through some tools. Help answer
questions and perform actions. Follow these rules:

- Make sure you always use sql_db_query_checker to validate SQL statements **before** running
them. In pseudocode: `checked_query = sql_db_query_checker(query);
sql_db_query(checked_query)`.
- Be creative and don't ask for permission! The database is ephemeral so it's OK to make some mistakes.
- The sqlite version is {sqlite3.sqlite_version} which supports multiple row inserts.
- Always prefer to insert multiple rows in a single call to the sql_db_query tool, if possible.
- You may request to execute multiple sql_db_query tool calls which will be run in parallel.

If you make a mistake, try to recover."""
)

INTRO_TEXT = """\
Starting agent using ephemeral SQLite DB {dbpath}. This demo allows you to chat with an Agent
that has full access to an ephemeral SQLite database. The database is initially empty. It is
built with the the LangGraph prebuilt **ReAct Agent** and the **SQLDatabaseToolkit**. Here are some samples you can try:

**Weather**
- Create a new table to hold weather data.
- Populate the weather database with 20 example rows.
- Add a new column for weather observer notes

**Pets**
- Create a database table for pets including an `owner_id` column.
- Add 20 example rows please.
- Create an owner table.
- Link the two tables together, adding new columns, values, and rows as needed.
- Write a query to join these tables and give the result of owners and their pets.
- Show me the query, then the output as a table

---
"""

tracer = trace.get_tracer(__name__)


def run_agent(*, model_name: str, recursion_limit: int = 50) -> None:
model = PatchedChatVertexAI(model=model_name)
checkpointer = InMemorySaver()

# Ephemeral sqlite database per run
_, dbpath = tempfile.mkstemp(suffix=".db")
engine = create_engine(
f"sqlite:///{dbpath}",
isolation_level="AUTOCOMMIT",
)

# The agent has access to the SQL database through these tools
db = SQLDatabase(engine)
toolkit = SQLDatabaseToolkit(db=db, llm=model)
# Filter out sql_db_list_tables since it only lists the initial tables
tools = [tool for tool in toolkit.get_tools() if tool.name != "sql_db_list_tables"]

# Use the prebuilt ReAct agent graph
# https://langchain-ai.github.io/langgraph/agents/agents/
agent = create_react_agent(
model, tools, checkpointer=checkpointer, prompt=SYSTEM_PROMPT
)
config: RunnableConfig = {
"configurable": {"thread_id": "default"},
"recursion_limit": recursion_limit,
}

print_markdown(INTRO_TEXT.format(dbpath=dbpath))

while True:
# Accept input from the user
try:
prompt_txt = ask_prompt()
except (EOFError, KeyboardInterrupt):
print_markdown("Exiting...")
break

if not prompt_txt:
continue
prompt = HumanMessage(prompt_txt)

with console.status("Agent is thinking"):
# [START opentelemetry_langgraph_agent_span]
# Invoke the agent within a span
with tracer.start_as_current_span("invoke agent"):
result = agent.invoke({"messages": [prompt]}, config=config)
# [END opentelemetry_langgraph_agent_span]

# Print history
render_messages(result["messages"])
102 changes: 102 additions & 0 deletions samples/langgraph-sql-agent/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import google.auth
import google.auth.transport.requests
import grpc
from google.auth.transport.grpc import AuthMetadataPlugin
from opentelemetry import _events as events
from opentelemetry import _logs as logs
from opentelemetry import metrics, trace
from opentelemetry.exporter.cloud_logging import CloudLoggingExporter
from opentelemetry.exporter.cloud_monitoring import CloudMonitoringMetricsExporter
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter,
)
from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor
from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor
from opentelemetry.sdk._events import EventLoggerProvider
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

from agent import run_agent


# [START opentelemetry_langgraph_otel_setup]
def setup_opentelemetry() -> None:
credentials, project_id = google.auth.default()
resource = Resource.create(
attributes={
SERVICE_NAME: "langgraph-sql-agent",
# The project to send spans to
"gcp.project_id": project_id,
}
)

# Set up OTLP auth
request = google.auth.transport.requests.Request()
auth_metadata_plugin = AuthMetadataPlugin(credentials=credentials, request=request)
channel_creds = grpc.composite_channel_credentials(
grpc.ssl_channel_credentials(),
grpc.metadata_call_credentials(auth_metadata_plugin),
)

# Set up OpenTelemetry Python SDK
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
credentials=channel_creds,
endpoint="https://telemetry.googleapis.com:443/v1/traces",
)
)
)
trace.set_tracer_provider(tracer_provider)

logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(CloudLoggingExporter())
)
logs.set_logger_provider(logger_provider)

event_logger_provider = EventLoggerProvider(logger_provider)
events.set_event_logger_provider(event_logger_provider)

reader = PeriodicExportingMetricReader(CloudMonitoringMetricsExporter())
meter_provider = MeterProvider(metric_readers=[reader], resource=resource)
metrics.set_meter_provider(meter_provider)

# Load instrumentors
SQLite3Instrumentor().instrument()
VertexAIInstrumentor().instrument()


# [END opentelemetry_langgraph_otel_setup]

# Make sure to set:
# OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true
# OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
# in order to full prompts and responses and logs messages.
setup_opentelemetry()
run_agent(
model_name="gemini-2.0-flash",
# You can increase this, but it may incur more token usage
# https://langchain-ai.github.io/langgraph/troubleshooting/errors/GRAPH_RECURSION_LIMIT/#troubleshooting
recursion_limit=50,
)
59 changes: 59 additions & 0 deletions samples/langgraph-sql-agent/patched_vertexai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from typing import Any

from google.cloud.aiplatform_v1.types import (
GenerateContentRequest as v1GenerateContentRequest,
)
from google.cloud.aiplatform_v1beta1.types import (
GenerateContentRequest,
)
from langchain_core.messages import (
BaseMessage,
)
from langchain_google_vertexai import ChatVertexAI


class PatchedChatVertexAI(ChatVertexAI):
def _prepare_request_gemini(
self, messages: list[BaseMessage], *args: Any, **kwargs: Any
) -> v1GenerateContentRequest | GenerateContentRequest:
# See https://github.com/langchain-ai/langchain-google/issues/886
#
# Filter out any blocked messages with no content which can appear if you have a blocked
# message from finish_reason SAFETY:
#
# AIMessage(
# content="",
# additional_kwargs={},
# response_metadata={
# "is_blocked": True,
# "safety_ratings": [ ... ],
# "finish_reason": "SAFETY",
# },
# ...
# )
#
# These cause `google.api_core.exceptions.InvalidArgument: 400 Unable to submit request
# because it must include at least one parts field`

messages = [
message
for message in messages
if not message.response_metadata.get("is_blocked", False)
]
return super()._prepare_request_gemini(messages, *args, **kwargs)
33 changes: 33 additions & 0 deletions samples/langgraph-sql-agent/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

[project]
name = "langgraph-sql-agent"
version = "0.1.0"
description = "A LangGraph ReAct agent that can run queries on an ephemeral SQLite database"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"langchain-community>=0.3.16",
"langchain-google-vertexai>=2.0.7",
"langgraph>=0.4.3",
"opentelemetry-exporter-gcp-logging>=1.9.0a0",
"opentelemetry-exporter-gcp-monitoring>=1.9.0a0",
"opentelemetry-exporter-otlp-proto-grpc>=1.33.0",
"opentelemetry-instrumentation-httpx>=0.54b0",
"opentelemetry-instrumentation-requests>=0.54b0",
"opentelemetry-instrumentation-sqlite3>=0.54b0",
"opentelemetry-instrumentation-vertexai>=2.0b0",
"rich>=14.0.0",
]
Loading