Hardman commited on
Commit
3e2678b
Β·
verified Β·
1 Parent(s): 915efcf

Upload 4 files

Browse files
Files changed (4) hide show
  1. config.py +16 -0
  2. main.py +213 -0
  3. requirements.txt +5 -0
  4. tools.py +45 -0
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)