chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,51 @@
# ========================================
# API Keys (REQUIRED)
# ========================================
# Google API Key (for ADK agents: Orchestrator, Analysis)
# Get your key from: https://aistudio.google.com/app/apikey
GOOGLE_API_KEY=your_google_api_key_here
# OpenAI API Key (for LangGraph agents: Research)
# Get your key from: https://platform.openai.com/api-keys
OPENAI_API_KEY=your_openai_api_key_here
# ========================================
# Agent URLs (Frontend → Agents)
# Optional - these are the defaults
# ========================================
# Orchestrator (ADK + AG-UI Protocol)
ORCHESTRATOR_URL=http://localhost:9000
# Research Agent (LangGraph + A2A Protocol)
RESEARCH_AGENT_URL=http://localhost:9001
# Analysis Agent (ADK + A2A Protocol)
ANALYSIS_AGENT_URL=http://localhost:9002
# ========================================
# Agent Ports (Python Agents)
# Optional - used by Python agents to bind to specific ports
# ========================================
# Orchestrator
ORCHESTRATOR_PORT=9000
# Research Agent
RESEARCH_PORT=9001
# Analysis Agent
ANALYSIS_PORT=9002
# ========================================
# CopilotKit Intelligence Threads (Optional)
# ========================================
# COPILOTKIT_LICENSE_TOKEN=
# INTELLIGENCE_API_KEY=
# INTELLIGENCE_API_URL=http://localhost:4201
# INTELLIGENCE_GATEWAY_WS_URL=ws://localhost:4401
@@ -0,0 +1,84 @@
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
*.pyc
venv/
env/
ENV/
env.bak/
venv.bak/
# Virtual environments
agents/.venv/
agents/venv/
.venv/
# Node
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
lerna-debug.log*
.next/
out/
.turbo
.vercel
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Testing
coverage/
.coverage
.pytest_cache/
.nyc_output/
htmlcov/
# Logs
logs/
*.log
# Temporary files
*.tmp
*.temp
.cache/
.parcel-cache/
# OS
Thumbs.db
Desktop.ini
# Build outputs
*.tsbuildinfo
next-env.d.ts
@@ -0,0 +1,261 @@
# A2A + AG-UI Multi-Agent Starter
A minimal starter template for building multi-agent applications with **A2A Protocol** (Agent-to-Agent) and **AG-UI Protocol** (Agent-UI). This project demonstrates how to coordinate multiple AI agents across different frameworks (LangGraph and Google ADK) to solve tasks collaboratively.
![Screenshot of a demo](demo.png)
## Quick Start
### Prerequisites
- **Node.js** 18+
- **Python** 3.10+
- **Google API Key** - [Get one here](https://aistudio.google.com/app/apikey)
- **OpenAI API Key** - [Get one here](https://platform.openai.com/api-keys)
### Installation
1. **Install frontend dependencies:**
```bash
npm install
```
2. **Install Python dependencies:**
```bash
cd agents
python3 -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install -r requirements.txt
cd ..
```
3. **Set up environment variables:**
```bash
cp .env.example .env
# Edit .env and add your API keys:
# GOOGLE_API_KEY=your_google_api_key
# OPENAI_API_KEY=your_openai_api_key
```
4. **Start all services:**
```bash
npm run dev
```
This will start:
- **UI**: http://localhost:3000
- **Orchestrator**: http://localhost:9000
- **Research Agent**: http://localhost:9001
- **Analysis Agent**: http://localhost:9002
## Usage
Try asking:
- "Research quantum computing"
- "Tell me about artificial intelligence"
- "Research renewable energy"
The orchestrator will:
1. Send your query to the **Research Agent** to gather information
2. Pass the research to the **Analysis Agent** for insights
3. Present a complete summary with both research and analysis
## Development Scripts
```bash
# Start everything
npm run dev
# Start individual services
npm run dev:ui # Next.js UI only
npm run dev:orchestrator # Orchestrator only
npm run dev:research # Research agent only
npm run dev:analysis # Analysis agent only
# Build for production
npm run build
# Lint code
npm run lint
```
## Customization
### Adding New Agents
1. **Create a new Python agent** in `agents/`:
- Implement A2A Protocol (see existing agents as examples)
- Choose a port (e.g., 9003)
- Define agent capabilities and skills
2. **Register in middleware** (`app/api/copilotkit/route.ts`):
```typescript
const newAgentUrl = "http://localhost:9003";
const a2aMiddlewareAgent = new A2AMiddlewareAgent({
agentUrls: [
researchAgentUrl,
analysisAgentUrl,
newAgentUrl, // Add here
],
// ...
});
```
3. **Add run script** in `package.json`:
```json
"dev:newagent": "python3 agents/new_agent.py"
```
4. **Update concurrently command** to include your new agent
### Changing UI
- **Main page**: Edit `app/page.tsx` for layout and result display
- **Chat**: Edit `components/chat.tsx` for chat behavior
- **Styling**: Edit `app/globals.css` and `tailwind.config.ts`
- **A2A badges**: Edit `components/a2a/` components
## What This Demonstrates
This starter shows how specialized agents built with different frameworks can communicate via the A2A protocol:
### Architecture
```
┌──────────────────────────────────────────┐
│ Next.js UI (CopilotKit) │
└────────────┬─────────────────────────────┘
│ AG-UI Protocol
┌────────────┴─────────────────────────────┐
│ A2A Middleware │
│ - Routes messages between agents │
└──────┬───────────────────────────────────┘
│ A2A Protocol
├─────► Research Agent (LangGraph)
│ - Gathers information
│ - Port 9001
└─────► Analysis Agent (ADK)
- Analyzes findings
- Port 9002
┌──────┴──────────┐
│ Orchestrator │
│ (ADK) │
│ Port 9000 │
└─────────────────┘
```
### Agents
1. **Orchestrator (ADK + AG-UI Protocol)**
- Receives requests from the UI
- Coordinates specialized agents
- Port: 9000
2. **Research Agent (LangGraph + A2A Protocol)**
- Gathers and summarizes information
- Returns structured JSON
- Port: 9001
3. **Analysis Agent (ADK + A2A Protocol)**
- Analyzes research findings
- Provides insights and conclusions
- Port: 9002
## Project Structure
```
starter/
├── app/
│ ├── api/copilotkit/route.ts # A2A middleware setup (KEY FILE!)
│ ├── layout.tsx # Root layout
│ ├── globals.css # Styles
│ └── page.tsx # Main UI
├── components/
│ ├── chat.tsx # Chat component with A2A visualization
│ └── a2a/ # A2A message components
│ ├── agent-styles.ts # Agent branding utilities
│ ├── MessageToA2A.tsx # Outgoing message badges
│ └── MessageFromA2A.tsx # Incoming message badges
├── agents/ # Python agents
│ ├── orchestrator.py # Orchestrator (ADK + AG-UI) - Port 9000
│ ├── research_agent.py # Research (LangGraph + A2A) - Port 9001
│ ├── analysis_agent.py # Analysis (ADK + A2A) - Port 9002
│ └── requirements.txt # Python dependencies
├── package.json # Frontend dependencies & scripts
├── .env.example # Environment variables template
└── README.md # This file
```
## Key Concepts
### AG-UI Protocol
The **AG-UI Protocol** standardizes communication between the frontend (CopilotKit) and agents. The orchestrator uses AG-UI to receive messages from the UI.
### A2A Protocol
The **A2A Protocol** standardizes agent-to-agent communication. The Research and Analysis agents use A2A to communicate with the orchestrator.
### A2A Middleware
The **A2A Middleware** (in `app/api/copilotkit/route.ts`) is the magic that connects everything:
- Wraps the orchestrator agent
- Registers A2A agents automatically
- Injects a `send_message_to_a2a_agent` tool into the orchestrator
- Routes messages between agents
## Troubleshooting
### Agents not connecting?
- Verify all services are running: `http://localhost:9000-9002`
- Check console for startup errors
### Missing API keys?
- Ensure `.env` file exists with `GOOGLE_API_KEY` and `OPENAI_API_KEY`
- Restart all services after adding keys
### Python import errors?
- Activate virtual environment: `source agents/.venv/bin/activate`
- Reinstall dependencies: `pip install -r agents/requirements.txt`
### Port conflicts?
- Change ports in `.env` file:
```
ORCHESTRATOR_PORT=9000
RESEARCH_PORT=9001
ANALYSIS_PORT=9002
```
## Learn More
- [AG-UI Protocol Documentation](https://docs.ag-ui.com)
- [A2A Protocol Specification](https://a2a-protocol.org)
- [Google ADK Documentation](https://google.github.io/adk-docs/)
- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/)
- [CopilotKit Documentation](https://docs.copilotkit.ai)
## License
MIT
@@ -0,0 +1,232 @@
"""
Analysis Agent - Analyzes research findings using ADK + Gemini.
Exposes A2A Protocol endpoint, returns structured JSON.
"""
import uvicorn
import os
import json
from typing import List
from dotenv import load_dotenv
from pydantic import BaseModel, Field
load_dotenv()
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import (
AgentCapabilities,
AgentCard,
AgentSkill,
)
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.utils import new_agent_text_message
from google.adk.agents.llm_agent import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
from google.adk.artifacts import InMemoryArtifactService
from google.genai import types
class InsightItem(BaseModel):
title: str = Field(description="Title of the insight")
description: str = Field(description="Detailed description of the insight")
importance: str = Field(description="Why this insight matters")
class StructuredAnalysis(BaseModel):
topic: str = Field(description="The topic being analyzed")
overview: str = Field(description="Brief overview of the analysis")
insights: List[InsightItem] = Field(description="List of key insights")
conclusion: str = Field(description="Concluding thoughts")
class AnalysisAgent:
def __init__(self):
self._agent = self._build_agent()
self._user_id = "remote_agent"
self._runner = Runner(
app_name=self._agent.name,
agent=self._agent,
artifact_service=InMemoryArtifactService(),
session_service=InMemorySessionService(),
memory_service=InMemoryMemoryService(),
)
def _build_agent(self) -> LlmAgent:
model_name = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
return LlmAgent(
model=model_name,
name="analysis_agent",
description="An agent that analyzes research findings and provides insights",
instruction="""
You are an analysis agent. Your role is to analyze research findings and provide meaningful insights.
When you receive research data, analyze it thoroughly and create an insightful analysis.
Return ONLY a valid JSON object with this exact structure:
{
"topic": "The topic being analyzed",
"overview": "A brief 2-3 sentence overview of the analysis",
"insights": [
{
"title": "Key Insight 1",
"description": "Detailed explanation of this insight",
"importance": "Why this matters"
},
{
"title": "Key Insight 2",
"description": "Detailed explanation of this insight",
"importance": "Why this matters"
},
{
"title": "Key Insight 3",
"description": "Detailed explanation of this insight",
"importance": "Why this matters"
}
],
"conclusion": "Concluding thoughts and recommendations"
}
Provide 3-5 meaningful insights based on the research.
Make the analysis thoughtful and actionable.
Return ONLY valid JSON, no markdown code blocks, no other text.
""",
tools=[],
)
async def invoke(self, query: str, session_id: str) -> str:
"""Generate analysis and return JSON string."""
session = await self._runner.session_service.get_session(
app_name=self._agent.name,
user_id=self._user_id,
session_id=session_id,
)
content = types.Content(role="user", parts=[types.Part.from_text(text=query)])
if session is None:
session = await self._runner.session_service.create_session(
app_name=self._agent.name,
user_id=self._user_id,
state={},
session_id=session_id,
)
response_text = ""
async for event in self._runner.run_async(
user_id=self._user_id, session_id=session.id, new_message=content
):
if event.is_final_response():
if (
event.content
and event.content.parts
and event.content.parts[0].text
):
response_text = "\n".join(
[p.text for p in event.content.parts if p.text]
)
break
content_str = response_text.strip()
if "```json" in content_str:
content_str = content_str.split("```json")[1].split("```")[0].strip()
elif "```" in content_str:
content_str = content_str.split("```")[1].split("```")[0].strip()
try:
structured_data = json.loads(content_str)
validated_analysis = StructuredAnalysis(**structured_data)
final_response = json.dumps(validated_analysis.model_dump(), indent=2)
print("✅ Successfully created structured analysis")
return final_response
except json.JSONDecodeError as e:
print(f"❌ JSON parsing error: {e}")
print(f"Content: {content_str}")
return json.dumps(
{
"error": "Failed to generate structured analysis",
"raw_content": content_str[:200],
}
)
except Exception as e:
print(f"❌ Validation error: {e}")
return json.dumps({"error": f"Validation failed: {str(e)}"})
# A2A Protocol executor wraps the ADK agent
class AnalysisAgentExecutor(AgentExecutor):
def __init__(self):
self.agent = AnalysisAgent()
async def execute(
self,
context: RequestContext,
event_queue: EventQueue,
) -> None:
query = context.get_user_input()
session_id = getattr(context, "context_id", "default_session")
final_content = await self.agent.invoke(query, session_id)
await event_queue.enqueue_event(new_agent_text_message(final_content))
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
raise Exception("cancel not supported")
port = int(os.getenv("ANALYSIS_PORT", 9002))
skill = AgentSkill(
id="analysis_agent",
name="Analysis Agent",
description="Analyzes research findings and provides meaningful insights using ADK",
tags=["research", "analysis", "insights", "adk"],
examples=[
"Analyze this research about quantum computing",
"What are the key insights from this data?",
"Provide analysis of these research findings",
],
)
public_agent_card = AgentCard(
name="Analysis Agent",
description="ADK-powered agent that analyzes research findings and provides meaningful insights",
url=f"http://localhost:{port}/",
version="1.0.0",
defaultInputModes=["text"],
defaultOutputModes=["text"],
capabilities=AgentCapabilities(streaming=True),
skills=[skill],
supportsAuthenticatedExtendedCard=False,
)
def main():
if not os.getenv("GOOGLE_API_KEY") and not os.getenv("GEMINI_API_KEY"):
print("⚠️ Warning: No API key found!")
print(" Set GOOGLE_API_KEY or GEMINI_API_KEY")
print(" Get a key from: https://aistudio.google.com/app/apikey")
print()
request_handler = DefaultRequestHandler(
agent_executor=AnalysisAgentExecutor(),
task_store=InMemoryTaskStore(),
)
server = A2AStarletteApplication(
agent_card=public_agent_card,
http_handler=request_handler,
extended_agent_card=public_agent_card,
)
print(f"💡 Starting Analysis Agent (ADK + A2A) on http://localhost:{port}")
print(f" Agent: {public_agent_card.name}")
print(f" Description: {public_agent_card.description}")
uvicorn.run(server.build(), host="0.0.0.0", port=port)
if __name__ == "__main__":
main()
@@ -0,0 +1,86 @@
"""
Orchestrator Agent - Coordinates between Research and Analysis agents.
Speaks AG-UI Protocol to the UI, delegates tasks to A2A agents via middleware.
"""
from __future__ import annotations
from dotenv import load_dotenv
load_dotenv()
import os
import uvicorn
from fastapi import FastAPI
from ag_ui_adk import ADKAgent, add_adk_fastapi_endpoint
from google.adk.agents import LlmAgent
orchestrator_agent = LlmAgent(
name="OrchestratorAgent",
model="gemini-2.5-pro",
instruction="""
You are an orchestrator agent that coordinates research and analysis tasks.
AVAILABLE SPECIALIZED AGENTS:
1. **Research Agent** (LangGraph) - Gathers and summarizes information about a topic
2. **Analysis Agent** (ADK) - Analyzes research findings and provides insights
CRITICAL CONSTRAINTS:
- You MUST call agents ONE AT A TIME, never make multiple tool calls simultaneously
- After making a tool call, WAIT for the result before making another tool call
- Do NOT make parallel/concurrent tool calls - this is not supported
WORKFLOW FOR RESEARCH TASKS:
When the user asks to research a topic:
1. **Research Agent** - First, gather information about the topic
- Pass: The user's research query or topic
- Wait for structured JSON response with research findings
2. **Analysis Agent** - Then, analyze the research results
- Pass: The research results from step 1
- Wait for structured JSON with analysis and insights
3. Present the complete research and analysis to the user
IMPORTANT WORKFLOW DETAILS:
- Always call the Research Agent first to gather information
- Then call the Analysis Agent to analyze the findings
- Wait for each agent to complete before calling the next one
- Build your final response using information from both agents
RESPONSE STRATEGY:
- After each agent response, briefly acknowledge what you received
- Build up the complete answer incrementally
- At the end, present a well-organized summary
- Don't just list agent responses - synthesize them into a cohesive answer
IMPORTANT: Once you have received a response from an agent, do NOT call that same
agent again for the same information. Use the information you already have.
""",
)
# Wrap with AG-UI middleware to expose via AG-UI Protocol
adk_orchestrator_agent = ADKAgent(
adk_agent=orchestrator_agent,
app_name="orchestrator_app",
user_id="demo_user",
session_timeout_seconds=3600,
use_in_memory_services=True,
)
app = FastAPI(title="A2A Orchestrator (ADK + AG-UI Protocol)")
add_adk_fastapi_endpoint(app, adk_orchestrator_agent, path="/")
if __name__ == "__main__":
if not os.getenv("GOOGLE_API_KEY"):
print("⚠️ Warning: GOOGLE_API_KEY not set!")
print(" Set it with: export GOOGLE_API_KEY='your-key-here'")
print(" Get a key from: https://aistudio.google.com/app/apikey")
print()
port = int(os.getenv("ORCHESTRATOR_PORT", 9000))
print(f"🚀 Starting Orchestrator Agent (ADK + AG-UI) on http://localhost:{port}")
uvicorn.run(app, host="0.0.0.0", port=port)
@@ -0,0 +1,62 @@
# ============================================================================
# A2A + AG-UI Starter - Python Agent Dependencies
# ============================================================================
# ============================================================================
# AG-UI Protocol Implementation
# ============================================================================
# ag-ui-adk: Allows ADK agents to communicate with the frontend via AG-UI Protocol
# This is used by the orchestrator agent to receive messages from the UI
ag-ui-adk>=0.0.1
# ============================================================================
# A2A Protocol Implementation
# ============================================================================
# a2a: Core A2A Protocol SDK for agent-to-agent communication
# a2a-sdk: Additional utilities including HTTP server support
# These packages enable the Research and Analysis agents to communicate via A2A
a2a>=0.1.0
a2a-sdk[http-server]
# ============================================================================
# Google ADK (Agent Development Kit)
# ============================================================================
# google-adk: Google's framework for building AI agents with Gemini models
# Used by: Orchestrator Agent, Analysis Agent
# litellm: Unified interface for multiple LLM providers (dependency of ADK)
google-adk>=0.1.0
litellm>=1.0.0
# ============================================================================
# LangGraph Framework
# ============================================================================
# langgraph: LangChain's framework for building stateful agent workflows
# langchain: Core LangChain library for LLM applications
# langchain-openai: OpenAI integration for LangChain
# Used by: Research Agent
langgraph>=0.2.0
langchain>=0.3.0
langchain-openai>=0.2.0
# ============================================================================
# Web Server
# ============================================================================
# fastapi: Modern Python web framework for building agent HTTP endpoints
# uvicorn: ASGI server for running FastAPI applications
# All agents expose HTTP endpoints using FastAPI
fastapi>=0.115.0
uvicorn>=0.30.0
# ============================================================================
# Utilities
# ============================================================================
# python-dotenv: Loads environment variables from .env file
# Used to configure API keys (GOOGLE_API_KEY, OPENAI_API_KEY)
python-dotenv>=1.0.0
# ============================================================================
# LLM Providers
# ============================================================================
# openai: OpenAI Python SDK for GPT models
# Used by the Research Agent (LangGraph)
openai>=1.0.0
@@ -0,0 +1,180 @@
"""
Research Agent - Gathers information using LangGraph + OpenAI.
Exposes A2A Protocol endpoint, returns structured JSON.
"""
import uvicorn
import json
import os
from dotenv import load_dotenv
load_dotenv()
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCapabilities, AgentCard, AgentSkill, Message
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.utils import new_agent_text_message
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from typing import TypedDict, Optional, List
from pydantic import BaseModel, Field
class ResearchFinding(BaseModel):
title: str = Field(description="Title or key point of the finding")
description: str = Field(description="Detailed description of the finding")
class StructuredResearch(BaseModel):
topic: str = Field(description="The research topic")
summary: str = Field(description="Brief summary of the research")
findings: List[ResearchFinding] = Field(description="List of key findings")
sources: str = Field(description="Note about information sources")
class ResearchState(TypedDict):
message: str
research: str
structured_research: Optional[dict]
class ResearchAgent:
def __init__(self):
self.llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
self.graph = self._build_graph()
def _build_graph(self):
workflow = StateGraph(ResearchState)
workflow.add_node("conduct_research", self._conduct_research)
workflow.set_entry_point("conduct_research")
workflow.add_edge("conduct_research", END)
return workflow.compile()
def _conduct_research(self, state: ResearchState) -> ResearchState:
"""Generate research findings using LLM and return structured JSON."""
message = state["message"]
prompt = f"""
Research the following topic and provide comprehensive information.
Topic: {message}
Return ONLY a valid JSON object with this exact structure:
{{
"topic": "The research topic",
"summary": "A brief 2-3 sentence summary of the topic",
"findings": [
{{
"title": "Key Point 1",
"description": "Detailed explanation of this point"
}},
{{
"title": "Key Point 2",
"description": "Detailed explanation of this point"
}},
{{
"title": "Key Point 3",
"description": "Detailed explanation of this point"
}}
],
"sources": "Note about where this information typically comes from"
}}
Include 3-5 key findings about the topic.
Make the research informative and well-structured.
Return ONLY valid JSON, no markdown code blocks, no other text.
"""
response = self.llm.invoke(prompt)
try:
structured_data = json.loads(response.content)
state["structured_research"] = structured_data
state["research"] = json.dumps(structured_data)
except json.JSONDecodeError as e:
state["research"] = f"Error: Failed to parse research results - {str(e)}"
state["structured_research"] = None
return state
async def invoke(self, message: Message) -> str:
"""Process A2A message and return research JSON."""
message_text = message.parts[0].root.text
result = self.graph.invoke(
{"message": message_text, "research": "", "structured_research": None}
)
return result["research"]
# A2A Protocol executor wraps the LangGraph agent
class ResearchAgentExecutor(AgentExecutor):
def __init__(self):
self.agent = ResearchAgent()
async def execute(
self,
context: RequestContext,
event_queue: EventQueue,
) -> None:
result = await self.agent.invoke(context.message)
await event_queue.enqueue_event(new_agent_text_message(result))
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
raise Exception("cancel not supported")
port = int(os.getenv("RESEARCH_PORT", 9001))
skill = AgentSkill(
id="research_agent",
name="Research Agent",
description="Gathers and summarizes information about a given topic using LangGraph",
tags=["research", "information", "summary", "langgraph"],
examples=[
"Research quantum computing",
"Tell me about artificial intelligence",
"Gather information on renewable energy",
],
)
public_agent_card = AgentCard(
name="Research Agent",
description="LangGraph-powered agent that gathers and summarizes information about any topic",
url=f"http://localhost:{port}/",
version="1.0.0",
defaultInputModes=["text"],
defaultOutputModes=["text"],
capabilities=AgentCapabilities(streaming=True),
skills=[skill],
supportsAuthenticatedExtendedCard=False,
)
def main():
if not os.getenv("OPENAI_API_KEY"):
print("⚠️ Warning: OPENAI_API_KEY not set!")
print(" Set it with: export OPENAI_API_KEY='your-key-here'")
print(" Get a key from: https://platform.openai.com/api-keys")
print()
request_handler = DefaultRequestHandler(
agent_executor=ResearchAgentExecutor(),
task_store=InMemoryTaskStore(),
)
server = A2AStarletteApplication(
agent_card=public_agent_card,
http_handler=request_handler,
extended_agent_card=public_agent_card,
)
print(f"🔍 Starting Research Agent (LangGraph + A2A) on http://localhost:{port}")
print(f" Agent: {public_agent_card.name}")
print(f" Description: {public_agent_card.description}")
uvicorn.run(server.build(), host="0.0.0.0", port=port)
if __name__ == "__main__":
main()
@@ -0,0 +1,161 @@
import {
CopilotRuntime,
CopilotKitIntelligence,
createCopilotEndpoint,
InMemoryAgentRunner,
} from "@copilotkit/runtime/v2";
import { HttpAgent } from "@ag-ui/client";
import type {
AgentSubscriber,
RunAgentInput,
RunAgentParameters,
RunAgentResult,
} from "@ag-ui/client";
import { A2AMiddlewareAgent } from "@ag-ui/a2a-middleware";
import type { A2AAgentConfig } from "@ag-ui/a2a-middleware";
import { handle } from "hono/vercel";
const researchAgentUrl =
process.env.RESEARCH_AGENT_URL || "http://localhost:9001";
const analysisAgentUrl =
process.env.ANALYSIS_AGENT_URL || "http://localhost:9002";
const orchestratorUrl = process.env.ORCHESTRATOR_URL || "http://localhost:9000";
type RuntimeRunAgentInput = RunAgentParameters &
Partial<Pick<RunAgentInput, "messages" | "state" | "threadId">>;
type RuntimeA2AMiddlewareAgentConfig = Omit<
A2AAgentConfig,
"orchestrationAgent"
> & {
orchestrationAgentUrl: string;
};
class RuntimeA2AMiddlewareAgent extends A2AMiddlewareAgent {
private readonly config: RuntimeA2AMiddlewareAgentConfig;
constructor(config: RuntimeA2AMiddlewareAgentConfig) {
super({
...config,
orchestrationAgent: new HttpAgent({
url: config.orchestrationAgentUrl,
}),
});
this.config = config;
}
async runAgent(
parameters: RuntimeRunAgentInput = {},
subscriber?: AgentSubscriber,
): Promise<RunAgentResult> {
const isolatedAgent = new A2AMiddlewareAgent({
...this.config,
agentId: this.agentId,
debug: this.debug,
description: this.description,
initialMessages: this.messages,
initialState: this.state,
threadId: parameters.threadId ?? this.threadId,
orchestrationAgent: new HttpAgent({
url: this.config.orchestrationAgentUrl,
}),
});
if (parameters.state) {
isolatedAgent.setState(parameters.state);
}
if (parameters.messages) {
isolatedAgent.setMessages(parameters.messages);
}
return isolatedAgent.runAgent(
{
context: parameters.context,
forwardedProps: parameters.forwardedProps,
runId: parameters.runId,
tools: parameters.tools,
},
subscriber,
);
}
clone(): RuntimeA2AMiddlewareAgent {
return new RuntimeA2AMiddlewareAgent({
...this.config,
agentId: this.agentId,
debug: this.debug,
description: this.description,
initialMessages: this.messages,
initialState: this.state,
threadId: this.threadId,
});
}
}
const a2aMiddlewareAgent = new RuntimeA2AMiddlewareAgent({
orchestrationAgentUrl: orchestratorUrl,
agentId: "a2a_chat",
description:
"Research assistant with 2 specialized agents: Research (LangGraph) and Analysis (ADK)",
agentUrls: [researchAgentUrl, analysisAgentUrl],
instructions: `
You are a research assistant that orchestrates between 2 specialized agents.
AVAILABLE AGENTS:
- Research Agent (LangGraph): Gathers and summarizes information about a topic
- Analysis Agent (ADK): Analyzes research findings and provides insights
WORKFLOW STRATEGY (SEQUENTIAL - ONE AT A TIME):
When the user asks to research a topic:
1. Research Agent - First, gather information about the topic
- Pass: The user's research query or topic
- The agent will return structured JSON with research findings
2. Analysis Agent - Then, analyze the research results
- Pass: The research results from step 1
- The agent will return structured JSON with analysis and insights
3. Present the complete research and analysis to the user
CRITICAL RULES:
- Call agents ONE AT A TIME, wait for results before making next call
- Pass information from earlier agents to later agents
- Synthesize all gathered information in final response
`,
});
const runtime = new CopilotRuntime({
agents: {
a2a_chat: a2aMiddlewareAgent,
},
// --- copilotkit:intelligence (remove this block to opt out) ---
...(process.env.COPILOTKIT_LICENSE_TOKEN
? {
intelligence: new CopilotKitIntelligence({
apiKey: process.env.INTELLIGENCE_API_KEY ?? "",
apiUrl: process.env.INTELLIGENCE_API_URL ?? "http://localhost:4201",
wsUrl:
process.env.INTELLIGENCE_GATEWAY_WS_URL ?? "ws://localhost:4401",
}),
// Demo stub - replace with your own auth-derived user identity (e.g. OIDC)
// before any multi-user deployment, or all users share one thread history.
identifyUser: () => ({ id: "demo-user", name: "Demo User" }),
licenseToken: process.env.COPILOTKIT_LICENSE_TOKEN,
}
: { runner: new InMemoryAgentRunner() }),
// --- /copilotkit:intelligence ---
});
const app = createCopilotEndpoint({
runtime,
basePath: "/api/copilotkit",
});
export const GET = handle(app);
export const POST = handle(app);
export const PATCH = handle(app);
export const DELETE = handle(app);
@@ -0,0 +1,87 @@
@import "tailwindcss";
@config "../tailwind.config.ts";
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
/* Custom elevation shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md:
0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg:
0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl:
0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-family: var(--font-plus-jakarta-sans), sans-serif;
}
}
/* A2A message animations */
@keyframes slide-in {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.a2a-message-enter {
animation: slide-in 0.3s ease-out;
}
.threadsLayout,
body > [role="presentation"] {
--foreground: oklch(0.145 0 0);
--background: oklch(1 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--radius: 0.625rem;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
@@ -0,0 +1,36 @@
import type { Metadata } from "next";
import { Plus_Jakarta_Sans, Spline_Sans_Mono } from "next/font/google";
import "./globals.css";
import "@copilotkit/react-core/v2/styles.css";
const plusJakartaSans = Plus_Jakarta_Sans({
variable: "--font-plus-jakarta-sans",
subsets: ["latin"],
});
const splineSansMono = Spline_Sans_Mono({
variable: "--font-spline-sans-mono",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
});
export const metadata: Metadata = {
title: "A2A + AG-UI Starter",
description: "Multi-agent communication demo with A2A Protocol and AG-UI",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${plusJakartaSans.variable} ${splineSansMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
@@ -0,0 +1,50 @@
.layout {
display: grid;
/*
Reserve the desktop drawer's width (its default 320px) as a fixed first
column so the layout doesn't shift when the client-only <CopilotThreadsDrawer>
mounts after hydration. On mobile the drawer is an off-canvas overlay (out
of flow), so the column collapses and the content fills the width.
*/
grid-template-columns: var(--cpk-drawer-reserved-width, 320px) minmax(0, 1fr);
/* Drawer sets --cpk-drawer-reserved-width to 0 on desktop-collapse, so the
reserved column collapses and the chat reclaims the space. */
transition: grid-template-columns 0.2s ease;
/*
Bound the single grid row to the viewport (minmax(0,1fr) lets it shrink
below content) so a long thread list scrolls INTERNALLY in the drawer with
the header pinned, instead of the drawer growing past the viewport and the
page scrolling the header away.
*/
grid-template-rows: minmax(0, 1fr);
height: 100dvh;
width: 100%;
overflow: hidden;
}
.mainPanel {
/*
Pin the content to the SECOND track explicitly. The client-only drawer
renders nothing during SSR, so without this the content would flow into the
reserved first column at first paint and then jump once the drawer mounts.
*/
grid-column: 2;
min-width: 0;
height: 100dvh;
overflow: hidden;
}
/*
Mobile (≤768px): the drawer is an off-canvas overlay — collapse to a single
track. MUST come after the base rules (media queries add no specificity, so a
later same-specificity base rule would otherwise win and leak the two-column
desktop layout onto mobile).
*/
@media (max-width: 768px) {
.layout {
grid-template-columns: minmax(0, 1fr);
}
.mainPanel {
grid-column: auto;
}
}
@@ -0,0 +1,229 @@
"use client";
import { useState } from "react";
import Chat from "@/components/chat";
import {
CopilotChatConfigurationProvider,
CopilotThreadsDrawer,
CopilotKitProvider,
} from "@copilotkit/react-core/v2";
import styles from "./page.module.css";
export type ResearchData = {
topic: string;
summary: string;
findings: Array<{
title: string;
description: string;
}>;
sources: string;
};
export type AnalysisData = {
topic: string;
overview: string;
insights: Array<{
title: string;
description: string;
importance: string;
}>;
conclusion: string;
};
// Disable static optimization for this page
export const dynamic = "force-dynamic";
function ResearchAssistant() {
const [researchData, setResearchData] = useState<ResearchData | null>(null);
const [analysisData, setAnalysisData] = useState<AnalysisData | null>(null);
return (
<div className="relative flex min-h-dvh overflow-hidden bg-[#DEDEE9] p-2">
{/* Background blur circles - Creating the gradient effect */}
<div
className="absolute w-[445px] h-[445px] left-[1040px] top-[11px] rounded-full z-0"
style={{ background: "rgba(255, 172, 77, 0.2)", filter: "blur(103px)" }}
/>
<div
className="absolute w-[609px] h-[609px] left-[1339px] top-[625px] rounded-full z-0"
style={{ background: "#C9C9DA", filter: "blur(103px)" }}
/>
<div
className="absolute w-[609px] h-[609px] left-[670px] top-[-365px] rounded-full z-0"
style={{ background: "#C9C9DA", filter: "blur(103px)" }}
/>
<div
className="absolute w-[445px] h-[445px] left-[128px] top-[331px] rounded-full z-0"
style={{
background: "rgba(255, 243, 136, 0.3)",
filter: "blur(103px)",
}}
/>
<div className="flex flex-1 flex-col gap-2 overflow-y-auto z-10 lg:flex-row lg:overflow-hidden">
<div className="flex min-h-[calc(100dvh-1rem)] w-full flex-shrink-0 flex-col overflow-hidden rounded-lg border-2 border-white bg-white/50 shadow-elevation-lg backdrop-blur-md lg:w-[450px]">
<div className="p-6 max-lg:pl-16 border-b border-[#DBDBE5]">
<h1 className="text-2xl font-semibold text-[#010507] mb-1">
Research Assistant
</h1>
<p className="text-sm text-[#57575B] leading-relaxed">
Multi-Agent A2A Demo:{" "}
<span className="text-[#1B936F] font-semibold">1 LangGraph</span>{" "}
+ <span className="text-[#BEC2FF] font-semibold">1 ADK</span>{" "}
agent
</p>
<p className="text-xs text-[#838389] mt-1">
Orchestrator-mediated A2A Protocol
</p>
</div>
<div className="flex-1 overflow-hidden">
<Chat
onResearchUpdate={setResearchData}
onAnalysisUpdate={setAnalysisData}
/>
</div>
</div>
<div className="min-h-[520px] flex-1 overflow-y-auto rounded-lg bg-white/30 backdrop-blur-sm lg:min-h-0">
<div className="mx-auto p-4 sm:p-8">
<div className="mb-8">
<h2 className="text-3xl font-semibold text-[#010507] mb-2">
Research Results
</h2>
<p className="text-[#57575B]">
Multi-agent coordination: LangGraph + ADK agents with A2A
Protocol
</p>
</div>
{!researchData && !analysisData && (
<div className="flex items-center justify-center h-[400px] bg-white/60 backdrop-blur-md rounded-xl border-2 border-dashed border-[#DBDBE5] shadow-elevation-sm">
<div className="text-center">
<div className="text-6xl mb-4">🔍</div>
<h3 className="text-xl font-semibold text-[#010507] mb-2">
Start Your Research
</h3>
<p className="text-[#57575B] max-w-md">
Ask the assistant to research any topic. Watch as 2
specialized agents collaborate through A2A Protocol to
gather information and provide insights.
</p>
</div>
</div>
)}
<div className="flex flex-col gap-2 items-stretch xl:flex-row">
{researchData && (
<div className="flex-1 bg-white/60 backdrop-blur-md rounded-xl border-2 border-[#DBDBE5] shadow-elevation-md p-6">
<div className="flex flex-col gap-0 mb-4">
<div className="flex items-center gap-2">
<span className="text-2xl">📚</span>
<h3 className="text-xl font-semibold text-[#010507]">
{researchData.topic}
</h3>
<span className="ml-auto px-3 py-1 rounded-full text-xs font-semibold bg-gradient-to-r from-emerald-100 to-green-100 text-emerald-800 border-2 border-emerald-400">
🔗 Research Agent
</span>
</div>
<h4 className="text-lg font-semibold text-gray-500">
Key Points
</h4>
</div>
<p className="text-[#57575B] mb-4">{researchData.summary}</p>
<div className="space-y-3">
{researchData.findings.map((finding, index) => (
<div key={index} className="bg-white/80 rounded-lg p-4">
<h4 className="font-semibold text-[#010507] mb-1">
{finding.title}
</h4>
<p className="text-sm text-[#57575B]">
{finding.description}
</p>
</div>
))}
</div>
<p className="text-xs text-[#838389] mt-4 italic">
{researchData.sources}
</p>
</div>
)}
{analysisData && (
<div className="flex-1 bg-white/60 backdrop-blur-md rounded-xl border-2 border-[#DBDBE5] shadow-elevation-md p-6">
<div className="flex flex-col gap-0 mb-4">
<div className="flex items-center gap-2">
<span className="text-2xl">💡</span>
<h3 className="text-xl font-semibold text-[#010507]">
{analysisData.topic}
</h3>
<span className="ml-auto px-3 py-1 rounded-full text-xs font-semibold bg-gradient-to-r from-blue-100 to-sky-100 text-blue-800 border-2 border-blue-400">
Analysis Agent
</span>
</div>
<h4 className="text-lg font-semibold text-gray-500">
Insights and Analysis
</h4>
</div>
<p className="text-[#57575B] mb-4">{analysisData.overview}</p>
<div className="space-y-3 mb-4">
{analysisData.insights.map((insight, index) => (
<div key={index} className="bg-white/80 rounded-lg p-4">
<h4 className="font-semibold text-[#010507] mb-1">
{insight.title}
</h4>
<p className="text-sm text-[#57575B] mb-2">
{insight.description}
</p>
<p className="text-xs text-blue-600 font-medium">
💡 {insight.importance}
</p>
</div>
))}
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-semibold text-blue-900 mb-1">
Conclusion
</h4>
<p className="text-sm text-blue-800">
{analysisData.conclusion}
</p>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}
export default function Home() {
return (
<CopilotKitProvider
runtimeUrl="/api/copilotkit"
showDevConsole="auto"
useSingleEndpoint={false}
>
{/*
One UNCONTROLLED CopilotChatConfigurationProvider (no `threadId` prop)
owns the active thread for the whole surface. The SDK <CopilotThreadsDrawer>
drives it directly — picking a row sets the active thread, "+ New"
resets to a fresh thread — with no host thread-state. The chat (inside
ResearchAssistant) reads the same active thread from the provider. A
*controlled* provider would block "+ New" from resetting, so
uncontrolled-inside-provider is required, not optional.
*/}
<CopilotChatConfigurationProvider agentId="a2a_chat">
<div className={`${styles.layout} threadsLayout`}>
{/* SDK threads drawer (replaces the hand-rolled fork). License-gated: the locked view's Upgrade CTA opens the Intelligence docs by default. */}
<CopilotThreadsDrawer agentId="a2a_chat" />
<div className={styles.mainPanel}>
<ResearchAssistant />
</div>
</div>
</CopilotChatConfigurationProvider>
</CopilotKitProvider>
);
}
@@ -0,0 +1,67 @@
/**
* Displays incoming A2A responses (Agent → Orchestrator).
* Blue box with sender/receiver badges. Actual data renders separately in main UI.
*/
import React from "react";
import { getAgentStyle } from "./agent-styles";
type MessageActionRenderProps = {
status: string;
args: {
agentName?: string;
};
};
export const MessageFromA2A: React.FC<MessageActionRenderProps> = ({
status,
args,
}) => {
switch (status) {
case "complete":
break;
default:
return null;
}
if (!args.agentName) {
return null;
}
const agentStyle = getAgentStyle(args.agentName);
return (
<div className="my-2">
<div className="bg-blue-50 border border-blue-200 rounded-lg px-4 py-3">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 min-w-[200px] flex-shrink-0">
<div className="flex flex-col items-center">
<span
className={`px-3 py-1 rounded-full text-xs font-semibold border-2 ${agentStyle.bgColor} ${agentStyle.textColor} ${agentStyle.borderColor} flex items-center gap-1`}
>
<span>{agentStyle.icon}</span>
<span>{args.agentName}</span>
</span>
{agentStyle.framework && (
<span className="text-[9px] text-gray-500 mt-0.5">
{agentStyle.framework}
</span>
)}
</div>
<span className="text-gray-400 text-sm"></span>
<div className="flex flex-col items-center">
<span className="px-3 py-1 rounded-full text-xs font-semibold bg-gray-700 text-white">
Orchestrator
</span>
<span className="text-[9px] text-gray-500 mt-0.5">ADK</span>
</div>
</div>
<span className="text-xs text-gray-600"> Response received</span>
</div>
</div>
</div>
);
};
@@ -0,0 +1,72 @@
/**
* Displays outgoing A2A messages (Orchestrator → Agent).
* Green box with sender/receiver badges and task description.
*/
import React from "react";
import { getAgentStyle, truncateTask } from "./agent-styles";
type MessageActionRenderProps = {
status: string;
args: {
agentName?: string;
task?: string;
};
};
export const MessageToA2A: React.FC<MessageActionRenderProps> = ({
status,
args,
}) => {
switch (status) {
case "executing":
case "complete":
break;
default:
return null;
}
if (!args.agentName || !args.task) {
return null;
}
const agentStyle = getAgentStyle(args.agentName);
return (
<div className="bg-green-50 border border-green-200 rounded-lg px-4 py-3 my-2 a2a-message-enter">
<div className="flex items-start gap-3">
<div className="flex items-center gap-2 flex-shrink-0">
<div className="flex flex-col items-center">
<span className="px-3 py-1 rounded-full text-xs font-semibold bg-gray-700 text-white">
Orchestrator
</span>
<span className="text-[9px] text-gray-500 mt-0.5">ADK</span>
</div>
<span className="text-gray-400 text-sm"></span>
<div className="flex flex-col items-center">
<span
className={`px-3 py-1 rounded-full text-xs font-semibold border-2 ${agentStyle.bgColor} ${agentStyle.textColor} ${agentStyle.borderColor} flex items-center gap-1`}
>
<span>{agentStyle.icon}</span>
<span>{args.agentName}</span>
</span>
{agentStyle.framework && (
<span className="text-[9px] text-gray-500 mt-0.5">
{agentStyle.framework}
</span>
)}
</div>
</div>
<span
className="text-gray-700 text-sm flex-1 min-w-0 break-words"
title={args.task}
>
{truncateTask(args.task)}
</span>
</div>
</div>
);
};
@@ -0,0 +1,61 @@
/**
* Agent styling utilities for consistent badge appearance.
* LangGraph agents use green, ADK agents use blue.
*/
export type AgentStyle = {
bgColor: string;
textColor: string;
borderColor: string;
icon: string;
framework?: string;
};
export function getAgentStyle(agentName: string): AgentStyle {
if (!agentName) {
return {
bgColor: "bg-gray-100",
textColor: "text-gray-700",
borderColor: "border-gray-300",
icon: "🤖",
framework: "",
};
}
const nameLower = agentName.toLowerCase();
// LangGraph agents (green)
if (nameLower.includes("research")) {
return {
bgColor: "bg-gradient-to-r from-emerald-100 to-green-100",
textColor: "text-emerald-800",
borderColor: "border-emerald-400",
icon: "🔗",
framework: "LangGraph",
};
}
// ADK agents (blue)
if (nameLower.includes("analysis")) {
return {
bgColor: "bg-gradient-to-r from-blue-100 to-sky-100",
textColor: "text-blue-800",
borderColor: "border-blue-400",
icon: "✨",
framework: "ADK",
};
}
return {
bgColor: "bg-gray-100",
textColor: "text-gray-700",
borderColor: "border-gray-300",
icon: "🤖",
framework: "",
};
}
export function truncateTask(text: string, maxLength: number = 50): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + "...";
}
@@ -0,0 +1,116 @@
"use client";
/**
* Chat Component - Main interface with A2A message visualization.
* Extracts structured data from agents and passes to parent for display.
*/
import React, { useEffect } from "react";
import {
useAgent,
useFrontendTool,
CopilotChat,
} from "@copilotkit/react-core/v2";
import { z } from "zod";
import { MessageToA2A } from "./a2a/MessageToA2A";
import { MessageFromA2A } from "./a2a/MessageFromA2A";
type ResearchData = {
topic: string;
summary: string;
findings: Array<{ title: string; description: string }>;
sources: string;
};
type AnalysisData = {
topic: string;
overview: string;
insights: Array<{ title: string; description: string; importance: string }>;
conclusion: string;
};
type ChatProps = {
onResearchUpdate: (data: ResearchData | null) => void;
onAnalysisUpdate: (data: AnalysisData | null) => void;
};
export default function Chat({
onResearchUpdate,
onAnalysisUpdate,
}: ChatProps) {
const { agent } = useAgent({ agentId: "a2a_chat" });
// Extract structured JSON from A2A agent responses and pass to parent
useEffect(() => {
const extractDataFromMessages = () => {
for (const message of agent.messages) {
const msg = message as any;
if (msg.role === "tool" && typeof msg.content !== "undefined") {
try {
const result = msg.content;
let parsed;
if (typeof result === "string") {
let cleanResult = result;
if (result.startsWith("A2A Agent Response: ")) {
cleanResult = result.slice("A2A Agent Response: ".length);
}
try {
parsed = JSON.parse(cleanResult);
} catch {
continue;
}
} else if (typeof result === "object") {
parsed = result;
} else {
continue;
}
if (parsed.findings && Array.isArray(parsed.findings)) {
onResearchUpdate(parsed as ResearchData);
} else if (parsed.insights && Array.isArray(parsed.insights)) {
onAnalysisUpdate(parsed as AnalysisData);
}
} catch (e) {
console.error("Failed to extract data from message:", e);
}
}
}
};
extractDataFromMessages();
}, [agent.messages, onResearchUpdate, onAnalysisUpdate]);
// Register action to render A2A message flow visualization
useFrontendTool({
name: "send_message_to_a2a_agent",
description: "Sends a message to an A2A agent",
available: true,
parameters: z.object({
agentName: z
.string()
.describe("The name of the A2A agent to send the message to"),
task: z.string().describe("The message to send to the A2A agent"),
}),
render: (actionRenderProps) => {
return (
<>
<MessageToA2A {...actionRenderProps} />
<MessageFromA2A {...actionRenderProps} />
</>
);
},
});
return (
<CopilotChat
labels={{
modalHeaderTitle: "Research Assistant",
welcomeMessageText:
'👋 Hi! I\'m your research assistant. I can help you research any topic.\n\nFor example, try:\n- "Research quantum computing"\n- "Tell me about artificial intelligence"\n- "Research renewable energy"\n\nI\'ll coordinate with specialized agents to gather information and provide insights!',
}}
className="h-full"
/>
);
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cdf40dca06f22c293a927fa7b70474382438692ec63aa338ab43e613a1a1bcf2
size 1570126
@@ -0,0 +1,13 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
serverExternalPackages: ["@copilotkit/runtime"],
env: {
NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED: process.env.COPILOTKIT_LICENSE_TOKEN
? "true"
: "false",
},
};
export default nextConfig;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,46 @@
{
"name": "a2a-agui-starter",
"version": "0.1.0",
"private": true,
"description": "Minimal starter template for building multi-agent applications with A2A Protocol and AG-UI",
"scripts": {
"dev": "concurrently --names \"UI,Orch,Research,Analysis\" --prefix-colors \"cyan,gray,green,blue\" \"npm run dev:ui\" \"npm run dev:orchestrator\" \"npm run dev:research\" \"npm run dev:analysis\"",
"dev:ui": "next dev --turbopack",
"dev:orchestrator": "agents/.venv/bin/python agents/orchestrator.py",
"dev:research": "agents/.venv/bin/python agents/research_agent.py",
"dev:analysis": "agents/.venv/bin/python agents/analysis_agent.py",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@a2a-js/sdk": "latest",
"@ag-ui/a2a-middleware": "0.0.2",
"@ag-ui/client": "0.0.57",
"@ag-ui/core": "0.0.57",
"@copilotkit/react-core": "1.62.3",
"@copilotkit/runtime": "1.62.3",
"hono": "^4",
"lucide-react": "^0.577.0",
"next": "15.5.15",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zod": "^3.24.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.13",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"concurrently": "^9.1.2",
"postcss": "^8",
"tailwindcss": "^4.1.13",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5"
},
"overrides": {
"@ag-ui/client": "0.0.57",
"@ag-ui/core": "0.0.57"
}
}
@@ -0,0 +1,6 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
@@ -0,0 +1,81 @@
import type { Config } from "tailwindcss";
const config = {
darkMode: "class",
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
fontFamily: {
sans: ["Plus Jakarta Sans", "ui-sans-serif", "system-ui", "sans-serif"],
mono: [
"Spline Sans Mono",
"ui-monospace",
"SFMono-Regular",
"monospace",
],
},
boxShadow: {
"elevation-sm": "var(--shadow-sm)",
"elevation-md": "var(--shadow-md)",
"elevation-lg": "var(--shadow-lg)",
"elevation-xl": "var(--shadow-xl)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
} satisfies Config;
export default config;
@@ -0,0 +1,33 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
},
"target": "ES2017"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}