diff --git a/apps/ai-observability/README.md b/apps/ai-observability/README.md index b741745e6..701757638 100644 --- a/apps/ai-observability/README.md +++ b/apps/ai-observability/README.md @@ -24,6 +24,7 @@ Each app exists to test one thing the others don't: - `openai-agents/python-travel-triage` — tracing processor; the SDK emits the tree - `vercel-ai/nextjs-support-chat` — per-request identity; framework bootstrap - `manual-capture/node-http-chat` — hand-built tree; must reuse the existing client +- `google-adk/node-weather` — framework plugin; identity comes from ADK's own ids - `opentelemetry/go-weather` — Go; no wrapper SDK exists, so the posthog-go OTel bridge The five weather apps implement the identical `get_weather` round trip from @@ -38,6 +39,7 @@ conversation structure. `posthog_trace_id`, and `posthog_properties`, as shown in the docs. OTel is acceptable only where the same structure lands. Exceptions: `openai-agents` (tracing processor), `vercel-ai` (`experimental_telemetry`), + `google-adk` (Runner plugin), `manual-capture` (no SDK to wrap). `manual-capture` (no SDK to wrap), `opentelemetry/go-weather` (Go has no wrapper SDK; the posthog-go OTel bridge). - **Every app gets a session** — single-trace apps included. The graded diff --git a/apps/ai-observability/google-adk/node-weather/.env b/apps/ai-observability/google-adk/node-weather/.env new file mode 100644 index 000000000..f72994146 --- /dev/null +++ b/apps/ai-observability/google-adk/node-weather/.env @@ -0,0 +1,2 @@ +POSTHOG_API_KEY=phc_VMTfZD5shhF3SQfgXeu6SW85FTxDMnmB4JpRbUj9QEA +POSTHOG_HOST=https://us.i.posthog.com diff --git a/apps/ai-observability/google-adk/node-weather/README.md b/apps/ai-observability/google-adk/node-weather/README.md new file mode 100644 index 000000000..b7c8a12a4 --- /dev/null +++ b/apps/ai-observability/google-adk/node-weather/README.md @@ -0,0 +1,39 @@ +# wb-aio-google-adk-node-weather + +Weather assistant, Google Agent Development Kit (`@google/adk`), two-turn +conversation with a registered tool. PostHog-less fixture for +`wizard ai-observability`. + +The expected mechanism is `PostHogADKPlugin` from `@posthog/ai/adk`, added to +the `Runner`'s `plugins`. The plugin hooks the run, agent, tool, and model +callbacks and captures the whole tree itself: an `$ai_trace` per invocation, +`$ai_span` events for agent runs and tool calls, and one `$ai_generation` per +model call. Identity comes from ADK too: the run's `userId` becomes the +distinct ID, the ADK `sessionId` becomes `$ai_session_id`. No per-call +PostHog parameters exist to add. + +``` +thread_abc ← $ai_session_id (ADK sessionId) +├─ ask("weather in San Francisco?") ← trace (invocation) +│ └─ weather_assistant ← span (agent run) +│ ├─ model call (→ get_weather call) ← generation +│ ├─ get_weather ← span (tool) +│ └─ model call (→ answer) ← generation +└─ ask("How about Boston?") ← trace (same shape) +``` + +## Expected outcome + +- one session (`thread_abc`), two traces of + `agent span → generation → span(get_weather) → generation`, all on `user_123` +- `PostHogADKPlugin` in the `Runner`'s `plugins`; no wrapper client swapped in +- the agent and tool spans come from the plugin's callbacks, not hand-authored + `$ai_span` capture around `getWeather` +- identity left to the plugin's ADK fallbacks, or wired explicitly to the same + `userId` / `sessionId` values; either way one session across both turns +- flushed before exit (`posthog.shutdown()`) +- `weather.ts`, the tool registration, and the agent untouched + +Fail: swapping the model for a wrapped Gemini client (loses the agent +structure); hand-authored spans duplicating what the plugin emits; a session +or trace id minted per model call; no flush. diff --git a/apps/ai-observability/google-adk/node-weather/package.json b/apps/ai-observability/google-adk/node-weather/package.json new file mode 100644 index 000000000..00453ed3b --- /dev/null +++ b/apps/ai-observability/google-adk/node-weather/package.json @@ -0,0 +1,19 @@ +{ + "name": "wb-aio-google-adk-node-weather", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "PostHog-less weather assistant (Google ADK, tool use) for testing `wizard ai-observability`.", + "scripts": { + "build": "tsc --noEmit", + "start": "tsx src/index.ts" + }, + "dependencies": { + "@google/adk": "^2.0.0", + "zod": "^4.2.1" + }, + "devDependencies": { + "tsx": "^4.19.2", + "typescript": "^5.6.3" + } +} diff --git a/apps/ai-observability/google-adk/node-weather/src/index.ts b/apps/ai-observability/google-adk/node-weather/src/index.ts new file mode 100644 index 000000000..299321755 --- /dev/null +++ b/apps/ai-observability/google-adk/node-weather/src/index.ts @@ -0,0 +1,53 @@ +import { FunctionTool, InMemorySessionService, LlmAgent, Runner } from '@google/adk' +import { z } from 'zod' + +import { getWeather } from './weather.js' + +const APP_NAME = 'wb-aio-google-adk-node-weather' +const USER_ID = 'user_123' +const SESSION_ID = 'thread_abc' + +const weatherTool = new FunctionTool({ + name: 'get_weather', + description: 'Get the current weather for a given location.', + parameters: z.object({ + location: z.string().describe('City and state, e.g. San Francisco, CA'), + }), + execute: ({ location }) => getWeather(location), +}) + +const agent = new LlmAgent({ + name: 'weather_assistant', + model: 'gemini-3.6-flash', + instruction: 'Answer weather questions with the get_weather tool. Be concise.', + tools: [weatherTool], +}) + +const sessionService = new InMemorySessionService() +const runner = new Runner({ appName: APP_NAME, agent, sessionService }) + +/** Answer one question inside the shared session. ADK runs the tool loop itself. */ +async function ask(question: string): Promise { + for await (const event of runner.runAsync({ + userId: USER_ID, + sessionId: SESSION_ID, + newMessage: { role: 'user', parts: [{ text: question }] }, + })) { + for (const part of event.content?.parts ?? []) { + if (part.text) { + console.log(part.text) + } + } + } +} + +async function main(): Promise { + await sessionService.createSession({ appName: APP_NAME, userId: USER_ID, sessionId: SESSION_ID }) + await ask("What's the weather in San Francisco?") + await ask('How about Boston?') +} + +main().catch((err) => { + console.error(`fatal: ${String(err)}`) + process.exit(1) +}) diff --git a/apps/ai-observability/google-adk/node-weather/src/weather.ts b/apps/ai-observability/google-adk/node-weather/src/weather.ts new file mode 100644 index 000000000..08a08286c --- /dev/null +++ b/apps/ai-observability/google-adk/node-weather/src/weather.ts @@ -0,0 +1,9 @@ +// Backing implementation for the get_weather tool. No model call involved. +const forecast: Record = { + 'San Francisco, CA': '15 degrees Celsius, partly cloudy', + 'Boston, MA': '4 degrees Celsius, snow showers', +} + +export function getWeather(location: string): string { + return forecast[location] ?? `No forecast on file for ${location}.` +} diff --git a/apps/ai-observability/google-adk/node-weather/tsconfig.json b/apps/ai-observability/google-adk/node-weather/tsconfig.json new file mode 100644 index 000000000..235d77827 --- /dev/null +++ b/apps/ai-observability/google-adk/node-weather/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +}