wt002 commited on
Commit
d956180
·
verified ·
1 Parent(s): 750ac07

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +429 -461
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import os
2
  import gradio as gr
3
  import requests
@@ -15,24 +16,6 @@ from langchain_community.document_loaders import WikipediaLoader
15
  from langchain_community.utilities import WikipediaAPIWrapper
16
  from langchain_community.document_loaders import ArxivLoader
17
 
18
- from typing import List, Union, Dict, Any, TypedDict # Ensure all types are imported
19
-
20
- import torch
21
- from langchain_core.messages import AIMessage, HumanMessage # Corrected import for message types
22
- from langchain_core.tools import BaseTool
23
- from langchain_community.embeddings import HuggingFaceEmbeddings
24
- from langchain_community.vectorstores import FAISS
25
- from langchain.text_splitter import RecursiveCharacterTextSplitter
26
- from langchain_core.documents import Document
27
- # No longer needed: from langchain.chains.Youtubeing import load_qa_chain (as it's unused)
28
- from langchain_community.llms import HuggingFacePipeline
29
- from langchain.prompts import ChatPromptTemplate # SystemMessage moved to langchain_core.messages
30
- from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
31
- from langgraph.graph import END, StateGraph
32
-
33
- # --- Import for actual YouTube transcription (if you make the tool functional) ---
34
- # from youtube_transcript_api import YouTubeTranscriptApi
35
-
36
 
37
  # (Keep Constants as is)
38
  # --- Constants ---
@@ -48,6 +31,8 @@ import json
48
  from typing import TypedDict, List, Union, Any, Dict, Optional
49
 
50
  # LangChain and LangGraph imports
 
 
51
  from langgraph.graph import StateGraph, END
52
  from langchain_community.llms import HuggingFacePipeline
53
 
@@ -65,540 +50,509 @@ import arxiv
65
  from transformers import pipeline as hf_pipeline # Renamed to avoid clash with main pipeline
66
  from youtube_transcript_api import YouTubeTranscriptApi
67
 
68
- from typing import List, Literal, TypedDict
69
-
70
-
71
-
72
  # --- Helper function for python_execution tool ---
73
  def indent_code(code: str, indent: str = " ") -> str:
74
  """Indents multi-line code for execution within a function."""
75
  return "\n".join(indent + line for line in code.splitlines())
76
 
77
  # --- Tool Definitions ---
78
- import wikipedia # <--- Make sure you have this installed: pip install wikipedia
79
-
80
- # --- Dummy Tools (replace with actual, robust implementations for full functionality) ---
81
- class DuckDuckGoSearchTool(BaseTool):
82
- name: str = "duckduckgo_search"
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."
91
- if "python creator" in query.lower():
92
- return "Python was created by Guido van Rossum."
93
- return f"Search result for '{query}': Information about {query}."
94
- async def _arun(self, query: str) -> str:
95
- raise NotImplementedError("Asynchronous execution not supported for now.")
96
-
97
- class WikipediaSearchTool(BaseTool):
98
- name: str = "wikipedia_search"
99
- description: str = "Performs a Wikipedia search for encyclopedic information, historical context, or detailed topics. Returns the first 3 sentences of the summary."
100
- def _run(self, query: str) -> str:
101
- print(f"DEBUG: wikipedia_search called with: {query}")
102
- try:
103
- return wikipedia.summary(query, sentences=3)
104
- except wikipedia.DisambiguationError as e:
105
- return f"Disambiguation options: {', '.join(e.options[:3])}. Please refine your query."
106
- except wikipedia.PageError:
107
- return "Wikipedia page not found for your query."
108
- except Exception as e:
109
- return f"Error performing Wikipedia search: {str(e)}"
110
- async def _arun(self, query: str) -> str:
111
- raise NotImplementedError("Asynchronous execution not supported for now.")
112
-
113
- class ArxivSearchTool(BaseTool):
114
- name: str = "arxiv_search"
115
- description: str = "Searches ArXiv for scientific papers, research, or cutting-edge technical information."
116
- def _run(self, query: str) -> str:
117
- print(f"DEBUG: Executing arxiv_search with query: {query}")
118
- return f"ArXiv result for '{query}': Scientific papers related to {query}."
119
- async def _arun(self, query: str) -> str:
120
- raise NotImplementedError("Asynchronous execution not supported for now.")
121
-
122
- class DocumentQATool(BaseTool):
123
- name: str = "document_qa"
124
- description: str = "Answers questions based on provided document text. Input format: 'document_text||question'."
125
- def _run(self, input_str: str) -> str:
126
- print(f"DEBUG: Executing document_qa with input: {input_str}")
127
- if "||" not in input_str:
128
- return "[Error] Invalid input for document_qa. Expected 'document_text||question'."
129
- doc_text, question = input_str.split("||", 1)
130
- if "Paris" in doc_text and "capital" in question:
131
- return "The capital of France is Paris."
132
- return f"Answer to '{question}' from document: '{doc_text[:50]}...' is not directly found."
133
- async def _arun(self, query: str) -> str:
134
- raise NotImplementedError("Asynchronous execution not supported for now.")
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'."
139
- def _run(self, code: str) -> str:
140
- print(f"DEBUG: Executing python_execution with code: {code}")
141
- try:
142
- local_vars = {}
143
- exec(code, globals(), local_vars)
144
- if '_result_value' in local_vars:
145
- return str(local_vars['_result_value'])
146
- return "Python code executed successfully, but no _result_value was assigned."
147
- except Exception as e:
148
- return f"[Python Error] {str(e)}"
149
- async def _arun(self, query: str) -> str:
150
- raise NotImplementedError("Asynchronous execution not supported for now.")
151
-
152
- class VideoTranscriptionTool(BaseTool):
153
- name: str = "transcript_video"
154
- description: str = "Transcribes video content from a given YouTube URL or video ID."
155
- def _run(self, query: str) -> str:
156
- print(f"DEBUG: Executing transcript_video with query: {query}")
157
- if "youtube.com" in query or "youtu.be" in query:
158
- return f"Transcription of YouTube video '{query}': This is a sample transcription of the video content."
159
- return "[Error] Invalid input for transcript_video. Please provide a valid YouTube URL or video ID."
160
- async def _arun(self, query: str) -> str:
161
- raise NotImplementedError("Asynchronous execution not supported for now.")
162
-
163
-
164
- # --- Agent State ---
165
- class AgentState(TypedDict):
166
- question: str
167
- history: List[Union[HumanMessage, AIMessage]]
168
- context: Dict[str, Any]
169
- reasoning: str
170
- iterations: int
171
- final_answer: Union[str, float, int, None]
172
- current_task: str
173
- current_thoughts: str
174
- tools: List[BaseTool]
175
-
176
- # --- Utility Functions ---
177
- def parse_agent_response(response_content: str) -> tuple[str, str, str]:
178
- """
179
- Parses the LLM's JSON output for reasoning, action, and action input.
180
- Returns (reasoning, action, action_input).
181
- If JSON parsing fails, it attempts heuristic parsing.
182
- """
183
  try:
184
- # Attempt to find the first valid JSON block
185
- json_start = response_content.find('{')
186
- json_end = response_content.rfind('}')
187
- if json_start != -1 and json_end != -1 and json_end > json_start:
188
- json_str = response_content[json_start : json_end + 1]
189
- response_json = json.loads(json_str)
190
- reasoning = response_json.get("Reasoning", "").strip()
191
- action = response_json.get("Action", "").strip()
192
- action_input = response_json.get("Action Input", "").strip()
193
- return reasoning, action, action_input
194
- else:
195
- raise json.JSONDecodeError("No valid JSON object found within the response.", response_content, 0)
196
- except json.JSONDecodeError:
197
- print(f"WARNING: JSONDecodeError: LLM response was not valid JSON. Attempting heuristic parse: {response_content[:200]}...")
198
- reasoning = ""
199
- action = ""
200
- action_input = ""
201
-
202
- reasoning_idx = response_content.find("Reasoning:")
203
- action_idx = response_content.find("Action:")
204
- if reasoning_idx != -1 and action_idx != -1 and reasoning_idx < action_idx:
205
- reasoning = response_content[reasoning_idx + len("Reasoning:"):action_idx].strip()
206
- if reasoning.startswith('"') and reasoning.endswith('"'):
207
- reasoning = reasoning[1:-1]
208
- elif reasoning_idx != -1:
209
- reasoning = response_content[reasoning_idx + len("Reasoning:"):].strip()
210
- if reasoning.startswith('"') and reasoning.endswith('"'):
211
- reasoning = reasoning[1:-1]
212
-
213
- if action_idx != -1:
214
- action_input_idx = response_content.find("Action Input:", action_idx)
215
- if action_input_idx != -1:
216
- action_part = response_content[action_idx + len("Action:"):action_input_idx].strip()
217
- action = action_part
218
- action_input = response_content[action_input_idx + len("Action Input:"):].strip()
219
- else:
220
- action = response_content[action_idx + len("Action:"):].strip()
221
 
222
- if action.startswith('"') and action.endswith('"'):
223
- action = action[1:-1]
224
- if action_input.startswith('"') and action_input.endswith('"'):
225
- action_input = action_input[1:-1]
 
 
 
 
 
 
 
 
226
 
227
- action = action.split('"', 1)[0].strip()
228
- action_input = action_input.split('"', 1)[0].strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
- return reasoning, action, action_input
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
- # --- Graph Nodes ---
233
- def should_continue(state: AgentState) -> str:
234
- """
235
- Determines if the agent should continue reasoning, use a tool, or end.
236
- Includes a maximum iteration limit to prevent infinite loops.
237
  """
238
- MAX_ITERATIONS = 8 # Set a sensible limit to prevent infinite loops
239
- print(f"DEBUG: Entering should_continue. Iteration: {state['iterations']}. Current context: {state.get('context', {})}")
 
 
 
 
 
 
 
 
 
240
 
241
- if state.get("final_answer") is not None:
242
- print("DEBUG: should_continue -> END (Final Answer set in state)")
243
- return "end"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
- if state["iterations"] >= MAX_ITERATIONS:
246
- print(f"DEBUG: should_continue -> END (Max iterations {MAX_ITERATIONS} reached)")
247
- if not state.get("final_answer"):
248
- state["final_answer"] = "Agent terminated due to maximum iteration limit without finding a conclusive answer."
249
- return "end"
250
 
251
- if state.get("context", {}).get("pending_action"):
252
- print("DEBUG: should_continue -> ACTION (Pending action in context)")
253
- return "action"
254
 
255
- print("DEBUG: should_continue -> REASON (Default to reasoning)")
256
- return "reason"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
- # ====== DOCUMENT PROCESSING SETUP ======
259
- def create_vector_store():
260
  """Create vector store with predefined documents using FAISS"""
261
- documents = [
262
- Document(page_content="The capital of France is Paris.", metadata={"source": "geography"}),
263
- Document(page_content="Python is a popular programming language created by Guido van Rossum.", metadata={"source": "tech"}),
264
- Document(page_content="The Eiffel Tower is located in Paris, France.", metadata={"source": "landmarks"}),
265
- Document(page_content="The highest mountain in New Zealand is Aoraki/Mount Cook.", metadata={"source": "geography"}),
266
- Document(page_content="Wellington is the capital city of New Zealand.", metadata={"source": "geography"}),
267
- ]
268
-
269
- embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
270
-
271
- text_splitter = RecursiveCharacterTextSplitter(
272
- chunk_size=500,
273
- chunk_overlap=100
274
- )
275
- chunks = text_splitter.split_documents(documents)
 
 
 
 
 
 
 
 
 
 
 
276
 
277
- return FAISS.from_documents(
278
- documents=chunks,
279
- embedding=embeddings
280
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
 
 
 
 
 
 
 
 
 
282
  def reasoning_node(state: AgentState) -> AgentState:
283
- """
284
- Node for the agent to analyze the question, determine next steps,
285
- and select tools.
286
- """
287
- # --- Defensive checks at the start of the node ---
288
- if state is None:
289
- raise ValueError("reasoning_node received a None state object.")
290
-
291
- # Ensure context is a dictionary
292
- if not isinstance(state.get("context"), dict):
293
- print("WARNING: state['context'] is not a dictionary on entry to reasoning_node. Re-initializing to empty dict.")
294
- state["context"] = {}
295
 
296
- # Ensure history is a list
297
- if not isinstance(state.get("history"), list):
298
- print("WARNING: state['history'] is not a list on entry to reasoning_node. Re-initializing to empty list.")
299
- state["history"] = []
300
-
301
- # Ensure tools is a list
302
- if not isinstance(state.get("tools"), list):
303
- print("WARNING: state['tools'] is not a list on entry to reasoning_node. This might cause issues downstream.")
304
- # If tools become None or corrupted, the tool_descriptions part will fail.
305
- # It's better to log and proceed, assuming agent init sets them correctly.
306
-
307
- print(f"DEBUG: Entering reasoning_node. Iteration: {state['iterations']}")
308
- # Use .get() for safety when printing history length
309
- print(f"DEBUG: Current history length: {len(state.get('history', []))}")
310
-
311
- # Set defaults for state components that might be missing, although TypedDict implies presence
312
- state.setdefault("context", {}) # Redundant if check above re-initializes, but harmless
313
  state.setdefault("reasoning", "")
314
  state.setdefault("iterations", 0)
315
  state.setdefault("current_task", "Understand the question and plan the next step.")
316
  state.setdefault("current_thoughts", "")
317
-
318
- state["iterations"] += 1
319
- if state["iterations"] > should_continue.__defaults__[0]:
320
- print(f"DEBUG: Max iterations reached in reasoning_node. Exiting gracefully.")
321
- state["final_answer"] = "Agent halted due to exceeding maximum allowed reasoning iterations."
322
- return state
323
-
324
- # Now that context is guaranteed a dict, this is safe
325
  state["context"].pop("pending_action", None)
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,
338
- tokenizer=tokenizer,
339
- max_new_tokens=1024,
340
- temperature=0.1,
341
- do_sample=True,
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 and that items are not None
348
  tool_descriptions = "\n".join([
349
- f"- **{t.name}**: {t.description}" for t in state.get("tools", []) if t is not None
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
- state["final_answer"] = "Internal error: Vector store not available."
360
- return state
361
-
362
- # Ensure question is a string for similarity_search
363
- query_for_docs = state["question"] if isinstance(state.get("question"), str) else str(state["question"])
364
- relevant_docs = vector_store.similarity_search(
365
- query_for_docs,
366
- k=3
367
- )
368
 
369
- # Filter out any None documents before joining page_content
370
- rag_context = "\n\n[Relevant Knowledge]\n"
371
- rag_context += "\n---\n".join([doc.page_content for doc in relevant_docs if doc is not None])
372
-
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
- system_prompt_template = (
 
375
  "You are an expert problem solver, designed to provide concise and accurate answers. "
376
  "Your process involves analyzing the question, intelligently selecting and using tools, "
377
  "and synthesizing information.\n\n"
378
  "**Available Tools:**\n"
379
  f"{tool_descriptions}\n\n"
380
  "**Tool Usage Guidelines:**\n"
381
- "- Use **duckduckgo_search** for current events, general facts, or quick lookups. Provide a concise search query. Example: `What is the population of New York?`\n"
382
- "- Use **wikipedia_search** for encyclopedic information, historical context, or detailed topics. Provide a concise search term. Example: `Eiffel Tower history`\n"
383
- "- Use **arxiv_search** for scientific papers, research, or cutting-edge technical information. Provide a concise search query. Example: `Large Language Models recent advances`\n"
384
- "- Use **document_qa** when the question explicitly refers to a specific document or when you have content to query. Input format: 'document_text||question'. Example: `The capital of France is Paris.||What is the capital of France?`\n"
385
- "- Use **python_execution** for complex calculations, data manipulation, or logical operations that cannot be done with simple reasoning. Always provide the full Python code, ensuring it's valid and executable, and assign the final result to a variable named '_result_value'. Example: `_result_value = 1 + 1`\n"
386
- "- Use **transcript_video** for any question involving video or audio content (e.g., YouTube). Provide the full YouTube URL or video ID. Example: `youtube.com`\n\n"
387
- "**Crucial Instructions:**\n"
388
- "1. **Always aim to provide a definitive answer.** If you have enough information, use the 'final answer' action.\n"
389
- "2. **To provide a final answer, use the Action 'final answer' with the complete answer in 'Action Input'.** This is how you tell me you're done. Example:\n"
390
- " ```json\n"
391
- " {\n"
392
- " \"Reasoning\": \"I have found the capital of France.\",\n"
393
- " \"Action\": \"final answer\",\n"
394
- " \"Action Input\": \"The capital of France is Paris.\"\n"
395
- " }\n"
396
- " ```\n"
397
- "3. **If you need more information or cannot answer yet, select an appropriate tool and provide a clear, concise query.**\n"
398
- "4. **Think step-by-step.** Reflect on previous tool outputs and the question.\n"
399
- "5. **Do NOT repeat actions or search queries unless the previous attempt yielded an error.**\n\n"
400
  "**Retrieved Context:**\n{rag_context}\n\n"
401
- "**Current Context (Tool Outputs/Intermediate Info):**\n{context}\n\n"
402
  "**Previous Reasoning Steps:**\n{reasoning}\n\n"
403
  "**Current Task:** {current_task}\n"
404
  "**Current Thoughts:** {current_thoughts}\n\n"
405
- "**Question:** {question}\n\n"
406
- "**Expected JSON Output Format:**\n"
407
  "```json\n"
408
  "{\n"
409
- " \"Reasoning\": \"Your reasoning process to decide the next step, including why a tool is chosen or how an answer is derived.\",\n"
410
- " \"Action\": \"The name of the tool to use (e.g., duckduckgo_search, final answer, No Action), if no tool is needed yet, use 'No Action'.\",\n"
411
- " \"Action Input\": \"The input for the tool (e.g., 'What is the capital of France?', 'The final answer is Paris.').\"\n"
412
  "}\n"
413
  "```\n"
414
- "Ensure your response is ONLY valid JSON and strictly follows this format. Begin your response with ````json`."
 
 
415
  )
416
 
 
417
  prompt = ChatPromptTemplate.from_messages([
418
- SystemMessage(content=system_prompt_template),
419
- *state["history"] # This assumes state["history"] is a list. The check at the start of the node handles if it's None.
420
  ])
421
 
 
422
  formatted_messages = prompt.format_messages(
423
  rag_context=rag_context,
424
- context=state["context"],
425
- reasoning=state["reasoning"],
426
- question=state["question"],
427
- current_task=state["current_task"],
428
- current_thoughts=state["current_thoughts"]
429
  )
430
 
431
- # Filter out any None messages if they somehow appeared before tokenization
432
- filtered_messages = [msg for msg in formatted_messages if msg is not None]
433
-
434
  try:
435
  full_input_string = tokenizer.apply_chat_template(
436
- filtered_messages,
437
- tokenize=False,
438
  add_generation_prompt=True
439
  )
440
  except Exception as e:
441
- print(f"WARNING: Failed to apply chat template: {e}. Falling back to simple string join. Model performance may be affected.")
442
- full_input_string = "\n".join([msg.content for msg in filtered_messages if msg is not None])
443
 
444
- def call_with_retry_local(inputs, retries=3):
 
445
  for attempt in range(retries):
446
  try:
447
- response_text = llm.invoke(inputs)
448
- if response_text is None:
449
- raise ValueError("LLM invoke returned None response_text.")
450
 
451
- content = response_text.replace(inputs, "").strip() if isinstance(response_text, str) else str(response_text).replace(inputs, "").strip()
 
 
 
 
452
 
453
  print(f"DEBUG: RAW LOCAL LLM Response (Attempt {attempt+1}):\n---\n{content}\n---")
454
-
455
- reasoning, action, action_input = parse_agent_response(content)
 
456
  return AIMessage(content=content)
457
- except Exception as e:
458
- print(f"[Retry {attempt+1}/{retries}] Local LLM returned invalid content or an error. Error: {e}. Retrying...")
459
- safe_content_preview = content[:200] if isinstance(content, str) else "Content was not a string or is None."
460
- print(f"Invalid content (partial): {safe_content_preview}...")
461
- 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}"))
462
- time.sleep(5)
463
- raise RuntimeError("Failed after multiple retries due to local Hugging Face model issues or invalid JSON.")
464
-
465
- response = call_with_retry_local(full_input_string)
466
- content = response.content
467
-
468
- if not content.startswith("[Parsing Error]") and not content.startswith("[Local LLM Error]"):
469
- state["history"].append(AIMessage(content=content))
470
-
471
- state["reasoning"] += f"\nStep {state['iterations']}: {reasoning}"
472
- state["current_thoughts"] = reasoning
473
-
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
- if action and action != "No Action":
483
- state["history"].append(AIMessage(content=f"Agent decided to use tool: {action} with input: {action_input}"))
484
- elif action == "No Action":
485
- state["history"].append(AIMessage(content=f"Agent decided to take 'No Action' but needs to proceed."))
486
- if not state.get("final_answer"):
487
- state["current_task"] = "Re-evaluate the situation and attempt to find a final answer or a new tool."
488
- state["current_thoughts"] = "The previous step resulted in 'No Action'. I need to re-think my next step."
489
- state["context"].pop("pending_action", None)
490
 
491
  print(f"DEBUG: Exiting reasoning_node. New history length: {len(state['history'])}")
492
  return state
493
 
 
494
  def tool_node(state: AgentState) -> AgentState:
495
- """
496
- Node for executing the chosen tool and returning its output.
497
- """
498
- # --- Defensive checks at the start of the node ---
499
- if state is None:
500
- raise ValueError("tool_node received a None state object.")
501
 
502
- # Ensure context is a dictionary
503
- if not isinstance(state.get("context"), dict):
504
- print("WARNING: state['context'] is not a dictionary on entry to tool_node. Re-initializing to empty dict.")
505
- state["context"] = {}
506
-
507
- # Ensure history is a list
508
- if not isinstance(state.get("history"), list):
509
- print("WARNING: state['history'] is not a list on entry to tool_node. Re-initializing to empty list.")
510
- state["history"] = []
511
 
512
- print(f"DEBUG: Entering tool_node. Iteration: {state['iterations']}")
513
-
514
- # Safely access tool_call_dict. Context is guaranteed to be a dict here.
515
- tool_call_dict = state["context"].pop("pending_action", None)
516
-
517
- if tool_call_dict is None:
518
- error_message = "[Tool Error] No pending_action found in context. This indicates an issue with graph flow or a previous error."
519
  print(f"ERROR: {error_message}")
520
- state["history"].append(AIMessage(content=error_message))
521
- state["current_task"] = "Re-evaluate the situation; previous tool selection failed or was missing."
522
- state["current_thoughts"] = "No tool action was found. I need to re-think my next step."
523
  return state
524
 
525
- tool_name = tool_call_dict.get("tool")
526
- tool_input = tool_call_dict.get("input")
527
 
528
- if not tool_name or tool_input is None:
529
- 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'."
530
- print(f"ERROR: {error_message}")
531
  state["history"].append(AIMessage(content=error_message))
532
- state["context"].pop("pending_action", None)
533
  return state
534
 
 
535
  available_tools = state.get("tools", [])
536
- # Filter out any None tools before iterating
537
- tool_fn = next((t for t in available_tools if t is not None and t.name == tool_name), None)
538
-
539
- tool_output = ""
540
 
541
  if tool_fn is None:
542
- 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])}"
543
  print(f"ERROR: {tool_output}")
544
  else:
545
  try:
546
  print(f"DEBUG: Invoking tool '{tool_name}' with input: '{tool_input[:100]}...'")
547
- raw_tool_output = tool_fn.run(tool_input)
548
- if raw_tool_output is None or raw_tool_output is False or raw_tool_output == "":
549
- tool_output = f"[{tool_name} output] No specific result found for '{tool_input}'. The tool might have returned an empty response."
550
- else:
551
- tool_output = f"[{tool_name} output]\n{raw_tool_output}"
552
  except Exception as e:
553
- tool_output = f"[Tool Error] An error occurred while running '{tool_name}': {str(e)}"
554
  print(f"ERROR: {tool_output}")
555
 
556
- state["history"].append(AIMessage(content=tool_output))
557
-
558
- print(f"DEBUG: Exiting tool_node. Tool output added to history. New history length: {len(state['history'])}")
559
  return state
560
 
561
- # ====== Agent Graph ======
562
- def create_agent_workflow(tools: List[BaseTool]):
563
- workflow = StateGraph(AgentState)
564
-
565
- workflow.add_node("reason", reasoning_node)
566
- workflow.add_node("action", tool_node)
567
-
568
- workflow.set_entry_point("reason")
569
-
570
- workflow.add_conditional_edges(
571
- "reason",
572
- should_continue,
573
- {
574
- "action": "action",
575
- "reason": "reason",
576
- "end": END
577
- }
578
- )
579
-
580
- workflow.add_edge("action", "reason")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
 
582
- app = workflow.compile()
583
- return app
584
 
585
- # ====== Agent Interface ======
586
  class BasicAgent:
587
  def __init__(self):
 
588
  self.tools = [
589
- DuckDuckGoSearchTool(),
590
- WikipediaSearchTool(),
591
- ArxivSearchTool(),
592
- DocumentQATool(),
593
- PythonExecutionTool(),
594
- VideoTranscriptionTool()
595
  ]
596
-
597
- self.vector_store = create_vector_store()
 
 
 
 
 
 
 
 
598
  self.workflow = create_agent_workflow(self.tools)
599
-
600
  def __call__(self, question: str) -> str:
601
- print(f"\n--- Agent received question: {question[:50]}{'...' if len(question) > 50 else ''} ---")
602
 
603
  state = {
604
  "question": question,
@@ -611,34 +565,48 @@ class BasicAgent:
611
  "final_answer": None,
612
  "current_task": "Understand the question and plan the next step.",
613
  "current_thoughts": "",
614
- "tools": self.tools
615
  }
616
 
617
  try:
618
- final_state = self.workflow.invoke(state, {"recursion_limit": 20})
619
-
620
- # It's highly unlikely final_state would be None if invoke completes,
621
- # but this check is harmless and covers an extreme edge case.
622
- if final_state is None:
623
- return "Agent workflow completed but returned a None state. This is unexpected."
624
-
625
  if final_state.get("final_answer") is not None:
626
  answer = final_state["final_answer"]
627
  print(f"--- Agent returning FINAL ANSWER: {answer} ---")
628
  return answer
629
  else:
630
- print(f"--- ERROR: Agent finished without setting 'final_answer' for question: {question} ---")
631
- current_history = final_state.get("history", []) # Safely get history
632
-
633
- if current_history:
634
- last_message = current_history[-1].content
635
- print(f"Last message in history: {last_message}")
636
- return f"Agent could not fully answer. Last message: {last_message}"
637
- else:
638
- return "Agent finished without providing a final answer and no history messages."
639
  except Exception as e:
640
- print(f"--- FATAL ERROR during agent execution: {e} ---")
641
- return f"An unexpected error occurred during agent execution: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
 
643
 
644
 
 
1
+
2
  import os
3
  import gradio as gr
4
  import requests
 
16
  from langchain_community.utilities import WikipediaAPIWrapper
17
  from langchain_community.document_loaders import ArxivLoader
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  # (Keep Constants as is)
21
  # --- Constants ---
 
31
  from typing import TypedDict, List, Union, Any, Dict, Optional
32
 
33
  # LangChain and LangGraph imports
34
+ from langchain.schema import HumanMessage, AIMessage, SystemMessage
35
+ from langchain.prompts import ChatPromptTemplate
36
  from langgraph.graph import StateGraph, END
37
  from langchain_community.llms import HuggingFacePipeline
38
 
 
50
  from transformers import pipeline as hf_pipeline # Renamed to avoid clash with main pipeline
51
  from youtube_transcript_api import YouTubeTranscriptApi
52
 
 
 
 
 
53
  # --- Helper function for python_execution tool ---
54
  def indent_code(code: str, indent: str = " ") -> str:
55
  """Indents multi-line code for execution within a function."""
56
  return "\n".join(indent + line for line in code.splitlines())
57
 
58
  # --- Tool Definitions ---
59
+ @tool
60
+ def duckduckgo_search(query: str) -> str:
61
+ """Search web using DuckDuckGo. Returns top 3 results."""
62
+ print(f"DEBUG: duckduckgo_search called with: {query}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  try:
64
+ with DDGS() as ddgs:
65
+ return "\n\n".join(
66
+ f"Title: {res['title']}\nURL: {res['href']}\nSnippet: {res['body']}"
67
+ for res in ddgs.text(query, max_results=3)
68
+ )
69
+ except Exception as e:
70
+ return f"Error performing DuckDuckGo search: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
+ @tool
73
+ def wikipedia_search(query: str) -> str:
74
+ """Get Wikipedia summaries. Returns first 3 sentences."""
75
+ print(f"DEBUG: wikipedia_search called with: {query}")
76
+ try:
77
+ return wikipedia.summary(query, sentences=3)
78
+ except wikipedia.DisambiguationError as e:
79
+ return f"Disambiguation options: {', '.join(e.options[:3])}"
80
+ except wikipedia.PageError:
81
+ return "Wikipedia page not found."
82
+ except Exception as e:
83
+ return f"Error performing Wikipedia search: {str(e)}"
84
 
85
+ @tool
86
+ def arxiv_search(query: str) -> str:
87
+ """Search academic papers on arXiv. Returns top 3 results."""
88
+ print(f"DEBUG: arxiv_search called with: {query}")
89
+ try:
90
+ results = arxiv.Search(
91
+ query=query,
92
+ max_results=3,
93
+ sort_by=arxiv.SortCriterion.Relevance
94
+ ).results()
95
+
96
+ return "\n\n".join(
97
+ f"Title: {r.title}\nAuthors: {', '.join(a.name for a in r.authors)}\n"
98
+ f"Published: {r.published.strftime('%Y-%m-%d')}\nSummary: {r.summary[:250]}..."
99
+ for r in results
100
+ )
101
+ except Exception as e:
102
+ return f"Error performing ArXiv search: {str(e)}"
103
 
104
+ @tool
105
+ def document_qa(input_str: str) -> str:
106
+ """Answer questions from documents. Input format: 'document_text||question'"""
107
+ print(f"DEBUG: document_qa called with: {input_str}")
108
+ try:
109
+ if '||' not in input_str:
110
+ return "Invalid format. Input must be: 'document_text||question'"
111
+
112
+ context, question = input_str.split('||', 1)
113
+ # Load QA model on first call or ensure it's loaded once globally.
114
+ # It's better to load once in __init__ for BasicAgent if possible,
115
+ # but this lazy loading prevents initial heavy load if tool is not used.
116
+ qa_model = hf_pipeline('question-answering', model='deepset/roberta-base-squad2')
117
+ return qa_model(question=question, context=context)['answer']
118
+ except Exception as e:
119
+ return f"Error answering question from document: {str(e)}"
120
 
121
+ @tool
122
+ def python_execution(code: str) -> str:
123
+ """Execute Python code and return output.
124
+ The code should assign its final result to a variable named '_result_value'.
125
+ Example: '_result_value = 1 + 1'
126
  """
127
+ print(f"DEBUG: python_execution called with: {code}")
128
+ try:
129
+ # Create isolated environment
130
+ env = {}
131
+ # Wrap code in a function to isolate scope and capture '_result_value'
132
+ # The exec function is used carefully here. In a production environment,
133
+ # consider a more robust and secure sandbox (e.g., Docker, dedicated service).
134
+ exec(f"def __exec_fn__():\n{indent_code(code)}\n_result_value = __exec_fn__()", globals(), env)
135
+ return str(env.get('_result_value', 'No explicit result assigned to "_result_value" variable.'))
136
+ except Exception as e:
137
+ return f"Python execution error: {str(e)}"
138
 
139
+ class VideoTranscriptionTool(BaseTool):
140
+ name: str = "transcript_video"
141
+ # CORRECTED LINE BELOW: Added '=' for assignment
142
+ description: str = "Fetch text transcript from YouTube videos using URL or ID. Use for any question involving video or audio. Input is the YouTube URL or ID."
143
+
144
+ def _run(self, url_or_id: str) -> str:
145
+ print(f"DEBUG: transcript_video called with: {url_or_id}")
146
+ video_id = None
147
+ # Basic parsing for common YouTube URL formats
148
+ if "youtube.com/watch?v=" in url_or_id:
149
+ video_id = url_or_id.split("v=")[1].split("&")[0]
150
+ elif "youtu.be/" in url_or_id:
151
+ video_id = url_or_id.split("youtu.be/")[1].split("?")[0]
152
+ elif len(url_or_id.strip()) == 11 and not ("http://" in url_or_id or "https://" in url_or_id):
153
+ video_id = url_or_id.strip() # Assume it's just the ID
154
+
155
+ if not video_id:
156
+ return f"Invalid or unsupported YouTube URL/ID: {url_or_id}. Please provide a valid YouTube URL or 11-character ID."
157
 
158
+ try:
159
+ transcription = YouTubeTranscriptApi.get_transcript(video_id)
160
+ return " ".join([part['text'] for part in transcription])
 
 
161
 
162
+ except Exception as e:
163
+ return f"Error fetching transcript for video ID '{video_id}': {str(e)}. It might not have an English transcript, or the video is unavailable."
 
164
 
165
+ def _arun(self, *args, **kwargs):
166
+ raise NotImplementedError("Async not supported for this tool.")
167
+
168
+
169
+ # ====== IMPORTS ======
170
+ import json
171
+ import time
172
+ from typing import Dict, List, Tuple, Any, Optional
173
+ from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
174
+ from langchain_core.prompts import ChatPromptTemplate
175
+ from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
176
+ import torch
177
+ from langchain_community.embeddings import HuggingFaceEmbeddings
178
+ from langchain_community.vectorstores import FAISS
179
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
180
+ from langchain_core.documents import Document
181
+
182
+ # ====== TYPE DEFINITIONS ======
183
+ AgentState = Dict[str, Any]
184
 
185
+ # ====== DOCUMENT PROCESSING ======
186
+ def create_vector_store() -> Optional[FAISS]:
187
  """Create vector store with predefined documents using FAISS"""
188
+ try:
189
+ # Define the documents
190
+ documents = [
191
+ Document(page_content="The capital of France is Paris.", metadata={"source": "geography"}),
192
+ Document(page_content="Python is a popular programming language created by Guido van Rossum.", metadata={"source": "tech"}),
193
+ Document(page_content="The Eiffel Tower is located in Paris, France.", metadata={"source": "landmarks"}),
194
+ ]
195
+
196
+ # Initialize embedding model
197
+ embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
198
+
199
+ # Split documents into chunks
200
+ text_splitter = RecursiveCharacterTextSplitter(
201
+ chunk_size=500,
202
+ chunk_overlap=100
203
+ )
204
+ chunks = text_splitter.split_documents(documents)
205
+
206
+ # Create FAISS vector store
207
+ return FAISS.from_documents(
208
+ documents=chunks,
209
+ embedding=embeddings
210
+ )
211
+ except Exception as e:
212
+ print(f"ERROR creating vector store: {str(e)}")
213
+ return None
214
 
215
+ # ====== AGENT HELPER FUNCTIONS ======
216
+ def parse_agent_response(content: str) -> Tuple[str, str, str]:
217
+ """Parse the agent's JSON response"""
218
+ try:
219
+ # Extract JSON from content
220
+ json_start = content.find('{')
221
+ json_end = content.rfind('}') + 1
222
+ json_str = content[json_start:json_end]
223
+
224
+ # Parse JSON
225
+ response_dict = json.loads(json_str)
226
+
227
+ reasoning = response_dict.get("Reasoning", "No reasoning provided")
228
+ action = response_dict.get("Action", "No action specified")
229
+ action_input = response_dict.get("Action Input", "No input provided")
230
+
231
+ return reasoning, action, action_input
232
+ except json.JSONDecodeError:
233
+ print(f"WARNING: Failed to parse JSON from response: {content[:200]}...")
234
+ return "Failed to parse response", "Final Answer", "Error: Could not parse agent response"
235
+ except Exception as e:
236
+ print(f"ERROR parsing agent response: {str(e)}")
237
+ return "Error in parsing", "Final Answer", f"Internal error: {str(e)}"
238
 
239
+ def should_continue(state: AgentState) -> str:
240
+ """Determine if we should continue processing or end"""
241
+ if state.get("final_answer"):
242
+ return "end"
243
+ if state.get("context", {}).get("pending_action"):
244
+ return "action"
245
+ return "reason"
246
+
247
+ # ====== REASONING NODE ======
248
  def reasoning_node(state: AgentState) -> AgentState:
249
+ """Node for analyzing questions and determining next steps"""
250
+ print(f"DEBUG: Entering reasoning_node. Iteration: {state.get('iterations', 0)}")
 
 
 
 
 
 
 
 
 
 
251
 
252
+ # Safely initialize state components
253
+ state.setdefault("context", {})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  state.setdefault("reasoning", "")
255
  state.setdefault("iterations", 0)
256
  state.setdefault("current_task", "Understand the question and plan the next step.")
257
  state.setdefault("current_thoughts", "")
258
+ state.setdefault("history", [])
259
+
260
+ # Safely remove pending_action
 
 
 
 
 
261
  state["context"].pop("pending_action", None)
262
 
263
+ # Initialize local HuggingFacePipeline
264
  model_name = "mistralai/Mistral-7B-Instruct-v0.2"
265
+
266
+ try:
267
+ print(f"DEBUG: Loading local model: {model_name}...")
268
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
269
+
270
+ # Determine torch dtype based on available hardware
271
+ if torch.cuda.is_available():
272
+ torch_dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
273
+ else:
274
+ torch_dtype = torch.float32
275
+
276
+ model = AutoModelForCausalLM.from_pretrained(
277
+ model_name,
278
+ torch_dtype=torch_dtype,
279
+ device_map="auto"
280
+ )
281
+
282
+ # Create transformers pipeline
283
+ pipe = pipeline(
284
+ "text-generation",
285
+ model=model,
286
+ tokenizer=tokenizer,
287
+ max_new_tokens=1024,
288
+ temperature=0.1,
289
+ do_sample=True,
290
+ top_p=0.9,
291
+ repetition_penalty=1.1,
292
+ )
293
+
294
+ llm = HuggingFacePipeline(pipeline=pipe)
295
+ except Exception as e:
296
+ print(f"ERROR loading model: {str(e)}")
297
+ state["history"].append(AIMessage(content=f"[ERROR] Failed to load model: {str(e)}"))
298
+ state["final_answer"] = "Error: Failed to initialize language model"
299
+ return state
300
 
301
+ # Prepare tool descriptions
302
  tool_descriptions = "\n".join([
303
+ f"- **{t.name}**: {t.description}" for t in state.get("tools", [])
304
  ])
305
 
306
+ # RAG Retrieval
307
+ rag_context = ""
 
 
308
  vector_store = state["context"].get("vector_store")
 
 
 
 
 
 
 
 
 
 
 
309
 
310
+ if vector_store:
311
+ try:
312
+ # Perform retrieval
313
+ relevant_docs = vector_store.similarity_search(
314
+ state.get("question", ""),
315
+ k=3
316
+ )
317
+
318
+ # Format context for LLM
319
+ rag_context = "\n\n[Relevant Knowledge]\n"
320
+ rag_context += "\n---\n".join([doc.page_content for doc in relevant_docs])
321
+ except Exception as e:
322
+ print(f"WARNING: RAG retrieval failed: {str(e)}")
323
+ rag_context = "\n\n[Relevant Knowledge] Retrieval failed. Proceeding without additional context."
324
+ else:
325
+ print("WARNING: No vector store available for RAG")
326
+ rag_context = "\n\n[Relevant Knowledge] No knowledge base available."
327
 
328
+ # Enhanced system prompt with RAG context
329
+ system_prompt = (
330
  "You are an expert problem solver, designed to provide concise and accurate answers. "
331
  "Your process involves analyzing the question, intelligently selecting and using tools, "
332
  "and synthesizing information.\n\n"
333
  "**Available Tools:**\n"
334
  f"{tool_descriptions}\n\n"
335
  "**Tool Usage Guidelines:**\n"
336
+ "- Use **duckduckgo_search** for current events, general facts, or quick lookups. Provide a concise search query.\n"
337
+ "- Use **wikipedia_search** for encyclopedic information, historical context, or detailed topics. Provide a concise search term.\n"
338
+ "- Use **arxiv_search** for scientific papers, research, or cutting-edge technical information. Provide a concise search query.\n"
339
+ "- Use **document_qa** when the question explicitly refers to a specific document or when you have content to query. Input format: 'document_text||question'.\n"
340
+ "- Use **python_execution** for complex calculations, data manipulation, or logical operations that cannot be done with simple reasoning. Always provide the full Python code, ensuring it's valid and executable, and assign the final result to a variable named '_result_value' (e.g., '_result_value = 1 + 1').\n"
341
+ "- Use **transcript_video** for any question involving video or audio content (e.g., YouTube). Provide the full YouTube URL or video ID.\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  "**Retrieved Context:**\n{rag_context}\n\n"
343
+ "**Current Context:**\n{context}\n\n"
344
  "**Previous Reasoning Steps:**\n{reasoning}\n\n"
345
  "**Current Task:** {current_task}\n"
346
  "**Current Thoughts:** {current_thoughts}\n\n"
347
+ "**Your Response MUST be a valid JSON object with the following keys:**\n"
 
348
  "```json\n"
349
  "{\n"
350
+ " \"Reasoning\": \"Your detailed analysis of the question and why you chose a specific action. Focus on the logical steps.\",\n"
351
+ " \"Action\": \"[Tool name OR 'Final Answer']\",\n"
352
+ " \"Action Input\": \"[Input for the selected tool OR the complete final answer]\"\n"
353
  "}\n"
354
  "```\n"
355
+ "**CRITICAL RULE: 'Action' and 'Action Input' MUST NOT be empty strings, unless 'Action' is 'Final Answer' and 'Action Input' is the conclusive response.**\n"
356
+ "If you cannot determine a suitable tool or a conclusive final answer after exhausting options, return Action: 'Final Answer' with a message like 'I cannot answer this question with the available tools.' or 'More information is needed.'\n"
357
+ "Ensure 'Action Input' is always the complete, valid input for the chosen 'Action'. If 'Action' is 'Final Answer', provide the complete, concise answer."
358
  )
359
 
360
+ # Create prompt
361
  prompt = ChatPromptTemplate.from_messages([
362
+ SystemMessage(content=system_prompt),
363
+ *state["history"]
364
  ])
365
 
366
+ # Format messages safely
367
  formatted_messages = prompt.format_messages(
368
  rag_context=rag_context,
369
+ context=state.get("context", {}),
370
+ reasoning=state.get("reasoning", ""),
371
+ question=state.get("question", ""),
372
+ current_task=state.get("current_task", ""),
373
+ current_thoughts=state.get("current_thoughts", "")
374
  )
375
 
376
+ # Format full input string
 
 
377
  try:
378
  full_input_string = tokenizer.apply_chat_template(
379
+ formatted_messages,
380
+ tokenize=False,
381
  add_generation_prompt=True
382
  )
383
  except Exception as e:
384
+ print(f"WARNING: Failed to apply chat template: {e}. Using simple join.")
385
+ full_input_string = "\n".join([msg.content for msg in formatted_messages])
386
 
387
+ # Call LLM with retry
388
+ def call_with_retry_local(inputs: str, retries: int = 3) -> AIMessage:
389
  for attempt in range(retries):
390
  try:
391
+ response_text = llm.invoke(inputs)
 
 
392
 
393
+ # Strip the prompt from the generated text
394
+ if response_text.startswith(inputs):
395
+ content = response_text[len(inputs):].strip()
396
+ else:
397
+ content = response_text.strip()
398
 
399
  print(f"DEBUG: RAW LOCAL LLM Response (Attempt {attempt+1}):\n---\n{content}\n---")
400
+
401
+ # Attempt to parse to validate structure
402
+ json.loads(content)
403
  return AIMessage(content=content)
404
+ except json.JSONDecodeError as e:
405
+ print(f"[Retry {attempt+1}/{retries}] Invalid JSON. Error: {e}.")
406
+ print(f"Invalid content: {content[:200]}...")
407
+ state["history"].append(AIMessage(content=f"[Parsing Error] The previous LLM output was not valid JSON. Expected format: ```json{{\"Reasoning\": \"...\", \"Action\": \"...\", \"Action Input\": \"...\"}}```. Please ensure your response is ONLY valid JSON and strictly follows the format. Error: {e}"))
408
+ time.sleep(3)
409
+ except Exception as e:
410
+ print(f"[Retry {attempt+1}/{retries}] Error: {e}.")
411
+ state["history"].append(AIMessage(content=f"[LLM Error] Failed to get response: {e}. Trying again."))
412
+ time.sleep(5)
413
+ return AIMessage(content='{"Reasoning": "Max retries exceeded", "Action": "Final Answer", "Action Input": "Error: Failed after multiple retries"}')
414
+
415
+ response = call_with_retry_local(full_input_string)
416
+ content = response.content
417
+
418
+ # Parse response
419
+ reasoning, action, action_input = parse_agent_response(content)
420
+
421
+ print(f"DEBUG: Parsed Action: '{action}', Action Input: '{action_input[:100]}...'")
422
+
423
+ # Update state
424
+ state["history"].append(AIMessage(content=content))
425
+ state["reasoning"] += f"\nStep {state['iterations'] + 1}: {reasoning}"
426
+ state["iterations"] += 1
427
+ state["current_thoughts"] = reasoning
428
+
429
+ if "final answer" in action.lower():
430
+ state["final_answer"] = action_input
431
  else:
432
  state["context"]["pending_action"] = {
433
  "tool": action,
434
  "input": action_input
435
  }
436
+ state["history"].append(AIMessage(content=f"Agent decided to use tool: {action} with input: {action_input}"))
 
 
 
 
 
 
 
437
 
438
  print(f"DEBUG: Exiting reasoning_node. New history length: {len(state['history'])}")
439
  return state
440
 
441
+ # ====== TOOL NODE ======
442
  def tool_node(state: AgentState) -> AgentState:
443
+ """Node for executing the chosen tool"""
444
+ print(f"DEBUG: Entering tool_node. Iteration: {state.get('iterations', 0)}")
 
 
 
 
445
 
446
+ # Safely get pending action
447
+ tool_call_dict = state.get("context", {}).pop("pending_action", None)
 
 
 
 
 
 
 
448
 
449
+ if not tool_call_dict:
450
+ error_message = "[Tool Error] No pending_action found in context."
 
 
 
 
 
451
  print(f"ERROR: {error_message}")
452
+ state.setdefault("history", []).append(AIMessage(content=error_message))
 
 
453
  return state
454
 
455
+ tool_name = tool_call_dict.get("tool", "")
456
+ tool_input = tool_call_dict.get("input", "")
457
 
458
+ if not tool_name or not tool_input:
459
+ error_message = f"[Tool Error] Invalid action: Tool name '{tool_name}' or input '{tool_input}' was empty."
460
+ print(f"ERROR: {error_message}")
461
  state["history"].append(AIMessage(content=error_message))
 
462
  return state
463
 
464
+ # Find and execute tool
465
  available_tools = state.get("tools", [])
466
+ tool_fn = next((t for t in available_tools if t.name == tool_name), None)
 
 
 
467
 
468
  if tool_fn is None:
469
+ tool_output = f"[Tool Error] Tool '{tool_name}' not found. Available: {', '.join([t.name for t in available_tools])}"
470
  print(f"ERROR: {tool_output}")
471
  else:
472
  try:
473
  print(f"DEBUG: Invoking tool '{tool_name}' with input: '{tool_input[:100]}...'")
474
+ tool_output = tool_fn.run(tool_input)
475
+ if tool_output is None:
476
+ tool_output = f"[{tool_name} output] No result returned for '{tool_input}'."
 
 
477
  except Exception as e:
478
+ tool_output = f"[Tool Error] Error running '{tool_name}': {str(e)}"
479
  print(f"ERROR: {tool_output}")
480
 
481
+ state["history"].append(AIMessage(content=f"[{tool_name} output]\n{tool_output}"))
482
+
483
+ print(f"DEBUG: Exiting tool_node. Tool output added to history.")
484
  return state
485
 
486
+ # ====== AGENT GRAPH ======
487
+ class StateGraph:
488
+ """Simple state graph implementation"""
489
+ def __init__(self, state: AgentState):
490
+ self.nodes = {}
491
+ self.entry_point = None
492
+ self.edges = {}
493
+ self.conditional_edges = {}
494
+
495
+ def add_node(self, name: str, func: callable):
496
+ self.nodes[name] = func
497
+
498
+ def set_entry_point(self, name: str):
499
+ self.entry_point = name
500
+
501
+ def add_conditional_edges(self, source: str, condition: callable, path_map: Dict[str, str]):
502
+ self.conditional_edges[source] = (condition, path_map)
503
+
504
+ def add_edge(self, source: str, dest: str):
505
+ self.edges[source] = dest
506
+
507
+ def compile(self):
508
+ def app(state: AgentState) -> AgentState:
509
+ current_node = self.entry_point
510
+
511
+ while current_node != END:
512
+ if current_node in self.nodes:
513
+ state = self.nodes[current_node](state)
514
+
515
+ if current_node in self.conditional_edges:
516
+ condition, path_map = self.conditional_edges[current_node]
517
+ next_node_key = condition(state)
518
+ current_node = path_map.get(next_node_key, END)
519
+ elif current_node in self.edges:
520
+ current_node = self.edges[current_node]
521
+ else:
522
+ current_node = END
523
+
524
+ return state
525
+
526
+ return app
527
 
528
+ END = "__END__"
 
529
 
530
+ # ====== AGENT INTERFACE ======
531
  class BasicAgent:
532
  def __init__(self):
533
+ # Instantiate tools (implementation not shown - should be defined elsewhere)
534
  self.tools = [
535
+ # duckduckgo_search,
536
+ # wikipedia_search,
537
+ # arxiv_search,
538
+ # document_qa,
539
+ # python_execution,
540
+ # VideoTranscriptionTool()
541
  ]
542
+
543
+ # Pre-initialize RAG vector store
544
+ try:
545
+ self.vector_store = create_vector_store()
546
+ if not self.vector_store:
547
+ print("WARNING: Vector store creation failed. Proceeding without RAG.")
548
+ except Exception as e:
549
+ print(f"ERROR creating vector store: {str(e)}")
550
+ self.vector_store = None
551
+
552
  self.workflow = create_agent_workflow(self.tools)
553
+
554
  def __call__(self, question: str) -> str:
555
+ print(f"\n--- Agent received question: {question[:80]}{'...' if len(question) > 80 else ''} ---")
556
 
557
  state = {
558
  "question": question,
 
565
  "final_answer": None,
566
  "current_task": "Understand the question and plan the next step.",
567
  "current_thoughts": "",
568
+ "tools": self.tools
569
  }
570
 
571
  try:
572
+ final_state = self.workflow.invoke(state)
 
 
 
 
 
 
573
  if final_state.get("final_answer") is not None:
574
  answer = final_state["final_answer"]
575
  print(f"--- Agent returning FINAL ANSWER: {answer} ---")
576
  return answer
577
  else:
578
+ print("--- ERROR: Agent finished without setting 'final_answer' ---")
579
+ if final_state.get("history"):
580
+ last_message = final_state["history"][-1].content
581
+ print(f"Last message: {last_message}")
582
+ return f"Agent could not answer. Last message: {last_message}"
583
+ return "Error: Agent failed to provide an answer"
 
 
 
584
  except Exception as e:
585
+ print(f"FATAL ERROR during agent execution: {str(e)}")
586
+ return f"Agent encountered a fatal error: {str(e)}"
587
+
588
+ def create_agent_workflow(tools: List[Any]):
589
+ workflow = StateGraph(AgentState)
590
+
591
+ workflow.add_node("reason", reasoning_node)
592
+ workflow.add_node("action", tool_node)
593
+
594
+ workflow.set_entry_point("reason")
595
+
596
+ workflow.add_conditional_edges(
597
+ "reason",
598
+ should_continue,
599
+ {
600
+ "action": "action",
601
+ "reason": "reason",
602
+ "end": END
603
+ }
604
+ )
605
+
606
+ workflow.add_edge("action", "reason")
607
+
608
+ app = workflow.compile()
609
+ return app
610
 
611
 
612