chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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;
|
||||
+12705
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"]
|
||||
}
|
||||
Reference in New Issue
Block a user