Skip to content

Chat UI repeats the previous assistant message after sequential function calls or new user messages #712

@haruiz

Description

@haruiz

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:

  1. Trigger a tool (e.g., getWeather, getLocation, changeBackground), and then
  2. 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:

  1. I asked: “What is the weather in New York?”
  2. The assistant responded correctly with a weather card.
  3. Then I asked: “Get me the location of San Francisco.”
  4. After the tool resolved, the chat displayed:
  5. ✔️ The correct location response
  6. ❌ 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):

Image

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);
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions