sergiosampayob commited on
Commit
4e38b79
·
1 Parent(s): 9cb1f7b

local app test

Browse files
Files changed (1) hide show
  1. app_local_test.py +470 -0
app_local_test.py ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Tuple
2
+ import requests
3
+ import os
4
+ import json
5
+
6
+ import ollama
7
+ from smolagents import CodeAgent, DuckDuckGoSearchTool, VisitWebpageTool, LiteLLMModel, Tool
8
+ from youtube_transcript_api import YouTubeTranscriptApi
9
+ import whisper
10
+ import pandas as pd
11
+ from pytubefix import YouTube
12
+ from pytubefix.cli import on_progress
13
+ from bs4 import BeautifulSoup
14
+ import wikipediaapi
15
+ import cv2
16
+ import numpy as np
17
+
18
+
19
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
20
+ CACHE_FILE = "answers_cache.json"
21
+
22
+
23
+ class ImageLoaderTool(Tool):
24
+ name = "image_loader"
25
+ description = (
26
+ "Loads an image from a given URL using cv2 and returns it as a numpy array. "
27
+ "Input: URL of the image."
28
+ "Output: Image as a numpy array."
29
+ "Note: This tool requires the 'cv2' library to be installed."
30
+ )
31
+ inputs = {
32
+ "image_url": {"type": "string", "description": "URL of the image."},
33
+ }
34
+ output_type = "numpy.ndarray"
35
+ def forward(self, image_url: str) -> str:
36
+ if not image_url.startswith("http"):
37
+ raise ValueError(f"Invalid URL: {image_url}")
38
+ try:
39
+ response = requests.get(image_url)
40
+ image = cv2.imdecode(np.frombuffer(response.content, np.uint8), cv2.IMREAD_COLOR)
41
+ return image
42
+ except Exception as e:
43
+ raise ValueError(f"Error loading image: {e}")
44
+
45
+
46
+ class SpeechToTextTool(Tool):
47
+ name = "speech_to_text"
48
+ description = (
49
+ "Converts an audio file to text. "
50
+ )
51
+ inputs = {
52
+ "audio_file_path": {"type": "string", "description": "Path to the audio file."},
53
+ }
54
+ output_type = "string"
55
+
56
+ def __init__(self):
57
+ super().__init__()
58
+ self.model = whisper.load_model("base")
59
+
60
+ def forward(self, audio_file_path: str) -> str:
61
+ if not os.path.exists(audio_file_path):
62
+ raise ValueError(f"Audio file not found: {audio_file_path}")
63
+ result = self.model.transcribe(audio_file_path)
64
+ return result.get("text", "")
65
+
66
+
67
+ class YoutubeSubtitlesTranscriptTool(Tool):
68
+ name = "youtube_subtitles_transcript"
69
+ description = (
70
+ "Fetches the transcript of a YouTube video. "
71
+ "Input: YouTube video URL."
72
+ "Output: Transcript text."
73
+ )
74
+ inputs = {
75
+ "video_url": {"type": "string", "description": "YouTube video URL."},
76
+ }
77
+ output_type = "string"
78
+
79
+ def forward(self, video_url: str) -> str:
80
+ if not video_url.startswith("https://www.youtube.com/watch?v="):
81
+ raise ValueError(f"Invalid YouTube URL: {video_url}")
82
+ video_id = video_url.split("v=")[-1]
83
+ try:
84
+ transcript = YouTubeTranscriptApi.get_transcript(video_id)
85
+ transcript_text = " ".join([entry["text"] for entry in transcript])
86
+ return transcript_text
87
+ except Exception as transcript_error:
88
+ print(f"Transcript not available: {transcript_error}")
89
+ try:
90
+ # Fallback: Download audio for processing
91
+ youtube_audio_transcript_tool = YoutubeAudioTranscriptTool()
92
+ transcript_text = youtube_audio_transcript_tool.forward(video_url)
93
+ print("Audio downloaded successfully.")
94
+ return transcript_text # Assuming the tool returns some text representation
95
+ except Exception as e:
96
+ raise ValueError(f"Error downloading audio or converting to text: {e}")
97
+
98
+
99
+ class YoutubeAudioTranscriptTool(Tool):
100
+ name = "youtube_audio_transcript"
101
+ description = (
102
+ "Downloads the audio from a YouTube video and converts it to text. "
103
+ "Input: YouTube video URL."
104
+ )
105
+ inputs = {
106
+ "video_url": {"type": "string", "description": "YouTube video URL."},
107
+ }
108
+ output_type = "string"
109
+
110
+ def forward(self, video_url: str) -> str:
111
+ if not video_url.startswith("https://www.youtube.com/watch?v="):
112
+ raise ValueError(f"Invalid YouTube URL: {video_url}")
113
+ try:
114
+ yt = YouTube(video_url, on_progress_callback=on_progress)
115
+ audio_stream = yt.streams.filter(progressive=True, file_extension='mp4').first()
116
+ audio_file_path = audio_stream.download(filename_prefix="audio_")
117
+ speech_to_text_tool = SpeechToTextTool()
118
+ transcript = speech_to_text_tool.forward(audio_file_path)
119
+ os.remove(audio_file_path) # Clean up the downloaded file
120
+ return transcript
121
+ except Exception as e:
122
+ raise ValueError(f"Error downloading audio or converting to text: {e}")
123
+
124
+
125
+ class WikipediaSearchTool(Tool):
126
+ name = "wikipedia_search"
127
+ description = (
128
+ "Searches Wikipedia for a given query and returns the summary of the first result."
129
+ "Input: Search query."
130
+ "Output: Wikipedia article."
131
+ )
132
+ inputs = {
133
+ "query": {"type": "string", "description": "Search query."},
134
+ }
135
+ output_type = "string"
136
+
137
+ def forward(self, query: str) -> str:
138
+ wiki_wiki = wikipediaapi.Wikipedia(
139
+ user_agent='wikipedia_agent',
140
+ language='en',
141
+ extract_format=wikipediaapi.ExtractFormat.WIKI
142
+ )
143
+ p_wiki = wiki_wiki.page(query)
144
+ if not p_wiki.exists():
145
+ raise ValueError(f"No Wikipedia page found for query: {query}")
146
+ print(p_wiki.text)
147
+ return p_wiki.text
148
+
149
+
150
+ class ParseURLTool(Tool):
151
+ name = "parse_url"
152
+ description = (
153
+ "Parses a URL and returns the text content of the webpage."
154
+ "Input: URL."
155
+ "Output: Text content of the webpage."
156
+ )
157
+ inputs = {
158
+ "url": {"type": "string", "description": "URL to parse."},
159
+ }
160
+ output_type = "string"
161
+
162
+ def forward(self, url: str) -> str:
163
+ if not url:
164
+ raise ValueError("URL cannot be empty.")
165
+ # Fetch the HTML content
166
+ response = requests.get(url)
167
+ # Retrieve the HTML content
168
+ html = response.text
169
+ # Create a BesutifulSoup Object
170
+ soup = BeautifulSoup(html, 'html.parser')
171
+ # Select all <p> tags
172
+ paragraphs = soup.select("p")
173
+ webpage_text_list = []
174
+ for para in paragraphs:
175
+ # Get the text content of each <p> tag
176
+ text = para.text
177
+ webpage_text_list.append(text)
178
+
179
+ webpage_text = ",".join(webpage_text_list)
180
+ print(f"Webpage text:\n {webpage_text}")
181
+ return webpage_text
182
+
183
+
184
+ class OllamaAgent:
185
+ def __init__(self, model_id: str = "llama3"):
186
+
187
+ model = LiteLLMModel(
188
+ model_id=f"ollama/{model_id}", # Ollama model ID
189
+ api_base="http://127.0.0.1:11434", # Ollama API base URL
190
+ # num_ctx=8096, # Increased context
191
+ # timeout=300, # 5-minute timeout
192
+ )
193
+
194
+ self.agent = CodeAgent(
195
+ model=model,
196
+ tools=[
197
+ DuckDuckGoSearchTool(),
198
+ VisitWebpageTool(),
199
+ WikipediaSearchTool(),
200
+ YoutubeSubtitlesTranscriptTool(),
201
+ YoutubeAudioTranscriptTool(),
202
+ SpeechToTextTool(),
203
+ ParseURLTool(),
204
+ ],
205
+ verbosity_level=2,
206
+ # planning_interval=10,
207
+ add_base_tools=True,
208
+ additional_authorized_imports=[
209
+ "re",
210
+ "requests",
211
+ "bs4",
212
+ "urllib",
213
+ "pytubefix",
214
+ "pytubefix.cli",
215
+ "youtube_transcript_api",
216
+ "wikipediaapi",
217
+ "whisper",
218
+ "pandas",
219
+ "cv2",
220
+ "numpy",
221
+ ],
222
+ max_steps=5,
223
+ )
224
+
225
+ print("OllamaAgent initialized.")
226
+
227
+ def __call__(self, question: str) -> str:
228
+ print(f"Agent received question (first 50 chars): {question[:50]}...")
229
+ answer = self.agent.run(question)
230
+ print(f"Agent returning answer: {answer}")
231
+ return answer
232
+
233
+
234
+ def cache_answers(answers_payload, results_log):
235
+ """
236
+ Cache answers and results log to a local file.
237
+ """
238
+ cache_data = {
239
+ "answers_payload": answers_payload,
240
+ "results_log": results_log,
241
+ }
242
+ with open(CACHE_FILE, "w") as f:
243
+ json.dump(cache_data, f)
244
+ print(f"Cached {len(answers_payload)} answers to {CACHE_FILE}.")
245
+
246
+
247
+ def load_cached_answers():
248
+ """
249
+ Load cached answers from the local file.
250
+ """
251
+ if os.path.exists(CACHE_FILE):
252
+ with open(CACHE_FILE, "r") as f:
253
+ cache_data = json.load(f)
254
+ print(f"Loaded {len(cache_data['answers_payload'])} cached answers from {CACHE_FILE}.")
255
+ return cache_data["answers_payload"], cache_data["results_log"]
256
+ return [], []
257
+
258
+
259
+ def ollama_pull_model(model_name: str) -> bool | tuple[str, None]:
260
+ """
261
+ Check if the model is available locally and pull it if not.
262
+
263
+ model_name: str
264
+ The name of the model to check.
265
+
266
+ Returns True if the model is available, False otherwise.
267
+ """
268
+ try:
269
+ # Try to pull the model (this will check availability)
270
+ ollama.pull(model_name)
271
+ print(f"Model {model_name} is available.")
272
+ return True
273
+ except Exception as e:
274
+ # If the model doesn't exist, it will raise an error
275
+ print(f"Error pulling model: {e}")
276
+ return f"Error pulling model: {e}", None
277
+
278
+
279
+ def fetch_questions(api_url: str) -> tuple[str, None] | List[Dict[str, str]]:
280
+ """
281
+ Fetch questions from the API.
282
+
283
+ api_url: str
284
+ The base URL of the API.
285
+
286
+ Returns a list of questions.
287
+ """
288
+ api_url = DEFAULT_API_URL
289
+ questions_url = f"{api_url}/questions"
290
+
291
+ print(f"Fetching questions from: {questions_url}")
292
+
293
+ try:
294
+ response = requests.get(questions_url, timeout=15)
295
+ response.raise_for_status()
296
+ questions_data = response.json()
297
+ if not questions_data:
298
+ print("Fetched questions list is empty.")
299
+ return "Fetched questions list is empty or invalid format.", None
300
+ print(f"Fetched {len(questions_data)} questions.")
301
+ return questions_data
302
+ except requests.exceptions.RequestException as e:
303
+ print(f"Error fetching questions: {e}")
304
+ return f"Error fetching questions: {e}", None
305
+ except requests.exceptions.JSONDecodeError as e:
306
+ print(f"Error decoding JSON response from questions endpoint: {e}")
307
+ print(f"Response text: {response.text[:500]}")
308
+ return f"Error decoding server response for questions: {e}", None
309
+ except Exception as e:
310
+ print(f"An unexpected error occurred fetching questions: {e}")
311
+ return f"An unexpected error occurred fetching questions: {e}", None
312
+
313
+
314
+ def improve_prompt(prompt: str) -> str:
315
+ """
316
+ Improve the prompt by adding specific instructions for the agent.
317
+
318
+ prompt: str
319
+ The original prompt.
320
+
321
+ Returns the improved prompt.
322
+ """
323
+
324
+ prompt = f"Question: {prompt}\n" \
325
+ "Additional Instructions:\n" \
326
+ "Put your Thoughts (Thought) with a '#' at the beggining of their lines to avoid Error: invalid syntax and Code parsing fails." \
327
+
328
+ return prompt
329
+
330
+
331
+ def run_agent(agent, questions_data) -> Tuple[List[Dict[str, str]], List[Dict[str, str]]]:
332
+ """
333
+ Run the agent on a list of questions and return the results.
334
+
335
+ Args:
336
+ agent: The agent to run.
337
+ questions_data: A list of dictionaries containing the questions and task IDs.
338
+
339
+ Returns:
340
+ results_log: A list of dictionaries containing the task ID, question, and submitted answer.
341
+ answers_payload: A list of dictionaries containing the task ID and submitted answer.
342
+ """
343
+ results_log = []
344
+ answers_payload = []
345
+ print(f"Running agent on {len(questions_data)} questions...")
346
+ for item in questions_data:
347
+ task_id = item.get("task_id")
348
+ question_text = item.get("question")
349
+ if not task_id or question_text is None:
350
+ print(f"Skipping item with missing task_id or question: {item}")
351
+ continue
352
+ try:
353
+ # question_text = improve_prompt(question_text)
354
+ submitted_answer = agent(question_text)
355
+ answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
356
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
357
+ except Exception as e:
358
+ print(f"Error running agent on task {task_id}: {e}")
359
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
360
+
361
+ if not answers_payload:
362
+ print("Agent did not produce any answers to submit.")
363
+
364
+ return results_log, answers_payload
365
+
366
+
367
+ def submit_answers(
368
+ username: str,
369
+ agent_code: str,
370
+ answers_payload: List[Dict[str, str]],
371
+ results_log: List[Dict[str, str]]
372
+ ) -> Tuple[str, pd.DataFrame]:
373
+ """
374
+ Submit the answers to the API and return the status message and results DataFrame.
375
+
376
+ Args:
377
+ username: The username of the person submitting the answers.
378
+ agent_code: The code of the agent used.
379
+ answers_payload: A list of dictionaries containing the task ID and submitted answer.
380
+ results_log: A list of dictionaries containing the task ID, question, and submitted answer.
381
+
382
+ Returns:
383
+ status_message: A message indicating the status of the submission.
384
+ results_df: A DataFrame containing the results log.
385
+ """
386
+ submit_url = f"{DEFAULT_API_URL}/submit"
387
+ submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
388
+ status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
389
+ print(status_update)
390
+ print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
391
+ try:
392
+ response = requests.post(submit_url, json=submission_data, timeout=60)
393
+ response.raise_for_status()
394
+ result_data = response.json()
395
+ final_status = (
396
+ f"Submission Successful!\n"
397
+ f"User: {result_data.get('username')}\n"
398
+ f"Overall Score: {result_data.get('score', 'N/A')}% "
399
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
400
+ f"Message: {result_data.get('message', 'No message received.')}"
401
+ )
402
+ print("Submission successful.")
403
+ results_df = pd.DataFrame(results_log)
404
+ return final_status, results_df
405
+ except requests.exceptions.HTTPError as e:
406
+ error_detail = f"Server responded with status {e.response.status_code}."
407
+ try:
408
+ error_json = e.response.json()
409
+ error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
410
+ except requests.exceptions.JSONDecodeError:
411
+ error_detail += f" Response: {e.response.text[:500]}"
412
+ status_message = f"Submission Failed: {error_detail}"
413
+ print(status_message)
414
+ results_df = pd.DataFrame(results_log)
415
+ return status_message, results_df
416
+ except requests.exceptions.Timeout:
417
+ status_message = "Submission Failed: The request timed out."
418
+ print(status_message)
419
+ results_df = pd.DataFrame(results_log)
420
+ return status_message, results_df
421
+ except requests.exceptions.RequestException as e:
422
+ status_message = f"Submission Failed: Network error - {e}"
423
+ print(status_message)
424
+ results_df = pd.DataFrame(results_log)
425
+ return status_message, results_df
426
+ except Exception as e:
427
+ status_message = f"An unexpected error occurred during submission: {e}"
428
+ print(status_message)
429
+ results_df = pd.DataFrame(results_log)
430
+ return status_message, results_df
431
+
432
+ def main():
433
+ model_id = 'qwen2.5:7b'
434
+ ollama_pull_model(model_id)
435
+
436
+ # Initialize the agent
437
+ try:
438
+ agent = OllamaAgent(model_id=model_id)
439
+ except Exception as e:
440
+ print(f"Error instantiating agent: {e}")
441
+ return f"Error initializing agent: {e}", None
442
+
443
+ # Fetch questions
444
+ questions_data = fetch_questions(DEFAULT_API_URL)[:3]
445
+
446
+ # Run the agent
447
+ if isinstance(questions_data, list):
448
+ results_log, answers_payload = run_agent(agent, questions_data)
449
+
450
+ # Cache answers
451
+ cache_answers(answers_payload, results_log)
452
+
453
+ # Load cached answers
454
+ answers_payload, results_log = load_cached_answers()
455
+
456
+ # Submit answers
457
+ status_message, results_df = submit_answers(
458
+ username="test_user",
459
+ agent_code="test_code_filler",
460
+ answers_payload=answers_payload,
461
+ results_log=results_log
462
+ )
463
+
464
+ print("Final status message:", status_message)
465
+ for TaskID, Question, SubmittedAnswer in zip(results_df["Task ID"], results_df["Question"], results_df["Submitted Answer"]):
466
+ print(f"Task ID: {TaskID}, Question: {Question}, Submitted Answer: {SubmittedAnswer}")
467
+
468
+
469
+ if __name__ == "__main__":
470
+ main()