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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Examples for teams using a specific framework who want to add Opik.

| | Description |
|---|---|
| [integrations/google_adk/agentic_rag](integrations/google_adk/agentic_rag/) | Trace a Google ADK Agentic RAG router with Opik |
| [integrations/otel/offline_evaluation](integrations/otel/offline_evaluation/) | OTel tracing alongside Opik's offline evaluation workflow |
| [integrations/otel/distributed_tracing](integrations/otel/distributed_tracing/) | Stitch out-of-process tool call spans into a single trace |

Expand Down
1 change: 1 addition & 0 deletions integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ Examples for adding Opik to a specific framework or library. Each folder covers

| Integration | Description |
|---|---|
| [google_adk/](./google_adk/) | Google ADK — Trace an Agentic RAG router with Opik |
| [otel/](./otel/) | OpenTelemetry — send OTel spans to Opik via OTLP |
7 changes: 7 additions & 0 deletions integrations/google_adk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Google ADK + Opik

Examples for tracing [Google ADK](https://google.github.io/adk-docs/) agents with Opik.

| Example | Description |
|---|---|
| [agentic_rag](./agentic_rag/) | Trace an Agentic RAG router (Qdrant + web search) with Opik |
10 changes: 10 additions & 0 deletions integrations/google_adk/agentic_rag/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Google (Gemini via ADK). Leave unset to run in DRY_RUN.
GOOGLE_API_KEY=your_google_api_key_here

# Optional: override the Gemini model the router runs on (default: gemini-2.5-flash).
# GADK_MODEL=gemini-2.0-flash-001

# Opik (Comet-hosted). Leave OPIK_API_KEY/OPIK_WORKSPACE unset to run in DRY_RUN.
OPIK_API_KEY=your_opik_api_key_here
OPIK_WORKSPACE=your_workspace
OPIK_PROJECT_NAME=google-adk-rag
3 changes: 3 additions & 0 deletions integrations/google_adk/agentic_rag/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Example-specific artifacts (generic ignores live in the repo-root .gitignore)
db/
annual_report.pdf
41 changes: 41 additions & 0 deletions integrations/google_adk/agentic_rag/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Google ADK Agentic RAG with Comet Opik

Trace a Google ADK router agent with Comet Opik.

## What this does

This example runs a Google ADK router agent that decides whether each query should use local RAG over the IMF Financial Access Survey PDF or live web search. It indexes the PDF into a local Qdrant database, exposes `retrieve_docs` and `web_search` tools, and traces the routing decision and tool calls with Opik.

## Prerequisites

This is a `uv` project — dependencies live in `pyproject.toml`.

```bash
uv sync
```

Copy `.env.example` to `.env` (or `export` the variables). With `GOOGLE_API_KEY` / Opik credentials unset, the example runs in **DRY_RUN** and prints what it would do instead of calling Gemini/Opik.

| Variable | Required | Description |
|---|---|---|
| `GOOGLE_API_KEY` | for a live run | Gemini API key (Google AI Studio). Unset → DRY_RUN. |
| `OPIK_API_KEY` | for a live run | Opik API key from [comet.com/opik](https://www.comet.com/opik). Unset → DRY_RUN. |
| `OPIK_WORKSPACE` | for a live run | Your Opik workspace. Unset → DRY_RUN. |
| `OPIK_PROJECT_NAME` | no | Project traces are logged to (default `google-adk-rag`). |
| `GADK_MODEL` | no | Gemini model the router runs on (default `gemini-2.5-flash`). |

## Running it

```bash
uv run python index.py # build the local Qdrant index from the IMF PDF
uv run python main.py # run the traced ADK router

# or both, the way CI does:
bash run.sh
```

## How it works

1. **Index the PDF** — `index.py` downloads the IMF report, chunks it, embeds it, and writes vectors into the local `db` directory.
2. **Expose tools** — `tools.py` defines `retrieve_docs` for Qdrant search and `web_search` for DuckDuckGo search.
3. **Run the router** — `main.py` creates the `router_agent`, traces it with `OpikTracer` (logging to `OPIK_PROJECT_NAME`, default `google-adk-rag`), and runs one query through ADK.
18 changes: 18 additions & 0 deletions integrations/google_adk/agentic_rag/constant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
DESCRIPTION = """
Routes user queries to the appropriate specialist tool based on the nature of the question.
"""

INSTRUCTION = """
You are a routing Agent. Your only job is to understand the user's query and delegate it
to the right tool. Never answer yourself.

Route to retrieve_docs tool when:
- The question is about the Financial, Banking or Anything related to Annual Survey Report
- The user asks about financials, revenue, growth, survey data, or statistics from the report

Route to web_search when:
- The question is about current events, recent news, or anything happening in the world
- The user asks about trends, market updates, or latest developments

If the query is ambiguous, route to retrieve_docs tool by default.
"""
76 changes: 76 additions & 0 deletions integrations/google_adk/agentic_rag/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import os
import urllib.request
from pathlib import Path

import pypdfium2 as pdfium
from fastembed import TextEmbedding
from qdrant_client import QdrantClient, models
from tqdm import tqdm

PDF_URL = "https://data.imf.org/-/media/iData/External-Storage/Documents/7FC05452C6C743D2BFB6188D2E248A38/en/2025-FAS-Annual-Report.pdf"
PDF_PATH = Path("annual_report.pdf")
DB_PATH = Path("db")
COLLECTION_NAME = "fas"
MODEL_NAME = "jinaai/jina-embeddings-v2-small-en"

GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
OPIK_API_KEY = os.environ.get("OPIK_API_KEY")
OPIK_WORKSPACE = os.environ.get("OPIK_WORKSPACE")
# Gate on the same creds as main.py: no point doing the heavy, networked download +
# embedding unless the traced run that consumes this index (main.py) will actually run.
DRY_RUN = not (GOOGLE_API_KEY and OPIK_API_KEY and OPIK_WORKSPACE)


def download_pdf() -> None:
if PDF_PATH.exists():
return
urllib.request.urlretrieve(PDF_URL, PDF_PATH)


def load_pdf_chunks(pdf_path: str, chunk_size: int = 500) -> list[str]:
pdf = pdfium.PdfDocument(pdf_path)
text = " ".join(page.get_textpage().get_text_range() for page in pdf)
words = text.split()
return [" ".join(words[i : i + chunk_size]) for i in range(0, len(words), chunk_size)]


def index_documents(chunks: list[str], batch_size: int = 16) -> None:
embed_model = TextEmbedding(model_name=MODEL_NAME)
client = QdrantClient(path=str(DB_PATH))

if client.collection_exists(COLLECTION_NAME):
client.delete_collection(COLLECTION_NAME)

client.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=models.VectorParams(size=512, distance=models.Distance.COSINE),
)

point_id = 0
for i in tqdm(range(0, len(chunks), batch_size)):
batch_chunks = chunks[i : i + batch_size]
vectors = [list(vec) for vec in embed_model.embed(batch_chunks)]
points = [
models.PointStruct(
id=point_id + j,
vector=vec,
payload={"doc_id": f"doc_{point_id + j}", "text": chunk},
)
for j, (vec, chunk) in enumerate(zip(vectors, batch_chunks, strict=False))
]
client.upsert(collection_name=COLLECTION_NAME, points=points)
point_id += len(batch_chunks)


def main() -> None:
if DRY_RUN:
print("[DRY RUN] Opik credentials not set — skipping PDF download + indexing.")
return
download_pdf()
chunks = load_pdf_chunks(str(PDF_PATH))
index_documents(chunks)
print("indexing done")


if __name__ == "__main__":
main()
84 changes: 84 additions & 0 deletions integrations/google_adk/agentic_rag/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""
Google ADK Agentic RAG router, traced with Opik.

Run end-to-end:
export GOOGLE_API_KEY="<your-google-api-key>"
export OPIK_API_KEY="<your-opik-api-key>"
export OPIK_WORKSPACE="<your-opik-workspace>"
python index.py && python main.py

With GOOGLE_API_KEY / Opik credentials unset, this prints a DRY_RUN line and exits 0.
You can create an Opik API key from https://www.comet.com/opik.
"""

import asyncio
import os

MODEL_NAME = os.environ.get("GADK_MODEL", "gemini-2.5-flash")
APP_NAME = "agentic-rag"
USER_ID = "user"
SESSION_ID = "session_01"
QUERY = "projected reach of the digital remittance market by 2034"

GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
OPIK_API_KEY = os.environ.get("OPIK_API_KEY")
OPIK_WORKSPACE = os.environ.get("OPIK_WORKSPACE")
OPIK_PROJECT_NAME = os.environ.get("OPIK_PROJECT_NAME", "google-adk-rag")

# No Google/Opik credentials -> print what would run instead of calling Gemini + Opik.
DRY_RUN = not (GOOGLE_API_KEY and OPIK_API_KEY and OPIK_WORKSPACE)


def build_agent():
from google.adk.agents import LlmAgent
from opik.integrations.adk import OpikTracer, track_adk_agent_recursive

from constant import DESCRIPTION, INSTRUCTION
from tools import retrieve_docs, web_search

agent = LlmAgent(
name="router_agent",
model=MODEL_NAME,
description=DESCRIPTION,
instruction=INSTRUCTION,
tools=[retrieve_docs, web_search],
)
opik_tracer = OpikTracer(name="router-agent", project_name=OPIK_PROJECT_NAME)
track_adk_agent_recursive(agent, opik_tracer)
return agent


async def _create_session(session_service) -> None:
await session_service.create_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID)


def run_agent(query: str) -> str:
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types

agent = build_agent()
session_service = InMemorySessionService()
asyncio.run(_create_session(session_service))
runner = Runner(agent=agent, app_name=APP_NAME, session_service=session_service)

content = types.Content(role="user", parts=[types.Part(text=query)])
events = runner.run(user_id=USER_ID, session_id=SESSION_ID, new_message=content)
for event in events:
if event.is_final_response() and event.content and event.content.parts:
return (event.content.parts[0].text or "").strip()
return ""


def main() -> None:
if DRY_RUN:
print(
"[DRY RUN] GOOGLE_API_KEY / Opik credentials not set — would route this query "
f"through the ADK router (retrieve_docs / web_search) and trace it to Opik:\n {QUERY}"
)
return
print(run_agent(QUERY))


if __name__ == "__main__":
main()
29 changes: 29 additions & 0 deletions integrations/google_adk/agentic_rag/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[project]
name = "google-adk-rag"
version = "0.1.0"
description = "Trace a Google ADK Agentic RAG router (Qdrant + web search) with Opik."
readme = "README.md"
requires-python = ">=3.12,<3.14"
dependencies = [
"google-adk",
"opik>=2.0",
"pypdfium2",
"fastembed",
"qdrant-client",
"ddgs",
"tqdm",
]

[dependency-groups]
dev = ["ruff"]

# WHY: loose runnable scripts, not an installable package — uv manages the env but builds nothing.
[tool.uv]
package = false

[tool.ruff]
line-length = 110
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]
12 changes: 12 additions & 0 deletions integrations/google_adk/agentic_rag/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -e

export OPIK_PROJECT_NAME="google-adk-rag"

uv sync

# index.py builds the local Qdrant DB; main.py runs the traced router. With no
# GOOGLE_API_KEY / Opik credentials both fall back to DRY_RUN and exit 0 (the
# secrets-free CI check). With credentials set, this indexes the PDF and logs a trace.
uv run python index.py
uv run python main.py
43 changes: 43 additions & 0 deletions integrations/google_adk/agentic_rag/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from ddgs import DDGS
from fastembed import TextEmbedding
from qdrant_client import QdrantClient

from index import COLLECTION_NAME, DB_PATH, MODEL_NAME

embed_model = TextEmbedding(model_name=MODEL_NAME)
client = QdrantClient(path=str(DB_PATH))


def retrieve_docs(query: str):
"""
Search similar documents from the knowledge base.
Args:
query: User query used to retrieve relevant documents.
"""
query_vector = list(embed_model.embed([query]))[0]

results = client.query_points(
collection_name=COLLECTION_NAME,
query=query_vector,
limit=4,
with_payload=True,
)
return [
{
"score": point.score,
"text": point.payload["text"],
}
for point in results.points
]


def web_search(query: str) -> str:
"""
Simple DuckDuckGo web search.
Args:
query: User query used to retrieve browsing results
"""
with DDGS() as ddgs:
results = ddgs.text(query, max_results=5)

return "\n\n".join(r.get("body", "") for r in results)
Loading