Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
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 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
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 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
226 |
|
227 |
-
|
228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
229 |
|
230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
231 |
|
232 |
-
|
233 |
-
def
|
234 |
-
"""
|
235 |
-
|
236 |
-
|
237 |
"""
|
238 |
-
|
239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
240 |
|
241 |
-
|
242 |
-
|
243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
244 |
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
state["final_answer"] = "Agent terminated due to maximum iteration limit without finding a conclusive answer."
|
249 |
-
return "end"
|
250 |
|
251 |
-
|
252 |
-
|
253 |
-
return "action"
|
254 |
|
255 |
-
|
256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
257 |
|
258 |
-
# ====== DOCUMENT PROCESSING
|
259 |
-
def create_vector_store():
|
260 |
"""Create vector store with predefined documents using FAISS"""
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
276 |
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
282 |
def reasoning_node(state: AgentState) -> AgentState:
|
283 |
-
"""
|
284 |
-
|
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 |
-
#
|
297 |
-
|
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 |
-
|
319 |
-
|
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 |
-
|
329 |
-
|
330 |
-
|
331 |
-
model_name
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
346 |
|
347 |
-
#
|
348 |
tool_descriptions = "\n".join([
|
349 |
-
f"- **{t.name}**: {t.description}" for t in state.get("tools", [])
|
350 |
])
|
351 |
|
352 |
-
|
353 |
-
|
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 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
373 |
|
374 |
-
|
|
|
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
|
382 |
-
"- Use **wikipedia_search** for encyclopedic information, historical context, or detailed topics. Provide a concise search term
|
383 |
-
"- Use **arxiv_search** for scientific papers, research, or cutting-edge technical information. Provide a concise search query
|
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'
|
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'.
|
386 |
-
"- Use **transcript_video** for any question involving video or audio content (e.g., YouTube). Provide the full YouTube URL or video ID
|
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
|
402 |
"**Previous Reasoning Steps:**\n{reasoning}\n\n"
|
403 |
"**Current Task:** {current_task}\n"
|
404 |
"**Current Thoughts:** {current_thoughts}\n\n"
|
405 |
-
"**
|
406 |
-
"**Expected JSON Output Format:**\n"
|
407 |
"```json\n"
|
408 |
"{\n"
|
409 |
-
" \"Reasoning\": \"Your
|
410 |
-
" \"Action\": \"
|
411 |
-
" \"Action Input\": \"
|
412 |
"}\n"
|
413 |
"```\n"
|
414 |
-
"
|
|
|
|
|
415 |
)
|
416 |
|
|
|
417 |
prompt = ChatPromptTemplate.from_messages([
|
418 |
-
SystemMessage(content=
|
419 |
-
*state["history"]
|
420 |
])
|
421 |
|
|
|
422 |
formatted_messages = prompt.format_messages(
|
423 |
rag_context=rag_context,
|
424 |
-
context=state
|
425 |
-
reasoning=state
|
426 |
-
question=state
|
427 |
-
current_task=state
|
428 |
-
current_thoughts=state
|
429 |
)
|
430 |
|
431 |
-
#
|
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 |
-
|
437 |
-
tokenize=False,
|
438 |
add_generation_prompt=True
|
439 |
)
|
440 |
except Exception as e:
|
441 |
-
print(f"WARNING: Failed to apply chat template: {e}.
|
442 |
-
full_input_string = "\n".join([msg.content for msg in
|
443 |
|
444 |
-
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
452 |
|
453 |
print(f"DEBUG: RAW LOCAL LLM Response (Attempt {attempt+1}):\n---\n{content}\n---")
|
454 |
-
|
455 |
-
|
|
|
456 |
return AIMessage(content=content)
|
457 |
-
except
|
458 |
-
print(f"[Retry {attempt+1}/{retries}]
|
459 |
-
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
477 |
else:
|
478 |
state["context"]["pending_action"] = {
|
479 |
"tool": action,
|
480 |
"input": action_input
|
481 |
}
|
482 |
-
|
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 |
-
|
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 |
-
#
|
503 |
-
|
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 |
-
|
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
|
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
|
529 |
-
error_message = f"[Tool Error] Invalid action
|
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 |
-
|
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
|
543 |
print(f"ERROR: {tool_output}")
|
544 |
else:
|
545 |
try:
|
546 |
print(f"DEBUG: Invoking tool '{tool_name}' with input: '{tool_input[:100]}...'")
|
547 |
-
|
548 |
-
if
|
549 |
-
tool_output = f"[{tool_name} output] No
|
550 |
-
else:
|
551 |
-
tool_output = f"[{tool_name} output]\n{raw_tool_output}"
|
552 |
except Exception as e:
|
553 |
-
tool_output = f"[Tool Error]
|
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.
|
559 |
return state
|
560 |
|
561 |
-
# ======
|
562 |
-
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
|
572 |
-
|
573 |
-
|
574 |
-
|
575 |
-
|
576 |
-
|
577 |
-
|
578 |
-
|
579 |
-
|
580 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
581 |
|
582 |
-
|
583 |
-
return app
|
584 |
|
585 |
-
# ======
|
586 |
class BasicAgent:
|
587 |
def __init__(self):
|
|
|
588 |
self.tools = [
|
589 |
-
|
590 |
-
|
591 |
-
|
592 |
-
|
593 |
-
|
594 |
-
VideoTranscriptionTool()
|
595 |
]
|
596 |
-
|
597 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
598 |
self.workflow = create_agent_workflow(self.tools)
|
599 |
-
|
600 |
def __call__(self, question: str) -> str:
|
601 |
-
print(f"\n--- Agent received question: {question[:
|
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
|
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(
|
631 |
-
|
632 |
-
|
633 |
-
|
634 |
-
|
635 |
-
|
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"
|
641 |
-
return f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|