Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,642 +1,78 @@
|
|
1 |
-
|
2 |
import os
|
|
|
|
|
3 |
import gradio as gr
|
4 |
-
import requests
|
5 |
-
import inspect
|
6 |
import pandas as pd
|
7 |
-
|
8 |
-
from
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
from
|
17 |
-
|
18 |
-
|
|
|
|
|
|
|
|
|
19 |
|
20 |
-
# (Keep Constants as is)
|
21 |
# --- Constants ---
|
22 |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
23 |
|
24 |
-
#
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
from langchain_community.vectorstores import FAISS
|
45 |
-
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
46 |
-
from langchain_core.documents import Document
|
47 |
-
from langchain_community.llms import HuggingFacePipeline
|
48 |
-
from langchain.prompts import ChatPromptTemplate # SystemMessage moved to langchain_core.messages
|
49 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
|
50 |
-
from langgraph.graph import END, StateGraph
|
51 |
-
|
52 |
-
# Corrected Tool import: Use 'tool' (lowercase)
|
53 |
-
from langchain_core.tools import BaseTool, tool
|
54 |
-
|
55 |
-
# Hugging Face local model imports
|
56 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
|
57 |
-
import torch
|
58 |
-
|
59 |
-
# Tool-specific imports
|
60 |
-
from duckduckgo_search import DDGS
|
61 |
-
import wikipedia
|
62 |
-
import arxiv
|
63 |
-
from transformers import pipeline as hf_pipeline # Renamed to avoid clash with main pipeline
|
64 |
-
from youtube_transcript_api import YouTubeTranscriptApi
|
65 |
-
|
66 |
-
# --- Helper function for python_execution tool ---
|
67 |
-
def indent_code(code: str, indent: str = " ") -> str:
|
68 |
-
"""Indents multi-line code for execution within a function."""
|
69 |
-
return "\n".join(indent + line for line in code.splitlines())
|
70 |
-
|
71 |
-
# --- Tool Definitions ---
|
72 |
-
@tool
|
73 |
-
def duckduckgo_search(query: str) -> str:
|
74 |
-
"""Search web using DuckDuckGo. Returns top 3 results."""
|
75 |
-
print(f"DEBUG: duckduckgo_search called with: {query}")
|
76 |
-
try:
|
77 |
-
with DDGS() as ddgs:
|
78 |
-
return "\n\n".join(
|
79 |
-
f"Title: {res['title']}\nURL: {res['href']}\nSnippet: {res['body']}"
|
80 |
-
for res in ddgs.text(query, max_results=3)
|
81 |
-
)
|
82 |
-
except Exception as e:
|
83 |
-
return f"Error performing DuckDuckGo search: {str(e)}"
|
84 |
-
|
85 |
-
@tool
|
86 |
-
def wikipedia_search(query: str) -> str:
|
87 |
-
"""Get Wikipedia summaries. Returns first 3 sentences."""
|
88 |
-
print(f"DEBUG: wikipedia_search called with: {query}")
|
89 |
-
try:
|
90 |
-
return wikipedia.summary(query, sentences=3)
|
91 |
-
except wikipedia.DisambiguationError as e:
|
92 |
-
return f"Disambiguation options: {', '.join(e.options[:3])}"
|
93 |
-
except wikipedia.PageError:
|
94 |
-
return "Wikipedia page not found."
|
95 |
-
except Exception as e:
|
96 |
-
return f"Error performing Wikipedia search: {str(e)}"
|
97 |
-
|
98 |
-
@tool
|
99 |
-
def arxiv_search(query: str) -> str:
|
100 |
-
"""Search academic papers on arXiv. Returns top 3 results."""
|
101 |
-
print(f"DEBUG: arxiv_search called with: {query}")
|
102 |
-
try:
|
103 |
-
results = arxiv.Search(
|
104 |
-
query=query,
|
105 |
-
max_results=3,
|
106 |
-
sort_by=arxiv.SortCriterion.Relevance
|
107 |
-
).results()
|
108 |
-
|
109 |
-
return "\n\n".join(
|
110 |
-
f"Title: {r.title}\nAuthors: {', '.join(a.name for a in r.authors)}\n"
|
111 |
-
f"Published: {r.published.strftime('%Y-%m-%d')}\nSummary: {r.summary[:250]}..."
|
112 |
-
for r in results
|
113 |
-
)
|
114 |
-
except Exception as e:
|
115 |
-
return f"Error performing ArXiv search: {str(e)}"
|
116 |
-
|
117 |
-
@tool
|
118 |
-
def document_qa(input_str: str) -> str:
|
119 |
-
"""Answer questions from documents. Input format: 'document_text||question'"""
|
120 |
-
print(f"DEBUG: document_qa called with: {input_str}")
|
121 |
-
try:
|
122 |
-
if '||' not in input_str:
|
123 |
-
return "Invalid format. Input must be: 'document_text||question'"
|
124 |
-
|
125 |
-
context, question = input_str.split('||', 1)
|
126 |
-
# Load QA model on first call or ensure it's loaded once globally.
|
127 |
-
# It's better to load once in __init__ for BasicAgent if possible,
|
128 |
-
# but this lazy loading prevents initial heavy load if tool is not used.
|
129 |
-
qa_model = hf_pipeline('question-answering', model='deepset/roberta-base-squad2')
|
130 |
-
return qa_model(question=question, context=context)['answer']
|
131 |
-
except Exception as e:
|
132 |
-
return f"Error answering question from document: {str(e)}"
|
133 |
-
|
134 |
-
@tool
|
135 |
-
def python_execution(code: str) -> str:
|
136 |
-
"""Execute Python code and return output.
|
137 |
-
The code should assign its final result to a variable named '_result_value'.
|
138 |
-
Example: '_result_value = 1 + 1'
|
139 |
-
"""
|
140 |
-
print(f"DEBUG: python_execution called with: {code}")
|
141 |
-
try:
|
142 |
-
# Create isolated environment
|
143 |
-
env = {}
|
144 |
-
# Wrap code in a function to isolate scope and capture '_result_value'
|
145 |
-
# The exec function is used carefully here. In a production environment,
|
146 |
-
# consider a more robust and secure sandbox (e.g., Docker, dedicated service).
|
147 |
-
exec(f"def __exec_fn__():\n{indent_code(code)}\n_result_value = __exec_fn__()", globals(), env)
|
148 |
-
return str(env.get('_result_value', 'No explicit result assigned to "_result_value" variable.'))
|
149 |
-
except Exception as e:
|
150 |
-
return f"Python execution error: {str(e)}"
|
151 |
-
|
152 |
-
class VideoTranscriptionTool(Tool):
|
153 |
-
name: str = "transcript_video"
|
154 |
-
# CORRECTED LINE BELOW: Added '=' for assignment
|
155 |
-
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."
|
156 |
-
|
157 |
-
def _run(self, url_or_id: str) -> str:
|
158 |
-
print(f"DEBUG: transcript_video called with: {url_or_id}")
|
159 |
-
video_id = None
|
160 |
-
# Basic parsing for common YouTube URL formats
|
161 |
-
if "youtube.com/watch?v=" in url_or_id:
|
162 |
-
video_id = url_or_id.split("v=")[1].split("&")[0]
|
163 |
-
elif "youtu.be/" in url_or_id:
|
164 |
-
video_id = url_or_id.split("youtu.be/")[1].split("?")[0]
|
165 |
-
elif len(url_or_id.strip()) == 11 and not ("http://" in url_or_id or "https://" in url_or_id):
|
166 |
-
video_id = url_or_id.strip() # Assume it's just the ID
|
167 |
-
|
168 |
-
if not video_id:
|
169 |
-
return f"Invalid or unsupported YouTube URL/ID: {url_or_id}. Please provide a valid YouTube URL or 11-character ID."
|
170 |
-
|
171 |
-
try:
|
172 |
-
transcription = YouTubeTranscriptApi.get_transcript(video_id)
|
173 |
-
return " ".join([part['text'] for part in transcription])
|
174 |
-
|
175 |
-
except Exception as e:
|
176 |
-
return f"Error fetching transcript for video ID '{video_id}': {str(e)}. It might not have an English transcript, or the video is unavailable."
|
177 |
-
|
178 |
-
def _arun(self, *args, **kwargs):
|
179 |
-
raise NotImplementedError("Async not supported for this tool.")
|
180 |
-
|
181 |
-
|
182 |
-
# ====== IMPORTS ======
|
183 |
-
import json
|
184 |
-
import time
|
185 |
-
from typing import Dict, List, Tuple, Any, Optional
|
186 |
-
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
|
187 |
-
from langchain_core.prompts import ChatPromptTemplate
|
188 |
-
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
|
189 |
-
import torch
|
190 |
-
from langchain_community.embeddings import HuggingFaceEmbeddings
|
191 |
-
from langchain_community.vectorstores import FAISS
|
192 |
-
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
193 |
-
from langchain_core.documents import Document
|
194 |
-
|
195 |
-
# ====== TYPE DEFINITIONS ======
|
196 |
-
AgentState = Dict[str, Any]
|
197 |
-
|
198 |
-
# ====== DOCUMENT PROCESSING ======
|
199 |
-
def create_vector_store() -> Optional[FAISS]:
|
200 |
-
"""Create vector store with predefined documents using FAISS"""
|
201 |
-
try:
|
202 |
-
# Define the documents
|
203 |
-
documents = [
|
204 |
-
Document(page_content="The capital of France is Paris.", metadata={"source": "geography"}),
|
205 |
-
Document(page_content="Python is a popular programming language created by Guido van Rossum.", metadata={"source": "tech"}),
|
206 |
-
Document(page_content="The Eiffel Tower is located in Paris, France.", metadata={"source": "landmarks"}),
|
207 |
-
]
|
208 |
-
|
209 |
-
# Initialize embedding model
|
210 |
-
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
|
211 |
-
|
212 |
-
# Split documents into chunks
|
213 |
-
text_splitter = RecursiveCharacterTextSplitter(
|
214 |
-
chunk_size=500,
|
215 |
-
chunk_overlap=100
|
216 |
-
)
|
217 |
-
chunks = text_splitter.split_documents(documents)
|
218 |
-
|
219 |
-
# Create FAISS vector store
|
220 |
-
return FAISS.from_documents(
|
221 |
-
documents=chunks,
|
222 |
-
embedding=embeddings
|
223 |
-
)
|
224 |
-
except Exception as e:
|
225 |
-
print(f"ERROR creating vector store: {str(e)}")
|
226 |
-
return None
|
227 |
-
|
228 |
-
# ====== AGENT HELPER FUNCTIONS ======
|
229 |
-
def parse_agent_response(content: str) -> Tuple[str, str, str]:
|
230 |
-
"""Parse the agent's JSON response"""
|
231 |
-
try:
|
232 |
-
# Extract JSON from content
|
233 |
-
json_start = content.find('{')
|
234 |
-
json_end = content.rfind('}') + 1
|
235 |
-
json_str = content[json_start:json_end]
|
236 |
-
|
237 |
-
# Parse JSON
|
238 |
-
response_dict = json.loads(json_str)
|
239 |
-
|
240 |
-
reasoning = response_dict.get("Reasoning", "No reasoning provided")
|
241 |
-
action = response_dict.get("Action", "No action specified")
|
242 |
-
action_input = response_dict.get("Action Input", "No input provided")
|
243 |
-
|
244 |
-
return reasoning, action, action_input
|
245 |
-
except json.JSONDecodeError:
|
246 |
-
print(f"WARNING: Failed to parse JSON from response: {content[:200]}...")
|
247 |
-
return "Failed to parse response", "Final Answer", "Error: Could not parse agent response"
|
248 |
-
except Exception as e:
|
249 |
-
print(f"ERROR parsing agent response: {str(e)}")
|
250 |
-
return "Error in parsing", "Final Answer", f"Internal error: {str(e)}"
|
251 |
-
|
252 |
-
def should_continue(state: AgentState) -> str:
|
253 |
-
"""Determine if we should continue processing or end"""
|
254 |
-
if state.get("final_answer"):
|
255 |
-
return "end"
|
256 |
-
if state.get("context", {}).get("pending_action"):
|
257 |
-
return "action"
|
258 |
-
return "reason"
|
259 |
-
|
260 |
-
# ====== REASONING NODE ======
|
261 |
-
def reasoning_node(state: AgentState) -> AgentState:
|
262 |
-
"""Node for analyzing questions and determining next steps"""
|
263 |
-
print(f"DEBUG: Entering reasoning_node. Iteration: {state.get('iterations', 0)}")
|
264 |
-
|
265 |
-
# Safely initialize state components
|
266 |
-
state.setdefault("context", {})
|
267 |
-
state.setdefault("reasoning", "")
|
268 |
-
state.setdefault("iterations", 0)
|
269 |
-
state.setdefault("current_task", "Understand the question and plan the next step.")
|
270 |
-
state.setdefault("current_thoughts", "")
|
271 |
-
state.setdefault("history", [])
|
272 |
-
|
273 |
-
# Safely remove pending_action
|
274 |
-
state["context"].pop("pending_action", None)
|
275 |
-
|
276 |
-
# Initialize local HuggingFacePipeline
|
277 |
-
model_name = "mistralai/Mistral-7B-Instruct-v0.2"
|
278 |
-
|
279 |
-
try:
|
280 |
-
print(f"DEBUG: Loading local model: {model_name}...")
|
281 |
-
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
282 |
-
|
283 |
-
# Determine torch dtype based on available hardware
|
284 |
-
if torch.cuda.is_available():
|
285 |
-
torch_dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
|
286 |
-
else:
|
287 |
-
torch_dtype = torch.float32
|
288 |
-
|
289 |
-
model = AutoModelForCausalLM.from_pretrained(
|
290 |
-
model_name,
|
291 |
-
torch_dtype=torch_dtype,
|
292 |
-
device_map="auto"
|
293 |
-
)
|
294 |
-
|
295 |
-
# Create transformers pipeline
|
296 |
-
pipe = pipeline(
|
297 |
-
"text-generation",
|
298 |
-
model=model,
|
299 |
-
tokenizer=tokenizer,
|
300 |
-
max_new_tokens=1024,
|
301 |
-
temperature=0.1,
|
302 |
-
do_sample=True,
|
303 |
-
top_p=0.9,
|
304 |
-
repetition_penalty=1.1,
|
305 |
)
|
306 |
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
if vector_store:
|
324 |
-
try:
|
325 |
-
# Perform retrieval
|
326 |
-
relevant_docs = vector_store.similarity_search(
|
327 |
-
state.get("question", ""),
|
328 |
-
k=3
|
329 |
-
)
|
330 |
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
except Exception as e:
|
335 |
-
print(f"WARNING: RAG retrieval failed: {str(e)}")
|
336 |
-
rag_context = "\n\n[Relevant Knowledge] Retrieval failed. Proceeding without additional context."
|
337 |
-
else:
|
338 |
-
print("WARNING: No vector store available for RAG")
|
339 |
-
rag_context = "\n\n[Relevant Knowledge] No knowledge base available."
|
340 |
-
|
341 |
-
# Renamed the variable to emphasize it's a template string, not a SystemMessage object
|
342 |
-
system_prompt_template_str = (
|
343 |
-
"You are an expert problem solver, designed to provide concise and accurate answers. "
|
344 |
-
"Your process involves analyzing the question, intelligently selecting and using tools, "
|
345 |
-
"and synthesizing information.\n\n"
|
346 |
-
"**Available Tools:**\n"
|
347 |
-
f"{tool_descriptions}\n\n"
|
348 |
-
"**Tool Usage Guidelines:**\n"
|
349 |
-
"- 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"
|
350 |
-
"- Use **wikipedia_search** for encyclopedic information, historical context, or detailed topics. Provide a concise search term. Example: `Eiffel Tower history`\n"
|
351 |
-
"- Use **arxiv_search** for scientific papers, research, or cutting-edge technical information. Provide a concise search query. Example: `Large Language Models recent advances`\n"
|
352 |
-
"- 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"
|
353 |
-
"- 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"
|
354 |
-
"- 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"
|
355 |
-
"**Crucial Instructions:**\n"
|
356 |
-
"1. **Always aim to provide a definitive answer.** If you have enough information, use the 'final answer' action.\n"
|
357 |
-
"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"
|
358 |
-
" ```json\n"
|
359 |
-
" {\n"
|
360 |
-
" \"Reasoning\": \"I have found the capital of France.\",\n"
|
361 |
-
" \"Action\": \"final answer\",\n"
|
362 |
-
" \"Action Input\": \"The capital of France is Paris.\"\n"
|
363 |
-
" }\n"
|
364 |
-
" ```\n"
|
365 |
-
"3. **If you need more information or cannot answer yet, select an appropriate tool and provide a clear, concise query.**\n"
|
366 |
-
"4. **Think step-by-step.** Reflect on previous tool outputs and the question.\n"
|
367 |
-
"5. **Do NOT repeat actions or search queries unless the previous attempt yielded an error.**\n\n"
|
368 |
-
"**Retrieved Context:**\n{rag_context}\n\n"
|
369 |
-
"**Current Context (Tool Outputs/Intermediate Info):**\n{context}\n\n"
|
370 |
-
"**Previous Reasoning Steps:**\n{reasoning}\n\n"
|
371 |
-
"**Current Task:** {current_task}\n"
|
372 |
-
"**Current Thoughts:** {current_thoughts}\n\n"
|
373 |
-
"**Question:** {question}\n\n"
|
374 |
-
"**Expected JSON Output Format:**\n"
|
375 |
-
"```json\n"
|
376 |
-
"{\n"
|
377 |
-
" \"Reasoning\": \"Your reasoning process to decide the next step, including why a tool is chosen or how an answer is derived.\",\n"
|
378 |
-
" \"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"
|
379 |
-
" \"Action Input\": \"The input for the tool (e.g., 'What is the capital of France?', 'The final answer is Paris.').\"\n"
|
380 |
-
"}\n"
|
381 |
-
"```\n"
|
382 |
-
"Ensure your response is ONLY valid JSON and strictly follows this format. Begin your response with ````json`."
|
383 |
-
)
|
384 |
-
|
385 |
-
prompt = ChatPromptTemplate.from_messages([
|
386 |
-
# --- CHANGE THIS LINE ---
|
387 |
-
# Pass the system message as a tuple (role, content_template_string)
|
388 |
-
("system", system_prompt_template_str),
|
389 |
-
*state["history"]
|
390 |
-
])
|
391 |
-
|
392 |
-
# Format messages safely
|
393 |
-
formatted_messages = prompt.format_messages(
|
394 |
-
rag_context=rag_context,
|
395 |
-
context=state.get("context", {}),
|
396 |
-
reasoning=state.get("reasoning", ""),
|
397 |
-
question=state.get("question", ""),
|
398 |
-
current_task=state.get("current_task", ""),
|
399 |
-
current_thoughts=state.get("current_thoughts", "")
|
400 |
-
)
|
401 |
-
|
402 |
-
# Format full input string
|
403 |
-
try:
|
404 |
-
full_input_string = tokenizer.apply_chat_template(
|
405 |
-
formatted_messages,
|
406 |
-
tokenize=False,
|
407 |
-
add_generation_prompt=True
|
408 |
-
)
|
409 |
-
except Exception as e:
|
410 |
-
print(f"WARNING: Failed to apply chat template: {e}. Using simple join.")
|
411 |
-
full_input_string = "\n".join([msg.content for msg in formatted_messages])
|
412 |
-
|
413 |
-
# Call LLM with retry
|
414 |
-
def call_with_retry_local(inputs: str, retries: int = 3) -> AIMessage:
|
415 |
-
for attempt in range(retries):
|
416 |
-
try:
|
417 |
-
response_text = llm.invoke(inputs)
|
418 |
-
|
419 |
-
# Strip the prompt from the generated text
|
420 |
-
if response_text.startswith(inputs):
|
421 |
-
content = response_text[len(inputs):].strip()
|
422 |
-
else:
|
423 |
-
content = response_text.strip()
|
424 |
-
|
425 |
-
print(f"DEBUG: RAW LOCAL LLM Response (Attempt {attempt+1}):\n---\n{content}\n---")
|
426 |
-
|
427 |
-
# Attempt to parse to validate structure
|
428 |
-
json.loads(content)
|
429 |
-
return AIMessage(content=content)
|
430 |
-
except json.JSONDecodeError as e:
|
431 |
-
print(f"[Retry {attempt+1}/{retries}] Invalid JSON. Error: {e}.")
|
432 |
-
print(f"Invalid content: {content[:200]}...")
|
433 |
-
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}"))
|
434 |
-
time.sleep(3)
|
435 |
-
except Exception as e:
|
436 |
-
print(f"[Retry {attempt+1}/{retries}] Error: {e}.")
|
437 |
-
state["history"].append(AIMessage(content=f"[LLM Error] Failed to get response: {e}. Trying again."))
|
438 |
-
time.sleep(5)
|
439 |
-
return AIMessage(content='{"Reasoning": "Max retries exceeded", "Action": "Final Answer", "Action Input": "Error: Failed after multiple retries"}')
|
440 |
-
|
441 |
-
response = call_with_retry_local(full_input_string)
|
442 |
-
content = response.content
|
443 |
-
|
444 |
-
# Parse response
|
445 |
-
reasoning, action, action_input = parse_agent_response(content)
|
446 |
-
|
447 |
-
print(f"DEBUG: Parsed Action: '{action}', Action Input: '{action_input[:100]}...'")
|
448 |
-
|
449 |
-
# Update state
|
450 |
-
state["history"].append(AIMessage(content=content))
|
451 |
-
state["reasoning"] += f"\nStep {state['iterations'] + 1}: {reasoning}"
|
452 |
-
state["iterations"] += 1
|
453 |
-
state["current_thoughts"] = reasoning
|
454 |
-
|
455 |
-
if "final answer" in action.lower():
|
456 |
-
state["final_answer"] = action_input
|
457 |
-
else:
|
458 |
-
state["context"]["pending_action"] = {
|
459 |
-
"tool": action,
|
460 |
-
"input": action_input
|
461 |
-
}
|
462 |
-
state["history"].append(AIMessage(content=f"Agent decided to use tool: {action} with input: {action_input}"))
|
463 |
-
|
464 |
-
print(f"DEBUG: Exiting reasoning_node. New history length: {len(state['history'])}")
|
465 |
-
return state
|
466 |
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
print(f"DEBUG: Entering tool_node. Iteration: {state.get('iterations', 0)}")
|
471 |
-
|
472 |
-
# Safely get pending action
|
473 |
-
tool_call_dict = state.get("context", {}).pop("pending_action", None)
|
474 |
|
475 |
-
|
476 |
-
error_message = "[Tool Error] No pending_action found in context."
|
477 |
-
print(f"ERROR: {error_message}")
|
478 |
-
state.setdefault("history", []).append(AIMessage(content=error_message))
|
479 |
-
return state
|
480 |
|
481 |
-
tool_name = tool_call_dict.get("tool", "")
|
482 |
-
tool_input = tool_call_dict.get("input", "")
|
483 |
|
484 |
-
if not tool_name or not tool_input:
|
485 |
-
error_message = f"[Tool Error] Invalid action: Tool name '{tool_name}' or input '{tool_input}' was empty."
|
486 |
-
print(f"ERROR: {error_message}")
|
487 |
-
state["history"].append(AIMessage(content=error_message))
|
488 |
-
return state
|
489 |
-
|
490 |
-
# Find and execute tool
|
491 |
-
available_tools = state.get("tools", [])
|
492 |
-
tool_fn = next((t for t in available_tools if t.name == tool_name), None)
|
493 |
-
|
494 |
-
if tool_fn is None:
|
495 |
-
tool_output = f"[Tool Error] Tool '{tool_name}' not found. Available: {', '.join([t.name for t in available_tools])}"
|
496 |
-
print(f"ERROR: {tool_output}")
|
497 |
-
else:
|
498 |
-
try:
|
499 |
-
print(f"DEBUG: Invoking tool '{tool_name}' with input: '{tool_input[:100]}...'")
|
500 |
-
tool_output = tool_fn.run(tool_input)
|
501 |
-
if tool_output is None:
|
502 |
-
tool_output = f"[{tool_name} output] No result returned for '{tool_input}'."
|
503 |
-
except Exception as e:
|
504 |
-
tool_output = f"[Tool Error] Error running '{tool_name}': {str(e)}"
|
505 |
-
print(f"ERROR: {tool_output}")
|
506 |
-
|
507 |
-
state["history"].append(AIMessage(content=f"[{tool_name} output]\n{tool_output}"))
|
508 |
-
|
509 |
-
print(f"DEBUG: Exiting tool_node. Tool output added to history.")
|
510 |
-
return state
|
511 |
-
|
512 |
-
# ====== AGENT GRAPH ======
|
513 |
-
class StateGraph:
|
514 |
-
"""Simple state graph implementation"""
|
515 |
-
def __init__(self, state: AgentState):
|
516 |
-
self.nodes = {}
|
517 |
-
self.entry_point = None
|
518 |
-
self.edges = {}
|
519 |
-
self.conditional_edges = {}
|
520 |
-
|
521 |
-
def add_node(self, name: str, func: callable):
|
522 |
-
self.nodes[name] = func
|
523 |
|
524 |
-
def set_entry_point(self, name: str):
|
525 |
-
self.entry_point = name
|
526 |
-
|
527 |
-
def add_conditional_edges(self, source: str, condition: callable, path_map: Dict[str, str]):
|
528 |
-
self.conditional_edges[source] = (condition, path_map)
|
529 |
-
|
530 |
-
def add_edge(self, source: str, dest: str):
|
531 |
-
self.edges[source] = dest
|
532 |
-
|
533 |
-
def compile(self):
|
534 |
-
def app(state: AgentState) -> AgentState:
|
535 |
-
current_node = self.entry_point
|
536 |
-
|
537 |
-
while current_node != END:
|
538 |
-
if current_node in self.nodes:
|
539 |
-
state = self.nodes[current_node](state)
|
540 |
-
|
541 |
-
if current_node in self.conditional_edges:
|
542 |
-
condition, path_map = self.conditional_edges[current_node]
|
543 |
-
next_node_key = condition(state)
|
544 |
-
current_node = path_map.get(next_node_key, END)
|
545 |
-
elif current_node in self.edges:
|
546 |
-
current_node = self.edges[current_node]
|
547 |
-
else:
|
548 |
-
current_node = END
|
549 |
-
|
550 |
-
return state
|
551 |
-
|
552 |
-
return app
|
553 |
-
|
554 |
-
END = "__END__"
|
555 |
-
|
556 |
-
# ====== AGENT INTERFACE ======
|
557 |
-
class BasicAgent:
|
558 |
-
def __init__(self):
|
559 |
-
# Instantiate tools (implementation not shown - should be defined elsewhere)
|
560 |
-
self.tools = [
|
561 |
-
duckduckgo_search,
|
562 |
-
wikipedia_search,
|
563 |
-
arxiv_search,
|
564 |
-
document_qa,
|
565 |
-
python_execution,
|
566 |
-
VideoTranscriptionTool()
|
567 |
-
]
|
568 |
-
|
569 |
-
# Pre-initialize RAG vector store
|
570 |
-
try:
|
571 |
-
self.vector_store = create_vector_store()
|
572 |
-
if not self.vector_store:
|
573 |
-
print("WARNING: Vector store creation failed. Proceeding without RAG.")
|
574 |
-
except Exception as e:
|
575 |
-
print(f"ERROR creating vector store: {str(e)}")
|
576 |
-
self.vector_store = None
|
577 |
-
|
578 |
-
self.workflow = create_agent_workflow(self.tools)
|
579 |
-
|
580 |
-
def __call__(self, question: str) -> str:
|
581 |
-
print(f"\n--- Agent received question: {question[:80]}{'...' if len(question) > 80 else ''} ---")
|
582 |
-
|
583 |
-
state = {
|
584 |
-
"question": question,
|
585 |
-
"context": {
|
586 |
-
"vector_store": self.vector_store
|
587 |
-
},
|
588 |
-
"reasoning": "",
|
589 |
-
"iterations": 0,
|
590 |
-
"history": [HumanMessage(content=question)],
|
591 |
-
"final_answer": None,
|
592 |
-
"current_task": "Understand the question and plan the next step.",
|
593 |
-
"current_thoughts": "",
|
594 |
-
"tools": self.tools
|
595 |
-
}
|
596 |
-
|
597 |
-
try:
|
598 |
-
final_state = self.workflow.invoke(state)
|
599 |
-
if final_state.get("final_answer") is not None:
|
600 |
-
answer = final_state["final_answer"]
|
601 |
-
print(f"--- Agent returning FINAL ANSWER: {answer} ---")
|
602 |
-
return answer
|
603 |
-
else:
|
604 |
-
print("--- ERROR: Agent finished without setting 'final_answer' ---")
|
605 |
-
if final_state.get("history"):
|
606 |
-
last_message = final_state["history"][-1].content
|
607 |
-
print(f"Last message: {last_message}")
|
608 |
-
return f"Agent could not answer. Last message: {last_message}"
|
609 |
-
return "Error: Agent failed to provide an answer"
|
610 |
-
except Exception as e:
|
611 |
-
print(f"FATAL ERROR during agent execution: {str(e)}")
|
612 |
-
return f"Agent encountered a fatal error: {str(e)}"
|
613 |
-
|
614 |
-
def create_agent_workflow(tools: List[Any]):
|
615 |
-
workflow = StateGraph(AgentState)
|
616 |
-
|
617 |
-
workflow.add_node("reason", reasoning_node)
|
618 |
-
workflow.add_node("action", tool_node)
|
619 |
-
|
620 |
-
workflow.set_entry_point("reason")
|
621 |
-
|
622 |
-
workflow.add_conditional_edges(
|
623 |
-
"reason",
|
624 |
-
should_continue,
|
625 |
-
{
|
626 |
-
"action": "action",
|
627 |
-
"reason": "reason",
|
628 |
-
"end": END
|
629 |
-
}
|
630 |
-
)
|
631 |
-
|
632 |
-
workflow.add_edge("action", "reason")
|
633 |
-
|
634 |
-
app = workflow.compile()
|
635 |
-
return app
|
636 |
-
|
637 |
-
|
638 |
-
|
639 |
-
|
640 |
def run_and_submit_all( profile: gr.OAuthProfile | None):
|
641 |
"""
|
642 |
Fetches all questions, runs the BasicAgent on them, submits all answers,
|
|
|
|
|
1 |
import os
|
2 |
+
import time
|
3 |
+
|
4 |
import gradio as gr
|
|
|
|
|
5 |
import pandas as pd
|
6 |
+
import requests
|
7 |
+
from smolagents import (
|
8 |
+
CodeAgent,
|
9 |
+
DuckDuckGoSearchTool,
|
10 |
+
LiteLLMModel,
|
11 |
+
WikipediaSearchTool,
|
12 |
+
FinalAnswerTool
|
13 |
+
)
|
14 |
+
|
15 |
+
from agent import (
|
16 |
+
analyze_audio_file,
|
17 |
+
analyze_image_file,
|
18 |
+
analyze_xlsx_file,
|
19 |
+
analyze_youtube_video,
|
20 |
+
download_file_of_task_id,
|
21 |
+
)
|
22 |
|
|
|
23 |
# --- Constants ---
|
24 |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
25 |
|
26 |
+
# --- Basic Agent Definition ---
|
27 |
+
# ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
|
28 |
+
class BasicAgent:
|
29 |
+
def __init__(self):
|
30 |
+
print("BasicAgent initialized.")
|
31 |
+
|
32 |
+
# Initialize the model
|
33 |
+
model = LiteLLMModel(model_id=os.getenv("MODEL_ID"),
|
34 |
+
api_key=os.getenv("API_KEY"))
|
35 |
+
|
36 |
+
# Initialize the searchs tool
|
37 |
+
duck_duck_go_search_tool = DuckDuckGoSearchTool()
|
38 |
+
wikipedia_search_tool = WikipediaSearchTool()
|
39 |
+
|
40 |
+
# Initialize Agent
|
41 |
+
self.agent = CodeAgent(
|
42 |
+
model = model,
|
43 |
+
tools=[download_file_of_task_id, analyze_audio_file, analyze_image_file,
|
44 |
+
analyze_xlsx_file, duck_duck_go_search_tool, wikipedia_search_tool,
|
45 |
+
analyze_youtube_video, FinalAnswerTool()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
)
|
47 |
|
48 |
+
def __call__(self, question: str, task_id: str) -> str:
|
49 |
+
task = f"""
|
50 |
+
You are a general AI assistant.
|
51 |
+
I will ask you a question and you can use 8 steps to answer it.
|
52 |
+
You can use the tools I provided to you to answer the question.
|
53 |
+
Every time you use a tool, the number of steps will decrease by one.
|
54 |
+
If you have a list of possible pages to visit, prefer the wikipedia ones.
|
55 |
+
If a page does not allow visit, skip it.
|
56 |
+
Report your thoughts, and finish your answer with the following template:
|
57 |
+
FINAL ANSWER: [YOUR FINAL ANSWER].
|
58 |
+
YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
|
59 |
+
If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise.
|
60 |
+
If the answer is a number, represent it with digits.
|
61 |
+
If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise.
|
62 |
+
If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
|
64 |
+
The taskid is {task_id} in case you need to get extra files, use taskid and not name of the file
|
65 |
+
and the question is {question}
|
66 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
|
68 |
+
fixed_answer = self.agent.run(task)
|
69 |
+
print(f"Agent returning fixed answer: {fixed_answer}")
|
70 |
+
time.sleep(50)
|
|
|
|
|
|
|
|
|
71 |
|
72 |
+
return fixed_answer
|
|
|
|
|
|
|
|
|
73 |
|
|
|
|
|
74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
76 |
def run_and_submit_all( profile: gr.OAuthProfile | None):
|
77 |
"""
|
78 |
Fetches all questions, runs the BasicAgent on them, submits all answers,
|