Skip to content

Commit b723bcb

Browse files
committed
ADK
1 parent dd2e6ee commit b723bcb

File tree

8 files changed

+227
-0
lines changed

8 files changed

+227
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
test("[ADK Middleware] Backend Tool Rendering displays weather cards", async ({ page }) => {
4+
// Set shorter default timeout for this test
5+
test.setTimeout(30000); // 30 seconds total
6+
7+
await page.goto("/adk-middleware/feature/backend_tool_rendering");
8+
9+
// Verify suggestion buttons are visible
10+
await expect(page.getByRole("button", { name: "Weather in San Francisco" })).toBeVisible({
11+
timeout: 5000,
12+
});
13+
14+
// Click first suggestion and verify weather card appears
15+
await page.getByRole("button", { name: "Weather in San Francisco" }).click();
16+
17+
// Wait for either test ID or fallback to "Current Weather" text
18+
const weatherCard = page.getByTestId("weather-card");
19+
const currentWeatherText = page.getByText("Current Weather");
20+
21+
// Try test ID first, fallback to text
22+
try {
23+
await expect(weatherCard).toBeVisible({ timeout: 10000 });
24+
} catch (e) {
25+
// Fallback to checking for "Current Weather" text
26+
await expect(currentWeatherText.first()).toBeVisible({ timeout: 10000 });
27+
}
28+
29+
// Verify weather content is present (use flexible selectors)
30+
const hasHumidity = await page
31+
.getByText("Humidity")
32+
.isVisible()
33+
.catch(() => false);
34+
const hasWind = await page
35+
.getByText("Wind")
36+
.isVisible()
37+
.catch(() => false);
38+
const hasCityName = await page
39+
.locator("h3")
40+
.filter({ hasText: /San Francisco/i })
41+
.isVisible()
42+
.catch(() => false);
43+
44+
// At least one of these should be true
45+
expect(hasHumidity || hasWind || hasCityName).toBeTruthy();
46+
47+
// Click second suggestion
48+
await page.getByRole("button", { name: "Weather in New York" }).click();
49+
await page.waitForTimeout(2000);
50+
51+
// Verify at least one weather-related element is still visible
52+
const weatherElements = await page.getByText(/Weather|Humidity|Wind|Temperature/i).count();
53+
expect(weatherElements).toBeGreaterThan(0);
54+
});

typescript-sdk/apps/dojo/src/agents.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ export const agentsIntegrations: AgentIntegrationConfig[] = [
7777
human_in_the_loop: new ADKAgent({
7878
url: `${envVars.adkMiddlewareUrl}/adk-human-in-loop-agent`,
7979
}),
80+
backend_tool_rendering: new ADKAgent({
81+
url: `${envVars.adkMiddlewareUrl}/backend_tool_rendering`,
82+
}),
8083
shared_state: new ADKAgent({ url: `${envVars.adkMiddlewareUrl}/adk-shared-state-agent` }),
8184
// predictive_state_updates: new ADKAgent({ url: `${envVars.adkMiddlewareUrl}/adk-predictive-state-agent` }),
8285
};

typescript-sdk/apps/dojo/src/files.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,32 @@
285285
"type": "file"
286286
}
287287
],
288+
"adk-middleware::backend_tool_rendering": [
289+
{
290+
"name": "page.tsx",
291+
"content": "\"use client\";\nimport React from \"react\";\nimport \"@copilotkit/react-ui/styles.css\";\nimport \"./style.css\";\nimport { CopilotKit, useCopilotAction } from \"@copilotkit/react-core\";\nimport { CopilotChat } from \"@copilotkit/react-ui\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC<AgenticChatProps> = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n <CopilotKit\n runtimeUrl={`/api/copilotkit/${integrationId}`}\n showDevConsole={false}\n agent=\"backend_tool_rendering\"\n >\n <Chat />\n </CopilotKit>\n );\n};\n\nconst Chat = () => {\n useCopilotAction({\n name: \"get_weather\",\n available: \"disabled\",\n parameters: [{ name: \"location\", type: \"string\", required: true }],\n render: ({ args, result, status }) => {\n console.log(result, args);\n if (status !== \"complete\") {\n return (\n <div className=\" bg-[#667eea] text-white p-4 rounded-lg max-w-md\">\n <span className=\"animate-spin\">⚙️ Retrieving weather...</span>\n </div>\n );\n }\n\n const weatherResult: WeatherToolResult = {\n temperature: result?.temperature || 0,\n conditions: result?.conditions || \"clear\",\n humidity: result?.humidity || 0,\n windSpeed: result?.wind_speed || 0,\n feelsLike: result?.feels_like || result?.temperature || 0,\n };\n\n const themeColor = getThemeColor(weatherResult.conditions);\n\n return (\n <WeatherCard\n location={args.location}\n themeColor={themeColor}\n result={weatherResult}\n status={status || \"complete\"}\n />\n );\n },\n });\n\n return (\n <div className=\"flex justify-center items-center h-full w-full\">\n <div className=\"h-full w-full md:w-8/10 md:h-8/10 rounded-lg\">\n <CopilotChat\n className=\"h-full rounded-2xl max-w-6xl mx-auto\"\n labels={{ initial: \"Hi! I can look up the weather for you. Just ask!\" }}\n suggestions={[\n {\n title: \"Weather in San Francisco\",\n message: \"What's the weather like in San Francisco?\",\n },\n {\n title: \"Weather in New York\",\n message: \"Tell me about the weather in New York.\",\n },\n {\n title: \"Weather in Tokyo\",\n message: \"How's the weather in Tokyo today?\",\n },\n ]}\n />\n </div>\n </div>\n );\n};\n\ninterface WeatherToolResult {\n temperature: number;\n conditions: string;\n humidity: number;\n windSpeed: number;\n feelsLike: number;\n}\n\nfunction getThemeColor(conditions: string): string {\n const conditionLower = conditions.toLowerCase();\n if (conditionLower.includes(\"clear\") || conditionLower.includes(\"sunny\")) {\n return \"#667eea\";\n }\n if (conditionLower.includes(\"rain\") || conditionLower.includes(\"storm\")) {\n return \"#4A5568\";\n }\n if (conditionLower.includes(\"cloud\")) {\n return \"#718096\";\n }\n if (conditionLower.includes(\"snow\")) {\n return \"#63B3ED\";\n }\n return \"#764ba2\";\n}\n\nfunction WeatherCard({\n location,\n themeColor,\n result,\n status,\n}: {\n location?: string;\n themeColor: string;\n result: WeatherToolResult;\n status: \"inProgress\" | \"executing\" | \"complete\";\n}) {\n return (\n <div\n data-testid=\"weather-card\"\n style={{ backgroundColor: themeColor }}\n className=\"rounded-xl mt-6 mb-4 max-w-md w-full\"\n >\n <div className=\"bg-white/20 p-4 w-full\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h3 data-testid=\"weather-city\" className=\"text-xl font-bold text-white capitalize\">\n {location}\n </h3>\n <p className=\"text-white\">Current Weather</p>\n </div>\n <WeatherIcon conditions={result.conditions} />\n </div>\n\n <div className=\"mt-4 flex items-end justify-between\">\n <div className=\"text-3xl font-bold text-white\">\n <span className=\"\">{result.temperature}° C</span>\n <span className=\"text-sm text-white/50\">\n {\" / \"}\n {((result.temperature * 9) / 5 + 32).toFixed(1)}° F\n </span>\n </div>\n <div className=\"text-sm text-white capitalize\">{result.conditions}</div>\n </div>\n\n <div className=\"mt-4 pt-4 border-t border-white\">\n <div className=\"grid grid-cols-3 gap-2 text-center\">\n <div data-testid=\"weather-humidity\">\n <p className=\"text-white text-xs\">Humidity</p>\n <p className=\"text-white font-medium\">{result.humidity}%</p>\n </div>\n <div data-testid=\"weather-wind\">\n <p className=\"text-white text-xs\">Wind</p>\n <p className=\"text-white font-medium\">{result.windSpeed} mph</p>\n </div>\n <div data-testid=\"weather-feels-like\">\n <p className=\"text-white text-xs\">Feels Like</p>\n <p className=\"text-white font-medium\">{result.feelsLike}°</p>\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\nfunction WeatherIcon({ conditions }: { conditions: string }) {\n if (!conditions) return null;\n\n if (conditions.toLowerCase().includes(\"clear\") || conditions.toLowerCase().includes(\"sunny\")) {\n return <SunIcon />;\n }\n\n if (\n conditions.toLowerCase().includes(\"rain\") ||\n conditions.toLowerCase().includes(\"drizzle\") ||\n conditions.toLowerCase().includes(\"snow\") ||\n conditions.toLowerCase().includes(\"thunderstorm\")\n ) {\n return <RainIcon />;\n }\n\n if (\n conditions.toLowerCase().includes(\"fog\") ||\n conditions.toLowerCase().includes(\"cloud\") ||\n conditions.toLowerCase().includes(\"overcast\")\n ) {\n return <CloudIcon />;\n }\n\n return <CloudIcon />;\n}\n\n// Simple sun icon for the weather card\nfunction SunIcon() {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"currentColor\"\n className=\"w-14 h-14 text-yellow-200\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"5\" />\n <path\n d=\"M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42\"\n strokeWidth=\"2\"\n stroke=\"currentColor\"\n />\n </svg>\n );\n}\n\nfunction RainIcon() {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"currentColor\"\n className=\"w-14 h-14 text-blue-200\"\n >\n {/* Cloud */}\n <path\n d=\"M7 15a4 4 0 0 1 0-8 5 5 0 0 1 10 0 4 4 0 0 1 0 8H7z\"\n fill=\"currentColor\"\n opacity=\"0.8\"\n />\n {/* Rain drops */}\n <path\n d=\"M8 18l2 4M12 18l2 4M16 18l2 4\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n fill=\"none\"\n />\n </svg>\n );\n}\n\nfunction CloudIcon() {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"currentColor\"\n className=\"w-14 h-14 text-gray-200\"\n >\n <path d=\"M7 15a4 4 0 0 1 0-8 5 5 0 0 1 10 0 4 4 0 0 1 0 8H7z\" fill=\"currentColor\" />\n </svg>\n );\n}\n\nexport default AgenticChat;\n",
292+
"language": "typescript",
293+
"type": "file"
294+
},
295+
{
296+
"name": "style.css",
297+
"content": ".copilotKitInput {\n border-bottom-left-radius: 0.75rem;\n border-bottom-right-radius: 0.75rem;\n border-top-left-radius: 0.75rem;\n border-top-right-radius: 0.75rem;\n border: 1px solid var(--copilot-kit-separator-color) !important;\n}\n\n.copilotKitChat {\n background-color: #fff !important;\n}\n",
298+
"language": "css",
299+
"type": "file"
300+
},
301+
{
302+
"name": "README.mdx",
303+
"content": "# 🤖 Agentic Chat with Frontend Tools\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n",
304+
"language": "markdown",
305+
"type": "file"
306+
},
307+
{
308+
"name": "backend_tool_rendering.py",
309+
"content": "\"\"\"Basic Chat feature.\"\"\"\n\nfrom __future__ import annotations\n\nfrom fastapi import FastAPI\nfrom ag_ui_adk import ADKAgent, add_adk_fastapi_endpoint\nfrom google.adk.agents import LlmAgent\nfrom google.adk import tools as adk_tools\n\ndef get_weather_condition(code: int) -> str:\n \"\"\"Map weather code to human-readable condition.\n\n Args:\n code: WMO weather code.\n\n Returns:\n Human-readable weather condition string.\n \"\"\"\n conditions = {\n 0: \"Clear sky\",\n 1: \"Mainly clear\",\n 2: \"Partly cloudy\",\n 3: \"Overcast\",\n 45: \"Foggy\",\n 48: \"Depositing rime fog\",\n 51: \"Light drizzle\",\n 53: \"Moderate drizzle\",\n 55: \"Dense drizzle\",\n 56: \"Light freezing drizzle\",\n 57: \"Dense freezing drizzle\",\n 61: \"Slight rain\",\n 63: \"Moderate rain\",\n 65: \"Heavy rain\",\n 66: \"Light freezing rain\",\n 67: \"Heavy freezing rain\",\n 71: \"Slight snow fall\",\n 73: \"Moderate snow fall\",\n 75: \"Heavy snow fall\",\n 77: \"Snow grains\",\n 80: \"Slight rain showers\",\n 81: \"Moderate rain showers\",\n 82: \"Violent rain showers\",\n 85: \"Slight snow showers\",\n 86: \"Heavy snow showers\",\n 95: \"Thunderstorm\",\n 96: \"Thunderstorm with slight hail\",\n 99: \"Thunderstorm with heavy hail\",\n }\n return conditions.get(code, \"Unknown\")\n\n\nasync def get_weather(location: str) -> str:\n \"\"\"Get current weather for a location.\n\n Args:\n location: City name.\n\n Returns:\n Dictionary with weather information including temperature, feels like,\n humidity, wind speed, wind gust, conditions, and location name.\n \"\"\"\n async with httpx.AsyncClient() as client:\n # Geocode the location\n geocoding_url = (\n f\"https://geocoding-api.open-meteo.com/v1/search?name={location}&count=1\"\n )\n geocoding_response = await client.get(geocoding_url)\n geocoding_data = geocoding_response.json()\n\n if not geocoding_data.get(\"results\"):\n raise ValueError(f\"Location '{location}' not found\")\n\n result = geocoding_data[\"results\"][0]\n latitude = result[\"latitude\"]\n longitude = result[\"longitude\"]\n name = result[\"name\"]\n\n # Get weather data\n weather_url = (\n f\"https://api.open-meteo.com/v1/forecast?\"\n f\"latitude={latitude}&longitude={longitude}\"\n f\"&current=temperature_2m,apparent_temperature,relative_humidity_2m,\"\n f\"wind_speed_10m,wind_gusts_10m,weather_code\"\n )\n weather_response = await client.get(weather_url)\n weather_data = weather_response.json()\n\n current = weather_data[\"current\"]\n\n return json.dumps({\n \"temperature\": current[\"temperature_2m\"],\n \"feelsLike\": current[\"apparent_temperature\"],\n \"humidity\": current[\"relative_humidity_2m\"],\n \"windSpeed\": current[\"wind_speed_10m\"],\n \"windGust\": current[\"wind_gusts_10m\"],\n \"conditions\": get_weather_condition(current[\"weather_code\"]),\n \"location\": name,\n })\n\n# Create a sample ADK agent (this would be your actual agent)\nsample_agent = LlmAgent(\n name=\"assistant\",\n model=\"gemini-2.0-flash\",\n instruction=\"\"\"\n You are a helpful weather assistant that provides accurate weather information.\n\n Your primary function is to help users get weather details for specific locations. When responding:\n - Always ask for a location if none is provided\n - If the location name isn’t in English, please translate it\n - If giving a location with multiple parts (e.g. \"New York, NY\"), use the most relevant part (e.g. \"New York\")\n - Include relevant details like humidity, wind conditions, and precipitation\n - Keep responses concise but informative\n\n Use the get_weather tool to fetch current weather data.\n \"\"\",\n tools=[adk_tools.preload_memory_tool.PreloadMemoryTool(), get_weather]\n)\n\n# Create ADK middleware agent instance\nchat_agent = ADKAgent(\n adk_agent=sample_agent,\n app_name=\"demo_app\",\n user_id=\"demo_user\",\n session_timeout_seconds=3600,\n use_in_memory_services=True\n)\n\n# Create FastAPI app\napp = FastAPI(title=\"ADK Middleware Weather Agent\")\n\n# Add the ADK endpoint\nadd_adk_fastapi_endpoint(app, chat_agent, path=\"/\")\n",
310+
"language": "python",
311+
"type": "file"
312+
}
313+
],
288314
"adk-middleware::shared_state": [
289315
{
290316
"name": "page.tsx",

typescript-sdk/integrations/adk-middleware/python/examples/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ readme = "README.md"
1010
requires-python = ">=3.12"
1111
dependencies = [
1212
"fastapi>=0.104.0",
13+
"httpx>=0.27.0",
1314
"uvicorn[standard]>=0.24.0",
1415
"python-dotenv>=1.0.0",
1516
"pydantic>=2.0.0",

typescript-sdk/integrations/adk-middleware/python/examples/server/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
tool_based_generative_ui_app,
2323
human_in_the_loop_app,
2424
shared_state_app,
25+
backend_tool_rendering_app,
2526
# predictive_state_updates_app,
2627
)
2728

@@ -32,6 +33,7 @@
3233
app.include_router(tool_based_generative_ui_app.router, prefix='/adk-tool-based-generative-ui', tags=['Tool Based Generative UI'])
3334
app.include_router(human_in_the_loop_app.router, prefix='/adk-human-in-loop-agent', tags=['Human in the Loop'])
3435
app.include_router(shared_state_app.router, prefix='/adk-shared-state-agent', tags=['Shared State'])
36+
app.include_router(backend_tool_rendering_app.router, prefix='/backend_tool_rendering', tags=['Backend Tool Rendering'])
3537
# app.include_router(predictive_state_updates_app.router, prefix='/adk-predictive-state-agent', tags=['Predictive State Updates'])
3638

3739

@@ -44,6 +46,7 @@ async def root():
4446
"tool_based_generative_ui": "/adk-tool-based-generative-ui",
4547
"human_in_the_loop": "/adk-human-in-loop-agent",
4648
"shared_state": "/adk-shared-state-agent",
49+
"backend_tool_rendering": "/backend_tool_rendering",
4750
# "predictive_state_updates": "/adk-predictive-state-agent",
4851
"docs": "/docs"
4952
}

typescript-sdk/integrations/adk-middleware/python/examples/server/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
from .human_in_the_loop import app as human_in_the_loop_app
66
from .shared_state import app as shared_state_app
77
from .predictive_state_updates import app as predictive_state_updates_app
8+
from .backend_tool_rendering import app as backend_tool_rendering_app
89

910
__all__ = [
1011
"agentic_chat_app",
1112
"tool_based_generative_ui_app",
1213
"human_in_the_loop_app",
1314
"shared_state_app",
1415
"predictive_state_updates_app",
16+
"backend_tool_rendering_app",
1517
]

0 commit comments

Comments
 (0)