SergeyO7 commited on
Commit
3fa7788
·
verified ·
1 Parent(s): 6732d88

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +277 -0
app.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import requests
4
+ import inspect
5
+ import pandas as pd
6
+ import aiohttp
7
+ import asyncio
8
+ import json
9
+ from agent import MagAgent
10
+ from token_bucket import Limiter, MemoryStorage
11
+ import aiofiles
12
+ from typing import Optional
13
+
14
+ # --- Constants ---
15
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
16
+
17
+ # Rate limiting configuration
18
+ MAX_MODEL_CALLS_PER_MINUTE = 14 # Conservative buffer below 15 RPM
19
+ RATE_LIMIT = MAX_MODEL_CALLS_PER_MINUTE
20
+ TOKEN_BUCKET_CAPACITY = RATE_LIMIT
21
+ TOKEN_BUCKET_REFILL_RATE = RATE_LIMIT / 60.0 # Tokens per second
22
+
23
+ # Initialize global token bucket with MemoryStorage
24
+ storage = MemoryStorage()
25
+ token_bucket = Limiter(rate=TOKEN_BUCKET_REFILL_RATE, capacity=TOKEN_BUCKET_CAPACITY, storage=storage)
26
+
27
+ async def check_n_load_attach(session: aiohttp.ClientSession, task_id: str, api_url: str = DEFAULT_API_URL) -> Optional[str]:
28
+ file_url = f"{api_url}/files/{task_id}"
29
+ try:
30
+ async with session.get(file_url, timeout=15) as response:
31
+ if response.status == 200:
32
+ # Get filename from Content-Disposition
33
+ filename = None
34
+ content_disposition = response.headers.get("Content-Disposition")
35
+ if content_disposition and "filename=" in content_disposition:
36
+ filename = content_disposition.split("filename=")[-1].strip('"')
37
+ if not filename:
38
+ # Determine extension from Content-Type
39
+ content_type = str(response.headers.get("Content-Type", "")).lower()
40
+ extension = ""
41
+ if "image/png" in content_type:
42
+ extension = ".png"
43
+ elif "image/jpeg" in content_type:
44
+ extension = ".jpg"
45
+ elif "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" in content_type:
46
+ extension = ".xlsx"
47
+ elif "audio/mpeg" in content_type:
48
+ extension = ".mp3"
49
+ elif "application/pdf" in content_type:
50
+ extension = ".pdf"
51
+ elif "text/x-python" in content_type:
52
+ extension = ".py"
53
+ else:
54
+ extension = ""
55
+ filename = f"{task_id}{extension}"
56
+
57
+ # Save the file
58
+ local_file_path = os.path.join("downloads", filename)
59
+ os.makedirs("downloads", exist_ok=True)
60
+ async with aiofiles.open(local_file_path, "wb") as file:
61
+ async for chunk in response.content.iter_chunked(8192):
62
+ await file.write(chunk)
63
+ print(f"File downloaded successfully: {local_file_path}")
64
+ return local_file_path
65
+ else:
66
+ print(f"No attachment found for task {task_id}")
67
+ return None
68
+ except aiohttp.ClientError as e:
69
+ print(f"Error downloading attachment for task {task_id}: {str(e)}")
70
+ return None
71
+
72
+ async def fetch_questions(session: aiohttp.ClientSession, questions_url: str) -> list:
73
+ """Fetch questions asynchronously."""
74
+ try:
75
+ async with session.get(questions_url, timeout=15) as response:
76
+ response.raise_for_status()
77
+ questions_data = await response.json()
78
+ if not questions_data:
79
+ print("Fetched questions list is empty.")
80
+ return []
81
+ print(f"Fetched {len(questions_data)} questions.")
82
+ return questions_data
83
+ except aiohttp.ClientError as e:
84
+ print(f"Error fetching questions: {e}")
85
+ return None
86
+ except Exception as e:
87
+ print(f"An unexpected error occurred fetching questions: {e}")
88
+ return None
89
+
90
+ async def submit_answers(session: aiohttp.ClientSession, submit_url: str, submission_data: dict) -> dict:
91
+ """Submit answers asynchronously."""
92
+ try:
93
+ async with session.post(submit_url, json=submission_data, timeout=60) as response:
94
+ response.raise_for_status()
95
+ return await response.json()
96
+ except aiohttp.ClientResponseError as e:
97
+ print(f"Submission Failed: Server responded with status {e.status}. Detail: {e.message}")
98
+ return None
99
+ except aiohttp.ClientError as e:
100
+ print(f"Submission Failed: Network error - {e}")
101
+ return None
102
+ except Exception as e:
103
+ print(f"An unexpected error occurred during submission: {e}")
104
+ return None
105
+
106
+ async def process_question(agent, question_text: str, task_id: str, file_path: Optional[str], results_log: list):
107
+ """Process a single question with global rate limiting."""
108
+ submitted_answer = None
109
+ max_retries = 3
110
+ retry_delay = 4 # seconds
111
+
112
+ for attempt in range(max_retries):
113
+ try:
114
+ while not token_bucket.consume(1):
115
+ print(f"Rate limit reached for task {task_id}. Waiting to retry...")
116
+ await asyncio.sleep(retry_delay)
117
+ print(f"Processing task {task_id} (attempt {attempt + 1})...")
118
+ submitted_answer = await asyncio.wait_for(
119
+ agent(question_text, file_path),
120
+ timeout=60
121
+ )
122
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
123
+ print(f"Completed task {task_id} with answer: {submitted_answer[:50]}...")
124
+ return {"task_id": task_id, "submitted_answer": submitted_answer}
125
+ except aiohttp.ClientResponseError as e:
126
+ if e.status == 429:
127
+ print(f"Rate limit hit for task {task_id}. Retrying after {retry_delay}s...")
128
+ retry_delay *= 2
129
+ await asyncio.sleep(retry_delay)
130
+ continue
131
+ else:
132
+ submitted_answer = f"AGENT ERROR: {e}"
133
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
134
+ print(f"Failed task {task_id}: {submitted_answer}")
135
+ return None
136
+ except asyncio.TimeoutError:
137
+ submitted_answer = f"AGENT ERROR: Timeout after 60 seconds"
138
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
139
+ print(f"Failed task {task_id}: {submitted_answer}")
140
+ return None
141
+ except Exception as e:
142
+ submitted_answer = f"AGENT ERROR: {e}"
143
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
144
+ print(f"Failed task {task_id}: {submitted_answer}")
145
+ return None
146
+
147
+ async def run_and_submit_all(profile: gr.OAuthProfile | None):
148
+ """
149
+ Fetches all questions asynchronously, runs the MagAgent on them, submits all answers,
150
+ and displays the results.
151
+ """
152
+ space_id = os.getenv("SPACE_ID")
153
+
154
+ if profile:
155
+ username = f"{profile.username}"
156
+ print(f"User logged in: {username}")
157
+ else:
158
+ print("User not logged in.")
159
+ return "Please Login to Hugging Face with the button.", None
160
+
161
+ api_url = DEFAULT_API_URL
162
+ questions_url = f"{api_url}/questions"
163
+ submit_url = f"{api_url}/submit"
164
+
165
+ try:
166
+ agent = MagAgent(rate_limiter=token_bucket)
167
+ except Exception as e:
168
+ print(f"Error instantiating agent: {e}")
169
+ return f"Error initializing agent: {e}", None
170
+
171
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
172
+ print(agent_code)
173
+
174
+ async with aiohttp.ClientSession() as session:
175
+ questions_data = await fetch_questions(session, questions_url)
176
+ if questions_data is None:
177
+ return "Error fetching questions.", None
178
+ if not questions_data:
179
+ return "Fetched questions list is empty or invalid format.", None
180
+
181
+ results_log = []
182
+ answers_payload = []
183
+ print(f"Running agent on {len(questions_data)} questions...")
184
+
185
+ for item in questions_data:
186
+ task_id = item.get("task_id")
187
+ question_text = item.get("question")
188
+ if not task_id or question_text is None:
189
+ print(f"Skipping item with missing task_id or question: {item}")
190
+ continue
191
+ if "1ht" in question_text.lower():
192
+ file_path = await check_n_load_attach(session, task_id)
193
+ result = await process_question(agent, question_text, task_id, file_path, results_log)
194
+ if result:
195
+ answers_payload.append(result)
196
+ else:
197
+ print(f"Skipping not related question: {task_id}")
198
+ results_log.append({
199
+ "Task ID": task_id,
200
+ "Question": question_text,
201
+ "Submitted Answer": "Question skipped - not related"
202
+ })
203
+
204
+ if not answers_payload:
205
+ print("Agent did not produce any answers to submit.")
206
+ return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
207
+
208
+ submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
209
+ status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
210
+ print(status_update)
211
+
212
+ result_data = await submit_answers(session, submit_url, submission_data)
213
+ if result_data is None:
214
+ status_message = "Submission Failed."
215
+ print(status_message)
216
+ results_df = pd.DataFrame(results_log)
217
+ return status_message, results_df
218
+
219
+ final_status = (
220
+ f"Submission Successful!\n"
221
+ f"User: {result_data.get('username')}\n"
222
+ f"Overall Score: {result_data.get('score', 'N/A')}% "
223
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
224
+ f"Message: {result_data.get('message', 'No message received.')}"
225
+ )
226
+ print("Submission successful.")
227
+ results_df = pd.DataFrame(results_log)
228
+ return final_status, results_df
229
+
230
+ # --- Build Gradio Interface using Blocks ---
231
+ with gr.Blocks() as demo:
232
+ gr.Markdown("# Magus Agent Evaluation Runner")
233
+ gr.Markdown(
234
+ """
235
+ **Instructions:**
236
+ 1. Log in to your Hugging Face account using the button below.
237
+ 2. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, and submit answers.
238
+ ---
239
+ **Notes:**
240
+ The agent uses asynchronous operations for efficiency. Answers are processed and submitted asynchronously.
241
+ """
242
+ )
243
+
244
+ gr.LoginButton()
245
+
246
+ run_button = gr.Button("Run Evaluation & Submit All Answers")
247
+
248
+ status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
249
+ results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
250
+
251
+ run_button.click(
252
+ fn=run_and_submit_all,
253
+ outputs=[status_output, results_table]
254
+ )
255
+
256
+ if __name__ == "__main__":
257
+ print("\n" + "-"*30 + " App Starting " + "-"*30)
258
+ space_host_startup = os.getenv("SPACE_HOST")
259
+ space_id_startup = os.getenv("SPACE_ID")
260
+
261
+ if space_host_startup:
262
+ print(f"✅ SPACE_HOST found: {space_host_startup}")
263
+ print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
264
+ else:
265
+ print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
266
+
267
+ if space_id_startup:
268
+ print(f"✅ SPACE_ID found: {space_id_startup}")
269
+ print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
270
+ print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
271
+ else:
272
+ print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
273
+
274
+ print("-"*(60 + len(" App Starting ")) + "\n")
275
+
276
+ print("Launching Gradio Interface for Mag Agent Evaluation...")
277
+ demo.launch(debug=True, share=False)