From 35caf1f8c60a7b04e5d8fa8572ac8b0e1ee3bd71 Mon Sep 17 00:00:00 2001 From: Nightingalelyy Date: Mon, 17 Aug 2026 00:36:00 +0800 Subject: [PATCH] fix(examples): repair Elasticsearch, Google ADK, and Google GenAI tracing --- .../tracing/elasticsearch/01_sync_client.py | 52 +++++++ .../tracing/elasticsearch/02_async_client.py | 48 +++++++ python/tracing/elasticsearch/README.md | 12 ++ python/tracing/elasticsearch/_shared.py | 135 ++++++++++++++++++ python/tracing/elasticsearch/requirements.txt | 4 + python/tracing/elasticsearch/run_all.py | 24 ++++ python/tracing/google-adk/01_hello_world.py | 9 +- python/tracing/google-adk/02_tool_use.py | 11 +- .../google-adk/03_respan_attributes.py | 41 +++--- python/tracing/google-adk/_shared.py | 81 ++++++++++- python/tracing/google-adk/pyproject.toml | 3 +- python/tracing/google-adk/run_all.py | 25 ++++ .../google-genai/01_generate_content.py | 10 +- .../tracing/google-genai/02_stream_content.py | 10 +- .../google-genai/03_async_generate_content.py | 10 +- .../tracing/google-genai/04_tool_calling.py | 58 ++++++-- python/tracing/google-genai/_shared.py | 18 ++- python/tracing/google-genai/run_all.py | 29 ++++ 18 files changed, 528 insertions(+), 52 deletions(-) create mode 100644 python/tracing/elasticsearch/01_sync_client.py create mode 100644 python/tracing/elasticsearch/02_async_client.py create mode 100644 python/tracing/elasticsearch/README.md create mode 100644 python/tracing/elasticsearch/_shared.py create mode 100644 python/tracing/elasticsearch/requirements.txt create mode 100644 python/tracing/elasticsearch/run_all.py create mode 100644 python/tracing/google-adk/run_all.py create mode 100644 python/tracing/google-genai/run_all.py diff --git a/python/tracing/elasticsearch/01_sync_client.py b/python/tracing/elasticsearch/01_sync_client.py new file mode 100644 index 0000000..4c15bb5 --- /dev/null +++ b/python/tracing/elasticsearch/01_sync_client.py @@ -0,0 +1,52 @@ +"""Trace synchronous Elasticsearch index, search, and error operations.""" + +from __future__ import annotations + +from elasticsearch import Elasticsearch, NotFoundError +from respan import workflow + +from _shared import example_attributes, local_elasticsearch, make_respan + +EXAMPLE_NAME = "sync-client" + + +@workflow(name="elasticsearch_sync_client") +def run_sync_client(prompt: str, endpoint: str) -> dict[str, object]: + client = Elasticsearch(endpoint) + try: + indexed = client.index( + index="audit-index", + id="doc-1", + document={"title": prompt, "category": "observability"}, + refresh=True, + ) + searched = client.search( + index="audit-index", + query={"match": {"title": "Tracing"}}, + ) + missing_status = 0 + try: + client.get(index="audit-index", id="missing") + except NotFoundError as exc: + missing_status = exc.status_code + return { + "indexed": indexed["result"], + "hits": searched["hits"]["total"]["value"], + "missing_status": missing_status, + } + finally: + client.close() + + +def main() -> None: + respan = make_respan(EXAMPLE_NAME) + try: + with local_elasticsearch() as endpoint, example_attributes(EXAMPLE_NAME): + result = run_sync_client("Tracing Elasticsearch", endpoint) + print(result) + finally: + respan.shutdown() + + +if __name__ == "__main__": + main() diff --git a/python/tracing/elasticsearch/02_async_client.py b/python/tracing/elasticsearch/02_async_client.py new file mode 100644 index 0000000..8222abf --- /dev/null +++ b/python/tracing/elasticsearch/02_async_client.py @@ -0,0 +1,48 @@ +"""Trace asynchronous Elasticsearch index and search operations.""" + +from __future__ import annotations + +import asyncio + +from elasticsearch import AsyncElasticsearch +from respan import workflow + +from _shared import example_attributes, local_elasticsearch, make_respan + +EXAMPLE_NAME = "async-client" + + +@workflow(name="elasticsearch_async_client") +async def run_async_client(prompt: str, endpoint: str) -> dict[str, object]: + client = AsyncElasticsearch(endpoint) + try: + indexed = await client.index( + index="audit-index", + id="doc-1", + document={"title": prompt, "category": "async-observability"}, + refresh=True, + ) + searched = await client.search( + index="audit-index", + query={"match": {"title": "Async"}}, + ) + return { + "indexed": indexed["result"], + "hits": searched["hits"]["total"]["value"], + } + finally: + await client.close() + + +def main() -> None: + respan = make_respan(EXAMPLE_NAME) + try: + with local_elasticsearch() as endpoint, example_attributes(EXAMPLE_NAME): + result = asyncio.run(run_async_client("Async Elasticsearch", endpoint)) + print(result) + finally: + respan.shutdown() + + +if __name__ == "__main__": + main() diff --git a/python/tracing/elasticsearch/README.md b/python/tracing/elasticsearch/README.md new file mode 100644 index 0000000..9582128 --- /dev/null +++ b/python/tracing/elasticsearch/README.md @@ -0,0 +1,12 @@ +# Elasticsearch tracing examples + +These examples use the real synchronous and asynchronous Elasticsearch Python +clients against a deterministic local HTTP server, so no Elasticsearch cluster +or credentials are required. Traces are exported with the `RESPAN_API_KEY` from +the repository root `.env` file. + +Run both examples with one marker: + +```bash +RESPAN_EXAMPLE_RUN_ID=my-audit-marker python run_all.py +``` diff --git a/python/tracing/elasticsearch/_shared.py b/python/tracing/elasticsearch/_shared.py new file mode 100644 index 0000000..e16c12e --- /dev/null +++ b/python/tracing/elasticsearch/_shared.py @@ -0,0 +1,135 @@ +"""Shared local Elasticsearch server and Respan setup for the examples.""" + +from __future__ import annotations + +import json +import os +import threading +from contextlib import contextmanager +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from typing import Iterator +from uuid import uuid4 + +from dotenv import load_dotenv +from respan import Respan, propagate_attributes +from respan_instrumentation_elasticsearch import ElasticsearchInstrumentor + +PROJECT_ROOT = Path(__file__).resolve().parents[3] + + +def example_run_id() -> str: + return os.getenv("RESPAN_EXAMPLE_RUN_ID") or f"elasticsearch-{uuid4().hex[:10]}" + + +def make_respan(example_name: str) -> Respan: + load_dotenv(PROJECT_ROOT / ".env", override=True) + api_key = os.getenv("RESPAN_API_KEY") + if not api_key: + raise RuntimeError("RESPAN_API_KEY must be set in respan-example-projects/.env") + return Respan( + api_key=api_key, + base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), + app_name="elasticsearch-examples", + instrumentations=[ElasticsearchInstrumentor()], + is_batching_enabled=False, + metadata={ + "integration": "elasticsearch", + "example": example_name, + "run_id": example_run_id(), + }, + ) + + +def example_attributes(example_name: str): + run_id = example_run_id() + return propagate_attributes( + custom_identifier=f"{run_id}:{example_name}", + trace_group_identifier=f"{run_id}:{example_name}", + metadata={ + "integration": "elasticsearch", + "example": example_name, + "run_id": run_id, + }, + ) + + +class _ElasticsearchHandler(BaseHTTPRequestHandler): + server_version = "Elasticsearch/9.5.0" + + def log_message(self, format: str, *args: object) -> None: + return None + + def _send_json(self, status: int, payload: dict[str, object]) -> None: + body = json.dumps(payload).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.send_header("X-Elastic-Product", "Elasticsearch") + self.end_headers() + self.wfile.write(body) + + def do_PUT(self) -> None: + self._send_json( + 201, + { + "_index": "audit-index", + "_id": "doc-1", + "_version": 1, + "result": "created", + "_shards": {"total": 1, "successful": 1, "failed": 0}, + "_seq_no": 0, + "_primary_term": 1, + }, + ) + + def do_POST(self) -> None: + self._send_json( + 200, + { + "took": 1, + "timed_out": False, + "_shards": {"total": 1, "successful": 1, "skipped": 0, "failed": 0}, + "hits": { + "total": {"value": 1, "relation": "eq"}, + "max_score": 1.0, + "hits": [ + { + "_index": "audit-index", + "_id": "doc-1", + "_score": 1.0, + "_source": {"title": "Tracing Elasticsearch"}, + } + ], + }, + }, + ) + + def do_GET(self) -> None: + self._send_json( + 404, + { + "_index": "audit-index", + "_id": "missing", + "found": False, + "error": { + "type": "document_missing_exception", + "reason": "document [missing] is absent", + }, + "status": 404, + }, + ) + + +@contextmanager +def local_elasticsearch() -> Iterator[str]: + server = ThreadingHTTPServer(("127.0.0.1", 0), _ElasticsearchHandler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + host, port = server.server_address + yield f"http://{host}:{port}" + finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) diff --git a/python/tracing/elasticsearch/requirements.txt b/python/tracing/elasticsearch/requirements.txt new file mode 100644 index 0000000..9d7c4ee --- /dev/null +++ b/python/tracing/elasticsearch/requirements.txt @@ -0,0 +1,4 @@ +elasticsearch[async]>=8.13.0,<10.0.0 +python-dotenv>=1.0.0 +respan-ai>=2.16.1 +respan-instrumentation-elasticsearch>=0.1.0 diff --git a/python/tracing/elasticsearch/run_all.py b/python/tracing/elasticsearch/run_all.py new file mode 100644 index 0000000..7c5cd65 --- /dev/null +++ b/python/tracing/elasticsearch/run_all.py @@ -0,0 +1,24 @@ +"""Run every Elasticsearch tracing example with one audit marker.""" + +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path +from uuid import uuid4 + +SCRIPTS = ("01_sync_client.py", "02_async_client.py") + + +def main() -> None: + directory = Path(__file__).resolve().parent + env = os.environ.copy() + env.setdefault("RESPAN_EXAMPLE_RUN_ID", f"elasticsearch-{uuid4().hex[:10]}") + print(f"RESPAN_EXAMPLE_RUN_ID={env['RESPAN_EXAMPLE_RUN_ID']}", flush=True) + for script in SCRIPTS: + subprocess.run([sys.executable, str(directory / script)], env=env, check=True) + + +if __name__ == "__main__": + main() diff --git a/python/tracing/google-adk/01_hello_world.py b/python/tracing/google-adk/01_hello_world.py index 6dc535f..e2e91bf 100644 --- a/python/tracing/google-adk/01_hello_world.py +++ b/python/tracing/google-adk/01_hello_world.py @@ -6,14 +6,14 @@ from google.adk.agents import Agent from respan import workflow -from _shared import create_gateway_model, create_respan, run_agent_once +from _shared import create_gateway_model, create_respan, example_attributes, run_agent_once SCRIPT_NAME = Path(__file__).name APP_NAME = SCRIPT_NAME.removesuffix(".py") @workflow(name=SCRIPT_NAME) -async def run_hello_world() -> str: +async def run_hello_world(prompt: str) -> str: agent = Agent( name="hello_world_agent", model=create_gateway_model(), @@ -22,7 +22,7 @@ async def run_hello_world() -> str: output = await run_agent_once( agent=agent, app_name=APP_NAME, - prompt="Say hello from a traced Google ADK agent.", + prompt=prompt, ) print(output) return output @@ -31,7 +31,8 @@ async def run_hello_world() -> str: async def main() -> None: respan = create_respan(APP_NAME) try: - await run_hello_world() + with example_attributes(APP_NAME): + await run_hello_world("Say hello from a traced Google ADK agent.") finally: respan.shutdown() diff --git a/python/tracing/google-adk/02_tool_use.py b/python/tracing/google-adk/02_tool_use.py index 663ccaf..bde6736 100644 --- a/python/tracing/google-adk/02_tool_use.py +++ b/python/tracing/google-adk/02_tool_use.py @@ -6,7 +6,7 @@ from google.adk.agents import Agent from respan import workflow -from _shared import create_gateway_model, create_respan, run_agent_once +from _shared import create_gateway_model, create_respan, example_attributes, run_agent_once SCRIPT_NAME = Path(__file__).name APP_NAME = SCRIPT_NAME.removesuffix(".py") @@ -18,7 +18,7 @@ def get_weather(city: str) -> str: @workflow(name=SCRIPT_NAME) -async def run_tool_use() -> str: +async def run_tool_use(prompt: str) -> str: agent = Agent( name="weather_agent", model=create_gateway_model(), @@ -31,7 +31,7 @@ async def run_tool_use() -> str: output = await run_agent_once( agent=agent, app_name=APP_NAME, - prompt="Use get_weather for San Francisco and summarize the result.", + prompt=prompt, ) print(output) return output @@ -40,7 +40,10 @@ async def run_tool_use() -> str: async def main() -> None: respan = create_respan(APP_NAME) try: - await run_tool_use() + with example_attributes(APP_NAME): + await run_tool_use( + "Use get_weather for San Francisco and summarize the result." + ) finally: respan.shutdown() diff --git a/python/tracing/google-adk/03_respan_attributes.py b/python/tracing/google-adk/03_respan_attributes.py index 1af5f6b..feaacfb 100644 --- a/python/tracing/google-adk/03_respan_attributes.py +++ b/python/tracing/google-adk/03_respan_attributes.py @@ -6,33 +6,30 @@ from google.adk.agents import Agent from respan import propagate_attributes, workflow -from _shared import create_gateway_model, create_respan, run_agent_once +from _shared import ( + create_gateway_model, + create_respan, + example_attributes, + example_run_id, + run_agent_once, +) SCRIPT_NAME = Path(__file__).name APP_NAME = SCRIPT_NAME.removesuffix(".py") @workflow(name=SCRIPT_NAME) -async def run_respan_attributes() -> str: +async def run_respan_attributes(prompt: str) -> str: agent = Agent( name="attribute_agent", model=create_gateway_model(), instruction="You answer in one concise sentence.", ) - with propagate_attributes( - customer_identifier="google-adk-example-user", - thread_identifier="google-adk-example-thread", - metadata={ - "example": "google-adk", - "scenario": "attributes", - "script": SCRIPT_NAME, - }, - ): - output = await run_agent_once( - agent=agent, - app_name=APP_NAME, - prompt="Explain why trace attributes are useful.", - ) + output = await run_agent_once( + agent=agent, + app_name=APP_NAME, + prompt=prompt, + ) print(output) return output @@ -40,7 +37,17 @@ async def run_respan_attributes() -> str: async def main() -> None: respan = create_respan(APP_NAME) try: - await run_respan_attributes() + with example_attributes(APP_NAME), propagate_attributes( + customer_identifier="google-adk-example-user", + thread_identifier=f"{example_run_id()}:{APP_NAME}", + metadata={ + "integration": "google-adk", + "example": APP_NAME, + "run_id": example_run_id(), + "scenario": "attributes", + }, + ): + await run_respan_attributes("Explain why trace attributes are useful.") finally: respan.shutdown() diff --git a/python/tracing/google-adk/_shared.py b/python/tracing/google-adk/_shared.py index 49c3342..48e42b7 100644 --- a/python/tracing/google-adk/_shared.py +++ b/python/tracing/google-adk/_shared.py @@ -5,14 +5,18 @@ import os from pathlib import Path from typing import Iterable +from uuid import uuid4 from dotenv import load_dotenv from google.adk.agents import Agent +from google.adk.models.base_llm import BaseLlm from google.adk.models.lite_llm import LiteLlm +from google.adk.models.llm_request import LlmRequest +from google.adk.models.llm_response import LlmResponse from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService from google.genai import types -from respan import Respan +from respan import Respan, propagate_attributes from respan_instrumentation_google_adk import GoogleADKInstrumentor APP_USER_ID = "respan-google-adk-user" @@ -38,8 +42,58 @@ def gateway_model_name() -> str: return f"openai/{model}" -def create_gateway_model() -> LiteLlm: +class DeterministicLlm(BaseLlm): + """Small local model used by the repeatable full example run.""" + + async def generate_content_async( + self, + llm_request: LlmRequest, + stream: bool = False, + ): + has_function_response = any( + getattr(part, "function_response", None) is not None + for content in llm_request.contents + for part in (content.parts or []) + ) + if llm_request.tools_dict and not has_function_response: + content = types.Content( + role="model", + parts=[ + types.Part( + function_call=types.FunctionCall( + id="call-adk-weather-1", + name="get_weather", + args={"city": "San Francisco"}, + ) + ) + ], + ) + elif has_function_response: + content = types.Content( + role="model", + parts=[types.Part(text="San Francisco is sunny, 72F, with light wind.")], + ) + else: + content = types.Content( + role="model", + parts=[types.Part(text="This traced Google ADK agent is ready.")], + ) + yield LlmResponse( + model_version=self.model, + content=content, + partial=False, + usage_metadata=types.GenerateContentResponseUsageMetadata( + prompt_token_count=8, + candidates_token_count=7, + total_token_count=15, + ), + ) + + +def create_gateway_model() -> BaseLlm: load_repo_env() + if os.getenv("RESPAN_ADK_MODEL_MODE", "gateway") == "local": + return DeterministicLlm(model="respan-adk-deterministic") gateway_api_key = os.getenv("RESPAN_GATEWAY_API_KEY") or require_env( "RESPAN_API_KEY" ) @@ -56,12 +110,35 @@ def create_gateway_model() -> LiteLlm: def create_respan(app_name: str) -> Respan: load_repo_env() + run_id = example_run_id() return Respan( app_name=app_name, api_key=require_env("RESPAN_API_KEY"), base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), instrumentations=[GoogleADKInstrumentor()], is_batching_enabled=False, + metadata={ + "integration": "google-adk", + "example": app_name, + "run_id": run_id, + }, + ) + + +def example_run_id() -> str: + return os.getenv("RESPAN_EXAMPLE_RUN_ID") or f"google-adk-{uuid4().hex[:10]}" + + +def example_attributes(app_name: str): + run_id = example_run_id() + return propagate_attributes( + custom_identifier=f"{run_id}:{app_name}", + trace_group_identifier=f"{run_id}:{app_name}", + metadata={ + "integration": "google-adk", + "example": app_name, + "run_id": run_id, + }, ) diff --git a/python/tracing/google-adk/pyproject.toml b/python/tracing/google-adk/pyproject.toml index 9fde96a..5ab1499 100644 --- a/python/tracing/google-adk/pyproject.toml +++ b/python/tracing/google-adk/pyproject.toml @@ -7,7 +7,8 @@ readme = "README.md" [tool.poetry.dependencies] python = ">=3.11,<3.14" -google-adk = { version = ">=1.17.0", extras = ["extensions"] } +google-adk = ">=1.17.0" +litellm = ">=1.80.0" python-dotenv = ">=1.0.0" respan-ai = ">=2.16.1" respan-instrumentation-google-adk = ">=0.1.0" diff --git a/python/tracing/google-adk/run_all.py b/python/tracing/google-adk/run_all.py new file mode 100644 index 0000000..be898cd --- /dev/null +++ b/python/tracing/google-adk/run_all.py @@ -0,0 +1,25 @@ +"""Run every Google ADK tracing example with one audit marker.""" + +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path +from uuid import uuid4 + +SCRIPTS = ("01_hello_world.py", "02_tool_use.py", "03_respan_attributes.py") + + +def main() -> None: + directory = Path(__file__).resolve().parent + env = os.environ.copy() + env.setdefault("RESPAN_EXAMPLE_RUN_ID", f"google-adk-{uuid4().hex[:10]}") + env.setdefault("RESPAN_ADK_MODEL_MODE", "local") + print(f"RESPAN_EXAMPLE_RUN_ID={env['RESPAN_EXAMPLE_RUN_ID']}", flush=True) + for script in SCRIPTS: + subprocess.run([sys.executable, str(directory / script)], env=env, check=True) + + +if __name__ == "__main__": + main() diff --git a/python/tracing/google-genai/01_generate_content.py b/python/tracing/google-genai/01_generate_content.py index cebd55d..e17526f 100644 --- a/python/tracing/google-genai/01_generate_content.py +++ b/python/tracing/google-genai/01_generate_content.py @@ -16,17 +16,17 @@ @workflow(name=workflow_name(EXAMPLE_NAME)) -def _generate_content_workflow(client) -> str: +def _generate_content_workflow(prompt: str) -> str: + client = make_client() response = client.models.generate_content( model=model_name(), - contents="Reply with one concise sentence about observability for Gemini apps.", + contents=prompt, ) return response.text or "" def run_generate_content() -> None: respan = make_respan(EXAMPLE_NAME) - client = make_client() custom_identifier = make_custom_identifier(EXAMPLE_NAME) text = "" @@ -34,7 +34,9 @@ def run_generate_content() -> None: with example_attributes(EXAMPLE_NAME, custom_identifier): print(f"custom_identifier={custom_identifier}", flush=True) print(f"workflow_name={workflow_name(EXAMPLE_NAME)}", flush=True) - text = _generate_content_workflow(client) + text = _generate_content_workflow( + "Reply with one concise sentence about observability for Gemini apps." + ) finally: respan.shutdown() diff --git a/python/tracing/google-genai/02_stream_content.py b/python/tracing/google-genai/02_stream_content.py index 7b95739..10e7135 100644 --- a/python/tracing/google-genai/02_stream_content.py +++ b/python/tracing/google-genai/02_stream_content.py @@ -16,11 +16,12 @@ @workflow(name=workflow_name(EXAMPLE_NAME)) -def _stream_content_workflow(client) -> str: +def _stream_content_workflow(prompt: str) -> str: + client = make_client() chunks: list[str] = [] for chunk in client.models.generate_content_stream( model=model_name(), - contents="Stream three short bullet points about production tracing.", + contents=prompt, ): if chunk.text: chunks.append(chunk.text) @@ -29,7 +30,6 @@ def _stream_content_workflow(client) -> str: def run_stream_content() -> None: respan = make_respan(EXAMPLE_NAME) - client = make_client() custom_identifier = make_custom_identifier(EXAMPLE_NAME) text = "" @@ -37,7 +37,9 @@ def run_stream_content() -> None: with example_attributes(EXAMPLE_NAME, custom_identifier): print(f"custom_identifier={custom_identifier}", flush=True) print(f"workflow_name={workflow_name(EXAMPLE_NAME)}", flush=True) - text = _stream_content_workflow(client) + text = _stream_content_workflow( + "Stream three short bullet points about production tracing." + ) finally: respan.shutdown() diff --git a/python/tracing/google-genai/03_async_generate_content.py b/python/tracing/google-genai/03_async_generate_content.py index fdc411f..6557043 100644 --- a/python/tracing/google-genai/03_async_generate_content.py +++ b/python/tracing/google-genai/03_async_generate_content.py @@ -18,20 +18,22 @@ @workflow(name=workflow_name(EXAMPLE_NAME)) -async def _async_generate_content_workflow(client) -> str: +async def _async_generate_content_workflow(prompt: str) -> str: + client = make_client() response = await client.aio.models.generate_content( model=model_name(), - contents="Reply with one sentence about async Gemini workloads.", + contents=prompt, ) return response.text or "" async def _run_async_generate_content(custom_identifier: str) -> str: - client = make_client() with example_attributes(EXAMPLE_NAME, custom_identifier): print(f"custom_identifier={custom_identifier}", flush=True) print(f"workflow_name={workflow_name(EXAMPLE_NAME)}", flush=True) - return await _async_generate_content_workflow(client) + return await _async_generate_content_workflow( + "Reply with one sentence about async Gemini workloads." + ) def run_async_generate_content() -> None: diff --git a/python/tracing/google-genai/04_tool_calling.py b/python/tracing/google-genai/04_tool_calling.py index 8c0d4a2..4e19cd0 100644 --- a/python/tracing/google-genai/04_tool_calling.py +++ b/python/tracing/google-genai/04_tool_calling.py @@ -2,7 +2,7 @@ from google.genai import types -from respan import workflow +from respan import tool, workflow from _shared import ( example_attributes, @@ -17,29 +17,69 @@ EXAMPLE_NAME = "tool-calling" +@tool(name="get_weather") def get_weather(city: str) -> str: """Return deterministic weather for a city.""" return f"Sunny and 22 C in {city}" @workflow(name=workflow_name(EXAMPLE_NAME)) -def _tool_calling_workflow(client) -> str: +def _tool_calling_workflow(prompt: str) -> str: + client = make_client() + declaration = types.FunctionDeclaration( + name="get_weather", + description="Return deterministic weather for a city.", + parameters_json_schema={ + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + }, + ) config = types.GenerateContentConfig( - tools=[get_weather], + tools=[types.Tool(function_declarations=[declaration])], temperature=0, system_instruction="Use the weather tool when a city forecast is requested.", + tool_config=types.ToolConfig( + function_calling_config=types.FunctionCallingConfig( + mode="ANY", + allowed_function_names=["get_weather"], + ) + ), ) - response = client.models.generate_content( + tool_request = client.models.generate_content( model=model_name(), - contents="What is the weather in Tokyo? Use the tool and answer briefly.", + contents=prompt, config=config, ) - return response.text or "" + function_calls = tool_request.function_calls or [] + if not function_calls: + raise RuntimeError("Gemini did not request get_weather") + + function_call = function_calls[0] + weather = get_weather(**dict(function_call.args or {})) + function_result = types.Content( + role="user", + parts=[ + types.Part.from_function_response( + name=function_call.name or "get_weather", + response={"result": weather}, + ) + ], + ) + final_response = client.models.generate_content( + model=model_name(), + contents=[ + types.Content(role="user", parts=[types.Part(text=prompt)]), + tool_request.candidates[0].content, + function_result, + ], + config=types.GenerateContentConfig(temperature=0), + ) + return final_response.text or "" def run_tool_calling() -> None: respan = make_respan(EXAMPLE_NAME) - client = make_client() custom_identifier = make_custom_identifier(EXAMPLE_NAME) text = "" @@ -47,7 +87,9 @@ def run_tool_calling() -> None: with example_attributes(EXAMPLE_NAME, custom_identifier): print(f"custom_identifier={custom_identifier}", flush=True) print(f"workflow_name={workflow_name(EXAMPLE_NAME)}", flush=True) - text = _tool_calling_workflow(client) + text = _tool_calling_workflow( + "What is the weather in Tokyo? Use the tool and answer briefly." + ) finally: respan.shutdown() diff --git a/python/tracing/google-genai/_shared.py b/python/tracing/google-genai/_shared.py index f3e95d2..60e1687 100644 --- a/python/tracing/google-genai/_shared.py +++ b/python/tracing/google-genai/_shared.py @@ -12,7 +12,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[3] DEFAULT_RESPAN_BASE_URL = "https://api.respan.ai/api" -DEFAULT_MODEL = "gemini-2.5-flash" +DEFAULT_MODEL = "gemini-3-flash-preview" def load_root_env() -> None: @@ -44,13 +44,18 @@ def model_name() -> str: def make_respan(example_name: str) -> Respan: api_key = require_respan_api_key() + run_id = example_run_id() return Respan( api_key=api_key, base_url=respan_base_url(), app_name="google-genai-examples", instrumentations=[GoogleGenAIInstrumentor()], environment=os.getenv("RESPAN_ENVIRONMENT", "example"), - metadata={"integration": "google-genai", "example": example_name}, + metadata={ + "integration": "google-genai", + "example": example_name, + "run_id": run_id, + }, ) @@ -76,7 +81,11 @@ def workflow_name(example_name: str) -> str: def make_custom_identifier(example_name: str) -> str: - return f"google-genai-{example_name}-{uuid4().hex[:8]}" + return f"{example_run_id()}:{example_name}" + + +def example_run_id() -> str: + return os.getenv("RESPAN_EXAMPLE_RUN_ID") or f"google-genai-{uuid4().hex[:10]}" @contextmanager @@ -88,7 +97,8 @@ def example_attributes(example_name: str, custom_identifier: str | None = None): trace_group_identifier=current_workflow_name, metadata={ "example": example_name, - "run_id": custom_identifier, + "integration": "google-genai", + "run_id": example_run_id(), "workflow_name": current_workflow_name, }, ): diff --git a/python/tracing/google-genai/run_all.py b/python/tracing/google-genai/run_all.py new file mode 100644 index 0000000..075d502 --- /dev/null +++ b/python/tracing/google-genai/run_all.py @@ -0,0 +1,29 @@ +"""Run every Google GenAI tracing example with one audit marker.""" + +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path +from uuid import uuid4 + +SCRIPTS = ( + "01_generate_content.py", + "02_stream_content.py", + "03_async_generate_content.py", + "04_tool_calling.py", +) + + +def main() -> None: + directory = Path(__file__).resolve().parent + env = os.environ.copy() + env.setdefault("RESPAN_EXAMPLE_RUN_ID", f"google-genai-{uuid4().hex[:10]}") + print(f"RESPAN_EXAMPLE_RUN_ID={env['RESPAN_EXAMPLE_RUN_ID']}", flush=True) + for script in SCRIPTS: + subprocess.run([sys.executable, str(directory / script)], env=env, check=True) + + +if __name__ == "__main__": + main()