TingWei0328 commited on
Commit
aa4025b
·
verified ·
1 Parent(s): 5dd4151

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -246
app.py CHANGED
@@ -1,272 +1,135 @@
1
- import re
2
  import json
3
  import tempfile
4
- from urllib.parse import urlparse
5
-
 
 
 
 
 
 
 
 
 
 
6
  class BasicAgent:
7
- """
8
- A pragmatic baseline agent:
9
- - If question contains a URL, fetch it and try to extract the answer.
10
- - If question implies there is an attached file, download it via /files/{task_id}.
11
- - Uses simple heuristics to return an exact-match style answer (no extra words).
12
- """
13
-
14
- def __init__(self, api_url: str = DEFAULT_API_URL):
15
  self.api_url = api_url
16
- self.session = requests.Session()
17
- self.session.headers.update({
18
- "User-Agent": "Mozilla/5.0 (compatible; BasicAgent/1.0)"
19
- })
20
  print("BasicAgent initialized.")
21
 
22
  def __call__(self, question: str, task_id: str | None = None) -> str:
23
- try:
24
- # 1) If there is a file for this task, download and use it (when needed)
25
- # Some questions explicitly mention "provided in the image" or "attached file"
26
- if task_id and self._looks_like_file_task(question):
27
- file_path = self._download_task_file(task_id)
28
- if file_path:
29
- ans = self._answer_from_file(question, file_path)
30
- if ans:
31
- return self._finalize(ans)
32
-
33
- # 2) If question contains URL(s), fetch and try to answer from the page
34
- urls = self._extract_urls(question)
35
- if urls:
36
- for u in urls:
37
- html_or_text = self._safe_get(u)
38
- if html_or_text:
39
- ans = self._answer_from_web(question, u, html_or_text)
40
- if ans:
41
- return self._finalize(ans)
42
-
43
- # 3) If no URL/file, try direct heuristics (grocery list, simple parsing, etc.)
44
- ans = self._answer_from_text_only(question)
45
- return self._finalize(ans) if ans else "unknown"
46
-
47
- except Exception as e:
48
- print(f"[Agent Error] {e}")
49
- return "unknown"
50
-
51
- # -------------------------
52
- # Helpers
53
- # -------------------------
54
- def _finalize(self, s: str) -> str:
55
- # EXACT MATCH friendly: strip spaces, avoid extra punctuation/labels
56
- return str(s).strip()
57
 
58
- def _extract_urls(self, text: str) -> list[str]:
59
- # robust URL regex
60
- urls = re.findall(r"(https?://[^\s)>\]]+)", text)
61
- # remove trailing punctuation
62
- clean = []
63
- for u in urls:
64
- clean.append(u.rstrip(".,;:!?\"'"))
65
- return clean
66
 
67
- def _safe_get(self, url: str, timeout: int = 20) -> str | None:
68
  try:
69
- r = self.session.get(url, timeout=timeout)
70
- r.raise_for_status()
71
- # return raw text; many pages are html
72
- return r.text
 
73
  except Exception as e:
74
- print(f"[GET failed] {url} -> {e}")
75
- return None
76
 
77
- def _looks_like_file_task(self, question: str) -> bool:
78
- q = question.lower()
79
- keywords = [
80
- "provided in the image", "provided in the file", "see the image",
81
- "in the attached", "download", "the file", "the image",
82
- "in the pdf", "in this spreadsheet", "in the document"
83
- ]
84
- return any(k in q for k in keywords)
85
-
86
- def _download_task_file(self, task_id: str) -> str | None:
87
- """
88
- Download task file from /files/{task_id}.
89
- """
90
- url = f"{self.api_url}/files/{task_id}"
91
- try:
92
- r = self.session.get(url, timeout=30)
93
- r.raise_for_status()
94
 
95
- # Try to infer extension from headers
96
- ctype = (r.headers.get("Content-Type") or "").lower()
97
- ext = ""
98
- if "pdf" in ctype:
99
- ext = ".pdf"
100
- elif "image" in ctype:
101
- ext = ".png"
102
- elif "text" in ctype:
103
- ext = ".txt"
104
- elif "csv" in ctype:
105
- ext = ".csv"
106
-
107
- fd, path = tempfile.mkstemp(suffix=ext)
108
- os.close(fd)
109
- with open(path, "wb") as f:
110
- f.write(r.content)
111
-
112
- print(f"[Downloaded] task file -> {path} ({ctype})")
113
- return path
114
- except Exception as e:
115
- print(f"[File download failed] {url} -> {e}")
116
- return None
117
 
118
- # -------------------------
119
- # Answering strategies
120
- # -------------------------
121
- def _answer_from_web(self, question: str, url: str, page_text: str) -> str | None:
122
- q = question.lower()
123
 
124
- # If question asks about Wikipedia (common): try to extract numbers / key fact around entity
125
- if "wikipedia" in q or "wikipedia.org" in url:
126
- return self._wiki_style_extract(question, page_text)
 
127
 
128
- # If question asks about "how many" and includes a year range, try find integer near keyword
129
- if "how many" in q or "number of" in q:
130
- num = self._find_best_number(question, page_text)
131
- if num is not None:
132
- return str(num)
133
 
134
- # If question asks "what is the surname" etc., try simple pattern match
135
- if "surname" in q:
136
- # naive: find "Surname" lines
137
- m = re.search(r"Surname[:\s]+([A-Z][a-zA-Z-]+)", page_text)
138
- if m:
139
- return m.group(1)
140
 
141
- # Generic fallback: if question contains quoted phrase, find it and return nearby
142
- return None
 
 
143
 
144
- def _wiki_style_extract(self, question: str, page_text: str) -> str | None:
145
- """
146
- Very light heuristic:
147
- - if asked "how many studio albums ... between YEAR and YEAR": count occurrences of 'studio album'
148
- won't be accurate from raw html, so fallback to best-number extractor.
149
- """
150
- q = question.lower()
151
- if "how many" in q:
152
- num = self._find_best_number(question, page_text)
153
- if num is not None:
154
- return str(num)
155
- # more could be added, but keep robust
156
- return None
157
 
158
- def _find_best_number(self, question: str, page_text: str) -> int | None:
159
- """
160
- Find a plausible answer number by looking at context keywords from question.
161
- """
162
- # Extract keywords (very simple)
163
- q = question.lower()
164
- # pick a few anchor words
165
- anchors = []
166
- for w in ["studio", "albums", "species", "camera", "published", "between", "highest", "number"]:
167
- if w in q:
168
- anchors.append(w)
169
 
170
- # Search within a reduced slice if possible
171
- text = page_text
172
- if anchors:
173
- # try find a region around first anchor appearance
174
- idx = text.lower().find(anchors[0])
175
- if idx != -1:
176
- start = max(0, idx - 2000)
177
- end = min(len(text), idx + 2000)
178
- text = text[start:end]
179
-
180
- # grab integers
181
- nums = re.findall(r"\b(\d{1,4})\b", text)
182
- if not nums:
183
- return None
184
-
185
- # Heuristic: prefer smaller counts (1-300) over years (e.g., 2008)
186
- candidates = []
187
- for n in nums:
188
- v = int(n)
189
- if 0 <= v <= 500:
190
- candidates.append(v)
191
-
192
- if candidates:
193
- # choose the most frequent candidate
194
- from collections import Counter
195
- c = Counter(candidates)
196
- return c.most_common(1)[0][0]
197
-
198
- return None
199
-
200
- def _answer_from_file(self, question: str, file_path: str) -> str | None:
201
- """
202
- Minimal file handling:
203
- - If it's text/csv: read and try parse.
204
- - If pdf/image: we won't OCR here; return None.
205
- """
206
  try:
207
- # try as text
208
- if file_path.endswith(".txt"):
209
- with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
210
- content = f.read()
211
- # try number extraction
212
- num = self._find_best_number(question, content)
213
- if num is not None:
214
- return str(num)
215
- return None
216
-
217
- if file_path.endswith(".csv"):
218
- df = pd.read_csv(file_path)
219
- # If asked something about a column, very naive: return first cell
220
- if df.shape[0] > 0 and df.shape[1] > 0:
221
- return str(df.iloc[0, 0])
222
- return None
223
-
224
- # pdf/png: not handled in this baseline
225
- return None
226
-
227
  except Exception as e:
228
- print(f"[File parse failed] {file_path} -> {e}")
229
- return None
230
-
231
- def _answer_from_text_only(self, question: str) -> str | None:
232
- """
233
- Pure text heuristics (no web/file).
234
- Covers common GAIA-L1 style tasks like grocery categorization, alphabetize, comma-separated.
235
- """
236
- q = question.strip()
237
-
238
- # Grocery list vegetable extraction pattern (example from your screenshot)
239
- if "grocery list" in q.lower() and "vegetables" in q.lower() and "comma" in q.lower():
240
- # Extract list after "Here's the list I have so far:"
241
- m = re.search(r"Here's the list I have so far:\s*(.+?)\.\s*I need to make headings", q, re.S | re.I)
242
- if not m:
243
- m = re.search(r"Here's the list I have so far:\s*(.+)", q, re.S | re.I)
244
- if m:
245
- items_blob = m.group(1)
246
- items = [x.strip().lower() for x in items_blob.split(",")]
247
- items = [x for x in items if x]
248
 
249
- # Simple botany rule of thumb for that sample: treat these as vegetables
250
- # (keep conservative: include obvious veggies, exclude fruits)
251
- veggies_set = {
252
- "bell pepper", "broccoli", "celery", "corn", "green beans",
253
- "lettuce", "sweet potatoes", "zucchini"
254
- }
255
- veggies = []
256
- for it in items:
257
- it_norm = it.strip()
258
- # normalize plurals
259
- if it_norm in veggies_set:
260
- veggies.append(it_norm)
261
-
262
- veggies = sorted(set(veggies))
263
- return ", ".join(veggies)
264
 
265
- # If asked to reverse a sentence (common trick)
266
- if "reverse" in q.lower() and "sentence" in q.lower():
267
- # find quoted
268
- m = re.search(r'"([^"]+)"', q)
269
- if m:
270
- return m.group(1)[::-1]
271
 
272
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
  import json
3
  import tempfile
4
+ import requests
5
+ import pandas as pd
6
+ import gradio as gr
7
+
8
+ # =========================
9
+ # Constants (不要改)
10
+ # =========================
11
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
12
+
13
+ # =========================
14
+ # Basic Agent
15
+ # =========================
16
  class BasicAgent:
17
+ def __init__(self, api_url: str):
 
 
 
 
 
 
 
18
  self.api_url = api_url
 
 
 
 
19
  print("BasicAgent initialized.")
20
 
21
  def __call__(self, question: str, task_id: str | None = None) -> str:
22
+ print(f"Agent received question: {question[:80]}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ # 若沒有 task_id,直接回答文字題
25
+ if task_id is None:
26
+ return "I don't know."
 
 
 
 
 
27
 
28
+ # 嘗試抓附件(有些題目會有)
29
  try:
30
+ file_url = f"{self.api_url}/files/{task_id}"
31
+ r = requests.get(file_url, timeout=10)
32
+ if r.status_code == 200 and r.headers.get("content-type", "").startswith("application/json"):
33
+ data = r.json()
34
+ return json.dumps(data)[:500]
35
  except Exception as e:
36
+ print("File fetch failed:", e)
 
37
 
38
+ # 預設保底回答(至少不會 crash)
39
+ return "I don't know."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ # =========================
42
+ # Main runner
43
+ # =========================
44
+ def run_and_submit_all(profile: gr.OAuthProfile | None):
45
+ space_id = os.getenv("SPACE_ID")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ if not profile:
48
+ return "Please login first.", None
 
 
 
49
 
50
+ username = profile.username
51
+ api_url = DEFAULT_API_URL
52
+ questions_url = f"{api_url}/questions"
53
+ submit_url = f"{api_url}/submit"
54
 
55
+ # 建立 Agent(⚠️ 這裡已經修正)
56
+ agent = BasicAgent(api_url=api_url)
 
 
 
57
 
58
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
 
 
 
 
 
59
 
60
+ # 取得題目
61
+ response = requests.get(questions_url, timeout=15)
62
+ response.raise_for_status()
63
+ questions_data = response.json()
64
 
65
+ answers_payload = []
66
+ results_log = []
 
 
 
 
 
 
 
 
 
 
 
67
 
68
+ for item in questions_data:
69
+ task_id = item.get("task_id")
70
+ question = item.get("question")
 
 
 
 
 
 
 
 
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  try:
73
+ answer = agent(question, task_id=task_id) # ⚠️ 關鍵修正點
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  except Exception as e:
75
+ answer = f"ERROR: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ answers_payload.append({
78
+ "task_id": task_id,
79
+ "submitted_answer": answer
80
+ })
 
 
 
 
 
 
 
 
 
 
 
81
 
82
+ results_log.append({
83
+ "Task ID": task_id,
84
+ "Question": question,
85
+ "Submitted Answer": answer
86
+ })
 
87
 
88
+ submission_data = {
89
+ "username": username,
90
+ "agent_code": agent_code,
91
+ "answers": answers_payload
92
+ }
93
+
94
+ # 提交
95
+ r = requests.post(submit_url, json=submission_data, timeout=60)
96
+ r.raise_for_status()
97
+ result = r.json()
98
+
99
+ status = (
100
+ f"Submission Successful!\n"
101
+ f"User: {result.get('username')}\n"
102
+ f"Score: {result.get('score')}%\n"
103
+ f"{result.get('correct_count')}/{result.get('total_attempted')} correct\n"
104
+ f"{result.get('message')}"
105
+ )
106
+
107
+ return status, pd.DataFrame(results_log)
108
+
109
+ # =========================
110
+ # Gradio UI
111
+ # =========================
112
+ with gr.Blocks() as demo:
113
+ gr.Markdown("# Basic Agent Evaluation Runner")
114
+
115
+ gr.Markdown("""
116
+ **Instructions**
117
+ 1. Login with Hugging Face
118
+ 2. Click the button
119
+ 3. Wait for submission result
120
+ """)
121
+
122
+ gr.LoginButton()
123
+
124
+ run_btn = gr.Button("Run Evaluation & Submit All Answers")
125
+
126
+ status_out = gr.Textbox(label="Status", lines=6)
127
+ table_out = gr.DataFrame(label="Results", wrap=True)
128
+
129
+ run_btn.click(
130
+ fn=run_and_submit_all,
131
+ outputs=[status_out, table_out]
132
+ )
133
+
134
+ if __name__ == "__main__":
135
+ demo.launch()