akashch1512 commited on
Commit
0789aad
·
verified ·
1 Parent(s): d6674a2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1 -449
app.py CHANGED
@@ -1,451 +1,3 @@
1
- import os
2
- import re
3
- import io
4
- import json
5
- import time
6
- import base64
7
- import mimetypes
8
- from pathlib import Path
9
- from urllib.parse import urlparse
10
-
11
- import gradio as gr
12
- import pandas as pd
13
- import requests
14
- import spaces
15
- from bs4 import BeautifulSoup
16
- from openai import OpenAI
17
-
18
- # ---------------------------------------------------------------------
19
- # Configuration
20
- # ---------------------------------------------------------------------
21
- DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
22
-
23
- # Configure these in Hugging Face Space → Settings → Variables and secrets.
24
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
25
- OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o")
26
- OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
27
- AUDIO_MODEL = os.getenv("AUDIO_MODEL", "whisper-1")
28
-
29
- MAX_ATTACHMENT_CHARS = 24000
30
- MAX_WEB_CHARS = 14000
31
- HTTP_TIMEOUT = 45
32
-
33
- # ZeroGPU startup compatibility. This function is deliberately never called.
34
- # The agent uses OpenAI's remote API, not the Space GPU.
35
- @spaces.GPU(duration=1)
36
- def _zerogpu_startup_probe():
37
- return None
38
-
39
-
40
- # ---------------------------------------------------------------------
41
- # Utility functions
42
- # ---------------------------------------------------------------------
43
- def clean_answer(text: str) -> str:
44
- """Remove common answer wrappers while preserving the answer itself."""
45
- answer = (text or "").strip()
46
- answer = re.sub(
47
- r"^\s*(?:FINAL ANSWER|FINAL|ANSWER)\s*[:\-]\s*",
48
- "",
49
- answer,
50
- flags=re.IGNORECASE,
51
- )
52
- answer = answer.strip()
53
- if len(answer) >= 2 and answer[0] == answer[-1] and answer[0] in {"'", '"'}:
54
- answer = answer[1:-1].strip()
55
- return answer
56
-
57
-
58
- def extract_urls(text: str) -> list[str]:
59
- urls = re.findall(r"https?://[^\s<>\])}]+", text or "")
60
- return [u.rstrip(".,;:!?") for u in urls]
61
-
62
-
63
- def image_to_data_url(filename: str, content: bytes) -> str:
64
- mime, _ = mimetypes.guess_type(filename)
65
- if not mime:
66
- mime = "image/jpeg"
67
- encoded = base64.b64encode(content).decode("ascii")
68
- return f"data:{mime};base64,{encoded}"
69
-
70
-
71
- # ---------------------------------------------------------------------
72
- # GAIA Agent
73
- # ---------------------------------------------------------------------
74
- class BasicAgent:
75
- def __init__(self):
76
- if not OPENAI_API_KEY:
77
- raise ValueError(
78
- "Missing OPENAI_API_KEY. Add it under Space Settings → "
79
- "Variables and secrets."
80
- )
81
-
82
- self.client = OpenAI(
83
- api_key=OPENAI_API_KEY,
84
- base_url=OPENAI_BASE_URL,
85
- timeout=120,
86
- max_retries=2,
87
- )
88
- self.api_url = DEFAULT_API_URL
89
- print(f"BasicAgent initialized with model: {OPENAI_MODEL}")
90
-
91
- def download_attachment(self, task_id: str, file_name: str) -> bytes | None:
92
- if not file_name:
93
- return None
94
-
95
- url = f"{self.api_url}/files/{task_id}"
96
- print(f"Downloading attachment for task {task_id}: {file_name}")
97
- response = requests.get(url, timeout=HTTP_TIMEOUT)
98
- response.raise_for_status()
99
- return response.content
100
-
101
- def extract_attachment(
102
- self, file_name: str, content: bytes
103
- ) -> tuple[str, list[dict]]:
104
- """
105
- Return extracted textual context and optional GPT-4o image content.
106
- Handles common GAIA attachment types.
107
- """
108
- suffix = Path(file_name).suffix.lower()
109
- text_parts: list[str] = []
110
- image_parts: list[dict] = []
111
-
112
- # Plain text and structured text.
113
- if suffix in {
114
- ".txt", ".md", ".json", ".csv", ".tsv", ".py", ".html",
115
- ".xml", ".log", ".yaml", ".yml",
116
- }:
117
- text_parts.append(
118
- content.decode("utf-8", errors="replace")[:MAX_ATTACHMENT_CHARS]
119
- )
120
-
121
- # PDF documents.
122
- elif suffix == ".pdf":
123
- try:
124
- from pypdf import PdfReader
125
- reader = PdfReader(io.BytesIO(content))
126
- pages = []
127
- for page in reader.pages[:35]:
128
- pages.append(page.extract_text() or "")
129
- text_parts.append("\n".join(pages)[:MAX_ATTACHMENT_CHARS])
130
- except Exception as exc:
131
- text_parts.append(f"[PDF extraction error: {exc}]")
132
-
133
- # Excel workbooks. Include rows, not just a small preview, where possible.
134
- elif suffix in {".xlsx", ".xls"}:
135
- try:
136
- engine = "openpyxl" if suffix == ".xlsx" else "xlrd"
137
- sheets = pd.read_excel(
138
- io.BytesIO(content),
139
- sheet_name=None,
140
- engine=engine,
141
- )
142
- for sheet_name, dataframe in list(sheets.items())[:8]:
143
- text_parts.append(
144
- f"SHEET: {sheet_name}\n"
145
- f"ROWS: {len(dataframe)}\n"
146
- f"DATA:\n{dataframe.to_csv(index=False)}"
147
- )
148
- if sum(len(part) for part in text_parts) > MAX_ATTACHMENT_CHARS:
149
- break
150
- text_parts = ["\n\n".join(text_parts)[:MAX_ATTACHMENT_CHARS]]
151
- except Exception as exc:
152
- text_parts.append(f"[Spreadsheet extraction error: {exc}]")
153
-
154
- # Word documents.
155
- elif suffix == ".docx":
156
- try:
157
- from docx import Document
158
- doc = Document(io.BytesIO(content))
159
- paragraphs = [p.text for p in doc.paragraphs if p.text.strip()]
160
- for table in doc.tables:
161
- for row in table.rows:
162
- paragraphs.append(" | ".join(cell.text for cell in row.cells))
163
- text_parts.append("\n".join(paragraphs)[:MAX_ATTACHMENT_CHARS])
164
- except Exception as exc:
165
- text_parts.append(f"[DOCX extraction error: {exc}]")
166
-
167
- # PowerPoint presentations.
168
- elif suffix == ".pptx":
169
- try:
170
- from pptx import Presentation
171
- presentation = Presentation(io.BytesIO(content))
172
- slides_text = []
173
- for slide_number, slide in enumerate(presentation.slides, start=1):
174
- slide_lines = []
175
- for shape in slide.shapes:
176
- if getattr(shape, "has_text_frame", False):
177
- slide_lines.append(shape.text)
178
- if getattr(shape, "has_table", False):
179
- for row in shape.table.rows:
180
- slide_lines.append(
181
- " | ".join(cell.text for cell in row.cells)
182
- )
183
- slides_text.append(
184
- f"SLIDE {slide_number}\n" + "\n".join(slide_lines)
185
- )
186
- text_parts.append("\n\n".join(slides_text)[:MAX_ATTACHMENT_CHARS])
187
- except Exception as exc:
188
- text_parts.append(f"[PPTX extraction error: {exc}]")
189
-
190
- # Images: send directly to GPT-4o vision.
191
- elif suffix in {".png", ".jpg", ".jpeg", ".webp", ".gif"}:
192
- image_parts.append({
193
- "type": "image_url",
194
- "image_url": {"url": image_to_data_url(file_name, content)},
195
- })
196
-
197
- # Audio: transcribe through OpenAI's audio transcription endpoint.
198
- elif suffix in {".mp3", ".wav", ".m4a", ".mpeg", ".mpga", ".webm"}:
199
- try:
200
- mime, _ = mimetypes.guess_type(file_name)
201
- with io.BytesIO(content) as audio_file:
202
- audio_file.name = Path(file_name).name
203
- transcription = self.client.audio.transcriptions.create(
204
- model=AUDIO_MODEL,
205
- file=audio_file,
206
- )
207
- text_parts.append(
208
- "AUDIO TRANSCRIPTION:\n" + (transcription.text or "")
209
- )
210
- except Exception as exc:
211
- text_parts.append(f"[Audio transcription error: {exc}]")
212
-
213
- else:
214
- text_parts.append(
215
- f"[Attachment '{file_name}' was downloaded, but its format "
216
- "is not supported by this implementation.]"
217
- )
218
-
219
- return "\n\n".join(text_parts), image_parts
220
-
221
- def search_web(self, query: str) -> str:
222
- """Best-effort DuckDuckGo search plus extraction of a few result pages."""
223
- try:
224
- from ddgs import DDGS
225
- results = DDGS().text(query, max_results=5)
226
- except Exception as exc:
227
- print(f"Web search unavailable: {exc}")
228
- return ""
229
-
230
- snippets = []
231
- urls_to_read = []
232
-
233
- for result in results or []:
234
- title = result.get("title", "")
235
- body = result.get("body", "")
236
- url = result.get("href") or result.get("url") or ""
237
- snippets.append(f"TITLE: {title}\nURL: {url}\nSNIPPET: {body}")
238
- if url.startswith("http"):
239
- urls_to_read.append(url)
240
-
241
- page_texts = []
242
- for url in urls_to_read[:2]:
243
- try:
244
- response = requests.get(
245
- url,
246
- timeout=20,
247
- headers={"User-Agent": "Mozilla/5.0 (compatible; GAIAAgent/1.0)"},
248
- )
249
- response.raise_for_status()
250
- content_type = response.headers.get("content-type", "").lower()
251
- if "text/html" not in content_type:
252
- continue
253
- soup = BeautifulSoup(response.text, "html.parser")
254
- for tag in soup(["script", "style", "nav", "footer", "header"]):
255
- tag.decompose()
256
- page_text = soup.get_text(" ", strip=True)
257
- if page_text:
258
- page_texts.append(
259
- f"SOURCE URL: {url}\nPAGE TEXT:\n{page_text[:5000]}"
260
- )
261
- except Exception as exc:
262
- print(f"Could not read search result {url}: {exc}")
263
-
264
- combined = "\n\n".join(snippets + page_texts)
265
- return combined[:MAX_WEB_CHARS]
266
-
267
- def fetch_given_urls(self, question: str) -> str:
268
- """Read URLs explicitly supplied in the question."""
269
- chunks = []
270
- for url in extract_urls(question)[:3]:
271
- try:
272
- response = requests.get(
273
- url,
274
- timeout=25,
275
- headers={"User-Agent": "Mozilla/5.0 (compatible; GAIAAgent/1.0)"},
276
- )
277
- response.raise_for_status()
278
- content_type = response.headers.get("content-type", "").lower()
279
- if "text/html" in content_type or "text/plain" in content_type:
280
- soup = BeautifulSoup(response.text, "html.parser")
281
- for tag in soup(["script", "style", "nav", "footer", "header"]):
282
- tag.decompose()
283
- content = soup.get_text(" ", strip=True)
284
- chunks.append(f"URL: {url}\nCONTENT:\n{content[:5000]}")
285
- except Exception as exc:
286
- chunks.append(f"URL: {url}\nCould not read page: {exc}")
287
- return "\n\n".join(chunks)[:MAX_WEB_CHARS]
288
-
289
- def ask_model(
290
- self,
291
- question: str,
292
- attachment_context: str = "",
293
- web_context: str = "",
294
- image_parts: list[dict] | None = None,
295
- ) -> str:
296
- image_parts = image_parts or []
297
-
298
- system_prompt = """
299
- You are a highly capable research and problem-solving assistant answering
300
- GAIA benchmark questions.
301
-
302
- Your objective is to return the exact answer requested by the question.
303
-
304
- Rules:
305
- 1. Analyze the question carefully and identify every constraint.
306
- 2. Use the supplied attachment content when relevant.
307
- 3. Use web evidence when the question asks for obscure, historical, current,
308
- or externally verifiable facts. Prefer reliable primary sources.
309
- 4. For calculations, calculate precisely. For spreadsheet questions, inspect
310
- all relevant rows and distinguish the requested categories.
311
- 5. Do not guess when the supplied evidence contradicts your assumptions.
312
- 6. Follow the requested output format exactly: alphabetize when requested,
313
- use comma-separated values when requested, and use the requested name form.
314
- 7. For numeric answers, do not add units, currency symbols, commas, or
315
- explanatory text unless the question explicitly requests them.
316
- 8. Return ONLY the final answer. No explanation, citations, markdown, or
317
- 'FINAL ANSWER:' prefix.
318
- """
319
-
320
- user_content = [
321
- {
322
- "type": "text",
323
- "text": (
324
- f"QUESTION:\n{question}\n\n"
325
- f"ATTACHMENT CONTENT:\n"
326
- f"{attachment_context or 'No attachment content.'}\n\n"
327
- f"WEB RESEARCH:\n"
328
- f"{web_context or 'No web research retrieved.'}"
329
- ),
330
- }
331
- ]
332
- user_content.extend(image_parts[:3])
333
-
334
- last_error = None
335
- for attempt in range(3):
336
- try:
337
- response = self.client.chat.completions.create(
338
- model=OPENAI_MODEL,
339
- temperature=0,
340
- max_tokens=700,
341
- messages=[
342
- {"role": "system", "content": system_prompt},
343
- {"role": "user", "content": user_content},
344
- ],
345
- )
346
- answer = response.choices[0].message.content or ""
347
- return clean_answer(answer)
348
- except Exception as exc:
349
- last_error = exc
350
- print(f"OpenAI request attempt {attempt + 1} failed: {exc}")
351
- if attempt < 2:
352
- time.sleep(2 ** attempt)
353
-
354
- raise RuntimeError(f"OpenAI model request failed: {last_error}")
355
-
356
- def __call__(self, task: dict) -> str:
357
- task_id = str(task.get("task_id", ""))
358
- question = str(task.get("question", ""))
359
- file_name = str(task.get("file_name") or "")
360
-
361
- print(f"Processing task {task_id}; attachment={file_name or 'none'}")
362
-
363
- attachment_context = ""
364
- image_parts = []
365
-
366
- if file_name:
367
- try:
368
- file_bytes = self.download_attachment(task_id, file_name)
369
- if file_bytes:
370
- attachment_context, image_parts = self.extract_attachment(
371
- file_name, file_bytes
372
- )
373
- except Exception as exc:
374
- attachment_context = f"[Attachment download failed: {exc}]"
375
-
376
- # Search for externally verifiable questions. Avoid unnecessary search
377
- # when the question is clearly about a provided file or simple sorting.
378
- search_context = self.fetch_given_urls(question)
379
- lower_question = question.lower()
380
- needs_research = any(
381
- marker in lower_question
382
- for marker in (
383
- "as of", "who is", "who was", "which", "what year",
384
- "what date", "first name", "last name", "nationality",
385
- "recipient", "according to", "number before", "number after",
386
- "historical", "born", "award", "competition",
387
- )
388
- )
389
-
390
- if needs_research and not attachment_context:
391
- query = question[:450]
392
- found = self.search_web(query)
393
- if found:
394
- search_context = (search_context + "\n\n" + found).strip()
395
-
396
- answer = self.ask_model(
397
- question=question,
398
- attachment_context=attachment_context,
399
- web_context=search_context,
400
- image_parts=image_parts,
401
- )
402
-
403
- print(f"Answer for {task_id}: {answer}")
404
- return answer
405
-
406
-
407
- # ---------------------------------------------------------------------
408
- # Course evaluation / submission workflow
409
- # ---------------------------------------------------------------------
410
- def run_and_submit_all(profile: gr.OAuthProfile | None, progress=gr.Progress()):
411
- space_id = os.getenv("SPACE_ID", "")
412
-
413
- if not profile:
414
- return "Please sign in to Hugging Face using the login button.", pd.DataFrame()
415
-
416
- username = str(profile.username).strip()
417
- questions_url = f"{DEFAULT_API_URL}/questions"
418
- submit_url = f"{DEFAULT_API_URL}/submit"
419
-
420
- try:
421
- agent = BasicAgent()
422
- except Exception as exc:
423
- return f"Agent initialization failed: {exc}", pd.DataFrame()
424
-
425
- agent_code = (
426
- f"https://huggingface.co/spaces/{space_id}/tree/main"
427
- if space_id else ""
428
- )
429
-
430
- try:
431
- response = requests.get(questions_url, timeout=HTTP_TIMEOUT)
432
- response.raise_for_status()
433
- questions_data = response.json()
434
- if not isinstance(questions_data, list) or not questions_data:
435
- return "The scoring API returned no questions.", pd.DataFrame()
436
- except Exception as exc:
437
- return f"Could not fetch benchmark questions: {exc}", pd.DataFrame()
438
-
439
- results_log = []
440
- answers_payload = []
441
- total = len(questions_data)
442
-
443
- for index, item in enumerate(questions_data, start=1):
444
- task_id = item.get("task_id")
445
- question_text = item.get("question")
446
-
447
- if not task_id or question_text is None:
448
- continue
449
 
450
  progress((index - 1) / max(total, 1), desc=f"Solving question {index}/{total}")
451
 
@@ -570,4 +122,4 @@ if __name__ == "__main__":
570
  print("Starting GAIA GPT-4o evaluation app...")
571
  print(f"Model: {OPENAI_MODEL}")
572
  print(f"Space ID: {os.getenv('SPACE_ID', 'local')}")
573
- demo.launch(debug=True, share=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
  progress((index - 1) / max(total, 1), desc=f"Solving question {index}/{total}")
3
 
 
122
  print("Starting GAIA GPT-4o evaluation app...")
123
  print(f"Model: {OPENAI_MODEL}")
124
  print(f"Space ID: {os.getenv('SPACE_ID', 'local')}")
125
+ demo.launch(debug=True, share=False)