-
Notifications
You must be signed in to change notification settings - Fork 936
Open
Description
I am experiencing a recurring issue in the chat UI where the previous assistant message is duplicated after a tool call completes or after I send a new message.
🔍 What Happens
When I:
- Trigger a tool (e.g., getWeather, getLocation, changeBackground), and then
- Immediately send another message or start another function call using the suggestions.
…the chat displays the previous assistant response again, even though I did not send or request it.
It seems to be a problem with the client's memory or message management.
📸 Screenshot Explanation (see attached image)
As shown in the screenshot:
- I asked: “What is the weather in New York?”
- The assistant responded correctly with a weather card.
- Then I asked: “Get me the location of San Francisco.”
- After the tool resolved, the chat displayed:
- ✔️ The correct location response
- ❌ But also re-rendered the previous weather response again (duplicated)
This duplication happens consistently whenever a tool call occurs.
🎯 Expected Behavior
Each assistant response should appear once.
A tool completion should not cause the chat to replay or re-render older turns.
I'm also sharing my code for review (https://github.com/haruiz/ag-ui-adk-demo-app):
main.py
from fastapi import FastAPI
from ag_ui_adk import ADKAgent, add_adk_fastapi_endpoint
from google.adk import Agent
from dotenv import load_dotenv
from google.adk.tools import MCPToolset
from google.adk.tools.mcp_tool import StreamableHTTPConnectionParams
from google.adk.tools.preload_memory_tool import PreloadMemoryTool
from tools import get_weather, get_place_location, get_place_details
import logging
# Initialize logger for debugging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Load environment variables from .env file
load_dotenv()
agent_instructions = """
You are a helpful assistant designed to answer user questions and provide useful information,
including weather updates and place details using Google Maps data.
Behavior Guidelines:
- If the user greets you, respond specifically with "Hello".
- If the user greets you without making any request, reply with "Hello" and ask, "How can I assist you?"
- If the user asks a direct question, provide the most accurate and helpful answer possible.
Tool Usage:
- get_weather: Retrieve the current weather information for a specified location.
- get_place_location: Obtain the precise latitude and longitude of a specified place.
- get_place_details: Fetch detailed information about a place using its geographic coordinates.
Always choose the most appropriate tool to fulfill the user's request, and respond clearly and concisely.
"""
# -------------------------------------------------------------------
# Create a base Google ADK agent definition
# This is the core LLM agent that will power the application
# -------------------------------------------------------------------
weather_agent = Agent(
name="assistant", # Internal agent name
model="gemini-2.5-flash", # LLM model to use
instruction=agent_instructions,
tools=[
# Provides persistent memory during the session (non-long-term)
PreloadMemoryTool(),
# Direct tool integration example
# get_weather,
get_place_location,
get_place_details,
# MCP Toolset integration
MCPToolset(
connection_params=StreamableHTTPConnectionParams(
url="http://127.0.0.1:8080/mcp" # Local MCP server endpoint
)
)
]
)
# -------------------------------------------------------------------
# Wrap the agent inside an ADKAgent middleware
# This provides sessions, user identity, in-memory services,
# and the unified ADK API that frontend UI components expect.
# -------------------------------------------------------------------
ag_weather_agent = ADKAgent(
adk_agent=weather_agent, # The core ADK agent
app_name="demo_app", # App identifier
user_id="demo_user", # Mock user ID (replace in production)
session_timeout_seconds=3600, # Session expiration
use_in_memory_services=True # Enables in-memory RAG + storage
)
# Create the FastAPI application
app = FastAPI(title="ADK Middleware Basic Chat")
# -------------------------------------------------------------------
# Register an ADK-compliant endpoint with FastAPI.
# This exposes the chat API at "/".
# Your frontend (Next.js + CopilotKit) will call this endpoint.
# -------------------------------------------------------------------
add_adk_fastapi_endpoint(app, ag_weather_agent, path="/")
# -------------------------------------------------------------------
# Run the development server using Uvicorn
# Only executes when running `python main.py`
# -------------------------------------------------------------------
if __name__ == '__main__':
import uvicorn
uvicorn.run(
"main:app",
host="localhost",
port=8000,
reload=True, # Auto-reload on code changes
workers=1 # Single worker recommended for MCP tools
)ag-ui/app/layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { CopilotKit } from "@copilotkit/react-core";
import "./globals.css";
import "@copilotkit/react-ui/styles.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<CopilotKit runtimeUrl="/api/copilotkit"
agent="weather_agent"
showDevConsole={false}
publicApiKey="ck_pub_5e7e08bb285e0ffbd7b4124fa9aaebf3">
{children}
</CopilotKit>
</body>
</html>
);
}ag-ui/app/page.tsx
"use client";
import React, { useState } from "react";
import "@copilotkit/react-ui/styles.css";
import "./style.css";
// CopilotKit core
import {
useFrontendTool,
useRenderToolCall,
} from "@copilotkit/react-core";
import { CopilotChat } from "@copilotkit/react-ui";
// Weather card component + types
import WeatherCard, {
getThemeColor,
WeatherToolResult,
WeatherMCPToolResult,
} from "@/app/components/WeatherCard";
import GoogleMap from "@/app/components/GoogleMap";
/* ------------------------------------------------------------------------------------------------
* TYPE GUARDS & PARSERS
* ----------------------------------------------------------------------------------------------*/
/**
* Narrow a result to WeatherMCPToolResult by checking for the "content" field.
*/
function isWeatherMCPToolResult(
result: WeatherToolResult | WeatherMCPToolResult
): result is WeatherMCPToolResult {
return "content" in result;
}
/**
* Convert MCP tool output into a WeatherToolResult if it contains valid JSON payload.
*/
function mcpToWeatherResult(
result: WeatherMCPToolResult
): WeatherToolResult | null {
if (!result.content || result.content.length === 0) return null;
const text = result.content[0].text?.trim();
if (!text) return null;
try {
const parsed = JSON.parse(text);
// Validate minimal expected structure
if (
typeof parsed.temperature === "number" &&
typeof parsed.conditions === "string"
) {
return parsed as WeatherToolResult;
}
return null;
} catch {
// JSON parse error
return null;
}
}
/* ------------------------------------------------------------------------------------------------
* CHAT COMPONENT
* ----------------------------------------------------------------------------------------------*/
const Chat = () => {
// Dynamic background controlled by tool usage
const [background, setBackground] = useState<string>(
"--copilot-kit-background-color"
);
// Dynamic suggestions for weather queries
const locations = ["San Francisco", "New York", "Tokyo"];
const suggestions = [
{
title: "Change background",
message: "Change the background to a nice gradient.",
},
...locations.map((location) => ({
title: `Get weather in ${location}`,
message: `What is the weather in ${location}?`,
})),
...locations.map((location) => ({
title: `Show me ${location} on a map`,
message: `Get me the location of ${location}.`,
}))
];
/* --------------------------------------------------------------------------------------------
* CHANGE BACKGROUND TOOL
* This tool allows the LLM to set the chat background.
* ------------------------------------------------------------------------------------------*/
useFrontendTool({
name: "change_background",
description:
"Change the chat's background using any CSS background value (color, gradient, etc.).",
parameters: [
{
name: "background",
type: "string",
description: "CSS background definition (colors, gradients, etc).",
},
],
// The tool handler executes when the LLM calls this tool.
handler: ({ background }) => {
setBackground(background);
return {
status: "success",
message: `Background changed to ${background}`,
};
},
});
/* --------------------------------------------------------------------------------------------
* RENDER PLACE LOCATION TOOL CALL
* This visually renders the result of the get_place_location tool.
* ------------------------------------------------------------------------------------------
*/
useRenderToolCall({
name:"get_place_location",
available: "disabled",
parameters: [{ name: "place_name", type: "string", required: true }],
render: ({ args, status, result }) => {
if (status === "inProgress") {
return (
<div className="bg-[#667eea] text-white p-4 rounded-lg max-w-md">
<span className="animate-spin">⚙️ Retrieving location...</span>
</div>
);
}
if (status === "complete" && result) {
const { result : coords } = result;
return <GoogleMap lat={coords?.latitude} lng={coords?.longitude} />;
}
return null;
}
})
/* --------------------------------------------------------------------------------------------
* RENDER WEATHER TOOL CALL
* This visually renders the result of the get_weather tool.
* ------------------------------------------------------------------------------------------*/
useRenderToolCall({
name: "get_weather",
available: "disabled", // Using MCP or manually invoking elsewhere
parameters: [{ name: "location", type: "string", required: true }],
render: ({ args, status, result }) => {
/* STATUS: inProgress --------------------------------------------------*/
if (status === "inProgress") {
return (
<div className="bg-[#667eea] text-white p-4 rounded-lg max-w-md">
<span className="animate-spin">⚙️ Retrieving weather...</span>
</div>
);
}
/* STATUS: complete ----------------------------------------------------*/
if (status === "complete" && result) {
console.log("Raw Weather Result:", result);
let weatherResult: WeatherToolResult | null = result;
// If MCP-style result, convert it
if (isWeatherMCPToolResult(result)) {
weatherResult = mcpToWeatherResult(result);
}
if (!weatherResult) {
return (
<div className="bg-red-300 text-red-900 p-4 rounded-lg max-w-md">
<strong>Weather parse error:</strong> Could not interpret tool
result.
</div>
);
}
// Choose color based on weather conditions
const themeColor = getThemeColor(weatherResult.conditions);
return (
<WeatherCard
location={args.location}
themeColor={themeColor}
result={weatherResult}
status={status || "complete"}
/>
);
}
return null;
},
});
/* --------------------------------------------------------------------------------------------
* MAIN UI RENDERING
* ------------------------------------------------------------------------------------------*/
return (
<div
className="flex justify-center items-center h-full w-full"
data-testid="background-container"
style={{ background }}
>
<div className="h-full w-full md:w-8/10 md:h-8/10 rounded-lg">
<CopilotChat
className="h-full rounded-2xl max-w-6xl mx-auto"
labels={{ initial: "Hi, I'm an agent. Want to chat?" }}
suggestions={suggestions}
/>
</div>
</div>
);
};
/* ------------------------------------------------------------------------------------------------
* HOME PAGE WRAPPER
* ----------------------------------------------------------------------------------------------*/
const HomePage = () => {
return (
<main className="h-screen w-screen">
<Chat />
</main>
);
};
export default HomePage;ag-ui/app/api/copilotkit/route.ts
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { HttpAgent } from "@ag-ui/client";
import { NextRequest } from "next/server";
// Create a service adapter for the CopilotKit runtime
const serviceAdapter = new ExperimentalEmptyAdapter();
// Create the main CopilotRuntime instance that manages communication between the frontend and backend agents
const runtime = new CopilotRuntime({
// Define the agents that will be available to the frontend
agents: {
// Configure the ADK agent connection
weather_agent: new HttpAgent({
// Specify the URL where the ADK agent is running
url: "http://localhost:8000/",
})
},
});
// Export the POST handler for the API route
export const POST = async (req: NextRequest) => {
// Create the request handler using CopilotKit's Next.js helper
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime, // The CopilotRuntime instance we configured
serviceAdapter, // The service adapter for agent coordination
endpoint: "/api/copilotkit", // The endpoint path (matches this file's location)
});
return handleRequest(req);
};bajayo
Metadata
Metadata
Assignees
Labels
No labels