Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files
config.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
|
| 4 |
+
# Load environment variables from .env file
|
| 5 |
+
load_dotenv()
|
| 6 |
+
|
| 7 |
+
# Load API Keys from environment or set them directly here
|
| 8 |
+
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")
|
| 9 |
+
TAVILY_API_KEY = os.getenv("TAVILY_API_KEY", "")
|
| 10 |
+
|
| 11 |
+
# Default System Configuration
|
| 12 |
+
DEFAULT_POLICY_TOPIC = "Implementation of Universal Basic Income (UBI) in developing economies"
|
| 13 |
+
OUTPUT_FILENAME = "policy_report.md"
|
| 14 |
+
|
| 15 |
+
if not GOOGLE_API_KEY or not TAVILY_API_KEY:
|
| 16 |
+
print("WARNING: API Keys are missing. Please set GOOGLE_API_KEY and TAVILY_API_KEY.")
|
main.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import config
|
| 3 |
+
import tools
|
| 4 |
+
import os
|
| 5 |
+
import logging
|
| 6 |
+
import uuid # CRITICAL: For unique session IDs per run
|
| 7 |
+
import asyncio
|
| 8 |
+
from typing import List, Any, Dict, Tuple, Optional
|
| 9 |
+
|
| 10 |
+
# ADK Imports
|
| 11 |
+
from google.adk.agents import LlmAgent
|
| 12 |
+
from google.adk.models.google_llm import Gemini
|
| 13 |
+
from google.adk.runners import Runner
|
| 14 |
+
from google.adk.sessions import InMemorySessionService
|
| 15 |
+
from google.adk.tools import google_search
|
| 16 |
+
from google.genai import types
|
| 17 |
+
|
| 18 |
+
# Configure Logging
|
| 19 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
# Global Session Service (Instantiated once to mimic a real DB connection)
|
| 23 |
+
# We will use unique IDs to keep runs separate.
|
| 24 |
+
session_service = InMemorySessionService()
|
| 25 |
+
|
| 26 |
+
# --- HELPER: Robust Agent Runner ---
|
| 27 |
+
async def run_agent_step(
|
| 28 |
+
agent_name: str,
|
| 29 |
+
agent: LlmAgent,
|
| 30 |
+
prompt: str,
|
| 31 |
+
run_id: str
|
| 32 |
+
) -> Tuple[str, str]:
|
| 33 |
+
"""
|
| 34 |
+
Executes a single agent step within a specific run context.
|
| 35 |
+
"""
|
| 36 |
+
logger.info(f"Starting Agent: {agent_name} [RunID: {run_id}]")
|
| 37 |
+
|
| 38 |
+
# 1. Fix App Name Mismatch: ADK defaults to "agents" for dynamic agents
|
| 39 |
+
APP_NAME = "agents"
|
| 40 |
+
|
| 41 |
+
# 2. Unique Session ID: Prevents history from previous button clicks from leaking in
|
| 42 |
+
session_id = f"sess_{agent_name.lower()}_{run_id}"
|
| 43 |
+
|
| 44 |
+
# 3. Initialize Runner
|
| 45 |
+
runner = Runner(agent=agent, app_name=APP_NAME, session_service=session_service)
|
| 46 |
+
|
| 47 |
+
# 4. Ensure Session Exists
|
| 48 |
+
try:
|
| 49 |
+
# We create a fresh session for this specific agent step to ensure clean context window
|
| 50 |
+
await session_service.create_session(app_name=APP_NAME, user_id="user", session_id=session_id)
|
| 51 |
+
except Exception:
|
| 52 |
+
# If session exists (rare with UUID), we just continue
|
| 53 |
+
pass
|
| 54 |
+
|
| 55 |
+
msg = types.Content(role="user", parts=[types.Part(text=prompt)])
|
| 56 |
+
final_text = ""
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
async for event in runner.run_async(user_id="user", session_id=session_id, new_message=msg):
|
| 60 |
+
if hasattr(event, 'function_call') and event.function_call:
|
| 61 |
+
logger.info(f"[{agent_name}] Tool Call: {event.function_call.name}")
|
| 62 |
+
|
| 63 |
+
if event.is_final_response() and event.content and event.content.parts:
|
| 64 |
+
text_parts = [p.text for p in event.content.parts if hasattr(p, 'text') and p.text]
|
| 65 |
+
final_text = '\n'.join(text_parts)
|
| 66 |
+
break
|
| 67 |
+
|
| 68 |
+
except Exception as e:
|
| 69 |
+
error_msg = f"Execution Error in {agent_name}: {str(e)}"
|
| 70 |
+
logger.error(error_msg)
|
| 71 |
+
return "", error_msg
|
| 72 |
+
|
| 73 |
+
if not final_text.strip():
|
| 74 |
+
return "", f"β οΈ {agent_name} returned empty response."
|
| 75 |
+
|
| 76 |
+
return final_text, f"β
{agent_name} Completed."
|
| 77 |
+
|
| 78 |
+
# --- CORE LOGIC ---
|
| 79 |
+
async def run_policy_analysis(topic, google_key, tavily_key):
|
| 80 |
+
# 1. Generate Run ID (Fixes Workflow Problem)
|
| 81 |
+
run_id = str(uuid.uuid4())[:8]
|
| 82 |
+
logger.info(f"--- Starting Analysis Run: {run_id} ---")
|
| 83 |
+
|
| 84 |
+
# 2. Authentication
|
| 85 |
+
active_google_key = google_key or getattr(config, 'GOOGLE_API_KEY', '')
|
| 86 |
+
active_tavily_key = tavily_key or getattr(config, 'TAVILY_API_KEY', '')
|
| 87 |
+
|
| 88 |
+
if not active_google_key or not active_tavily_key:
|
| 89 |
+
err = "β Authentication Error: Missing API Keys."
|
| 90 |
+
yield err, err, err, err
|
| 91 |
+
return
|
| 92 |
+
|
| 93 |
+
os.environ["GOOGLE_API_KEY"] = active_google_key
|
| 94 |
+
|
| 95 |
+
# 3. Model & Tool Init
|
| 96 |
+
try:
|
| 97 |
+
# ADK Recommended: specific safety settings for robustness
|
| 98 |
+
safety_settings = [
|
| 99 |
+
types.SafetySetting(
|
| 100 |
+
category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
| 101 |
+
threshold=types.HarmBlockThreshold.BLOCK_ONLY_HIGH
|
| 102 |
+
),
|
| 103 |
+
types.SafetySetting(
|
| 104 |
+
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
| 105 |
+
threshold=types.HarmBlockThreshold.BLOCK_ONLY_HIGH
|
| 106 |
+
)
|
| 107 |
+
]
|
| 108 |
+
|
| 109 |
+
model = Gemini(
|
| 110 |
+
model="gemini-2.5-flash-lite",
|
| 111 |
+
safety_settings=safety_settings
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
tavily_tool = tools.get_tavily_search_tool(api_key=active_tavily_key)
|
| 115 |
+
google_tool = google_search
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
yield f"Setup Failed: {e}", "", "", ""
|
| 119 |
+
return
|
| 120 |
+
|
| 121 |
+
# 4. Pipeline Execution (Sequential Workflow)
|
| 122 |
+
state = {"analysis": "", "critique": "", "lobbyist": "", "summary": ""}
|
| 123 |
+
def get_ui(): return state["analysis"], state["critique"], state["lobbyist"], state["summary"]
|
| 124 |
+
|
| 125 |
+
# --- ANALYST ---
|
| 126 |
+
yield "π Analyst: Researching...", "", "", ""
|
| 127 |
+
|
| 128 |
+
analyst_agent = LlmAgent(
|
| 129 |
+
name="Analyst", model=model, tools=[tavily_tool],
|
| 130 |
+
instruction="Analyze impact on: Rural, Urban, Working Class, Farmers, Women, Youth, Mfg/Services. "
|
| 131 |
+
"Cite 1 data point per sector." #"You are a Data-Driven Policy Analyst. Cite specific data points using search tools."
|
| 132 |
+
)
|
| 133 |
+
res, log = await run_agent_step("Analyst", analyst_agent, f"Analyze topic: {topic}", run_id)
|
| 134 |
+
if "Error" in log: state["analysis"] = log; yield get_ui(); return
|
| 135 |
+
state["analysis"] = res; yield get_ui()
|
| 136 |
+
|
| 137 |
+
# --- CRITIC ---
|
| 138 |
+
state["critique"] = "βοΈ Critic: Reviewing..."
|
| 139 |
+
yield get_ui()
|
| 140 |
+
|
| 141 |
+
critic_agent = LlmAgent(
|
| 142 |
+
name="Critic", model=model, tools=[tavily_tool],
|
| 143 |
+
instruction="You are a Policy Critic. Find flaws, costs, and missing demographics in the analysis and cite 2 failed examples seperately."
|
| 144 |
+
)
|
| 145 |
+
res, log = await run_agent_step("Critic", critic_agent, f"Critique this analysis:\n{state['analysis']}", run_id)
|
| 146 |
+
if "Error" in log: state["critique"] = log; yield get_ui(); return
|
| 147 |
+
state["critique"] = res; yield get_ui()
|
| 148 |
+
|
| 149 |
+
# --- LOBBYIST ---
|
| 150 |
+
state["lobbyist"] = "π€ Lobbyist: Strategizing..."
|
| 151 |
+
yield get_ui()
|
| 152 |
+
|
| 153 |
+
lobbyist_agent = LlmAgent(
|
| 154 |
+
name="Lobbyist", model=model, tools=[google_tool],
|
| 155 |
+
instruction="You are a Strategist. Propose 3 directives based on the analysis and critique. Use Google Search for news."
|
| 156 |
+
)
|
| 157 |
+
prompt = f"Analysis: {state['analysis']}\nCritique: {state['critique']}\nTask: Propose 3 directives."
|
| 158 |
+
res, log = await run_agent_step("Lobbyist", lobbyist_agent, prompt, run_id)
|
| 159 |
+
if "Error" in log: state["lobbyist"] = log; yield get_ui(); return
|
| 160 |
+
state["lobbyist"] = res; yield get_ui()
|
| 161 |
+
|
| 162 |
+
# --- SYNTHESIZER ---
|
| 163 |
+
state["summary"] = "π Synthesizer: Writing Report..."
|
| 164 |
+
yield get_ui()
|
| 165 |
+
|
| 166 |
+
summary_agent = LlmAgent(
|
| 167 |
+
name="Synthesizer", model=model, tools=[], # No tools needed for summary
|
| 168 |
+
instruction="Create a 400-word Executive Summary. Verdict(Pass/Reject with why?), Data, Risks, Roadmap."
|
| 169 |
+
)
|
| 170 |
+
prompt = f"Context:\n{state['analysis']}\n{state['critique']}\n{state['lobbyist']}\nSummarize."
|
| 171 |
+
res, log = await run_agent_step("Synthesizer", summary_agent, prompt, run_id)
|
| 172 |
+
state["summary"] = res; yield get_ui()
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def generate_markdown_report(topic, analysis, critique, lobbyist, summary):
|
| 176 |
+
import datetime, tempfile
|
| 177 |
+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 178 |
+
md_content = f"# Policy Report: {topic}\n**Date**: {timestamp}\n\n## π Analysis\n{analysis}\n\n## βοΈ Critique\n{critique}\n\n## π’ Directives\n{lobbyist}\n\n## π Summary\n{summary}"
|
| 179 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as tmp:
|
| 180 |
+
tmp.write(md_content)
|
| 181 |
+
return tmp.name
|
| 182 |
+
|
| 183 |
+
# --- UI ---
|
| 184 |
+
with gr.Blocks(title="ADK Policy Analyzer", theme=gr.themes.Soft()) as demo:
|
| 185 |
+
gr.Markdown("# ποΈ Data-Driven Policy Analyzer (ADK-Powered)")
|
| 186 |
+
with gr.Row():
|
| 187 |
+
with gr.Column(scale=1):
|
| 188 |
+
topic_input = gr.Textbox(label="Topic", value="Universal Basic Income in India")
|
| 189 |
+
with gr.Accordion("API Keys", open=False):
|
| 190 |
+
google_key_input = gr.Textbox(label="Google API Key", type="password")
|
| 191 |
+
tavily_key_input = gr.Textbox(label="Tavily API Key", type="password")
|
| 192 |
+
analyze_btn = gr.Button("Run", variant="primary")
|
| 193 |
+
with gr.Column(scale=2):
|
| 194 |
+
with gr.Tabs():
|
| 195 |
+
with gr.TabItem("π Analysis"): analysis_out = gr.Markdown()
|
| 196 |
+
with gr.TabItem("βοΈ Critique"): critique_out = gr.Markdown()
|
| 197 |
+
with gr.TabItem("π’ Lobbyist"): lobbyist_out = gr.Markdown()
|
| 198 |
+
with gr.TabItem("π Summary"): summary_out = gr.Markdown()
|
| 199 |
+
|
| 200 |
+
dl_btn = gr.DownloadButton("π₯ Download Report", interactive=False)
|
| 201 |
+
|
| 202 |
+
analyze_btn.click(
|
| 203 |
+
fn=run_policy_analysis,
|
| 204 |
+
inputs=[topic_input, google_key_input, tavily_key_input],
|
| 205 |
+
outputs=[analysis_out, critique_out, lobbyist_out, summary_out]
|
| 206 |
+
).then(
|
| 207 |
+
fn=generate_markdown_report,
|
| 208 |
+
inputs=[topic_input, analysis_out, critique_out, lobbyist_out, summary_out],
|
| 209 |
+
outputs=[dl_btn]
|
| 210 |
+
).then(lambda: gr.DownloadButton(interactive=True), outputs=[dl_btn])
|
| 211 |
+
|
| 212 |
+
if __name__ == "__main__":
|
| 213 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
google-adk>=0.0.1
|
| 2 |
+
google-generativeai>=0.3.0
|
| 3 |
+
tavily-python>=0.3.0
|
| 4 |
+
gradio>=4.0.0
|
| 5 |
+
python-dotenv>=1.0.0
|
tools.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import config
|
| 2 |
+
from tavily import TavilyClient
|
| 3 |
+
from google.adk.tools import FunctionTool
|
| 4 |
+
|
| 5 |
+
def get_tavily_search_tool(api_key=None):
|
| 6 |
+
"""
|
| 7 |
+
Returns a FunctionTool configured with the Tavily API key.
|
| 8 |
+
"""
|
| 9 |
+
tavily_api_key = api_key if api_key else config.TAVILY_API_KEY
|
| 10 |
+
|
| 11 |
+
if not tavily_api_key:
|
| 12 |
+
raise ValueError("Tavily API Key is missing.")
|
| 13 |
+
|
| 14 |
+
tavily_client = TavilyClient(api_key=tavily_api_key)
|
| 15 |
+
|
| 16 |
+
def fetch_policy_data(query: str) -> str:
|
| 17 |
+
"""
|
| 18 |
+
Searches the web for real-time data, statistics, and news about the policy.
|
| 19 |
+
ALWAYS use this to get facts before answering.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
query: The search query string.
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
String containing formatted search results with sources.
|
| 26 |
+
"""
|
| 27 |
+
print(f"\nπ [Tavily Tool] Searching for: '{query}'...")
|
| 28 |
+
try:
|
| 29 |
+
# Use 'advanced' depth for better facts
|
| 30 |
+
response = tavily_client.search(query, search_depth="advanced", max_results=3)
|
| 31 |
+
context = []
|
| 32 |
+
if 'results' in response:
|
| 33 |
+
for result in response['results']:
|
| 34 |
+
context.append(f"Source: {result['title']}\nURL: {result['url']}\nData: {result['content']}")
|
| 35 |
+
|
| 36 |
+
result_text = "\n\n".join(context) if context else "No results found."
|
| 37 |
+
print(f"β
[Tavily Tool] Found {len(context)} results.")
|
| 38 |
+
return result_text
|
| 39 |
+
except Exception as e:
|
| 40 |
+
error_msg = f"Error during search: {str(e)}"
|
| 41 |
+
print(f"β [Tavily Tool] Failed: {error_msg}")
|
| 42 |
+
return error_msg
|
| 43 |
+
|
| 44 |
+
# Return the function wrapped as a tool
|
| 45 |
+
return FunctionTool(fetch_policy_data)
|