tatianija commited on
Commit
5d98e50
·
verified ·
1 Parent(s): 7155971

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +93 -795
app.py CHANGED
@@ -1,827 +1,125 @@
1
- import os
2
- import gradio as gr
3
- import requests
4
- import inspect
5
- import time
6
- import pandas as pd
7
- from smolagents import DuckDuckGoSearchTool
8
- import threading
9
- from typing import Dict, List, Optional, Tuple, Union
10
- import json
11
- from huggingface_hub import InferenceClient
12
- import base64
13
- from PIL import Image
14
- import io
15
- import tempfile
16
- import urllib.parse
17
- from pathlib import Path
18
- import re
19
- from bs4 import BeautifulSoup
20
- import mimetypes
21
-
22
- # --- Constants ---
23
- DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
24
-
25
- # --- Global Cache for Answers ---
26
- cached_answers = {}
27
- cached_questions = []
28
- processing_status = {"is_processing": False, "progress": 0, "total": 0}
29
-
30
- # --- Web Content Fetcher ---
31
- class WebContentFetcher:
32
- def __init__(self, debug: bool = True):
33
- self.debug = debug
34
- self.session = requests.Session()
35
- self.session.headers.update({
36
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
37
- })
38
-
39
- def extract_urls_from_text(self, text: str) -> List[str]:
40
- """Extract URLs from text using regex."""
41
- url_pattern = r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
42
- urls = re.findall(url_pattern, text)
43
- return list(set(urls)) # Remove duplicates
44
-
45
- def fetch_url_content(self, url: str) -> Dict[str, str]:
46
- """
47
- Fetch content from a URL and extract text, handling different content types.
48
- Returns a dictionary with 'content', 'title', 'content_type', and 'error' keys.
49
- """
50
- try:
51
- # Clean the URL
52
- url = url.strip()
53
- if not url.startswith(('http://', 'https://')):
54
- url = 'https://' + url
55
-
56
- if self.debug:
57
- print(f"Fetching URL: {url}")
58
-
59
- response = self.session.get(url, timeout=30, allow_redirects=True)
60
- response.raise_for_status()
61
-
62
- content_type = response.headers.get('content-type', '').lower()
63
-
64
- result = {
65
- 'url': url,
66
- 'content_type': content_type,
67
- 'title': '',
68
- 'content': '',
69
- 'error': None
70
- }
71
-
72
- # Handle different content types
73
- if 'text/html' in content_type:
74
- # Parse HTML content
75
- soup = BeautifulSoup(response.content, 'html.parser')
76
-
77
- # Extract title
78
- title_tag = soup.find('title')
79
- result['title'] = title_tag.get_text().strip() if title_tag else 'No title'
80
-
81
- # Remove script and style elements
82
- for script in soup(["script", "style"]):
83
- script.decompose()
84
-
85
- # Extract text content
86
- text_content = soup.get_text()
87
-
88
- # Clean up text
89
- lines = (line.strip() for line in text_content.splitlines())
90
- chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
91
- text_content = ' '.join(chunk for chunk in chunks if chunk)
92
-
93
- # Limit content length
94
- if len(text_content) > 8000:
95
- text_content = text_content[:8000] + "... (truncated)"
96
-
97
- result['content'] = text_content
98
-
99
- elif 'text/plain' in content_type:
100
- # Handle plain text
101
- text_content = response.text
102
- if len(text_content) > 8000:
103
- text_content = text_content[:8000] + "... (truncated)"
104
- result['content'] = text_content
105
- result['title'] = f"Text document from {url}"
106
-
107
- elif 'application/json' in content_type:
108
- # Handle JSON content
109
- try:
110
- json_data = response.json()
111
- result['content'] = json.dumps(json_data, indent=2)[:8000]
112
- result['title'] = f"JSON document from {url}"
113
- except:
114
- result['content'] = response.text[:8000]
115
- result['title'] = f"JSON document from {url}"
116
-
117
- elif any(x in content_type for x in ['application/pdf', 'application/msword', 'application/vnd.openxmlformats']):
118
- # Handle document files
119
- result['content'] = f"Document file detected ({content_type}). Content extraction for this file type is not implemented."
120
- result['title'] = f"Document from {url}"
121
-
122
- else:
123
- # Handle other content types
124
- if response.text:
125
- content = response.text[:8000]
126
- result['content'] = content
127
- result['title'] = f"Content from {url}"
128
- else:
129
- result['content'] = f"Non-text content detected ({content_type})"
130
- result['title'] = f"File from {url}"
131
-
132
- if self.debug:
133
- print(f"Successfully fetched content from {url}: {len(result['content'])} characters")
134
-
135
- return result
136
-
137
- except requests.exceptions.RequestException as e:
138
- error_msg = f"Failed to fetch {url}: {str(e)}"
139
- if self.debug:
140
- print(error_msg)
141
- return {
142
- 'url': url,
143
- 'content_type': 'error',
144
- 'title': f"Error fetching {url}",
145
- 'content': '',
146
- 'error': error_msg
147
- }
148
- except Exception as e:
149
- error_msg = f"Unexpected error fetching {url}: {str(e)}"
150
- if self.debug:
151
- print(error_msg)
152
- return {
153
- 'url': url,
154
- 'content_type': 'error',
155
- 'title': f"Error fetching {url}",
156
- 'content': '',
157
- 'error': error_msg
158
- }
159
-
160
- def fetch_multiple_urls(self, urls: List[str]) -> List[Dict[str, str]]:
161
- """Fetch content from multiple URLs."""
162
- results = []
163
- for url in urls[:5]: # Limit to 5 URLs to avoid excessive processing
164
- result = self.fetch_url_content(url)
165
- results.append(result)
166
- time.sleep(1) # Be respectful to servers
167
- return results
168
-
169
- # --- File Processing Utility ---
170
- def save_attachment_to_file(attachment_data: Union[str, bytes, dict], temp_dir: str, file_name: str = None) -> Optional[str]:
171
  """
172
- Save attachment data to a temporary file.
173
- Returns the local file path if successful, None otherwise.
174
  """
 
 
 
 
 
 
 
175
  try:
176
- # Determine file name and extension
177
- if not file_name:
178
- file_name = f"attachment_{int(time.time())}"
179
-
180
- # Handle different data types
181
- if isinstance(attachment_data, dict):
182
- # Handle dict with file data
183
- if 'data' in attachment_data:
184
- file_data = attachment_data['data']
185
- file_type = attachment_data.get('type', '').lower()
186
- original_name = attachment_data.get('name', file_name)
187
- elif 'content' in attachment_data:
188
- file_data = attachment_data['content']
189
- file_type = attachment_data.get('mime_type', '').lower()
190
- original_name = attachment_data.get('filename', file_name)
191
- else:
192
- # Try to use the dict as file data directly
193
- file_data = str(attachment_data)
194
- file_type = ''
195
- original_name = file_name
196
-
197
- # Use original name if available
198
- if original_name and original_name != file_name:
199
- file_name = original_name
200
-
201
- elif isinstance(attachment_data, str):
202
- # Could be base64 encoded data or plain text
203
- file_data = attachment_data
204
- file_type = ''
205
-
206
- elif isinstance(attachment_data, bytes):
207
- # Binary data
208
- file_data = attachment_data
209
- file_type = ''
210
-
211
- else:
212
- print(f"Unknown attachment data type: {type(attachment_data)}")
213
- return None
214
-
215
- # Ensure file has an extension
216
- if '.' not in file_name:
217
- # Try to determine extension from type
218
- if 'image' in file_type:
219
- if 'jpeg' in file_type or 'jpg' in file_type:
220
- file_name += '.jpg'
221
- elif 'png' in file_type:
222
- file_name += '.png'
223
- else:
224
- file_name += '.img'
225
- elif 'audio' in file_type:
226
- if 'mp3' in file_type:
227
- file_name += '.mp3'
228
- elif 'wav' in file_type:
229
- file_name += '.wav'
230
- else:
231
- file_name += '.audio'
232
- elif 'python' in file_type or 'text' in file_type:
233
- file_name += '.py'
234
- else:
235
- file_name += '.file'
236
-
237
- file_path = os.path.join(temp_dir, file_name)
238
-
239
- # Save the file
240
- if isinstance(file_data, str):
241
- # Try to decode if it's base64
242
- try:
243
- # Check if it looks like base64
244
- if len(file_data) > 100 and '=' in file_data[-5:]:
245
- decoded_data = base64.b64decode(file_data)
246
- with open(file_path, 'wb') as f:
247
- f.write(decoded_data)
248
- else:
249
- # Plain text
250
- with open(file_path, 'w', encoding='utf-8') as f:
251
- f.write(file_data)
252
- except:
253
- # If base64 decode fails, save as text
254
- with open(file_path, 'w', encoding='utf-8') as f:
255
- f.write(file_data)
256
- else:
257
- # Binary data
258
- with open(file_path, 'wb') as f:
259
- f.write(file_data)
260
-
261
- print(f"Saved attachment: {file_path}")
262
- return file_path
263
 
264
- except Exception as e:
265
- print(f"Failed to save attachment: {e}")
266
- return None
 
 
267
 
268
- # --- Code Processing Tool ---
269
- class CodeAnalysisTool:
270
- def __init__(self, model_name: str = "meta-llama/Llama-3.1-8B-Instruct"):
271
- self.client = InferenceClient(model=model_name, provider="sambanova")
272
-
273
- def analyze_code(self, code_path: str) -> str:
274
- """
275
- Analyze Python code and return insights.
276
- """
277
- try:
278
- with open(code_path, 'r', encoding='utf-8') as f:
279
- code_content = f.read()
280
-
281
- # Limit code length for analysis
282
- if len(code_content) > 5000:
283
- code_content = code_content[:5000] + "\n... (truncated)"
284
-
285
- analysis_prompt = f"""Analyze this Python code and provide a concise summary of:
286
- 1. What the code does (main functionality)
287
- 2. Key functions/classes
288
- 3. Any notable patterns or issues
289
- 4. Input/output behavior if applicable
290
 
291
- Code:
292
- ```python
293
- {code_content}
294
- ```
 
 
 
 
 
 
295
 
296
- Provide a brief, focused analysis:"""
 
 
 
 
 
 
 
 
 
297
 
298
- messages = [{"role": "user", "content": analysis_prompt}]
299
- response = self.client.chat_completion(
300
- messages=messages,
301
- max_tokens=500,
302
- temperature=0.3
303
- )
304
-
305
- return response.choices[0].message.content.strip()
306
-
307
- except Exception as e:
308
- return f"Code analysis failed: {e}"
309
 
310
- # --- Image Processing Tool ---
311
- class ImageAnalysisTool:
312
- def __init__(self, model_name: str = "microsoft/Florence-2-large"):
313
- self.client = InferenceClient(model=model_name)
314
-
315
- def analyze_image(self, image_path: str, prompt: str = "Describe this image in detail") -> str:
316
- """
317
- Analyze an image and return a description.
318
- """
319
- try:
320
- # Open and process the image
321
- with open(image_path, "rb") as f:
322
- image_bytes = f.read()
323
-
324
- # Use the vision model to analyze the image
325
- response = self.client.image_to_text(
326
- image=image_bytes,
327
- model="microsoft/Florence-2-large"
328
- )
329
-
330
- return response.get("generated_text", "Could not analyze image")
331
-
332
- except Exception as e:
333
- try:
334
- # Fallback: use a different vision model
335
- response = self.client.image_to_text(
336
- image=image_bytes,
337
- model="Salesforce/blip-image-captioning-large"
338
- )
339
- return response.get("generated_text", f"Image analysis error: {e}")
340
- except:
341
- return f"Image analysis failed: {e}"
342
 
343
- def extract_text_from_image(self, image_path: str) -> str:
344
- """
345
- Extract text from an image using OCR.
346
- """
347
- try:
348
- with open(image_path, "rb") as f:
349
- image_bytes = f.read()
350
-
351
- # Use an OCR model
352
- response = self.client.image_to_text(
353
- image=image_bytes,
354
- model="microsoft/trocr-base-printed"
355
- )
356
 
357
- return response.get("generated_text", "No text found in image")
358
-
359
- except Exception as e:
360
- return f"OCR failed: {e}"
361
 
362
- # --- Audio Processing Tool ---
363
- class AudioTranscriptionTool:
364
- def __init__(self, model_name: str = "openai/whisper-large-v3"):
365
- self.client = InferenceClient(model=model_name)
366
-
367
- def transcribe_audio(self, audio_path: str) -> str:
368
- """
369
- Transcribe audio file to text.
370
- """
371
- try:
372
- with open(audio_path, "rb") as f:
373
- audio_bytes = f.read()
374
-
375
- # Use Whisper for transcription
376
- response = self.client.automatic_speech_recognition(
377
- audio=audio_bytes
378
- )
379
-
380
- return response.get("text", "Could not transcribe audio")
381
-
382
- except Exception as e:
383
- try:
384
- # Fallback to a different ASR model
385
- response = self.client.automatic_speech_recognition(
386
- audio=audio_bytes,
387
- model="facebook/wav2vec2-large-960h-lv60-self"
388
- )
389
- return response.get("text", f"Audio transcription error: {e}")
390
- except:
391
- return f"Audio transcription failed: {e}"
392
 
393
- # --- Enhanced Intelligent Agent with Direct Attachment Processing ---
394
- class IntelligentAgent:
395
- def __init__(self, debug: bool = True, model_name: str = "meta-llama/Llama-3.1-8B-Instruct"):
396
- self.search = DuckDuckGoSearchTool()
397
- self.client = InferenceClient(model=model_name, provider="sambanova")
398
- self.image_tool = ImageAnalysisTool()
399
- self.audio_tool = AudioTranscriptionTool()
400
- self.code_tool = CodeAnalysisTool(model_name)
401
- self.web_fetcher = WebContentFetcher(debug)
402
- self.debug = debug
403
  if self.debug:
404
- print(f"IntelligentAgent initialized with model: {model_name}")
405
-
406
- def _chat_completion(self, prompt: str, max_tokens: int = 500, temperature: float = 0.3) -> str:
407
- """
408
- Use chat completion instead of text generation to avoid provider compatibility issues.
409
- """
410
- try:
411
- messages = [{"role": "user", "content": prompt}]
412
-
413
- # Try chat completion first
414
- try:
415
- response = self.client.chat_completion(
416
- messages=messages,
417
- max_tokens=max_tokens,
418
- temperature=temperature
419
- )
420
- return response.choices[0].message.content.strip()
421
- except Exception as chat_error:
422
- if self.debug:
423
- print(f"Chat completion failed: {chat_error}, trying text generation...")
424
-
425
- # Fallback to text generation
426
- response = self.client.conversational(
427
- prompt,
428
- max_new_tokens=max_tokens,
429
- temperature=temperature,
430
- do_sample=temperature > 0
431
- )
432
- return response.strip()
433
-
434
- except Exception as e:
435
- if self.debug:
436
- print(f"Both chat completion and text generation failed: {e}")
437
- raise e
438
-
439
- def _extract_and_process_urls(self, question_text: str) -> str:
440
- """
441
- Extract URLs from question text and fetch their content.
442
- Returns formatted content from all URLs.
443
- """
444
- urls = self.web_fetcher.extract_urls_from_text(question_text)
445
 
446
- if not urls:
447
- return ""
448
 
 
 
 
449
  if self.debug:
450
- print(f"...Found {len(urls)} URLs in question: {urls}")
451
-
452
- url_contents = self.web_fetcher.fetch_multiple_urls(urls)
453
 
454
- if not url_contents:
455
- return ""
456
-
457
- # Format the content
458
- formatted_content = []
459
- for content_data in url_contents:
460
- if content_data['error']:
461
- formatted_content.append(f"URL: {content_data['url']}\nError: {content_data['error']}")
462
- else:
463
- formatted_content.append(
464
- f"URL: {content_data['url']}\n"
465
- f"Title: {content_data['title']}\n"
466
- f"Content Type: {content_data['content_type']}\n"
467
- f"Content: {content_data['content']}"
468
- )
469
-
470
- return "\n\n" + "="*50 + "\n".join(formatted_content) + "\n" + "="*50
471
-
472
- def _detect_and_process_direct_attachments(self, file_name: str) -> Tuple[List[str], List[str], List[str]]:
473
- """
474
- Detect and process a single attachment directly attached to a question (not as a URL).
475
- Returns (image_files, audio_files, code_files)
476
- """
477
- image_files = []
478
- audio_files = []
479
- code_files = []
480
-
481
- try:
482
- # Here, file_type should ideally come from metadata or inferred from content —
483
- # since only attachment_name is passed, we'll rely on the file extension.
484
- # Get file extension
485
- file_ext = Path(file_name).suffix.lower()
486
-
487
- # Determine category
488
- is_image = (
489
- file_ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff']
490
- )
491
- is_audio = (
492
- file_ext in ['.mp3', '.wav', '.m4a', '.ogg', '.flac', '.aac']
493
- )
494
- is_code = (
495
- file_ext in ['.py', '.txt', '.js', '.html', '.css', '.json', '.xml']
496
- )
497
-
498
- # Categorize the file
499
- if is_image:
500
- image_files.append(file_path)
501
- elif is_audio:
502
- audio_files.append(file_path)
503
- elif is_code:
504
- code_files.append(file_path)
505
- else:
506
- # Default to code/text for unknown types
507
- code_files.append(file_path)
508
-
509
- except Exception as e:
510
- if getattr(self, 'debug', False):
511
- print(f"Error processing attachment {file_name}: {e}")
512
-
513
- if getattr(self, 'debug', False):
514
- print(f"...Processed attachment: {len(image_files)} images, {len(audio_files)} audio, {len(code_files)} code files")
515
-
516
- return image_files, audio_files, code_files
517
-
518
- def _process_attachments(self, image_files: List[str] = None, audio_files: List[str] = None, code_files: List[str] = None) -> str:
519
- """
520
- Process all types of attachments and return their content as text.
521
- """
522
- attachment_content = []
523
-
524
- # Process code files
525
- if code_files:
526
- for code_file in code_files:
527
- if code_file and os.path.exists(code_file):
528
- try:
529
- # First, include the raw code content (truncated)
530
- with open(code_file, 'r', encoding='utf-8') as f:
531
- code_content = f.read()
532
-
533
- if len(code_content) > 1000:
534
- code_preview = code_content[:1000] + "\n... (truncated)"
535
- else:
536
- code_preview = code_content
537
-
538
- attachment_content.append(f"Code File Content:\n```python\n{code_preview}\n```")
539
-
540
- # Then add analysis
541
- code_analysis = self.code_tool.analyze_code(code_file)
542
- attachment_content.append(f"Code Analysis: {code_analysis}")
543
-
544
- except Exception as e:
545
- attachment_content.append(f"Error processing code file {code_file}: {e}")
546
-
547
- # Process images
548
- if image_files:
549
- for image_file in image_files:
550
- if image_file and os.path.exists(image_file):
551
- try:
552
- # Analyze the image
553
- image_description = self.image_tool.analyze_image(image_file)
554
- attachment_content.append(f"Image Analysis: {image_description}")
555
-
556
- # Try to extract text from image
557
- extracted_text = self.image_tool.extract_text_from_image(image_file)
558
- if extracted_text and "No text found" not in extracted_text:
559
- attachment_content.append(f"Text from Image: {extracted_text}")
560
-
561
- except Exception as e:
562
- attachment_content.append(f"Error processing image {image_file}: {e}")
563
-
564
- # Process audio files
565
- if audio_files:
566
- for audio_file in audio_files:
567
- if audio_file and os.path.exists(audio_file):
568
- try:
569
- # Transcribe the audio
570
- transcription = self.audio_tool.transcribe_audio(audio_file)
571
- attachment_content.append(f"Audio Transcription: {transcription}")
572
-
573
- except Exception as e:
574
- attachment_content.append(f"Error processing audio {audio_file}: {e}")
575
 
576
- return "\n\n".join(attachment_content) if attachment_content else ""
577
-
578
- def _should_search(self, question: str, attachment_context: str = "", url_context: str = "") -> bool:
579
- """
580
- Use LLM to determine if search is needed for the question, considering attachment and URL context.
581
- Returns True if search is recommended, False otherwise.
582
- """
583
- decision_prompt = f"""Analyze this question and decide if it requires real-time information, recent data, or specific facts that might not be in your training data.
584
-
585
- SEARCH IS NEEDED for:
586
- - Current events, news, recent developments
587
- - Real-time data (weather, stock prices, sports scores)
588
- - Specific factual information that changes frequently
589
- - Recent product releases, company information
590
- - Current status of people, organizations, or projects
591
- - Location-specific current information
592
-
593
- SEARCH IS NOT NEEDED for:
594
- - General knowledge questions
595
- - Mathematical calculations
596
- - Programming concepts and syntax
597
- - Historical facts (older than 1 year)
598
- - Definitions of well-established concepts
599
- - How-to instructions for common tasks
600
- - Creative writing or opinion-based responses
601
- - Questions that can be answered from attached files (code, images, audio)
602
- - Questions that can be answered from URL content provided
603
- - Code analysis, debugging, or explanation questions
604
- - Questions about uploaded or linked content
605
-
606
- Question: "{question}"
607
-
608
- {f"Attachment Context Available: {attachment_context[:1000]}..." if attachment_context else "No attachment context available."}
609
-
610
- {f"URL Content Available: {url_context[:1000]}..." if url_context else "No URL content available."}
611
-
612
- If you cannot provide an answer, reply with "NO_SEARCH". Respond with only "SEARCH" or "NO_SEARCH" followed by a brief reason (max 20 words).
613
-
614
- Example responses:
615
- - "SEARCH - Current weather data needed"
616
- - "NO_SEARCH - Mathematical concept, general knowledge sufficient"
617
- - "NO_SEARCH - Can be answered from attached code/image/URL content"
618
- """
619
-
620
- try:
621
- response = self._chat_completion(decision_prompt, max_tokens=50, temperature=0.1)
622
-
623
- decision = response.strip().upper()
624
- should_search = decision.startswith("SEARCH")
625
- time.sleep(5)
626
-
627
- if self.debug:
628
- print(f"4. Decision regarding the search: {decision}")
629
-
630
- return should_search
631
 
632
- except Exception as e:
633
- if self.debug:
634
- print(f"Error in search decision: {e}, defaulting to no search for questions with context")
635
- # Default to no search if decision fails and there is context available
636
- return len(attachment_context) == 0 and len(url_context) == 0
637
-
638
- def _answer_with_llm(self, question: str, attachment_context: str = "", url_context: str = "") -> str:
639
- """
640
- Generate answer using LLM without search, considering attachment and URL context.
641
- """
642
- context_sections = []
643
 
644
- if attachment_context:
645
- context_sections.append(f"Attachment Context:\n{attachment_context}")
646
 
647
- if url_context:
648
- context_sections.append(f"URL Content:\n{url_context}")
649
-
650
- context_section = "\n\n".join(context_sections) if context_sections else ""
651
-
652
- answer_prompt = f"""\no_think You are a general AI assistant. I will ask you a question.
653
- YOUR ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
654
- 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.
655
- 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.
656
- 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.
657
- Do not add a dot after the numbers.
658
- Do not report on your thoughts. Do not provide explanations.
659
- {context_section}
660
-
661
- Question: {question}
662
-
663
- Answer:"""
664
-
665
- try:
666
- response = self._chat_completion(answer_prompt, max_tokens=500, temperature=0.3)
667
- return response
668
-
669
- except Exception as e:
670
- return f"Sorry, I encountered an error generating the response: {e}"
671
-
672
- def _answer_with_search(self, question: str, attachment_context: str = "", url_context: str = "") -> str:
673
- """
674
- Generate answer using search results and LLM, considering attachment and URL context.
675
- """
676
- try:
677
- # Perform search
678
- time.sleep(10)
679
- search_results = self.search(question)
680
-
681
- if not search_results:
682
- return "No search results found. Let me try to answer based on my knowledge:\n\n" + self._answer_with_llm(question, attachment_context, url_context)
683
-
684
- # Format search results - handle different result formats
685
- if isinstance(search_results, str):
686
- search_context = search_results
687
- else:
688
- # Handle list of results
689
- formatted_results = []
690
- for i, result in enumerate(search_results[:3]): # Use top 3 results
691
- if isinstance(result, dict):
692
- title = result.get("title", "No title")
693
- snippet = result.get("snippet", "").strip()
694
- link = result.get("link", "")
695
- formatted_results.append(f"Title: {title}\nContent: {snippet}\nSource: {link}")
696
- elif isinstance(result, str):
697
- formatted_results.append(result)
698
- else:
699
- formatted_results.append(str(result))
700
-
701
- search_context = "\n\n".join(formatted_results)
702
-
703
-
704
- # Generate answer using search context, attachment context, and URL context
705
- context_sections = [f"Search Results:\n{search_context}"]
706
-
707
- if attachment_context:
708
- context_sections.append(f"Attachment Context:\n{attachment_context}")
709
-
710
- if url_context:
711
- context_sections.append(f"URL Content:\n{url_context}")
712
-
713
- full_context = "\n\n".join(context_sections)
714
 
 
 
715
  if self.debug:
716
- print(f"{full_context}")
717
-
718
-
719
- answer_prompt = f"""\no_think You are a general AI assistant. I will ask you a question.
720
- Based on the search results and the context sections below, provide an answer to the question.
721
- If the search results don't fully answer the question, you can supplement with information from other context sections or your general knowledge.
722
- Your ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
723
- Do not add dot if your answer is a number.
724
- 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.
725
- 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.
726
- 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.
727
- Do not report on your thoughts. Do not provide explanations.
728
-
729
- Question: {question}
730
-
731
- {full_context}
732
-
733
- Answer:"""
734
-
735
- try:
736
- response = self._chat_completion(answer_prompt, max_tokens=600, temperature=0.3)
737
- return response
738
-
739
- except Exception as e:
740
- if self.debug:
741
- print(f"LLM generation error: {e}")
742
- # Fallback to simple search result formatting
743
- if search_results:
744
- if isinstance(search_results, str):
745
- return search_results
746
- elif isinstance(search_results, list) and len(search_results) > 0:
747
- first_result = search_results[0]
748
- if isinstance(first_result, dict):
749
- title = first_result.get("title", "Search Result")
750
- snippet = first_result.get("snippet", "").strip()
751
- link = first_result.get("link", "")
752
- return f"**{title}**\n\n{snippet}\n\n{f'Source: {link}' if link else ''}"
753
- else:
754
- return str(first_result)
755
- else:
756
- return str(search_results)
757
- else:
758
- return "Search completed but no usable results found."
759
-
760
- except Exception as e:
761
- return f"Search failed: {e}. Let me try to answer based on my knowledge:\n\n" + self._answer_with_llm(question, attachment_context, url_context)
762
-
763
- def process_question_with_attachments(self, question_data: dict) -> str:
764
- """
765
- Process a question that may have attachments and URLs.
766
- """
767
- question_text = question_data.get('question', '')
768
- print(question_data)
769
- if self.debug:
770
- print(f"\n1. Processing question with potential attachments and URLs: {question_text[:300]}...")
771
-
772
- try:
773
- # Detect and process URLs
774
- print(f"2. Detecting and processing URLs...")
775
-
776
- url_context = self._extract_and_process_urls(question_text)
777
-
778
  if self.debug:
779
- print(f"URL context: {url_context[:200]}...")
780
- except Exception as e:
781
- answer = f"Sorry, I encountered an error extracting URLs: {e}"
782
-
783
- try:
784
- # Detect and download attachments
785
- print(f"3. Searching for images, audio or code attachments...")
786
- attachment_name = question_data.get('file_name', '')
787
- image_files, audio_files, code_files = self._detect_and_process_direct_attachments(attachment_name)
788
-
789
- # Process attachments to get context
790
- attachment_context = self._process_attachments(image_files, audio_files, code_files)
791
-
792
- if self.debug and attachment_context:
793
- print(f"Attachment context: {attachment_context[:200]}...")
794
-
795
- # Decide whether to search
796
- if self._should_search(question_text, attachment_context):
797
- if self.debug:
798
- print("5. Using search-based approach")
799
- answer = self._answer_with_search(question_text, attachment_context)
800
- else:
801
- if self.debug:
802
- print("5. Using LLM-only approach")
803
- answer = self._answer_with_llm(question_text, attachment_context)
804
- print("here")
805
- print(answer)
806
- # Cleanup temporary files
807
- if image_files or audio_files or code_files:
808
- try:
809
- all_files = image_files + audio_files + code_files
810
- temp_dirs = set(os.path.dirname(f) for f in all_files)
811
- for temp_dir in temp_dirs:
812
- import shutil
813
- shutil.rmtree(temp_dir, ignore_errors=True)
814
- except Exception as cleanup_error:
815
- if self.debug:
816
- print(f"Cleanup error: {cleanup_error}")
817
 
818
- except Exception as e:
819
- answer = f"Sorry, I encountered an error: {e}"
820
 
 
821
  if self.debug:
822
- print(f"6. Agent returning answer: {answer[:100]}...")
823
- return answer
824
 
 
 
 
825
  def fetch_questions() -> Tuple[str, Optional[pd.DataFrame]]:
826
  """
827
  Fetch questions from the API and cache them.
 
1
+ def _detect_and_process_direct_attachments(self, file_name: str) -> Tuple[List[str], List[str], List[str]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  """
3
+ Detect and process a single attachment directly attached to a question (not as a URL).
4
+ Returns (image_files, audio_files, code_files)
5
  """
6
+ image_files = []
7
+ audio_files = []
8
+ code_files = []
9
+
10
+ if not file_name:
11
+ return image_files, audio_files, code_files
12
+
13
  try:
14
+ # Construct the file path (assuming file is in current directory)
15
+ file_path = os.path.join(os.getcwd(), file_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ # Check if file exists
18
+ if not os.path.exists(file_path):
19
+ if self.debug:
20
+ print(f"File not found: {file_path}")
21
+ return image_files, audio_files, code_files
22
 
23
+ # Get file extension
24
+ file_ext = Path(file_name).suffix.lower()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
+ # Determine category
27
+ is_image = (
28
+ file_ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff']
29
+ )
30
+ is_audio = (
31
+ file_ext in ['.mp3', '.wav', '.m4a', '.ogg', '.flac', '.aac']
32
+ )
33
+ is_code = (
34
+ file_ext in ['.py', '.txt', '.js', '.html', '.css', '.json', '.xml', '.md', '.c', '.cpp', '.java']
35
+ )
36
 
37
+ # Categorize the file
38
+ if is_image:
39
+ image_files.append(file_path)
40
+ elif is_audio:
41
+ audio_files.append(file_path)
42
+ elif is_code:
43
+ code_files.append(file_path)
44
+ else:
45
+ # Default to code/text for unknown types
46
+ code_files.append(file_path)
47
 
48
+ if self.debug:
49
+ print(f"Processed file: {file_name} -> {'image' if is_image else 'audio' if is_audio else 'code'}")
 
 
 
 
 
 
 
 
 
50
 
51
+ except Exception as e:
52
+ if self.debug:
53
+ print(f"Error processing attachment {file_name}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ if self.debug:
56
+ print(f"Processed attachment: {len(image_files)} images, {len(audio_files)} audio, {len(code_files)} code files")
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ return image_files, audio_files, code_files
 
 
 
59
 
60
+ def process_question_with_attachments(self, question_data: dict) -> str:
61
+ """
62
+ Process a question that may have attachments and URLs.
63
+ """
64
+ question_text = question_data.get('question', '')
65
+ if self.debug:
66
+ print(f"Question data keys: {list(question_data.keys())}")
67
+ print(f"\n1. Processing question with potential attachments and URLs: {question_text[:300]}...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
+ try:
70
+ # Detect and process URLs
 
 
 
 
 
 
 
 
71
  if self.debug:
72
+ print(f"2. Detecting and processing URLs...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
+ url_context = self._extract_and_process_urls(question_text)
 
75
 
76
+ if self.debug and url_context:
77
+ print(f"URL context found: {len(url_context)} characters")
78
+ except Exception as e:
79
  if self.debug:
80
+ print(f"Error extracting URLs: {e}")
81
+ url_context = ""
 
82
 
83
+ try:
84
+ # Detect and download attachments
85
+ if self.debug:
86
+ print(f"3. Searching for images, audio or code attachments...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
+ attachment_name = question_data.get('file_name', '')
89
+ if self.debug:
90
+ print(f"Attachment name from question_data: '{attachment_name}'")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
+ image_files, audio_files, code_files = self._detect_and_process_direct_attachments(attachment_name)
 
 
 
 
 
 
 
 
 
 
93
 
94
+ # Process attachments to get context
95
+ attachment_context = self._process_attachments(image_files, audio_files, code_files)
96
 
97
+ if self.debug and attachment_context:
98
+ print(f"Attachment context: {attachment_context[:200]}...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ # Decide whether to search
101
+ if self._should_search(question_text, attachment_context, url_context):
102
  if self.debug:
103
+ print("5. Using search-based approach")
104
+ answer = self._answer_with_search(question_text, attachment_context, url_context)
105
+ else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  if self.debug:
107
+ print("5. Using LLM-only approach")
108
+ answer = self._answer_with_llm(question_text, attachment_context, url_context)
109
+ if self.debug:
110
+ print(f"LLM answer: {answer}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
+ # Note: We don't cleanup files here since they're not temporary files we created
113
+ # They are actual files in the working directory
114
 
115
+ except Exception as e:
116
  if self.debug:
117
+ print(f"Error in attachment processing: {e}")
118
+ answer = f"Sorry, I encountered an error: {e}"
119
 
120
+ if self.debug:
121
+ print(f"6. Agent returning answer: {answer[:100]}...")
122
+ return answer
123
  def fetch_questions() -> Tuple[str, Optional[pd.DataFrame]]:
124
  """
125
  Fetch questions from the API and cache them.