chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user