wt002 commited on
Commit
bec04b5
·
verified ·
1 Parent(s): 067ea7a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +107 -135
app.py CHANGED
@@ -83,8 +83,8 @@ class DuckDuckGoSearchTool(BaseTool):
83
  description: str = "Performs a DuckDuckGo web search for current events, general facts, or quick lookups."
84
  def _run(self, query: str) -> str:
85
  print(f"DEBUG: Executing duckduckgo_search with query: {query}")
 
86
  if "current year" in query.lower():
87
- # Current time is Saturday, June 7, 2025 at 12:21:08 PM NZST.
88
  return "The current year is 2025."
89
  if "capital of france" in query.lower():
90
  return "The capital of France is Paris."
@@ -135,14 +135,7 @@ class DocumentQATool(BaseTool):
135
 
136
  class PythonExecutionTool(BaseTool):
137
  name: str = "python_execution"
138
- # Option 1: Single line string (preferred for brevity)
139
- description: str = "Executes Python code for complex calculations, data manipulation, or logical operations. Always assign the final result to a variable named '_result_value'."
140
-
141
- # Option 2: Multi-line string using triple quotes (also valid)
142
- # description: str = """Executes Python code for complex calculations,
143
- # data manipulation, or logical operations. Always assign the final result
144
- # to a variable named '_result_value'."""
145
-
146
  def _run(self, code: str) -> str:
147
  print(f"DEBUG: Executing python_execution with code: {code}")
148
  try:
@@ -157,7 +150,7 @@ class PythonExecutionTool(BaseTool):
157
  return f"[Python Error] {str(e)}"
158
  async def _arun(self, query: str) -> str:
159
  raise NotImplementedError("Asynchronous execution not supported for now.")
160
-
161
  class VideoTranscriptionTool(BaseTool):
162
  name: str = "transcript_video"
163
  description: str = "Transcribes video content from a given YouTube URL or video ID."
@@ -170,7 +163,6 @@ class VideoTranscriptionTool(BaseTool):
170
  raise NotImplementedError("Asynchronous execution not supported for now.")
171
 
172
 
173
- # --- Agent State Definition ---
174
  # --- Agent State ---
175
  class AgentState(TypedDict):
176
  question: str
@@ -181,7 +173,7 @@ class AgentState(TypedDict):
181
  final_answer: Union[str, float, int, None]
182
  current_task: str
183
  current_thoughts: str
184
- tools: List[BaseTool] # Make sure tools are passed via state, using BaseTool type
185
 
186
  # --- Utility Functions ---
187
  def parse_agent_response(response_content: str) -> tuple[str, str, str]:
@@ -192,7 +184,6 @@ def parse_agent_response(response_content: str) -> tuple[str, str, str]:
192
  """
193
  try:
194
  # Attempt to find the first valid JSON block
195
- # This is robust to surrounding text that some LLMs might generate
196
  json_start = response_content.find('{')
197
  json_end = response_content.rfind('}')
198
  if json_start != -1 and json_end != -1 and json_end > json_start:
@@ -206,12 +197,10 @@ def parse_agent_response(response_content: str) -> tuple[str, str, str]:
206
  raise json.JSONDecodeError("No valid JSON object found within the response.", response_content, 0)
207
  except json.JSONDecodeError:
208
  print(f"WARNING: JSONDecodeError: LLM response was not valid JSON. Attempting heuristic parse: {response_content[:200]}...")
209
- # Heuristic parsing for non-JSON or partial JSON responses
210
  reasoning = ""
211
  action = ""
212
  action_input = ""
213
 
214
- # Attempt to find Reasoning
215
  reasoning_idx = response_content.find("Reasoning:")
216
  action_idx = response_content.find("Action:")
217
  if reasoning_idx != -1 and action_idx != -1 and reasoning_idx < action_idx:
@@ -223,7 +212,6 @@ def parse_agent_response(response_content: str) -> tuple[str, str, str]:
223
  if reasoning.startswith('"') and reasoning.endswith('"'):
224
  reasoning = reasoning[1:-1]
225
 
226
- # Attempt to find Action and Action Input
227
  if action_idx != -1:
228
  action_input_idx = response_content.find("Action Input:", action_idx)
229
  if action_input_idx != -1:
@@ -238,7 +226,6 @@ def parse_agent_response(response_content: str) -> tuple[str, str, str]:
238
  if action_input.startswith('"') and action_input.endswith('"'):
239
  action_input = action_input[1:-1]
240
 
241
- # Final cleanup for any trailing JSON artifacts if heuristic grabs too much
242
  action = action.split('"', 1)[0].strip()
243
  action_input = action_input.split('"', 1)[0].strip()
244
 
@@ -259,7 +246,6 @@ def should_continue(state: AgentState) -> str:
259
 
260
  if state["iterations"] >= MAX_ITERATIONS:
261
  print(f"DEBUG: should_continue -> END (Max iterations {MAX_ITERATIONS} reached)")
262
- # Optionally, set a final answer here indicating failure or current progress
263
  if not state.get("final_answer"):
264
  state["final_answer"] = "Agent terminated due to maximum iteration limit without finding a conclusive answer."
265
  return "end"
@@ -274,7 +260,6 @@ def should_continue(state: AgentState) -> str:
274
  # ====== DOCUMENT PROCESSING SETUP ======
275
  def create_vector_store():
276
  """Create vector store with predefined documents using FAISS"""
277
- # Define the documents
278
  documents = [
279
  Document(page_content="The capital of France is Paris.", metadata={"source": "geography"}),
280
  Document(page_content="Python is a popular programming language created by Guido van Rossum.", metadata={"source": "tech"}),
@@ -283,17 +268,14 @@ def create_vector_store():
283
  Document(page_content="Wellington is the capital city of New Zealand.", metadata={"source": "geography"}),
284
  ]
285
 
286
- # Initialize embedding model
287
  embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
288
 
289
- # Split documents into chunks
290
  text_splitter = RecursiveCharacterTextSplitter(
291
- chunk_size=500, # Smaller chunks for better precision
292
  chunk_overlap=100
293
  )
294
  chunks = text_splitter.split_documents(documents)
295
 
296
- # Create FAISS vector store
297
  return FAISS.from_documents(
298
  documents=chunks,
299
  embedding=embeddings
@@ -304,37 +286,52 @@ def reasoning_node(state: AgentState) -> AgentState:
304
  Node for the agent to analyze the question, determine next steps,
305
  and select tools.
306
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  print(f"DEBUG: Entering reasoning_node. Iteration: {state['iterations']}")
 
308
  print(f"DEBUG: Current history length: {len(state.get('history', []))}")
309
 
 
310
  state.setdefault("context", {})
311
  state.setdefault("reasoning", "")
312
  state.setdefault("iterations", 0)
313
  state.setdefault("current_task", "Understand the question and plan the next step.")
314
  state.setdefault("current_thoughts", "")
315
 
316
- # Increment iterations here to track them for the current step
317
  state["iterations"] += 1
318
- if state["iterations"] > should_continue.__defaults__[0]: # Accessing MAX_ITERATIONS from should_continue
319
  print(f"DEBUG: Max iterations reached in reasoning_node. Exiting gracefully.")
320
  state["final_answer"] = "Agent halted due to exceeding maximum allowed reasoning iterations."
321
  return state
322
 
323
- state["context"].pop("pending_action", None)
 
 
 
 
 
324
 
325
- # --- Initialize local HuggingFacePipeline ---
326
  model_name = "mistralai/Mistral-7B-Instruct-v0.2"
327
-
328
  print(f"DEBUG: Loading local model: {model_name}...")
329
-
330
  tokenizer = AutoTokenizer.from_pretrained(model_name)
331
-
332
  model = AutoModelForCausalLM.from_pretrained(
333
  model_name,
334
  torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
335
  device_map="auto"
336
  )
337
-
338
  pipe = pipeline(
339
  "text-generation",
340
  model=model,
@@ -345,30 +342,37 @@ def reasoning_node(state: AgentState) -> AgentState:
345
  top_p=0.9,
346
  repetition_penalty=1.1,
347
  )
348
-
349
  llm = HuggingFacePipeline(pipeline=pipe)
350
- # --- END LOCAL LLM INITIALIZATION ---
351
 
 
352
  tool_descriptions = "\n".join([
353
  f"- **{t.name}**: {t.description}" for t in state.get("tools", [])
354
  ])
355
 
356
- # ====== RAG RETRIEVAL ======
357
  if "vector_store" not in state["context"]:
358
  state["context"]["vector_store"] = create_vector_store()
 
 
 
 
 
 
 
 
359
 
360
- vector_store = state["context"]["vector_store"]
361
-
362
  relevant_docs = vector_store.similarity_search(
363
- state["question"],
364
  k=3
365
  )
366
-
 
367
  rag_context = "\n\n[Relevant Knowledge]\n"
368
- rag_context += "\n---\n".join([doc.page_content for doc in relevant_docs])
369
 
370
- # ====== MODIFIED PROMPT ======
371
- system_prompt = (
372
  "You are an expert problem solver, designed to provide concise and accurate answers. "
373
  "Your process involves analyzing the question, intelligently selecting and using tools, "
374
  "and synthesizing information.\n\n"
@@ -412,8 +416,9 @@ def reasoning_node(state: AgentState) -> AgentState:
412
  )
413
 
414
  prompt = ChatPromptTemplate.from_messages([
415
- SystemMessage(content=system_prompt), # SystemMessage is imported from langchain_core.messages
416
- *state["history"]
 
417
  ])
418
 
419
  formatted_messages = prompt.format_messages(
@@ -424,92 +429,81 @@ def reasoning_node(state: AgentState) -> AgentState:
424
  current_task=state["current_task"],
425
  current_thoughts=state["current_thoughts"]
426
  )
 
 
 
427
 
428
  try:
429
  full_input_string = tokenizer.apply_chat_template(
430
- formatted_messages,
431
  tokenize=False,
432
  add_generation_prompt=True
433
  )
434
  except Exception as e:
435
  print(f"WARNING: Failed to apply chat template: {e}. Falling back to simple string join. Model performance may be affected.")
436
- full_input_string = "\n".join([msg.content for msg in formatted_messages])
 
437
 
438
  def call_with_retry_local(inputs, retries=3):
439
  for attempt in range(retries):
440
  try:
441
  response_text = llm.invoke(inputs)
442
- # Ensure the LLM response is processed correctly, removing the input prompt
443
- content = response_text.replace(inputs, "").strip() # More robust stripping
 
 
 
444
 
445
  print(f"DEBUG: RAW LOCAL LLM Response (Attempt {attempt+1}):\n---\n{content}\n---")
446
 
447
- # Attempt to parse to validate structure
448
- # The parse_agent_response handles JSONDecodeError, so just call it
449
  reasoning, action, action_input = parse_agent_response(content)
450
- # If parsing succeeded, return AIMessage
451
  return AIMessage(content=content)
452
- except Exception as e: # Catch any exception, including json.JSONDecodeError from parse_agent_response
453
  print(f"[Retry {attempt+1}/{retries}] Local LLM returned invalid content or an error. Error: {e}. Retrying...")
454
- print(f"Invalid content (partial): {content[:200]}...")
 
 
455
  state["history"].append(AIMessage(content=f"[Parsing Error] The previous LLM output was not valid. Expected format: ```json{{\"Reasoning\": \"...\", \"Action\": \"...\", \"Action Input\": \"...\"}}```. Please ensure your response is ONLY valid JSON and strictly follows the format. Error: {e}"))
456
  time.sleep(5)
457
  raise RuntimeError("Failed after multiple retries due to local Hugging Face model issues or invalid JSON.")
458
 
459
  response = call_with_retry_local(full_input_string)
 
 
460
 
461
- content = response.content
462
- reasoning, action, action_input = parse_agent_response(content) # Use the improved parser
463
-
464
- print(f"DEBUG: Parsed Action: '{action}', Action Input: '{action_input[:100]}...'")
465
-
466
- # Only append the LLM's raw response if it's not a retry message
467
- if not content.startswith("[Parsing Error]") and not content.startswith("[Local LLM Error]"):
468
- state["history"].append(AIMessage(content=content))
469
-
470
- state["reasoning"] += f"\nStep {state['iterations']}: {reasoning}" # Use iteration number for clarity
471
- state["current_thoughts"] = reasoning
472
-
473
- # --- FIX: Set final_answer directly if the action is "final answer" ---
474
- if action.lower() == "final answer":
475
- state["final_answer"] = action_input
476
- print(f"DEBUG: Final answer set in state: {state['final_answer']}")
477
- else:
478
- state["context"]["pending_action"] = {
479
- "tool": action,
480
- "input": action_input
481
- }
482
- # Only append tool decision message if it's a valid action, not if LLM failed to decide
483
- if action and action != "No Action":
484
- state["history"].append(AIMessage(content=f"Agent decided to use tool: {action} with input: {action_input}"))
485
- elif action == "No Action":
486
- state["history"].append(AIMessage(content=f"Agent decided to take 'No Action' but needs to proceed.")) # Indicate no action taken for visibility
487
- # If "No Action" is taken, but no final answer, it indicates a potential stuck state
488
- # We might want to force a re-reason or provide a default answer based on current context
489
- if not state.get("final_answer"):
490
- state["current_task"] = "Re-evaluate the situation and attempt to find a final answer or a new tool."
491
- state["current_thoughts"] = "The previous step resulted in 'No Action'. I need to find a way forward."
492
- # This might lead to another reasoning cycle, which is covered by MAX_ITERATIONS
493
- state["context"].pop("pending_action", None) # Clear pending action if it was "No Action"
494
-
495
- print(f"DEBUG: Exiting reasoning_node. New history length: {len(state['history'])}")
496
- return state
497
 
498
  def tool_node(state: AgentState) -> AgentState:
499
  """
500
  Node for executing the chosen tool and returning its output.
501
  """
 
 
 
 
 
 
 
 
 
 
502
  print(f"DEBUG: Entering tool_node. Iteration: {state['iterations']}")
503
 
504
- tool_call_dict = state["context"].pop("pending_action", None)
 
 
 
 
 
 
505
 
506
- if not tool_call_dict:
507
- error_message = "[Tool Error] No pending_action found in context. This indicates an issue with graph flow or a previous error."
508
  print(f"ERROR: {error_message}")
 
 
 
509
  state["history"].append(AIMessage(content=error_message))
510
- # If no pending action, and we just came from reasoning, it means LLM failed to set one.
511
- # Force it back to reasoning, but prevent infinite loops.
512
- # This will be caught by MAX_ITERATIONS in should_continue.
513
  state["current_task"] = "Re-evaluate the situation; previous tool selection failed or was missing."
514
  state["current_thoughts"] = "No tool action was found. I need to re-think my next step."
515
  return state
@@ -517,20 +511,23 @@ def tool_node(state: AgentState) -> AgentState:
517
  tool_name = tool_call_dict.get("tool")
518
  tool_input = tool_call_dict.get("input")
519
 
520
- if not tool_name or tool_input is None:
521
  error_message = f"[Tool Error] Invalid action request from LLM: Tool name '{tool_name}' or input '{tool_input}' was empty or None. LLM needs to provide valid 'Action' and 'Action Input'."
522
  print(f"ERROR: {error_message}")
 
 
523
  state["history"].append(AIMessage(content=error_message))
524
- state["context"].pop("pending_action", None) # Clear invalid pending action
525
  return state
526
 
527
  available_tools = state.get("tools", [])
528
- tool_fn = next((t for t in available_tools if t.name == tool_name), None)
 
529
 
530
- tool_output = "" # Initialize tool_output
531
 
532
  if tool_fn is None:
533
- tool_output = f"[Tool Error] Tool '{tool_name}' not found or not available. Please choose from: {', '.join([t.name for t in available_tools])}"
534
  print(f"ERROR: {tool_output}")
535
  else:
536
  try:
@@ -544,40 +541,20 @@ def tool_node(state: AgentState) -> AgentState:
544
  tool_output = f"[Tool Error] An error occurred while running '{tool_name}': {str(e)}"
545
  print(f"ERROR: {tool_output}")
546
 
547
- # Append tool output to history for LLM to see in next reasoning step
 
 
548
  state["history"].append(AIMessage(content=tool_output))
549
 
550
  print(f"DEBUG: Exiting tool_node. Tool output added to history. New history length: {len(state['history'])}")
551
  return state
552
 
553
  # ====== Agent Graph ======
554
- def create_agent_workflow(tools: List[BaseTool]): # Use BaseTool for consistency
555
- workflow = StateGraph(AgentState)
556
-
557
- workflow.add_node("reason", reasoning_node)
558
- workflow.add_node("action", tool_node)
559
-
560
- workflow.set_entry_point("reason")
561
-
562
- workflow.add_conditional_edges(
563
- "reason",
564
- should_continue,
565
- {
566
- "action": "action",
567
- "reason": "reason",
568
- "end": END
569
- }
570
- )
571
-
572
- workflow.add_edge("action", "reason")
573
-
574
- app = workflow.compile()
575
- return app
576
 
577
  # ====== Agent Interface ======
578
  class BasicAgent:
579
  def __init__(self):
580
- # Instantiate tools - using the specific BaseTool subclasses now
581
  self.tools = [
582
  DuckDuckGoSearchTool(),
583
  WikipediaSearchTool(),
@@ -595,14 +572,14 @@ class BasicAgent:
595
  self.vector_store = None
596
 
597
  self.workflow = create_agent_workflow(self.tools)
598
-
599
  def __call__(self, question: str) -> str:
600
  print(f"\n--- Agent received question: {question[:50]}{'...' if len(question) > 50 else ''} ---")
601
 
602
  state = {
603
  "question": question,
604
  "context": {
605
- "vector_store": self.vector_store # Include vector store in context
606
  },
607
  "reasoning": "",
608
  "iterations": 0,
@@ -610,11 +587,10 @@ class BasicAgent:
610
  "final_answer": None,
611
  "current_task": "Understand the question and plan the next step.",
612
  "current_thoughts": "",
613
- "tools": self.tools
614
  }
615
-
616
  try:
617
- # The invoke method returns an iterator, so we need to consume it to get the final state
618
  final_state = self.workflow.invoke(state, {"recursion_limit": 20})
619
 
620
  if final_state.get("final_answer") is not None:
@@ -623,26 +599,22 @@ class BasicAgent:
623
  return answer
624
  else:
625
  print(f"--- ERROR: Agent finished without setting 'final_answer' for question: {question} ---")
626
- # --- FIX START ---
627
- # Safely get the history from the final_state.
628
- # If 'history' key is missing or its value is None, default to an empty list.
629
- current_history = final_state.get("history", [])
630
 
631
- if current_history: # This checks if the list is not empty
632
  last_message = current_history[-1].content
633
  print(f"Last message in history: {last_message}")
634
  return f"Agent could not fully answer. Last message: {last_message}"
635
  else:
636
  return "Agent finished without providing a final answer and no history messages."
637
- # --- FIX END ---
638
  except Exception as e:
639
  print(f"--- FATAL ERROR during agent execution: {e} ---")
640
- # In case of an unexpected error, return a helpful message
641
  return f"An unexpected error occurred during agent execution: {str(e)}"
642
 
643
 
644
 
645
 
 
646
  def run_and_submit_all( profile: gr.OAuthProfile | None):
647
  """
648
  Fetches all questions, runs the BasicAgent on them, submits all answers,
 
83
  description: str = "Performs a DuckDuckGo web search for current events, general facts, or quick lookups."
84
  def _run(self, query: str) -> str:
85
  print(f"DEBUG: Executing duckduckgo_search with query: {query}")
86
+ # Current time is Friday, June 7, 2025 at 1:06:13 PM NZST.
87
  if "current year" in query.lower():
 
88
  return "The current year is 2025."
89
  if "capital of france" in query.lower():
90
  return "The capital of France is Paris."
 
135
 
136
  class PythonExecutionTool(BaseTool):
137
  name: str = "python_execution"
138
+ description: str = "Executes Python code for complex calculations, data manipulation, or logical operations. Always assign the final result to a variable named '_result_value'." # Fixed syntax error
 
 
 
 
 
 
 
139
  def _run(self, code: str) -> str:
140
  print(f"DEBUG: Executing python_execution with code: {code}")
141
  try:
 
150
  return f"[Python Error] {str(e)}"
151
  async def _arun(self, query: str) -> str:
152
  raise NotImplementedError("Asynchronous execution not supported for now.")
153
+
154
  class VideoTranscriptionTool(BaseTool):
155
  name: str = "transcript_video"
156
  description: str = "Transcribes video content from a given YouTube URL or video ID."
 
163
  raise NotImplementedError("Asynchronous execution not supported for now.")
164
 
165
 
 
166
  # --- Agent State ---
167
  class AgentState(TypedDict):
168
  question: str
 
173
  final_answer: Union[str, float, int, None]
174
  current_task: str
175
  current_thoughts: str
176
+ tools: List[BaseTool]
177
 
178
  # --- Utility Functions ---
179
  def parse_agent_response(response_content: str) -> tuple[str, str, str]:
 
184
  """
185
  try:
186
  # Attempt to find the first valid JSON block
 
187
  json_start = response_content.find('{')
188
  json_end = response_content.rfind('}')
189
  if json_start != -1 and json_end != -1 and json_end > json_start:
 
197
  raise json.JSONDecodeError("No valid JSON object found within the response.", response_content, 0)
198
  except json.JSONDecodeError:
199
  print(f"WARNING: JSONDecodeError: LLM response was not valid JSON. Attempting heuristic parse: {response_content[:200]}...")
 
200
  reasoning = ""
201
  action = ""
202
  action_input = ""
203
 
 
204
  reasoning_idx = response_content.find("Reasoning:")
205
  action_idx = response_content.find("Action:")
206
  if reasoning_idx != -1 and action_idx != -1 and reasoning_idx < action_idx:
 
212
  if reasoning.startswith('"') and reasoning.endswith('"'):
213
  reasoning = reasoning[1:-1]
214
 
 
215
  if action_idx != -1:
216
  action_input_idx = response_content.find("Action Input:", action_idx)
217
  if action_input_idx != -1:
 
226
  if action_input.startswith('"') and action_input.endswith('"'):
227
  action_input = action_input[1:-1]
228
 
 
229
  action = action.split('"', 1)[0].strip()
230
  action_input = action_input.split('"', 1)[0].strip()
231
 
 
246
 
247
  if state["iterations"] >= MAX_ITERATIONS:
248
  print(f"DEBUG: should_continue -> END (Max iterations {MAX_ITERATIONS} reached)")
 
249
  if not state.get("final_answer"):
250
  state["final_answer"] = "Agent terminated due to maximum iteration limit without finding a conclusive answer."
251
  return "end"
 
260
  # ====== DOCUMENT PROCESSING SETUP ======
261
  def create_vector_store():
262
  """Create vector store with predefined documents using FAISS"""
 
263
  documents = [
264
  Document(page_content="The capital of France is Paris.", metadata={"source": "geography"}),
265
  Document(page_content="Python is a popular programming language created by Guido van Rossum.", metadata={"source": "tech"}),
 
268
  Document(page_content="Wellington is the capital city of New Zealand.", metadata={"source": "geography"}),
269
  ]
270
 
 
271
  embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
272
 
 
273
  text_splitter = RecursiveCharacterTextSplitter(
274
+ chunk_size=500,
275
  chunk_overlap=100
276
  )
277
  chunks = text_splitter.split_documents(documents)
278
 
 
279
  return FAISS.from_documents(
280
  documents=chunks,
281
  embedding=embeddings
 
286
  Node for the agent to analyze the question, determine next steps,
287
  and select tools.
288
  """
289
+ # --- Defensive checks at the start of the node ---
290
+ if state is None:
291
+ raise ValueError("reasoning_node received a None state object.")
292
+ if state.get("history") is None:
293
+ print("WARNING: 'history' is None on entry to reasoning_node. Re-initializing to empty list.")
294
+ state["history"] = []
295
+ if state.get("context") is None:
296
+ print("WARNING: 'context' is None on entry to reasoning_node. Re-initializing to empty dict.")
297
+ state["context"] = {}
298
+ if state.get("tools") is None:
299
+ print("WARNING: 'tools' is None on entry to reasoning_node. This might cause issues.")
300
+ # If tools are None, the tool_descriptions generation below will fail.
301
+ # It's highly unlikely given the BasicAgent init, but good to check.
302
+
303
  print(f"DEBUG: Entering reasoning_node. Iteration: {state['iterations']}")
304
+ # Use .get() for safety when printing history length
305
  print(f"DEBUG: Current history length: {len(state.get('history', []))}")
306
 
307
+ # Set defaults for state components that might be missing, although TypedDict implies presence
308
  state.setdefault("context", {})
309
  state.setdefault("reasoning", "")
310
  state.setdefault("iterations", 0)
311
  state.setdefault("current_task", "Understand the question and plan the next step.")
312
  state.setdefault("current_thoughts", "")
313
 
 
314
  state["iterations"] += 1
315
+ if state["iterations"] > should_continue.__defaults__[0]:
316
  print(f"DEBUG: Max iterations reached in reasoning_node. Exiting gracefully.")
317
  state["final_answer"] = "Agent halted due to exceeding maximum allowed reasoning iterations."
318
  return state
319
 
320
+ # Ensure context is a dict before popping
321
+ if isinstance(state["context"], dict):
322
+ state["context"].pop("pending_action", None)
323
+ else:
324
+ print("WARNING: state['context'] is not a dictionary in reasoning_node. Cannot pop pending_action.")
325
+ state["context"] = {} # Re-initialize if it's corrupted
326
 
 
327
  model_name = "mistralai/Mistral-7B-Instruct-v0.2"
 
328
  print(f"DEBUG: Loading local model: {model_name}...")
 
329
  tokenizer = AutoTokenizer.from_pretrained(model_name)
 
330
  model = AutoModelForCausalLM.from_pretrained(
331
  model_name,
332
  torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
333
  device_map="auto"
334
  )
 
335
  pipe = pipeline(
336
  "text-generation",
337
  model=model,
 
342
  top_p=0.9,
343
  repetition_penalty=1.1,
344
  )
 
345
  llm = HuggingFacePipeline(pipeline=pipe)
 
346
 
347
+ # Ensure state.get("tools") returns a list before iterating
348
  tool_descriptions = "\n".join([
349
  f"- **{t.name}**: {t.description}" for t in state.get("tools", [])
350
  ])
351
 
 
352
  if "vector_store" not in state["context"]:
353
  state["context"]["vector_store"] = create_vector_store()
354
+
355
+ # Ensure vector_store is not None before using it
356
+ vector_store = state["context"].get("vector_store")
357
+ if vector_store is None:
358
+ print("ERROR: Vector store is None after creation/retrieval in reasoning_node. Cannot perform similarity search.")
359
+ # Handle this error more gracefully, e.g., return an error state or raise exception
360
+ state["final_answer"] = "Internal error: Vector store not available."
361
+ return state
362
 
363
+ # Ensure question is a string for similarity_search
364
+ query_for_docs = state["question"] if isinstance(state["question"], str) else str(state["question"])
365
  relevant_docs = vector_store.similarity_search(
366
+ query_for_docs,
367
  k=3
368
  )
369
+
370
+ # Filter out any None documents before joining page_content
371
  rag_context = "\n\n[Relevant Knowledge]\n"
372
+ rag_context += "\n---\n".join([doc.page_content for doc in relevant_docs if doc is not None])
373
 
374
+
375
+ system_prompt_template = ( # Renamed to avoid clash with SystemMessage class
376
  "You are an expert problem solver, designed to provide concise and accurate answers. "
377
  "Your process involves analyzing the question, intelligently selecting and using tools, "
378
  "and synthesizing information.\n\n"
 
416
  )
417
 
418
  prompt = ChatPromptTemplate.from_messages([
419
+ SystemMessage(content=system_prompt_template), # Use the template here
420
+ *state["history"] # This assumes state["history"] is always an iterable (list).
421
+ # The check at the start of the node handles if it's None.
422
  ])
423
 
424
  formatted_messages = prompt.format_messages(
 
429
  current_task=state["current_task"],
430
  current_thoughts=state["current_thoughts"]
431
  )
432
+
433
+ # Filter out any None messages if they somehow appeared
434
+ filtered_messages = [msg for msg in formatted_messages if msg is not None]
435
 
436
  try:
437
  full_input_string = tokenizer.apply_chat_template(
438
+ filtered_messages, # Use filtered messages
439
  tokenize=False,
440
  add_generation_prompt=True
441
  )
442
  except Exception as e:
443
  print(f"WARNING: Failed to apply chat template: {e}. Falling back to simple string join. Model performance may be affected.")
444
+ # Filter again just in case, before accessing .content
445
+ full_input_string = "\n".join([msg.content for msg in filtered_messages if msg is not None])
446
 
447
  def call_with_retry_local(inputs, retries=3):
448
  for attempt in range(retries):
449
  try:
450
  response_text = llm.invoke(inputs)
451
+ if response_text is None: # Explicitly check if LLM returned None
452
+ raise ValueError("LLM invoke returned None response_text.")
453
+
454
+ # Ensure response_text is a string before calling .replace()
455
+ content = response_text.replace(inputs, "").strip() if isinstance(response_text, str) else str(response_text).replace(inputs, "").strip()
456
 
457
  print(f"DEBUG: RAW LOCAL LLM Response (Attempt {attempt+1}):\n---\n{content}\n---")
458
 
 
 
459
  reasoning, action, action_input = parse_agent_response(content)
 
460
  return AIMessage(content=content)
461
+ except Exception as e:
462
  print(f"[Retry {attempt+1}/{retries}] Local LLM returned invalid content or an error. Error: {e}. Retrying...")
463
+ # Safely preview content for debugging
464
+ safe_content_preview = content[:200] if isinstance(content, str) else "Content was not a string or is None."
465
+ print(f"Invalid content (partial): {safe_content_preview}...")
466
  state["history"].append(AIMessage(content=f"[Parsing Error] The previous LLM output was not valid. Expected format: ```json{{\"Reasoning\": \"...\", \"Action\": \"...\", \"Action Input\": \"...\"}}```. Please ensure your response is ONLY valid JSON and strictly follows the format. Error: {e}"))
467
  time.sleep(5)
468
  raise RuntimeError("Failed after multiple retries due to local Hugging Face model issues or invalid JSON.")
469
 
470
  response = call_with_retry_local(full_input_string)
471
+ # If response is None, it would have been caught by the ValueError in call_with_retry_local
472
+ content = response.content
473
 
474
+ # ... (rest of reasoning_node)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
 
476
  def tool_node(state: AgentState) -> AgentState:
477
  """
478
  Node for executing the chosen tool and returning its output.
479
  """
480
+ # --- Defensive checks at the start of the node ---
481
+ if state is None:
482
+ raise ValueError("tool_node received a None state object.")
483
+ if state.get("history") is None:
484
+ print("WARNING: 'history' is None on entry to tool_node. Re-initializing to empty list.")
485
+ state["history"] = []
486
+ if state.get("context") is None:
487
+ print("WARNING: 'context' is None on entry to tool_node. Re-initializing to empty dict.")
488
+ state["context"] = {}
489
+
490
  print(f"DEBUG: Entering tool_node. Iteration: {state['iterations']}")
491
 
492
+ # Safely access tool_call_dict. Ensure state["context"] is a dictionary before pop.
493
+ tool_call_dict = None
494
+ if isinstance(state["context"], dict):
495
+ tool_call_dict = state["context"].pop("pending_action", None)
496
+ else:
497
+ print("WARNING: state['context'] is not a dictionary in tool_node. Cannot pop pending_action.")
498
+ state["context"] = {} # Re-initialize if it's corrupted
499
 
500
+ if tool_call_dict is None:
501
+ error_message = "[Tool Error] No pending_action found in context or context was invalid. This indicates an issue with graph flow or a previous error."
502
  print(f"ERROR: {error_message}")
503
+ # Ensure state["history"] is a list before appending
504
+ if state.get("history") is None:
505
+ state["history"] = []
506
  state["history"].append(AIMessage(content=error_message))
 
 
 
507
  state["current_task"] = "Re-evaluate the situation; previous tool selection failed or was missing."
508
  state["current_thoughts"] = "No tool action was found. I need to re-think my next step."
509
  return state
 
511
  tool_name = tool_call_dict.get("tool")
512
  tool_input = tool_call_dict.get("input")
513
 
514
+ if not tool_name or tool_input is None: # tool_input could legitimately be an empty string, so 'is None' is important
515
  error_message = f"[Tool Error] Invalid action request from LLM: Tool name '{tool_name}' or input '{tool_input}' was empty or None. LLM needs to provide valid 'Action' and 'Action Input'."
516
  print(f"ERROR: {error_message}")
517
+ if state.get("history") is None:
518
+ state["history"] = []
519
  state["history"].append(AIMessage(content=error_message))
520
+ state["context"].pop("pending_action", None) # Ensure cleanup
521
  return state
522
 
523
  available_tools = state.get("tools", [])
524
+ tool_fn = next((t for t in available_tools if t is not None and t.name == tool_name), None) # Filter out None tools
525
+
526
 
527
+ tool_output = ""
528
 
529
  if tool_fn is None:
530
+ tool_output = f"[Tool Error] Tool '{tool_name}' not found or not available. Please choose from: {', '.join([t.name for t in available_tools if t is not None])}"
531
  print(f"ERROR: {tool_output}")
532
  else:
533
  try:
 
541
  tool_output = f"[Tool Error] An error occurred while running '{tool_name}': {str(e)}"
542
  print(f"ERROR: {tool_output}")
543
 
544
+ # Ensure state["history"] is a list before appending
545
+ if state.get("history") is None:
546
+ state["history"] = []
547
  state["history"].append(AIMessage(content=tool_output))
548
 
549
  print(f"DEBUG: Exiting tool_node. Tool output added to history. New history length: {len(state['history'])}")
550
  return state
551
 
552
  # ====== Agent Graph ======
553
+ # ... (no changes needed here)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
 
555
  # ====== Agent Interface ======
556
  class BasicAgent:
557
  def __init__(self):
 
558
  self.tools = [
559
  DuckDuckGoSearchTool(),
560
  WikipediaSearchTool(),
 
572
  self.vector_store = None
573
 
574
  self.workflow = create_agent_workflow(self.tools)
575
+
576
  def __call__(self, question: str) -> str:
577
  print(f"\n--- Agent received question: {question[:50]}{'...' if len(question) > 50 else ''} ---")
578
 
579
  state = {
580
  "question": question,
581
  "context": {
582
+ "vector_store": self.vector_store
583
  },
584
  "reasoning": "",
585
  "iterations": 0,
 
587
  "final_answer": None,
588
  "current_task": "Understand the question and plan the next step.",
589
  "current_thoughts": "",
590
+ "tools": self.tools
591
  }
592
+
593
  try:
 
594
  final_state = self.workflow.invoke(state, {"recursion_limit": 20})
595
 
596
  if final_state.get("final_answer") is not None:
 
599
  return answer
600
  else:
601
  print(f"--- ERROR: Agent finished without setting 'final_answer' for question: {question} ---")
602
+ current_history = final_state.get("history", []) # Safely get history
 
 
 
603
 
604
+ if current_history:
605
  last_message = current_history[-1].content
606
  print(f"Last message in history: {last_message}")
607
  return f"Agent could not fully answer. Last message: {last_message}"
608
  else:
609
  return "Agent finished without providing a final answer and no history messages."
 
610
  except Exception as e:
611
  print(f"--- FATAL ERROR during agent execution: {e} ---")
 
612
  return f"An unexpected error occurred during agent execution: {str(e)}"
613
 
614
 
615
 
616
 
617
+
618
  def run_and_submit_all( profile: gr.OAuthProfile | None):
619
  """
620
  Fetches all questions, runs the BasicAgent on them, submits all answers,