Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -15,6 +15,7 @@ from langchain_community.document_loaders import WikipediaLoader
|
|
15 |
from langchain_community.utilities import WikipediaAPIWrapper
|
16 |
from langchain_community.document_loaders import ArxivLoader
|
17 |
|
|
|
18 |
# (Keep Constants as is)
|
19 |
# --- Constants ---
|
20 |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
@@ -22,6 +23,7 @@ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
|
22 |
#Load environment variables
|
23 |
load_dotenv()
|
24 |
|
|
|
25 |
from langgraph.graph import END, StateGraph
|
26 |
from langchain_core.prompts import ChatPromptTemplate
|
27 |
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
|
@@ -142,99 +144,208 @@ class VideoTranscriptionTool(BaseTool):
|
|
142 |
def _arun(self, *args, **kwargs):
|
143 |
raise NotImplementedError("Async not supported for this tool.")
|
144 |
|
145 |
-
|
146 |
|
147 |
-
def indent_code(code: str) -> str:
|
148 |
-
return '\n '.join(code.splitlines())
|
149 |
|
150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
151 |
class AgentState(TypedDict):
|
152 |
question: str
|
153 |
-
history:
|
154 |
-
context: str
|
155 |
reasoning: str
|
156 |
iterations: int
|
|
|
|
|
|
|
157 |
|
158 |
-
#
|
159 |
-
def
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
"
|
166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
167 |
|
|
|
|
|
168 |
|
|
|
|
|
|
|
|
|
169 |
|
170 |
-
def should_continue(state: AgentState) -> str:
|
171 |
-
history = state.get("history", [])
|
172 |
|
173 |
-
|
174 |
-
return "reason" # No history yet, reason first
|
175 |
|
176 |
-
|
|
|
|
|
|
|
|
|
177 |
|
178 |
-
#
|
179 |
-
if isinstance(
|
|
|
180 |
return "end"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
181 |
|
182 |
-
#
|
|
|
|
|
183 |
for msg in reversed(history):
|
184 |
-
if isinstance(msg, dict) and msg.get("
|
185 |
-
|
|
|
186 |
|
187 |
-
#
|
|
|
188 |
return "reason"
|
189 |
|
190 |
|
191 |
-
|
192 |
def reasoning_node(state: AgentState) -> AgentState:
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
|
200 |
# Load API key
|
201 |
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
202 |
if not GOOGLE_API_KEY:
|
203 |
raise ValueError("GOOGLE_API_KEY not set in environment variables.")
|
204 |
|
205 |
-
# Ensure history is well-formed
|
206 |
if "history" not in state or not isinstance(state["history"], list):
|
207 |
state["history"] = []
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
# Ensure context and reasoning fields
|
212 |
state.setdefault("context", {})
|
213 |
state.setdefault("reasoning", "")
|
214 |
state.setdefault("iterations", 0)
|
|
|
|
|
215 |
|
216 |
# Create Gemini model wrapper
|
217 |
llm = ChatGoogleGenerativeAI(
|
218 |
-
model="gemini-1.5-flash",
|
219 |
-
temperature=0.1,
|
220 |
google_api_key=GOOGLE_API_KEY
|
221 |
)
|
222 |
|
223 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
224 |
prompt = ChatPromptTemplate.from_messages([
|
225 |
-
(
|
226 |
-
|
227 |
-
"and provide reasoning. Available tools: duckduckgo_search, wikipedia_search, "
|
228 |
-
"arxiv_search, document_qa, python_execution.\n\n"
|
229 |
-
"Important: You must select a tool for questions involving video, audio, or code.\n\n"
|
230 |
-
"Current Context:\n{context}\n\n"
|
231 |
-
"Reasoning Steps:\n{reasoning}\n\n"
|
232 |
-
"Response Format:\n"
|
233 |
-
"Reasoning: [Your analysis]\n"
|
234 |
-
"Action: [Tool name OR 'Final Answer']\n"
|
235 |
-
"Action Input: [Input for tool OR final response]"
|
236 |
-
)),
|
237 |
-
*state["history"]
|
238 |
])
|
239 |
|
240 |
chain = prompt | llm
|
@@ -243,103 +354,118 @@ def reasoning_node(state: AgentState) -> AgentState:
|
|
243 |
def call_with_retry(inputs, retries=3, delay=60):
|
244 |
for attempt in range(retries):
|
245 |
try:
|
246 |
-
|
|
|
|
|
|
|
247 |
except ResourceExhausted as e:
|
248 |
-
print(f"[Retry {attempt+1}] Gemini rate limit hit. Waiting {delay}s...")
|
249 |
time.sleep(delay)
|
250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
251 |
|
252 |
# Call model with retry protection
|
253 |
response = call_with_retry({
|
254 |
"context": state["context"],
|
255 |
"reasoning": state["reasoning"],
|
256 |
-
"question": state["question"]
|
|
|
|
|
257 |
})
|
258 |
|
259 |
-
# Parse output
|
260 |
content = response.content
|
261 |
reasoning, action, action_input = parse_agent_response(content)
|
|
|
|
|
|
|
262 |
|
263 |
# Update state
|
264 |
-
state["history"].append(AIMessage(content=content))
|
265 |
state["reasoning"] += f"\nStep {state['iterations'] + 1}: {reasoning}"
|
266 |
state["iterations"] += 1
|
|
|
267 |
|
268 |
if "final answer" in action.lower():
|
269 |
state["history"].append(AIMessage(content=f"FINAL ANSWER: {action_input}"))
|
|
|
270 |
else:
|
271 |
-
|
|
|
|
|
272 |
"tool": action,
|
273 |
"input": action_input
|
274 |
-
}
|
275 |
|
|
|
276 |
return state
|
277 |
|
278 |
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
def tool_node(state: AgentState) -> AgentState:
|
284 |
-
|
|
|
|
|
|
|
285 |
|
286 |
# Ensure history exists
|
287 |
if "history" not in state or not isinstance(state["history"], list):
|
288 |
raise ValueError("Invalid or missing history in state")
|
289 |
|
290 |
# Find the most recent action request in history
|
291 |
-
|
292 |
for msg in reversed(state["history"]):
|
293 |
-
if isinstance(msg, dict) and msg.get("
|
294 |
-
|
295 |
break
|
296 |
|
297 |
-
if not
|
298 |
-
|
|
|
|
|
299 |
|
300 |
-
tool_name =
|
301 |
-
tool_input =
|
302 |
|
303 |
# Defensive check for missing tool or input
|
304 |
if not tool_name or tool_input is None:
|
305 |
-
raise ValueError("Tool name or input missing from action request")
|
306 |
|
307 |
-
# Look up and invoke the tool
|
308 |
-
|
309 |
-
tool_fn = next((t for t in
|
310 |
|
311 |
if tool_fn is None:
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
318 |
|
319 |
# Add output to history as an AIMessage
|
|
|
|
|
|
|
320 |
state["history"].append(AIMessage(content=f"[{tool_name} output]\n{tool_output}"))
|
321 |
-
|
|
|
322 |
return state
|
323 |
|
324 |
|
325 |
-
|
326 |
-
def parse_agent_response(response: str) -> tuple:
|
327 |
-
"""Extract reasoning, action, and input from response"""
|
328 |
-
reasoning = response.split("Reasoning:")[1].split("Action:")[0].strip()
|
329 |
-
action_part = response.split("Action:")[1].strip()
|
330 |
-
|
331 |
-
if "Action Input:" in action_part:
|
332 |
-
action, action_input = action_part.split("Action Input:", 1)
|
333 |
-
action = action.strip()
|
334 |
-
action_input = action_input.strip()
|
335 |
-
else:
|
336 |
-
action = action_part
|
337 |
-
action_input = ""
|
338 |
-
|
339 |
-
return reasoning, action, action_input
|
340 |
-
|
341 |
# ====== Agent Graph ======
|
342 |
-
def create_agent_workflow():
|
343 |
workflow = StateGraph(AgentState)
|
344 |
|
345 |
# Define nodes
|
@@ -354,58 +480,72 @@ def create_agent_workflow():
|
|
354 |
"reason",
|
355 |
should_continue,
|
356 |
{
|
357 |
-
"
|
358 |
-
"reason": "reason",
|
359 |
-
"end": END
|
360 |
}
|
361 |
)
|
362 |
|
363 |
-
workflow.add_edge("action", "reason")
|
364 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
365 |
|
366 |
# ====== Agent Interface ======
|
367 |
class BasicAgent:
|
368 |
def __init__(self):
|
369 |
-
|
|
|
370 |
self.tools = [
|
371 |
-
duckduckgo_search,
|
372 |
-
wikipedia_search,
|
373 |
-
arxiv_search,
|
374 |
-
document_qa,
|
375 |
-
python_execution,
|
376 |
-
VideoTranscriptionTool()
|
377 |
]
|
|
|
378 |
|
379 |
def __call__(self, question: str) -> str:
|
380 |
-
print(f"Agent received question: {question[:50]}{'...' if len(question) > 50 else ''}")
|
381 |
|
382 |
-
# Initialize state with proper structure
|
383 |
state = {
|
384 |
"question": question,
|
385 |
-
"context": {},
|
386 |
"reasoning": "",
|
387 |
"iterations": 0,
|
388 |
-
"history": [HumanMessage(content=question)]
|
|
|
|
|
|
|
|
|
389 |
}
|
390 |
|
|
|
391 |
final_state = self.workflow.invoke(state)
|
392 |
|
393 |
-
print(f"Final state keys: {list(final_state.keys())}")
|
394 |
-
if 'history' in final_state:
|
395 |
-
print(f"History length: {len(final_state['history'])}")
|
396 |
-
for i, msg in enumerate(final_state['history']):
|
397 |
-
if isinstance(msg, dict):
|
398 |
-
print(f"Message {i}: dict - {msg}")
|
399 |
-
else:
|
400 |
-
print(f"Message {i}: {type(msg).__name__} - {msg.content[:100]}...")
|
401 |
-
|
402 |
# Extract the FINAL ANSWER from history
|
|
|
|
|
|
|
|
|
|
|
|
|
403 |
for msg in reversed(final_state["history"]):
|
404 |
if isinstance(msg, AIMessage) and "FINAL ANSWER:" in msg.content:
|
405 |
answer = msg.content.split("FINAL ANSWER:")[1].strip()
|
406 |
-
print(f"Agent returning
|
407 |
return answer
|
408 |
|
|
|
409 |
raise ValueError("No FINAL ANSWER found in agent history.")
|
410 |
|
411 |
|
|
|
15 |
from langchain_community.utilities import WikipediaAPIWrapper
|
16 |
from langchain_community.document_loaders import ArxivLoader
|
17 |
|
18 |
+
|
19 |
# (Keep Constants as is)
|
20 |
# --- Constants ---
|
21 |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
|
|
23 |
#Load environment variables
|
24 |
load_dotenv()
|
25 |
|
26 |
+
|
27 |
from langgraph.graph import END, StateGraph
|
28 |
from langchain_core.prompts import ChatPromptTemplate
|
29 |
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
|
|
|
144 |
def _arun(self, *args, **kwargs):
|
145 |
raise NotImplementedError("Async not supported for this tool.")
|
146 |
|
|
|
147 |
|
|
|
|
|
148 |
|
149 |
+
|
150 |
+
|
151 |
+
import os
|
152 |
+
import time
|
153 |
+
import json
|
154 |
+
from typing import TypedDict, List, Union, Any, Dict
|
155 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
156 |
+
from langchain.schema import HumanMessage, AIMessage, SystemMessage
|
157 |
+
from langchain.prompts import ChatPromptTemplate
|
158 |
+
from langgraph.graph import StateGraph, END
|
159 |
+
from google.api_core.exceptions import ResourceExhausted
|
160 |
+
|
161 |
+
# Assume these tools are defined elsewhere and imported
|
162 |
+
# Placeholder for your actual tool implementations
|
163 |
+
# For example:
|
164 |
+
# from your_tools_module import duckduckgo_search, wikipedia_search, arxiv_search, document_qa, python_execution
|
165 |
+
# And ensure you have a proper VideoTranscriptionTool
|
166 |
+
def duckduckgo_search(query: str) -> str:
|
167 |
+
"""Performs a DuckDuckGo search for current events or general facts."""
|
168 |
+
# Placeholder for actual implementation
|
169 |
+
print(f"DEBUG: duckduckgo_search called with: {query}")
|
170 |
+
return f"Search result for '{query}': Example relevant information from web."
|
171 |
+
|
172 |
+
def wikipedia_search(query: str) -> str:
|
173 |
+
"""Searches Wikipedia for encyclopedic information."""
|
174 |
+
# Placeholder for actual implementation
|
175 |
+
print(f"DEBUG: wikipedia_search called with: {query}")
|
176 |
+
return f"Wikipedia result for '{query}': Found detailed article."
|
177 |
+
|
178 |
+
def arxiv_search(query: str) -> str:
|
179 |
+
"""Searches ArXiv for scientific preprints and papers."""
|
180 |
+
# Placeholder for actual implementation
|
181 |
+
print(f"DEBUG: arxiv_search called with: {query}")
|
182 |
+
return f"ArXiv result for '{query}': Found relevant research paper."
|
183 |
+
|
184 |
+
def document_qa(document_path: str, question: str) -> str:
|
185 |
+
"""Answers questions based on the content of a given document file (PDF, DOCX, TXT)."""
|
186 |
+
# Placeholder for actual implementation
|
187 |
+
print(f"DEBUG: document_qa called with: {document_path}, question: {question}")
|
188 |
+
return f"Document QA result for '{question}': Answer extracted from document."
|
189 |
+
|
190 |
+
def python_execution(code: str) -> str:
|
191 |
+
"""Executes Python code in a sandboxed environment for calculations or data manipulation."""
|
192 |
+
# Placeholder for actual implementation - IMPORTANT: Implement this securely!
|
193 |
+
# Example (UNSAFE for real use without proper sandboxing):
|
194 |
+
try:
|
195 |
+
exec_globals = {}
|
196 |
+
exec_locals = {}
|
197 |
+
exec(code, exec_globals, exec_locals)
|
198 |
+
return str(exec_locals.get('result', 'Code executed, no explicit result assigned to "result" variable.'))
|
199 |
+
except Exception as e:
|
200 |
+
return f"Python execution error: {str(e)}"
|
201 |
+
|
202 |
+
class VideoTranscriptionTool:
|
203 |
+
"""Transcribes and analyzes video content from a URL or ID."""
|
204 |
+
def __call__(self, video_id_or_url: str) -> str:
|
205 |
+
# Placeholder for actual implementation using youtube-transcript-api etc.
|
206 |
+
print(f"DEBUG: VideoTranscriptionTool called with: {video_id_or_url}")
|
207 |
+
return f"Video transcription/analysis result for '{video_id_or_url}': Summary of video content."
|
208 |
+
|
209 |
+
|
210 |
+
# --- Agent State Definition ---
|
211 |
class AgentState(TypedDict):
|
212 |
question: str
|
213 |
+
history: List[Union[HumanMessage, AIMessage, Dict[str, Any]]] # Allows for tool calls as dicts
|
214 |
+
context: Dict[str, Any]
|
215 |
reasoning: str
|
216 |
iterations: int
|
217 |
+
final_answer: Union[str, float, int, None]
|
218 |
+
current_task: str # Added for more focused reasoning
|
219 |
+
current_thoughts: str # Added for more focused reasoning
|
220 |
|
221 |
+
# --- Utility Functions ---
|
222 |
+
def parse_agent_response(response_content: str) -> tuple[str, str, str]:
|
223 |
+
"""
|
224 |
+
Parses the LLM's JSON output for reasoning, action, and action input.
|
225 |
+
"""
|
226 |
+
try:
|
227 |
+
response_json = json.loads(response_content)
|
228 |
+
reasoning = response_json.get("Reasoning", "").strip()
|
229 |
+
action = response_json.get("Action", "").strip()
|
230 |
+
action_input = response_json.get("Action Input", "").strip()
|
231 |
+
return reasoning, action, action_input
|
232 |
+
except json.JSONDecodeError:
|
233 |
+
# Fallback for when LLM doesn't return perfect JSON (less likely with good prompt)
|
234 |
+
print(f"WARNING: LLM response not perfectly JSON: {response_content[:200]}...")
|
235 |
+
# Attempt heuristic parsing as a last resort
|
236 |
+
reasoning_match = response_content.split("Reasoning:", 1)
|
237 |
+
reasoning = reasoning_match[1].split("Action:", 1)[0].strip() if len(reasoning_match) > 1 else ""
|
238 |
|
239 |
+
action_part_match = response_content.split("Action:", 1)
|
240 |
+
action_part = action_part_match[1].strip() if len(action_part_match) > 1 else ""
|
241 |
|
242 |
+
action_input_match = action_part.split("Action Input:", 1)
|
243 |
+
action = action_input_match[0].strip()
|
244 |
+
action_input = action_input_match[1].strip() if len(action_input_match) > 1 else ""
|
245 |
+
return reasoning, action, action_input
|
246 |
|
|
|
|
|
247 |
|
248 |
+
# --- Graph Nodes ---
|
|
|
249 |
|
250 |
+
def should_continue(state: AgentState) -> str:
|
251 |
+
"""
|
252 |
+
Determines if the agent should continue reasoning, use a tool, or end.
|
253 |
+
"""
|
254 |
+
history = state.get("history", [])
|
255 |
|
256 |
+
# Check for final answer in the last AIMessage
|
257 |
+
if history and isinstance(history[-1], AIMessage) and "FINAL ANSWER:" in history[-1].content:
|
258 |
+
print("DEBUG: should_continue -> END (Final Answer detected)")
|
259 |
return "end"
|
260 |
+
|
261 |
+
# Check if a tool was just executed (its output is in history)
|
262 |
+
# and the next step should be reasoning over that output
|
263 |
+
for msg in reversed(history):
|
264 |
+
if isinstance(msg, AIMessage) and any(f"[{tool.name} output]" in msg.content for tool in state.get("tools", [])):
|
265 |
+
print("DEBUG: should_continue -> REASON (Tool output detected, need to process)")
|
266 |
+
return "reason"
|
267 |
|
268 |
+
# Check if there's an action request to be executed
|
269 |
+
# This happens *after* reasoning has determined a tool is needed,
|
270 |
+
# but *before* the tool has run.
|
271 |
for msg in reversed(history):
|
272 |
+
if isinstance(msg, dict) and msg.get("type") == "action_request":
|
273 |
+
print("DEBUG: should_continue -> ACTION (Action request pending)")
|
274 |
+
return "action"
|
275 |
|
276 |
+
# If nothing else, assume we need to reason
|
277 |
+
print("DEBUG: should_continue -> REASON (Default to reasoning)")
|
278 |
return "reason"
|
279 |
|
280 |
|
|
|
281 |
def reasoning_node(state: AgentState) -> AgentState:
|
282 |
+
"""
|
283 |
+
Node for the agent to analyze the question, determine next steps,
|
284 |
+
and select tools.
|
285 |
+
"""
|
286 |
+
print(f"DEBUG: Entering reasoning_node. Iteration: {state['iterations']}")
|
287 |
+
print(f"DEBUG: Current history length: {len(state.get('history', []))}")
|
288 |
|
289 |
# Load API key
|
290 |
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
291 |
if not GOOGLE_API_KEY:
|
292 |
raise ValueError("GOOGLE_API_KEY not set in environment variables.")
|
293 |
|
294 |
+
# Ensure history is well-formed for the LLM prompt
|
295 |
if "history" not in state or not isinstance(state["history"], list):
|
296 |
state["history"] = []
|
297 |
+
|
298 |
+
# Initialize/update state fields
|
|
|
|
|
299 |
state.setdefault("context", {})
|
300 |
state.setdefault("reasoning", "")
|
301 |
state.setdefault("iterations", 0)
|
302 |
+
state.setdefault("current_task", "Understand the question and plan the next step.")
|
303 |
+
state.setdefault("current_thoughts", "")
|
304 |
|
305 |
# Create Gemini model wrapper
|
306 |
llm = ChatGoogleGenerativeAI(
|
307 |
+
model="gemini-1.5-flash", # Use a fast model for agentic loops
|
308 |
+
temperature=0.1, # Keep it low for more deterministic reasoning
|
309 |
google_api_key=GOOGLE_API_KEY
|
310 |
)
|
311 |
|
312 |
+
# Dynamically generate tool descriptions for the prompt
|
313 |
+
tool_descriptions = "\n".join([
|
314 |
+
f"- **{t.name}**: {t.description}" for t in state.get("tools", [])
|
315 |
+
])
|
316 |
+
|
317 |
+
# Craft a more robust and explicit system prompt
|
318 |
+
system_prompt = (
|
319 |
+
"You are an expert problem solver, designed to provide concise and accurate answers. "
|
320 |
+
"Your process involves analyzing the question, intelligently selecting and using tools, "
|
321 |
+
"and synthesizing information.\n\n"
|
322 |
+
"**Available Tools:**\n"
|
323 |
+
f"{tool_descriptions}\n\n"
|
324 |
+
"**Tool Usage Guidelines:**\n"
|
325 |
+
"- Use **duckduckgo_search** for current events, general facts, or quick lookups.\n"
|
326 |
+
"- Use **wikipedia_search** for encyclopedic information, historical context, or detailed topics.\n"
|
327 |
+
"- Use **arxiv_search** for scientific papers, research, or cutting-edge technical information.\n"
|
328 |
+
"- Use **document_qa** when the question explicitly refers to a specific document file (e.g., 'Analyze this PDF').\n"
|
329 |
+
"- Use **python_execution** for complex calculations, data manipulation, or logical operations that cannot be done with simple reasoning. Always provide the full Python code.\n"
|
330 |
+
"- Use **VideoTranscriptionTool** for any question involving video or audio content.\n\n"
|
331 |
+
"**Current Context:**\n{context}\n\n"
|
332 |
+
"**Previous Reasoning Steps:**\n{reasoning}\n\n"
|
333 |
+
"**Current Task:** {current_task}\n"
|
334 |
+
"**Current Thoughts:** {current_thoughts}\n\n"
|
335 |
+
"**Your Response MUST be a valid JSON object with the following keys:**\n"
|
336 |
+
"```json\n"
|
337 |
+
"{\n"
|
338 |
+
" \"Reasoning\": \"Your detailed analysis of the question and why you chose a specific action.\",\n"
|
339 |
+
" \"Action\": \"[Tool name OR 'Final Answer']\",\n"
|
340 |
+
" \"Action Input\": \"[Input for the selected tool OR the final response]\"\n"
|
341 |
+
"}\n"
|
342 |
+
"```\n"
|
343 |
+
"Ensure 'Action Input' is appropriate for the chosen 'Action'. If 'Action' is 'Final Answer', provide the complete, concise answer."
|
344 |
+
)
|
345 |
+
|
346 |
prompt = ChatPromptTemplate.from_messages([
|
347 |
+
SystemMessage(content=system_prompt),
|
348 |
+
*state["history"] # Include full history for conversational context
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
349 |
])
|
350 |
|
351 |
chain = prompt | llm
|
|
|
354 |
def call_with_retry(inputs, retries=3, delay=60):
|
355 |
for attempt in range(retries):
|
356 |
try:
|
357 |
+
response = chain.invoke(inputs)
|
358 |
+
# Attempt to parse immediately to catch bad JSON before returning
|
359 |
+
parse_agent_response(response.content)
|
360 |
+
return response
|
361 |
except ResourceExhausted as e:
|
362 |
+
print(f"[Retry {attempt+1}/{retries}] Gemini rate limit hit. Waiting {delay}s...")
|
363 |
time.sleep(delay)
|
364 |
+
except json.JSONDecodeError as e:
|
365 |
+
print(f"[Retry {attempt+1}/{retries}] LLM returned invalid JSON. Retrying...")
|
366 |
+
print(f"Invalid JSON content: {response.content[:200]}...")
|
367 |
+
time.sleep(5) # Shorter delay for parsing errors
|
368 |
+
except Exception as e:
|
369 |
+
print(f"[Retry {attempt+1}/{retries}] An unexpected error occurred during LLM call: {e}. Retrying...")
|
370 |
+
time.sleep(delay)
|
371 |
+
raise RuntimeError("Failed after multiple retries due to Gemini quota limit or invalid JSON.")
|
372 |
|
373 |
# Call model with retry protection
|
374 |
response = call_with_retry({
|
375 |
"context": state["context"],
|
376 |
"reasoning": state["reasoning"],
|
377 |
+
"question": state["question"], # Redundant as it's in history, but keeps prompt consistent
|
378 |
+
"current_task": state["current_task"],
|
379 |
+
"current_thoughts": state["current_thoughts"]
|
380 |
})
|
381 |
|
382 |
+
# Parse output using the robust JSON parser
|
383 |
content = response.content
|
384 |
reasoning, action, action_input = parse_agent_response(content)
|
385 |
+
|
386 |
+
print(f"DEBUG: LLM Response Content: {content[:200]}...")
|
387 |
+
print(f"DEBUG: Parsed Action: {action}, Action Input: {action_input[:100]}...")
|
388 |
|
389 |
# Update state
|
390 |
+
state["history"].append(AIMessage(content=content)) # Store the raw LLM response
|
391 |
state["reasoning"] += f"\nStep {state['iterations'] + 1}: {reasoning}"
|
392 |
state["iterations"] += 1
|
393 |
+
state["current_thoughts"] = reasoning # Update current thoughts for next iteration
|
394 |
|
395 |
if "final answer" in action.lower():
|
396 |
state["history"].append(AIMessage(content=f"FINAL ANSWER: {action_input}"))
|
397 |
+
state["final_answer"] = action_input # Set final answer directly in state
|
398 |
else:
|
399 |
+
# Store the action request in history for tool_node
|
400 |
+
state["history"].append({
|
401 |
+
"type": "action_request",
|
402 |
"tool": action,
|
403 |
"input": action_input
|
404 |
+
})
|
405 |
|
406 |
+
print(f"DEBUG: Exiting reasoning_node. New history length: {len(state['history'])}")
|
407 |
return state
|
408 |
|
409 |
|
|
|
|
|
|
|
|
|
410 |
def tool_node(state: AgentState) -> AgentState:
|
411 |
+
"""
|
412 |
+
Node for executing the chosen tool and returning its output.
|
413 |
+
"""
|
414 |
+
print(f"DEBUG: Entering tool_node. Iteration: {state['iterations']}")
|
415 |
|
416 |
# Ensure history exists
|
417 |
if "history" not in state or not isinstance(state["history"], list):
|
418 |
raise ValueError("Invalid or missing history in state")
|
419 |
|
420 |
# Find the most recent action request in history
|
421 |
+
tool_call_dict = None
|
422 |
for msg in reversed(state["history"]):
|
423 |
+
if isinstance(msg, dict) and msg.get("type") == "action_request":
|
424 |
+
tool_call_dict = msg
|
425 |
break
|
426 |
|
427 |
+
if not tool_call_dict:
|
428 |
+
# This shouldn't happen if should_continue logic is correct, but for robustness
|
429 |
+
print("WARNING: No action_request found in history, skipping tool execution.")
|
430 |
+
return state
|
431 |
|
432 |
+
tool_name = tool_call_dict.get("tool")
|
433 |
+
tool_input = tool_call_dict.get("input")
|
434 |
|
435 |
# Defensive check for missing tool or input
|
436 |
if not tool_name or tool_input is None:
|
437 |
+
raise ValueError(f"Tool name '{tool_name}' or input '{tool_input}' missing from action request: {tool_call_dict}")
|
438 |
|
439 |
+
# Look up and invoke the tool from the state's tool list
|
440 |
+
available_tools = state.get("tools", [])
|
441 |
+
tool_fn = next((t for t in available_tools if t.name == tool_name), None) # Assuming tools are LangChain Tool objects now
|
442 |
|
443 |
if tool_fn is None:
|
444 |
+
# Fallback for unrecognized tool - feedback to LLM
|
445 |
+
tool_output = f"[Tool Error] Tool '{tool_name}' not found or not available. Please choose from: {', '.join([t.name for t in available_tools])}"
|
446 |
+
print(f"ERROR: {tool_output}")
|
447 |
+
else:
|
448 |
+
try:
|
449 |
+
print(f"DEBUG: Invoking tool '{tool_name}' with input: '{tool_input[:100]}...'")
|
450 |
+
tool_output = tool_fn.run(tool_input) # Assuming tool.run() method for LangChain Tools
|
451 |
+
if not tool_output: # Handle empty tool output
|
452 |
+
tool_output = f"[{tool_name} output] No specific result found for '{tool_input}'. The tool might have returned an empty response."
|
453 |
+
except Exception as e:
|
454 |
+
tool_output = f"[Tool Error] An error occurred while running '{tool_name}': {str(e)}"
|
455 |
+
print(f"ERROR: {tool_output}")
|
456 |
|
457 |
# Add output to history as an AIMessage
|
458 |
+
# Ensure the history only contains HumanMessage and AIMessage objects for LangGraph's internal processing.
|
459 |
+
# The action_request dict can be removed or transformed if it's no longer needed for internal state.
|
460 |
+
# For now, we'll just add the tool output.
|
461 |
state["history"].append(AIMessage(content=f"[{tool_name} output]\n{tool_output}"))
|
462 |
+
|
463 |
+
print(f"DEBUG: Exiting tool_node. Tool output added to history. New history length: {len(state['history'])}")
|
464 |
return state
|
465 |
|
466 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
467 |
# ====== Agent Graph ======
|
468 |
+
def create_agent_workflow(tools: List[Any]): # tools are passed in now
|
469 |
workflow = StateGraph(AgentState)
|
470 |
|
471 |
# Define nodes
|
|
|
480 |
"reason",
|
481 |
should_continue,
|
482 |
{
|
483 |
+
"action": "action", # Go to action node if a tool is requested
|
484 |
+
"reason": "reason", # Loop back to reason if more thinking is needed
|
485 |
+
"end": END # End if final answer detected
|
486 |
}
|
487 |
)
|
488 |
|
489 |
+
workflow.add_edge("action", "reason") # Always go back to reasoning after a tool action
|
490 |
+
|
491 |
+
# Compile the graph
|
492 |
+
app = workflow.compile()
|
493 |
+
|
494 |
+
# Pass tools into the state so nodes can access them.
|
495 |
+
# This is a bit of a hacky way to get them into the state, but works for now.
|
496 |
+
# A cleaner way might be to make `tool_node` receive tools as a closure or directly from agent init.
|
497 |
+
# For this example, we'll modify the initial state for each invocation.
|
498 |
+
return app
|
499 |
+
|
500 |
|
501 |
# ====== Agent Interface ======
|
502 |
class BasicAgent:
|
503 |
def __init__(self):
|
504 |
+
# Tools need to be LangChain Tool objects for name and description
|
505 |
+
from langchain.tools import Tool
|
506 |
self.tools = [
|
507 |
+
Tool(name="duckduckgo_search", func=duckduckgo_search, description="Performs a DuckDuckGo search for current events or general facts."),
|
508 |
+
Tool(name="wikipedia_search", func=wikipedia_search, description="Searches Wikipedia for encyclopedic information."),
|
509 |
+
Tool(name="arxiv_search", func=arxiv_search, description="Searches ArXiv for scientific preprints and papers."),
|
510 |
+
Tool(name="document_qa", func=document_qa, description="Answers questions based on the content of a given document file (PDF, DOCX, TXT). Requires 'attachment_path' and 'question' as input."),
|
511 |
+
Tool(name="python_execution", func=python_execution, description="Executes Python code in a sandboxed environment for complex calculations or data manipulation."),
|
512 |
+
Tool(name="VideoTranscriptionTool", func=VideoTranscriptionTool(), description="Transcribes and analyzes video content from a URL or ID. Use for any question involving video or audio.")
|
513 |
]
|
514 |
+
self.workflow = create_agent_workflow(self.tools) # Pass tools to workflow creator
|
515 |
|
516 |
def __call__(self, question: str) -> str:
|
517 |
+
print(f"\n--- Agent received question: {question[:50]}{'...' if len(question) > 50 else ''} ---")
|
518 |
|
519 |
+
# Initialize state with proper structure and pass tools
|
520 |
state = {
|
521 |
"question": question,
|
522 |
+
"context": {},
|
523 |
"reasoning": "",
|
524 |
"iterations": 0,
|
525 |
+
"history": [HumanMessage(content=question)],
|
526 |
+
"final_answer": None,
|
527 |
+
"current_task": "Understand the question and plan the next step.",
|
528 |
+
"current_thoughts": "",
|
529 |
+
"tools": self.tools # Pass tools into the state
|
530 |
}
|
531 |
|
532 |
+
# Invoke the workflow
|
533 |
final_state = self.workflow.invoke(state)
|
534 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
535 |
# Extract the FINAL ANSWER from history
|
536 |
+
if final_state.get("final_answer"):
|
537 |
+
answer = final_state["final_answer"]
|
538 |
+
print(f"--- Agent returning FINAL ANSWER: {answer} ---")
|
539 |
+
return answer
|
540 |
+
|
541 |
+
# Fallback if final_answer wasn't set correctly in state
|
542 |
for msg in reversed(final_state["history"]):
|
543 |
if isinstance(msg, AIMessage) and "FINAL ANSWER:" in msg.content:
|
544 |
answer = msg.content.split("FINAL ANSWER:")[1].strip()
|
545 |
+
print(f"--- Agent returning FINAL ANSWER (from history): {answer} ---")
|
546 |
return answer
|
547 |
|
548 |
+
print(f"--- ERROR: No FINAL ANSWER found in agent history for question: {question} ---")
|
549 |
raise ValueError("No FINAL ANSWER found in agent history.")
|
550 |
|
551 |
|