dmfelder commited on
Commit
fa8ab53
·
verified ·
1 Parent(s): 241015f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +450 -661
app.py CHANGED
@@ -6,6 +6,18 @@ from typing import Dict, List, Optional, Any, Generator, Tuple
6
  from dataclasses import dataclass
7
  from pathlib import Path
8
  import hashlib
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  # Setup log file path
11
  log_path = os.path.join(os.getcwd(), f"agent_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
@@ -33,36 +45,11 @@ logging.getLogger().addHandler(console_handler)
33
  logging.info(f"===== Application Startup at {datetime.now().isoformat()} =====")
34
  logging.info(f"📂 Log file created at: {log_path}")
35
 
36
- import requests
37
- import pandas as pd
38
- import tempfile
39
- import gradio as gr
40
- from transformers import pipeline
41
- from PIL import Image
42
- import soundfile as sf
43
- import numpy as np
44
- from requests.adapters import HTTPAdapter
45
- from urllib3.util.retry import Retry
46
-
47
- # Configuration constants
48
- API_URL = "https://agents-course-unit4-scoring.hf.space"
49
- DEFAULT_TIMEOUT = 30 # seconds
50
- MAX_RETRIES = 3
51
- BACKOFF_FACTOR = 0.3
52
- MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB limit
53
- TEMP_DIR = tempfile.gettempdir()
54
 
55
- @dataclass
56
- class ProcessingResult:
57
- """Structured result for processing operations"""
58
- success: bool
59
- answer: str
60
- error: Optional[str] = None
61
- processing_time: float = 0.0
62
-
63
  @dataclass
64
  class QuestionLog:
65
- """Structured log entry for questions"""
66
  question_num: int
67
  question_preview: str
68
  question_type: str
@@ -70,677 +57,479 @@ class QuestionLog:
70
  processing_time: float
71
  status: str
72
 
73
- class RobustHTTPSession:
74
- """HTTP session with retry logic and proper error handling"""
75
-
76
- def __init__(self, timeout: int = DEFAULT_TIMEOUT):
77
- self.session = requests.Session()
78
- self.timeout = timeout
79
-
80
- # Configure retry strategy
81
- retry_strategy = Retry(
82
- total=MAX_RETRIES,
83
- backoff_factor=BACKOFF_FACTOR,
84
- status_forcelist=[429, 500, 502, 503, 504],
85
- allowed_methods=["HEAD", "GET", "POST"]
86
- )
87
-
88
- adapter = HTTPAdapter(max_retries=retry_strategy)
89
- self.session.mount("http://", adapter)
90
- self.session.mount("https://", adapter)
91
-
92
- # Set headers
93
- self.session.headers.update({
94
- 'User-Agent': 'SmartAgent/1.0',
95
- 'Accept': 'application/json, */*'
96
- })
97
-
98
- def get(self, url: str, **kwargs) -> requests.Response:
99
- """GET request with error handling"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  try:
101
- return self.session.get(url, timeout=self.timeout, **kwargs)
102
- except requests.exceptions.RequestException as e:
103
- logging.error(f"HTTP GET error for {url}: {e}")
 
104
  raise
105
-
106
- def post(self, url: str, **kwargs) -> requests.Response:
107
- """POST request with error handling"""
108
  try:
109
- return self.session.post(url, timeout=self.timeout * 2, **kwargs) # Double timeout for posts
110
- except requests.exceptions.RequestException as e:
111
- logging.error(f"HTTP POST error for {url}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  raise
113
 
114
- class SmartAgent:
115
- """Enhanced AI agent with robust error handling and performance optimizations"""
116
-
117
- def __init__(self):
118
- self.models_loaded = {"text": False, "image": False, "audio": False}
119
- self.http_session = RobustHTTPSession()
120
- self.temp_files = set() # Track temp files for cleanup
121
-
122
- # Initialize models with better error handling
123
- self._initialize_models()
124
-
125
- # Add a simple knowledge base for common patterns
126
- self.knowledge_patterns = {
127
- "opposite": {"left": "right", "right": "left", "up": "down", "down": "up", "in": "out", "out": "in"},
128
- "math": {"plus": "+", "minus": "-", "times": "*", "divided by": "/"},
129
- "common_responses": {
130
- "youtube": "I cannot access YouTube videos",
131
- "wikipedia": "I cannot access Wikipedia articles",
132
- "image": "I cannot process images in text questions",
133
- "video": "I cannot process video content",
134
- "excel": "I cannot process Excel files",
135
- "python": "I cannot execute Python code"
136
- }
137
- }
138
-
139
- def _initialize_models(self) -> None:
140
- """Initialize all ML models with comprehensive error handling"""
141
- model_configs = [
142
- ("text", "text-generation", "distilgpt2", "text_gen"),
143
- ("image", "image-classification", "google/vit-base-patch16-224", "image_cls"),
144
- ("audio", "automatic-speech-recognition", "openai/whisper-tiny", "audio_stt")
145
- ]
146
-
147
- for model_type, task, model_name, attr_name in model_configs:
148
- try:
149
- start_time = time.time()
150
- model = pipeline(task, model=model_name, device=-1)
151
- load_time = time.time() - start_time
152
-
153
- setattr(self, attr_name, model)
154
- self.models_loaded[model_type] = True
155
- logging.info(f"✅ {model_type.title()} model loaded successfully in {load_time:.2f}s")
156
-
157
- except Exception as e:
158
- logging.exception(f"❌ {model_type.title()} model load failed: {e}")
159
- setattr(self, attr_name, None)
160
-
161
- def _simple_pattern_matching(self, question: str) -> Optional[str]:
162
- """Simple pattern matching for common question types"""
163
- question_lower = question.lower()
164
-
165
- # Handle reversed text questions
166
- if question_lower.startswith(".rewsna eht sa"):
167
- # This is asking for the opposite of "left"
168
- return "right"
169
-
170
- # Handle opposite questions
171
- if "opposite" in question_lower:
172
- for word, opposite in self.knowledge_patterns["opposite"].items():
173
- if word in question_lower:
174
- return opposite
175
-
176
- # Handle grocery/shopping lists
177
- if "grocery" in question_lower or "shopping" in question_lower:
178
- return "I cannot create specific shopping lists without more context"
179
-
180
- # Handle specific platform references
181
- for platform, response in self.knowledge_patterns["common_responses"].items():
182
- if platform in question_lower:
183
- return response
184
-
185
- return None
186
-
187
- def _generate_file_hash(self, task_id: str) -> str:
188
- """Generate a unique hash for file naming"""
189
- return hashlib.md5(f"{task_id}_{datetime.now().isoformat()}".encode()).hexdigest()[:8]
190
-
191
- def get_file_for_task(self, task_id: str) -> Optional[str]:
192
- """Download file with enhanced error handling and validation"""
193
  try:
194
- logging.debug(f"📥 Downloading file for task {task_id}")
195
- resp = self.http_session.get(f"{API_URL}/files/{task_id}")
196
-
197
- if resp.status_code != 200:
198
- logging.error(f"❌ File download failed with status {resp.status_code} for task {task_id}")
199
- return None
200
-
201
- # Check file size
202
- content_length = resp.headers.get('content-length')
203
- if content_length and int(content_length) > MAX_FILE_SIZE:
204
- logging.error(f"❌ File too large ({content_length} bytes) for task {task_id}")
205
- return None
206
-
207
- # Create secure temp file
208
- file_hash = self._generate_file_hash(task_id)
209
- file_path = os.path.join(TEMP_DIR, f"agent_{file_hash}_{task_id}.tmp")
210
-
211
- with open(file_path, "wb") as f:
212
- f.write(resp.content)
213
-
214
- # Verify file was written correctly
215
- if not os.path.exists(file_path) or os.path.getsize(file_path) == 0:
216
- logging.error(f"❌ File write verification failed for task {task_id}")
217
- return None
218
-
219
- self.temp_files.add(file_path)
220
- logging.debug(f"📁 File downloaded successfully for task {task_id}: {file_path}")
221
- return file_path
222
-
223
  except Exception as e:
224
- logging.exception(f"❌ Download error for task {task_id}: {e}")
225
- return None
226
-
227
- def _cleanup_temp_file(self, file_path: str) -> None:
228
- """Safely cleanup temporary files"""
 
 
 
229
  try:
230
- if file_path and os.path.exists(file_path):
231
- os.remove(file_path)
232
- self.temp_files.discard(file_path)
233
- logging.debug(f"🗑️ Cleaned up temp file: {file_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  except Exception as e:
235
- logging.warning(f"⚠️ Failed to cleanup temp file {file_path}: {e}")
236
-
237
- def _process_text_question(self, question: str, task_id: str) -> ProcessingResult:
238
- """Process text-based questions with enhanced validation"""
239
- if not self.text_gen:
240
- return ProcessingResult(False, "SKIPPED", "Text model not available")
241
-
242
- start_time = time.time()
243
- try:
244
- # First try simple pattern matching
245
- simple_answer = self._simple_pattern_matching(question)
246
- if simple_answer:
247
- processing_time = time.time() - start_time
248
- logging.debug(f"📝 Pattern-matched answer for {task_id}: '{simple_answer}'")
249
- return ProcessingResult(True, simple_answer, None, processing_time)
250
-
251
- # For these complex questions, we need a different approach
252
- # Many of these questions require web search or specific knowledge
253
- # Let's try a more direct approach with better prompting
254
-
255
- # Check if question seems to need web search/external data
256
- web_indicators = ["youtube.com", "wikipedia", "article", "published", "http", "www"]
257
- if any(indicator in question.lower() for indicator in web_indicators):
258
- # Return a generic response indicating web search is needed
259
- return ProcessingResult(True, "I need access to web search to answer this question accurately.", None, time.time() - start_time)
260
-
261
- # For other questions, use a simple conversational approach
262
- try:
263
- # Use a cleaner prompt that's more likely to work
264
- prompt = f"{question.strip()}\n\nAnswer:"
265
-
266
- result = self.text_gen(
267
- prompt,
268
- max_new_tokens=30, # Even shorter responses
269
- do_sample=False, # Use greedy decoding for consistency
270
- temperature=0.1, # Very low temperature
271
- top_p=0.5,
272
- repetition_penalty=1.2,
273
- pad_token_id=self.text_gen.tokenizer.eos_token_id if hasattr(self.text_gen.tokenizer, 'eos_token_id') else None
274
- )
275
-
276
- processing_time = time.time() - start_time
277
- logging.debug(f"🧪 Raw text-gen output for {task_id}: {result}")
278
-
279
- # Extract the generated text
280
- generated_text = result[0]["generated_text"]
281
-
282
- # Extract only the answer part
283
- if "\n\nAnswer:" in generated_text:
284
- answer = generated_text.split("\n\nAnswer:")[-1].strip()
285
- elif "Answer:" in generated_text:
286
- answer = generated_text.split("Answer:")[-1].strip()
287
- else:
288
- # Take everything after the original question
289
- if question.strip() in generated_text:
290
- answer = generated_text.replace(question.strip(), "").strip()
291
- else:
292
- answer = generated_text.strip()
293
-
294
- # Clean the answer more aggressively
295
- answer = ' '.join(answer.split()) # Normalize whitespace
296
- answer = answer.replace('\n', ' ').replace('\r', ' ').strip()
297
-
298
- # Remove common artifacts and prefixes
299
- artifacts = ["Answer:", "answer:", "A:", "a:", "Response:", "response:", "The answer is", "the answer is"]
300
- for artifact in artifacts:
301
- if answer.startswith(artifact):
302
- answer = answer[len(artifact):].strip()
303
-
304
- # Remove trailing punctuation that might be artifacts
305
- if answer.endswith(('.', '!', '?', ':')):
306
- answer = answer[:-1].strip()
307
-
308
- # Final validation
309
- if not answer or len(answer) < 2:
310
- logging.warning(f"⚠️ Generated answer too short for {task_id}: '{answer}'")
311
- return ProcessingResult(True, "Unable to determine answer", None, processing_time)
312
-
313
- # Check for gibberish patterns more aggressively
314
- words = answer.split()
315
- if len(words) > 3:
316
- # Check for excessive repetition
317
- unique_words = set(words)
318
- if len(unique_words) / len(words) < 0.4: # Less than 40% unique words
319
- logging.warning(f"⚠️ Detected repetitive output for {task_id}: '{answer}'")
320
- return ProcessingResult(True, "Unable to determine answer", None, processing_time)
321
-
322
- # Check for specific nonsensical patterns from tiny-gpt2
323
- nonsense_indicators = [
324
- "Observ", "Motorola", "Hancock", "ESV", "Rh", "Prob", "antibiotic", "trilogy", "dispatch",
325
- "credibility", "autonomy", "intermittent", "circumcised", "hauled", "vendors", "scalp",
326
- "Participation", "conservation", "reborn", "stairs", "pawn", "Brew", "Jr", "Amph"
327
- ]
328
- if any(indicator in answer for indicator in nonsense_indicators):
329
- logging.warning(f"⚠️ Detected nonsensical output for {task_id}: '{answer}'")
330
- return ProcessingResult(True, "Unable to determine answer", None, processing_time)
331
-
332
- # Check for very long answers that are likely gibberish
333
- if len(words) > 20:
334
- logging.warning(f"⚠️ Answer too long, likely gibberish for {task_id}: '{answer[:100]}...'")
335
- return ProcessingResult(True, "Unable to determine answer", None, processing_time)
336
-
337
- logging.debug(f"📝 Text answer for {task_id}: '{answer}' (processed in {processing_time:.2f}s)")
338
- return ProcessingResult(True, answer, None, processing_time)
339
 
340
- except Exception as inner_e:
341
- logging.warning(f"⚠️ Text generation failed for {task_id}: {inner_e}")
342
- return ProcessingResult(True, "Unable to determine answer", None, time.time() - start_time)
343
-
344
- except Exception as e:
345
- processing_time = time.time() - start_time
346
- logging.exception(f"❌ Text processing error for {task_id}: {e}")
347
- return ProcessingResult(False, "ERROR", str(e), processing_time)
348
-
349
- def _process_image_question(self, task_id: str) -> ProcessingResult:
350
- """Process image-based questions with enhanced validation"""
351
- if not self.image_cls:
352
- return ProcessingResult(False, "SKIPPED", "Image model not available")
353
 
354
- file_path = self.get_file_for_task(task_id)
355
- if not file_path:
356
- return ProcessingResult(False, "SKIPPED", "File download failed")
357
-
358
- start_time = time.time()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  try:
360
- # Validate image file
361
- with Image.open(file_path) as img:
362
- # Convert to RGB and validate
363
- img = img.convert("RGB")
364
-
365
- # Check image dimensions
366
- width, height = img.size
367
- if width < 10 or height < 10:
368
- raise ValueError(f"Image too small: {width}x{height}")
369
-
370
- # Classify image
371
- results = self.image_cls(img)
372
-
373
- processing_time = time.time() - start_time
374
-
375
- if not results or len(results) == 0:
376
- logging.warning(f"⚠️ No classification results for {task_id}")
377
- return ProcessingResult(False, "ERROR", "No classification results", processing_time)
378
-
379
- answer = results[0]["label"]
380
- confidence = results[0].get("score", 0.0)
381
-
382
- logging.debug(f"🖼️ Image classification for {task_id}: '{answer}' (confidence: {confidence:.3f}, processed in {processing_time:.2f}s)")
383
- return ProcessingResult(True, answer, None, processing_time)
384
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  except Exception as e:
386
- processing_time = time.time() - start_time
387
- logging.exception(f"❌ Image processing error for {task_id}: {e}")
388
- return ProcessingResult(False, "ERROR", str(e), processing_time)
389
- finally:
390
- self._cleanup_temp_file(file_path)
391
-
392
- def _process_audio_question(self, task_id: str) -> ProcessingResult:
393
- """Process audio-based questions with enhanced validation"""
394
- if not self.audio_stt:
395
- return ProcessingResult(False, "SKIPPED", "Audio model not available")
396
-
397
- file_path = self.get_file_for_task(task_id)
398
- if not file_path:
399
- return ProcessingResult(False, "SKIPPED", "File download failed")
400
-
401
- start_time = time.time()
402
- try:
403
- # Load and validate audio
404
- audio_data, sample_rate = sf.read(file_path)
405
-
406
- # Validate audio data
407
- if len(audio_data) == 0:
408
- raise ValueError("Empty audio file")
409
-
410
- # Convert to mono if stereo
411
- if len(audio_data.shape) > 1:
412
- audio_data = np.mean(audio_data, axis=1)
413
-
414
- # Normalize audio
415
- if np.max(np.abs(audio_data)) > 0:
416
- audio_data = audio_data / np.max(np.abs(audio_data))
417
-
418
- # Transcribe audio
419
- result = self.audio_stt({
420
- "array": audio_data.astype(np.float32),
421
- "sampling_rate": sample_rate
422
- })
423
-
424
- processing_time = time.time() - start_time
425
- logging.debug(f"🧠 Whisper raw output for {task_id}: {result}")
426
-
427
- text = result.get("text", "").strip()
428
-
429
- # Enhanced validation
430
- if not text:
431
- logging.warning(f"⚠️ Empty transcription for {task_id}")
432
- return ProcessingResult(False, "ERROR", "Empty transcription", processing_time)
433
-
434
- # Check for common transcription artifacts
435
- invalid_patterns = ["\x00", "�", "653", "[BLANK_AUDIO]", "Thank you.", "Thanks for watching."]
436
- if any(pattern in text for pattern in invalid_patterns):
437
- logging.warning(f"⚠️ Invalid transcription pattern detected for {task_id}: '{text}'")
438
- return ProcessingResult(False, "ERROR", "Invalid transcription pattern", processing_time)
439
-
440
- # Additional validation for very short or repetitive text
441
- if len(text) < 2 or len(set(text.split())) == 1:
442
- logging.warning(f"⚠️ Suspicious transcription for {task_id}: '{text}'")
443
- return ProcessingResult(False, "ERROR", "Suspicious transcription", processing_time)
444
-
445
- logging.debug(f"🎵 Audio transcription for {task_id}: '{text}' (processed in {processing_time:.2f}s)")
446
- return ProcessingResult(True, text, None, processing_time)
447
-
448
- except Exception as e:
449
- processing_time = time.time() - start_time
450
- logging.exception(f"❌ Audio processing error for {task_id}: {e}")
451
- return ProcessingResult(False, "ERROR", str(e), processing_time)
452
  finally:
453
- self._cleanup_temp_file(file_path)
454
-
455
- def __call__(self, data: Dict[str, Any]) -> str:
456
- """Process a question with comprehensive error handling and logging"""
457
- question_type = data.get("type", "text")
458
- question = data.get("question", "")
459
- task_id = data.get("task_id", "UNKNOWN")
460
-
461
- logging.info(f"🔄 Processing {question_type} question for task {task_id}")
462
-
463
- # Route to appropriate processor
464
- if question_type == "text":
465
- result = self._process_text_question(question, task_id)
466
- elif question_type == "image":
467
- result = self._process_image_question(task_id)
468
- elif question_type == "audio":
469
- result = self._process_audio_question(task_id)
470
- else:
471
- logging.error(f"❌ Unknown question type: {question_type}")
472
- return "ERROR"
473
-
474
- # Log result
475
- if result.success:
476
- logging.info(f"✅ Successfully processed {question_type} task {task_id} in {result.processing_time:.2f}s")
477
- else:
478
- logging.warning(f"⚠️ Failed to process {question_type} task {task_id}: {result.error}")
479
-
480
- return result.answer
481
-
482
- def cleanup(self) -> None:
483
- """Cleanup all temporary files"""
484
- for file_path in list(self.temp_files):
485
- self._cleanup_temp_file(file_path)
486
- logging.info("🧹 Cleanup completed")
487
-
488
- def run_and_submit(username: str) -> Generator[Tuple[str, str, str, str, Optional[pd.DataFrame]], None, None]:
489
- """Enhanced main execution function with better error handling"""
490
 
491
- def generate_updates():
492
- yield f"✅ Logged in as {username}", "Initializing...", "", "", None
493
-
494
- # Initialize agent
495
- agent = SmartAgent()
496
-
497
- # Report model loading status
498
- loaded_models = [k for k, v in agent.models_loaded.items() if v]
499
- if loaded_models:
500
- yield f"🤖 Models loaded: {', '.join(loaded_models)}", f"Models ready: {', '.join(loaded_models)}", "", "", None
501
- else:
502
- yield "❌ No models loaded successfully", "No models available", "", "", None
503
- return
 
 
 
 
 
504
 
505
  # Fetch questions
506
- try:
507
- logging.info("📥 Fetching questions from API...")
508
- resp = agent.http_session.get(f"{API_URL}/questions")
509
- resp.raise_for_status()
510
- questions = resp.json()
511
- logging.info(f"✅ Successfully fetched {len(questions)} questions")
512
- except Exception as e:
513
- error_msg = f"❌ Error fetching questions: {e}"
514
- logging.exception(error_msg)
515
- yield error_msg, "Failed to fetch questions", "", "", None
516
- return
517
-
518
- # Process questions
519
- answers = []
520
- logs = []
521
- total_questions = len(questions)
522
- total_processing_time = 0
523
-
524
- logging.info(f"🚀 Starting to process {total_questions} questions")
525
- yield f"📋 Processing {total_questions} questions...", f"0/{total_questions} questions processed", "", "", None
526
-
527
- for i, question_data in enumerate(questions, 1):
528
- start_time = time.time()
529
-
530
- # Extract question details
531
- question_preview = question_data.get('question', 'UNKNOWN')[:80] + "..." if len(question_data.get('question', '')) > 80 else question_data.get('question', 'UNKNOWN')
532
- full_question = question_data.get('question', 'UNKNOWN')
533
- question_type = question_data.get('type', 'UNKNOWN')
534
- task_id = question_data.get('task_id', 'UNKNOWN')
535
-
536
- logging.info(f"📦 Processing Q{i}/{total_questions} (ID: {task_id}): {question_preview}")
537
-
538
- # Update UI
 
 
539
  yield (
540
- f"📦 Processing question {i}/{total_questions}: {question_preview}...",
541
  f"{i-1}/{total_questions} questions processed",
542
  f"Q{i} [{question_type}]: {full_question}",
543
  "Processing...",
544
- pd.DataFrame(logs) if logs else None
545
  )
546
 
547
- # Process question
 
 
 
548
  try:
549
- answer = agent(question_data)
 
550
  processing_time = time.time() - start_time
551
- total_processing_time += processing_time
552
-
553
- logging.info(f"✅ Q{i} completed in {processing_time:.2f}s: {answer}")
554
-
555
  except Exception as e:
556
- processing_time = time.time() - start_time
557
- logging.exception(f"❌ Unexpected error processing Q{i}: {e}")
558
- answer = "ERROR"
559
-
560
- # Store results
561
- answers.append({
562
- "task_id": task_id,
563
- "submitted_answer": answer
564
- })
565
-
566
- # Update logs
567
- log_entry = QuestionLog(
568
  question_num=i,
569
  question_preview=question_preview,
570
  question_type=question_type,
571
- answer=answer or "SKIPPED",
572
  processing_time=processing_time,
573
- status="✅" if answer not in ["ERROR", "SKIPPED"] else "❌" if answer == "ERROR" else "⏭️"
574
- )
575
- logs.append(log_entry.__dict__)
576
 
577
- # Update UI with completed question
 
 
 
 
 
 
578
  yield (
579
- f"✅ Completed question {i}/{total_questions} in {processing_time:.2f}s",
580
  f"{i}/{total_questions} questions processed",
581
  f"Q{i} [{question_type}]: {full_question}",
582
- answer or "SKIPPED",
583
  pd.DataFrame(logs)
584
  )
585
-
586
- # Submit results
587
- logging.info(f"📤 Submitting {len(answers)} answers (total processing time: {total_processing_time:.2f}s)")
588
- yield f"📤 Submitting {len(answers)} answers...", f"{total_questions}/{total_questions} questions processed", "Submitting results...", "", pd.DataFrame(logs)
589
-
590
- try:
591
- space_id = "dmfelder/DF_Final_Assignment_Template"
592
- submission_body = {
593
- "username": username,
594
- "agent_code": f"https://huggingface.co/spaces/{space_id}/tree/main",
595
- "answers": answers
596
- }
597
-
598
- logging.debug(f"📤 Submitting payload: {len(answers)} answers")
599
- response = agent.http_session.post(f"{API_URL}/submit", json=submission_body)
600
- response.raise_for_status()
601
-
602
- result = response.json()
603
- score = result.get("score", "N/A")
604
-
605
- success_msg = f"🏁 Final Score: {score}% (processed in {total_processing_time:.2f}s total)"
606
- logging.info(f"✅ Submission successful: {score}%")
607
-
608
- yield success_msg, f"Complete! Score: {score}%", "Evaluation complete", f"Final score: {score}%", pd.DataFrame(logs)
609
-
610
- except Exception as e:
611
- error_msg = f"❌ Submission error: {e}"
612
- logging.exception(error_msg)
613
- yield error_msg, "Submission failed", "Error occurred", str(e), pd.DataFrame(logs)
614
- finally:
615
- # Cleanup
616
- agent.cleanup()
617
-
618
- return generate_updates()
619
-
620
- # Enhanced Gradio interface
621
- with gr.Blocks(
622
- title="Enhanced Smart Agent",
623
- theme=gr.themes.Soft(),
624
- css="""
625
- .gradio-container {
626
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
627
- }
628
- .status-box {
629
- background-color: #f8f9fa;
630
- border-radius: 8px;
631
- padding: 10px;
632
- }
633
- """
634
- ) as app:
635
- gr.Markdown("""
636
- # 🤖 Enhanced Smart Agent Evaluation
637
-
638
- **Features:**
639
- - Robust error handling and recovery
640
- - Enhanced model performance
641
- - Comprehensive logging and monitoring
642
- - Improved file handling and cleanup
643
- - Better answer validation and processing
644
- """)
645
-
646
- with gr.Row():
647
- login = gr.LoginButton(value="Login Required")
648
- run = gr.Button("🚀 Run Enhanced Agent", variant="primary", size="lg")
649
-
650
- with gr.Row():
651
- with gr.Column(scale=1):
652
- status = gr.Textbox(
653
- lines=12,
654
- label="📊 Status & Progress",
655
- placeholder="Ready to start...",
656
- elem_classes=["status-box"]
657
- )
658
- progress = gr.Textbox(
659
- label="🔄 Current Progress",
660
- value="Ready to start...",
661
- interactive=False
662
- )
663
-
664
- with gr.Column(scale=1):
665
- current_question = gr.Textbox(
666
- lines=4,
667
- label="❓ Current Question",
668
- placeholder="No question being processed...",
669
- interactive=False
670
- )
671
- current_answer = gr.Textbox(
672
- label="💬 Current Answer",
673
- placeholder="No answer yet...",
674
- interactive=False
675
- )
676
-
677
- results = gr.DataFrame(
678
- label="📈 Processing Results",
679
- headers=["Q", "Question", "Type", "Answer", "Processing Time", "Status"],
680
- wrap=True
681
- )
682
-
683
- with gr.Row():
684
- log_file = gr.File(label="📋 Download Detailed Log")
685
- gr.Markdown("*Log file will be available after processing completes*")
686
-
687
- def start_processing(profile):
688
- """Start the agent processing with enhanced error handling"""
689
- if not profile:
690
- yield "❌ You must log in first.", "Not logged in", "", "", pd.DataFrame(), None
691
- return
692
-
693
- updates = []
694
- final_df = None
695
-
696
- try:
697
- for msg, prog, question, answer, df in run_and_submit(profile):
698
- updates.append(msg)
699
- if df is not None:
700
- final_df = df
701
- yield (
702
- "\n".join(updates[-10:]), # Show last 10 updates
703
- prog,
704
- question,
705
- answer,
706
- final_df if final_df is not None else pd.DataFrame(),
707
- None
708
- )
709
-
710
- # Final yield includes log file
711
- yield (
712
- "\n".join(updates[-10:]),
713
- prog,
714
- question,
715
- answer,
716
- final_df if final_df is not None else pd.DataFrame(),
717
- log_path
718
- )
719
-
720
- except Exception as e:
721
- error_msg = f"❌ Critical error during processing: {e}"
722
- logging.exception(error_msg)
723
- yield (
724
- error_msg,
725
- "Critical error occurred",
726
- "Error",
727
- str(e),
728
- pd.DataFrame(),
729
- log_path
730
- )
731
-
732
- run.click(
733
- start_processing,
734
- inputs=[login],
735
- outputs=[status, progress, current_question, current_answer, results, log_file]
736
  )
737
 
738
  if __name__ == "__main__":
739
- logging.info("🚀 Starting Enhanced Smart Agent application")
740
- app.launch(
741
- share=False,
742
- server_name="0.0.0.0",
743
- server_port=7860,
744
- show_error=True,
745
- quiet=False
746
- )
 
6
  from dataclasses import dataclass
7
  from pathlib import Path
8
  import hashlib
9
+ import re
10
+ import requests
11
+ import pandas as pd
12
+ import tempfile
13
+ import gradio as gr
14
+ from transformers import pipeline
15
+ from PIL import Image
16
+ import soundfile as sf
17
+ import numpy as np
18
+ from requests.adapters import HTTPAdapter
19
+ from urllib3.util.retry import Retry
20
+ from huggingface_hub import InferenceClient, get_inference_client, Agent
21
 
22
  # Setup log file path
23
  log_path = os.path.join(os.getcwd(), f"agent_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
 
45
  logging.info(f"===== Application Startup at {datetime.now().isoformat()} =====")
46
  logging.info(f"📂 Log file created at: {log_path}")
47
 
48
+ # --- Constants and Data Classes ---
49
+ API_URL = "https://huggingface.co/api/agent-course"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
 
 
 
 
 
 
 
 
51
  @dataclass
52
  class QuestionLog:
 
53
  question_num: int
54
  question_preview: str
55
  question_type: str
 
57
  processing_time: float
58
  status: str
59
 
60
+ # Define some common patterns for cleaning up agent output
61
+ ANSWER_PATTERNS = [
62
+ r"the answer is:\s*(.*)",
63
+ r"final_answer\((.*)\)",
64
+ r"final_answer\(\s*\"(.*?)\"\s*\)",
65
+ r"final_answer\(\s*'(.*?)'\s*\)",
66
+ r"final_answer\(\s*```(.*?)```\s*\)",
67
+ r"final_answer\(\s*`(.*?)`\s*\)",
68
+ r"final_answer\(\s*\[(.*?)\]\s*\)", # For list-like answers
69
+ r"final_answer\(\s*\{(.*?)\}\s*\)", # For dict-like answers
70
+ r"final_answer\(\s*(.*?)\s*\)", # Most general final_answer
71
+ r"answer is\s*(.*)",
72
+ r"final answer is\s*(.*)",
73
+ r"final answer:\s*(.*)",
74
+ r"final answer of the question is:\s*(.*)",
75
+ r"the final answer is:\s*(.*)",
76
+ r"final answer\s*:\s*(.*)",
77
+ r"final_answer\s*:\s*(.*)",
78
+ r"final_answer is\s*(.*)",
79
+ r"the value is:\s*(.*)",
80
+ r" (.*)",
81
+ r"The final answer is: (.*)",
82
+ r"final answer of the question is (.*)",
83
+ r"final answer to the question is (.*)",
84
+ r"final response: (.*)",
85
+ r"Final Response: (.*)",
86
+ r"the final response is (.*)",
87
+ r"Final Response is: (.*)",
88
+ r"Final Answer:\s*(.*)",
89
+ r"Final Answer is:\s*(.*)",
90
+ r"Answer:\s*(.*)",
91
+ r"The answer is:\s*(.*)",
92
+ r"Final Answer\s*\[([^\]]+)\]",
93
+ r"The final answer is\s*\[([^\]]+)\]",
94
+ r"The answer is\s*\[([^\]]+)\]",
95
+ r"Answer\s*\[([^\]]+)\]",
96
+ r"```json\n\{\"answer\":\s*\"(.*?)\"\n\}```", # For JSON encapsulated answers
97
+ r"```json\n\{\"answer\":\s*(.*?)\n\}```", # For JSON encapsulated answers (non-string)
98
+ r"\"answer\":\s*\"(.*?)\"",
99
+ r"\"answer\":\s*(.*)",
100
+ r"(\w[\w\s\.\-,\/]*)\s*$", # Catch any trailing string
101
+ ]
102
+
103
+ # --- SmartAgent Class ---
104
+ class SmartAgent:
105
+ def __init__(self, username: str, http_session: requests.Session):
106
+ self.username = username
107
+ self.http_session = http_session
108
+ self.agent_id = None
109
+ self.agent: Optional[Agent] = None # Type hint for clarity
110
+ self.pipelines = {} # Cache for pipelines
111
+
112
+ self.tool_code_cache: Dict[str, str] = {} # To cache tool code
113
+ logging.info("SmartAgent initialized.")
114
+
115
+ def setup(self):
116
+ logging.info("Setting up agent...")
117
  try:
118
+ self.agent = self._initialize_agent()
119
+ logging.info("Agent setup complete.")
120
+ except Exception as e:
121
+ logging.exception(f"Error during agent setup: {e}")
122
  raise
123
+
124
+ def _initialize_agent(self) -> Agent:
125
+ logging.info("Initializing Hugging Face Agent...")
126
  try:
127
+ # Ensure HF_TOKEN is available in the environment or via login
128
+ hf_token = os.environ.get("HF_TOKEN")
129
+ if not hf_token:
130
+ # Fallback to get_inference_client's default token handling (e.g., from ~/.cache/huggingface/token)
131
+ logging.warning("HF_TOKEN environment variable not set. Relying on Hugging Face Hub client token.")
132
+ client = get_inference_client() # This will pick up token from env or cache
133
+ else:
134
+ client = InferenceClient(token=hf_token)
135
+
136
+ # Check if agent already exists for this username
137
+ list_agents_resp = self.http_session.get(f"{API_URL}/agents")
138
+ list_agents_resp.raise_for_status()
139
+ existing_agents = list_agents_resp.json()
140
+ logging.debug(f"Existing agents: {existing_agents}")
141
+
142
+ for agent_info in existing_agents:
143
+ if agent_info.get("username") == self.username:
144
+ self.agent_id = agent_info["agent_id"]
145
+ logging.info(f"Re-using existing agent with ID: {self.agent_id}")
146
+ return Agent(id=self.agent_id, client=client)
147
+
148
+ # If no existing agent, create a new one
149
+ create_agent_resp = self.http_session.post(f"{API_URL}/agents", json={"username": self.username})
150
+ create_agent_resp.raise_for_status()
151
+ created_agent_info = create_agent_resp.json()
152
+ self.agent_id = created_agent_info["agent_id"]
153
+ logging.info(f"Created new agent with ID: {self.agent_id}")
154
+ return Agent(id=self.agent_id, client=client)
155
+
156
+ except requests.exceptions.RequestException as req_e:
157
+ logging.error(f"Network or API error during agent initialization: {req_e}")
158
+ raise
159
+ except Exception as e:
160
+ logging.error(f"Unexpected error during agent initialization: {e}")
161
  raise
162
 
163
+ def _get_tool_code(self, tool_code_hash: str) -> str:
164
+ if tool_code_hash in self.tool_code_cache:
165
+ return self.tool_code_cache[tool_code_hash]
166
+
167
+ logging.info(f"Fetching tool code for hash: {tool_code_hash}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  try:
169
+ resp = self.http_session.get(f"{API_URL}/tool_code/{tool_code_hash}")
170
+ resp.raise_for_status()
171
+ tool_code = resp.json().get("tool_code", "")
172
+ if not tool_code:
173
+ raise ValueError(f"Tool code for hash {tool_code_hash} is empty.")
174
+ self.tool_code_cache[tool_code_hash] = tool_code
175
+ return tool_code
176
+ except requests.exceptions.RequestException as req_e:
177
+ logging.error(f"Failed to fetch tool code for {tool_code_hash}: {req_e}")
178
+ raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  except Exception as e:
180
+ logging.error(f"Error getting tool code: {e}")
181
+ raise
182
+
183
+ def _execute_agent(self, question: str, question_type: str, tools_code: Optional[List[Dict]] = None) -> str:
184
+ if not self.agent:
185
+ raise ValueError("Agent not initialized. Call setup() first.")
186
+
187
+ logging.info(f"Executing agent for question type '{question_type}': {question[:50]}...")
188
  try:
189
+ # Prepare special tools if provided by the API
190
+ special_tools = []
191
+ if tools_code:
192
+ for tool_def in tools_code:
193
+ tool_code_hash = tool_def.get("tool_code_hash")
194
+ tool_name = tool_def.get("tool_name")
195
+ if tool_code_hash and tool_name:
196
+ tool_code_str = self._get_tool_code(tool_code_hash)
197
+ # Create a unique function name to avoid conflicts if `exec` is used multiple times
198
+ unique_func_name = f"dynamic_tool_func_{hashlib.md5(tool_code_str.encode()).hexdigest()}"
199
+ tool_code_str = tool_code_str.replace("def run_tool", f"def {unique_func_name}")
200
+
201
+ global_vars = {}
202
+ local_vars = {"inputs": None, "tool_code_hash": tool_code_hash} # inputs will be passed by agent
203
+ exec(tool_code_str, global_vars, local_vars)
204
+
205
+ if unique_func_name not in global_vars:
206
+ raise ValueError(f"Function {unique_func_name} not found after executing tool code.")
207
+
208
+ special_tools.append(
209
+ Agent.create_tool(
210
+ name=tool_name,
211
+ description=f"Dynamically loaded tool for {tool_name}",
212
+ function=global_vars[unique_func_name]
213
+ )
214
+ )
215
+ logging.debug(f"Added dynamic tool: {tool_name}")
216
+
217
+ # Run the agent
218
+ agent_output = self.agent.run(question, additional_tools=special_tools if special_tools else None)
219
+
220
+ # Extract the last response from chat history
221
+ raw_answer = agent_output.chat_history[-1].response
222
+ logging.debug(f"Agent raw output: {raw_answer}")
223
+ return raw_answer
224
  except Exception as e:
225
+ logging.error(f"Error during agent execution: {e}")
226
+ raise
227
+
228
+ def _extract_answer(self, raw_answer: str, question_type: str) -> str:
229
+ logging.debug(f"Extracting answer from raw_answer: {raw_answer}")
230
+ answer = "ERROR"
231
+ # Try to find a match using the defined patterns
232
+ for pattern in ANSWER_PATTERNS:
233
+ match = re.search(pattern, raw_answer, re.IGNORECASE | re.DOTALL)
234
+ if match:
235
+ extracted_content = match.group(1).strip()
236
+ # Basic cleanup
237
+ extracted_content = extracted_content.replace("\\n", "").replace("\\", "")
238
+ # Remove quotes if the answer is just a quoted string (e.g., "answer")
239
+ if extracted_content.startswith('"') and extracted_content.endswith('"'):
240
+ extracted_content = extracted_content[1:-1]
241
+ if extracted_content.startswith("'") and extracted_content.endswith("'"):
242
+ extracted_content = extracted_content[1:-1]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
+ answer = extracted_content
245
+ logging.debug(f"Extracted answer using pattern '{pattern}': {answer}")
246
+ break # Stop after the first successful extraction
 
 
 
 
 
 
 
 
 
 
247
 
248
+ if answer == "ERROR" and raw_answer:
249
+ # If no pattern matches, try to use the raw answer if it looks reasonable
250
+ # (e.g., if the agent just directly gave the answer without a wrapper)
251
+ if len(raw_answer) < 200 and not any(kw in raw_answer.lower() for kw in ["thought", "tool", "action", "observation"]):
252
+ answer = raw_answer.strip()
253
+ logging.debug(f"No pattern matched, using raw answer directly: {answer}")
254
+
255
+ if not answer: # Ensure answer is not empty if extraction failed
256
+ answer = "ERROR"
257
+
258
+ return answer
259
+
260
+ def _load_pipeline(self, pipeline_name: str, **kwargs):
261
+ if pipeline_name not in self.pipelines:
262
+ logging.info(f"Loading pipeline: {pipeline_name}")
263
+ self.pipelines[pipeline_name] = pipeline(pipeline_name, **kwargs)
264
+ return self.pipelines[pipeline_name]
265
+
266
+ def _run_tool(self, tool_name: str, inputs: Dict[str, Any]) -> Any:
267
+ logging.info(f"Running tool: {tool_name} with inputs: {inputs}")
268
+ result = None
269
+ temp_file_paths = [] # To keep track of temp files for cleanup
270
+
271
  try:
272
+ if tool_name == "image-to-text":
273
+ image_bytes = self.http_session.get(inputs["image"]).content
274
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_img_file:
275
+ tmp_img_file.write(image_bytes)
276
+ image_path = tmp_img_file.name
277
+ temp_file_paths.append(image_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
+ image = Image.open(image_path)
280
+ image_to_text_pipeline = self._load_pipeline("image-to-text")
281
+ result = image_to_text_pipeline(image)[0]["generated_text"]
282
+ logging.info(f"Image-to-text result: {result[:50]}...")
283
+
284
+ elif tool_name == "text-to-image":
285
+ text_to_image_pipeline = self._load_pipeline("text-to-image", model="runwayml/stable-diffusion-v1-5")
286
+ images = text_to_image_pipeline(inputs["text"])
287
+ if images and images.images:
288
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_output_img_file:
289
+ images.images[0].save(tmp_output_img_file.name)
290
+ result = tmp_output_img_file.name # Return path or bytes as needed by agent
291
+ temp_file_paths.append(result)
292
+ logging.info(f"Text-to-image result saved to: {result}")
293
+ else:
294
+ logging.warning("Text-to-image pipeline returned no images.")
295
+ result = None
296
+
297
+ elif tool_name == "speech-to-text":
298
+ audio_bytes = self.http_session.get(inputs["audio"]).content
299
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".flac") as tmp_audio_file:
300
+ tmp_audio_file.write(audio_bytes)
301
+ audio_path = tmp_audio_file.name
302
+ temp_file_paths.append(audio_path)
303
+
304
+ speech_to_text_pipeline = self._load_pipeline("automatic-speech-recognition", model="openai/whisper-tiny.en")
305
+ result = speech_to_text_pipeline(audio_path)["text"]
306
+ logging.info(f"Speech-to-text result: {result[:50]}...")
307
+
308
+ elif tool_name == "text-to-speech":
309
+ text_to_speech_pipeline = self._load_pipeline("text-to-speech", model="suno/bark-small")
310
+ speech = text_to_speech_pipeline(inputs["text"])
311
+ if speech and speech.audio is not None:
312
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".flac") as tmp_output_audio_file:
313
+ sf.write(tmp_output_audio_file.name, speech.audio.numpy(), samplerate=speech.sampling_rate)
314
+ result = tmp_output_audio_file.name # Return path
315
+ temp_file_paths.append(result)
316
+ logging.info(f"Text-to-speech result saved to: {result}")
317
+ else:
318
+ logging.warning("Text-to-speech pipeline returned no audio.")
319
+ result = None
320
+ else:
321
+ logging.warning(f"Unknown tool: {tool_name}. Skipping execution.")
322
+ return f"Error: Unknown tool {tool_name}"
323
+
324
+ return result
325
+
326
  except Exception as e:
327
+ logging.error(f"Error running tool '{tool_name}': {e}", exc_info=True)
328
+ return f"Error running tool {tool_name}: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  finally:
330
+ # Clean up temporary files
331
+ for fp in temp_file_paths:
332
+ if os.path.exists(fp):
333
+ try:
334
+ os.unlink(fp)
335
+ logging.debug(f"Cleaned up temporary file: {fp}")
336
+ except OSError as e:
337
+ logging.warning(f"Could not delete temporary file {fp}: {e}")
338
+
339
+ def cleanup(self):
340
+ logging.info("Cleaning up agent resources...")
341
+ if self.agent and self.agent_id:
342
+ try:
343
+ # Assuming the Hugging Face Agent object has a delete method or similar
344
+ # For this course, you might need to call a specific API endpoint or rely on
345
+ # the agent session expiring if no direct delete method is provided by the lib.
346
+ # If there's a specific API endpoint for deleting agents, use it here.
347
+ # For now, we'll log its ID as deleted.
348
+ # If agent.delete() exists, use it:
349
+ # self.agent.delete()
350
+ # For the course, simply logging and clearing internal state might be enough.
351
+ logging.info(f"Agent with ID {self.agent_id} is conceptually deleted (or will expire).")
352
+ except Exception as e:
353
+ logging.warning(f"Failed to delete agent or clean up its remote state: {e}")
354
+ # Clear cached pipelines and tool code
355
+ self.pipelines.clear()
356
+ self.tool_code_cache.clear()
357
+ logging.info("SmartAgent resources cleaned up.")
358
+
359
+
360
+ # --- Main Run and Submit Function ---
361
+ def run_and_submit(username: str) -> Generator[Tuple[str, str, str, str, pd.DataFrame], None, None]:
362
+ logging.info(f"Starting run and submit for user: {username}")
 
 
 
 
363
 
364
+ # Configure HTTP session with retries
365
+ retry_strategy = Retry(
366
+ total=3,
367
+ backoff_factor=1,
368
+ status_forcelist=[429, 500, 502, 503, 504],
369
+ allowed_methods=["HEAD", "GET", "POST", "PUT", "DELETE", "OPTIONS", "TRACE"] # Added "POST" for retries on agent API calls
370
+ )
371
+ adapter = HTTPAdapter(max_retries=retry_strategy)
372
+ http_session = requests.Session()
373
+ http_session.mount("https://", adapter)
374
+ http_session.mount("http://", adapter)
375
+
376
+ agent = SmartAgent(username=username, http_session=http_session)
377
+ answers: List[Dict[str, str]] = []
378
+ logs: List[Dict[str, Any]] = [] # List of dicts for DataFrame
379
+
380
+ try:
381
+ agent.setup()
382
 
383
  # Fetch questions
384
+ logging.info("Fetching questions from API...")
385
+ questions_resp = agent.http_session.get(f"{API_URL}/questions")
386
+ questions_resp.raise_for_status()
387
+ questions_data = questions_resp.json()
388
+ total_questions = len(questions_data)
389
+ logging.info(f"Received {total_questions} questions.")
390
+
391
+ yield (
392
+ "🚀 Starting agent run...",
393
+ f"0/{total_questions} questions processed",
394
+ "Initializing...",
395
+ "",
396
+ pd.DataFrame(logs)
397
+ )
398
+
399
+ for i, question_data in enumerate(questions_data, 1):
400
+ task_id = question_data.get("task_id")
401
+ question_type = question_data.get("question_type")
402
+ question_preview = question_data.get("question_preview")
403
+ full_question = question_data.get("question")
404
+ tools_code = question_data.get("tools_code") # List of tool definitions
405
+
406
+ if not all([task_id, question_type, question_preview, full_question]):
407
+ logging.warning(f"Skipping malformed question data: {question_data}")
408
+ logs.append(QuestionLog(
409
+ question_num=i,
410
+ question_preview="Malformed Data",
411
+ question_type="N/A",
412
+ answer="ERROR: Malformed Question",
413
+ processing_time=0.0,
414
+ status="ERROR"
415
+ ).__dict__)
416
+ continue
417
+
418
+ logging.info(f"Processing Q{i}/{total_questions} [Type: {question_type}]: {question_preview}")
419
  yield (
420
+ f"Working on Q{i}/{total_questions}...",
421
  f"{i-1}/{total_questions} questions processed",
422
  f"Q{i} [{question_type}]: {full_question}",
423
  "Processing...",
424
+ pd.DataFrame(logs)
425
  )
426
 
427
+ start_time = time.time()
428
+ answer = "ERROR" # Default answer in case of unhandled error
429
+ status = "ERROR" # Default status
430
+
431
  try:
432
+ agent_answer = agent._execute_agent(full_question, question_type, tools_code)
433
+ answer = agent._extract_answer(agent_answer, question_type)
434
  processing_time = time.time() - start_time
435
+ logging.info(f"Q{i} [{question_type}] Answer: {answer} (Took {processing_time:.2f}s)")
436
+ status = "OK" if answer != "ERROR" else "ERROR" # Update status based on extracted answer
437
+
 
438
  except Exception as e:
439
+ logging.exception(f"❌ Q{i} [{question_type}] Error processing question: {e}")
440
+ answer = "ERROR" # Ensure answer is "ERROR" on exception
441
+ # Calculate processing_time even on error if start_time was set
442
+ processing_time = time.time() - start_time if 'start_time' in locals() else 0.0
443
+ status = "ERROR" # Explicitly set status to ERROR
444
+
445
+ logs.append(QuestionLog(
 
 
 
 
 
446
  question_num=i,
447
  question_preview=question_preview,
448
  question_type=question_type,
449
+ answer=answer,
450
  processing_time=processing_time,
451
+ status=status
452
+ ).__dict__)
 
453
 
454
+ # Only append to answers list if not a critical error preventing an answer
455
+ # The course might expect "ERROR" as a valid answer for submission in some cases
456
+ answers.append({
457
+ "task_id": task_id,
458
+ "answer": answer
459
+ })
460
+
461
  yield (
462
+ f"✅ Q{i}/{total_questions} processed",
463
  f"{i}/{total_questions} questions processed",
464
  f"Q{i} [{question_type}]: {full_question}",
465
+ f"Answer: {answer}",
466
  pd.DataFrame(logs)
467
  )
468
+
469
+ # Submit answers
470
+ logging.info("📤 Submitting answers to API")
471
+ resp = agent.http_session.post(f"{API_URL}/submit", json={
472
+ "username": username,
473
+ "answers": answers
474
+ })
475
+ resp.raise_for_status()
476
+ result = resp.json()
477
+ logging.info(f"✅ Submission successful: {result}")
478
+ yield (
479
+ "🎉 Submission completed successfully!",
480
+ f"Final Score: {result.get('score', 'Unknown')}",
481
+ f"Details: {result.get('message', '')}",
482
+ "",
483
+ pd.DataFrame(logs)
484
+ )
485
+ except requests.exceptions.RequestException as req_e:
486
+ logging.exception(f"❌ API Request failed during run or submission: {req_e}")
487
+ yield (
488
+ "❌ API Error",
489
+ "Submission failed due to network/API error",
490
+ str(req_e),
491
+ "",
492
+ pd.DataFrame(logs)
493
+ )
494
+ except Exception as e:
495
+ logging.exception(f"❌ An unexpected error occurred during run or submission: {e}")
496
+ yield (
497
+ "❌ Run Failed",
498
+ "An unexpected error occurred",
499
+ str(e),
500
+ "",
501
+ pd.DataFrame(logs)
502
+ )
503
+ finally:
504
+ agent.cleanup()
505
+
506
+
507
+ # --- Gradio UI ---
508
+ with gr.Blocks() as demo:
509
+ username_input = gr.Textbox(label="Username")
510
+ status_display = gr.Textbox(label="Status")
511
+ progress_display = gr.Textbox(label="Progress")
512
+ question_display = gr.Textbox(label="Current Question")
513
+ answer_display = gr.Textbox(label="Answer")
514
+ logs_display = gr.Dataframe(label="Log", headers=["question_num", "question_preview", "question_type", "answer", "processing_time", "status"])
515
+
516
+ run_button = gr.Button("Run Agent")
517
+
518
+ def start_run(username):
519
+ # This function acts as a simple wrapper to call the generator
520
+ yield from run_and_submit(username)
521
+
522
+ run_button.click(
523
+ fn=start_run,
524
+ inputs=[username_input],
525
+ outputs=[
526
+ status_display,
527
+ progress_display,
528
+ question_display,
529
+ answer_display,
530
+ logs_display
531
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
  )
533
 
534
  if __name__ == "__main__":
535
+ demo.launch()