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