wt002 commited on
Commit
8a9ad42
·
verified ·
1 Parent(s): 1ced237

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +268 -128
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
- # ====== Agent State ======
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  class AgentState(TypedDict):
152
  question: str
153
- history: Annotated[List[Dict], operator.add]
154
- context: str
155
  reasoning: str
156
  iterations: int
 
 
 
157
 
158
- # ====== Graph Components ======
159
- def init_state(question: str) -> AgentState:
160
- return {
161
- "question": question,
162
- "history": [],
163
- "context": f"User question: {question}",
164
- "reasoning": "",
165
- "iterations": 0
166
- }
 
 
 
 
 
 
 
 
167
 
 
 
168
 
 
 
 
 
169
 
170
- def should_continue(state: AgentState) -> str:
171
- history = state.get("history", [])
172
 
173
- if not history:
174
- return "reason" # No history yet, reason first
175
 
176
- last_message = history[-1]
 
 
 
 
177
 
178
- # End if agent has produced a final answer
179
- if isinstance(last_message, AIMessage) and "FINAL ANSWER:" in last_message.content:
 
180
  return "end"
 
 
 
 
 
 
 
181
 
182
- # If an action_request exists, trigger tool use
 
 
183
  for msg in reversed(history):
184
- if isinstance(msg, dict) and msg.get("role") == "action_request":
185
- return "continue"
 
186
 
187
- # Otherwise, go back to reasoning
 
188
  return "reason"
189
 
190
 
191
-
192
  def reasoning_node(state: AgentState) -> AgentState:
193
- import os
194
- import time
195
- from langchain_google_genai import ChatGoogleGenerativeAI
196
- from langchain.schema import HumanMessage, AIMessage
197
- from langchain.prompts import ChatPromptTemplate
198
- from google.api_core.exceptions import ResourceExhausted
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
- if not state["history"] or not isinstance(state["history"][-1], HumanMessage):
209
- state["history"].append(HumanMessage(content="Continue."))
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
- # Create prompt chain
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  prompt = ChatPromptTemplate.from_messages([
225
- ("system", (
226
- "You're an expert problem solver. Analyze the question, select the best tool, "
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
- return chain.invoke(inputs)
 
 
 
247
  except ResourceExhausted as e:
248
- print(f"[Retry {attempt+1}] Gemini rate limit hit. Waiting {delay}s...")
249
  time.sleep(delay)
250
- raise RuntimeError("Failed after multiple retries due to Gemini quota limit.")
 
 
 
 
 
 
 
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
- state["context"]["current_tool"] = {
 
 
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
- from langchain.schema import AIMessage
 
 
 
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
- tool_call = None
292
  for msg in reversed(state["history"]):
293
- if isinstance(msg, dict) and msg.get("role") == "action_request":
294
- tool_call = msg
295
  break
296
 
297
- if not tool_call:
298
- raise ValueError("No tool call found in history")
 
 
299
 
300
- tool_name = tool_call.get("tool")
301
- tool_input = tool_call.get("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
- agent = BasicAgent() # Create agent to access tools
309
- tool_fn = next((t for t in agent.tools if t.__name__ == tool_name), None)
310
 
311
  if tool_fn is None:
312
- raise ValueError(f"Tool '{tool_name}' not found")
313
-
314
- try:
315
- tool_output = tool_fn(tool_input)
316
- except Exception as e:
317
- tool_output = f"[Tool Error] {str(e)}"
 
 
 
 
 
 
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
- "continue": "action",
358
- "reason": "reason",
359
- "end": END
360
  }
361
  )
362
 
363
- workflow.add_edge("action", "reason")
364
- return workflow.compile()
 
 
 
 
 
 
 
 
 
365
 
366
  # ====== Agent Interface ======
367
  class BasicAgent:
368
  def __init__(self):
369
- self.workflow = create_agent_workflow()
 
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": {}, # Ensure it's a dict
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 answer: {answer}")
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