omega21 commited on
Commit
6172d6b
ยท
verified ยท
1 Parent(s): 06d2f25

Update app.py

Browse files

import json
import os
import re
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

import gradio as gr
import pandas as pd
import requests
from smolagents import (
CodeAgent,
DuckDuckGoSearchTool,
InferenceClientModel,
LiteLLMModel,
VisitWebpageTool,
tool,
)

# ZeroGPU ํ•˜๋“œ์›จ์–ด์—์„œ ์‹œ์ž‘ ์˜ค๋ฅ˜๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•œ ๋”๋ฏธ ํ•จ์ˆ˜ (CPU ํ•˜๋“œ์›จ์–ด์—์„œ๋Š” ๋ฌด์‹œ๋จ)
try:
import spaces

@spaces.GPU
def _gpu_placeholder():
return "ok"
except ImportError:
pass

# ---------------------------------------------------------------------------
# ์„ค์ • (Space์˜ Variables์—์„œ ์กฐ์ • ๊ฐ€๋Šฅ)
# ---------------------------------------------------------------------------
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
FILES_DIR = "/tmp/task_files"
CACHE_PATH = "/tmp/answers_cache.json"
os.makedirs(FILES_DIR, exist_ok=True)

WORKERS = int(os.getenv("WORKERS", "3")) # ๋™์‹œ ์‹คํ–‰ ๋ฌธํ•ญ ์ˆ˜
QUESTION_TIMEOUT = int(os.getenv("QUESTION_TIMEOUT", "240")) # ๋ฌธํ•ญ๋‹น ์ œํ•œ ์‹œ๊ฐ„(์ดˆ)
MAX_STEPS = int(os.getenv("MAX_STEPS", "8")) # ์—์ด์ „ํŠธ ์ตœ๋Œ€ ๋‹จ๊ณ„

ANSWER_RULES = """
You are a careful research and reasoning agent solving GAIA-style questions.
Rules:
1. Use the tools (web search, webpage visit, file reading, python code) whenever the
question needs facts you are not 100% sure about. Do not guess.
2. The final answer is graded by EXACT MATCH. Return ONLY the answer value, with no
explanation, no units unless asked, no trailing period, no "FINAL ANSWER:" prefix.
3. If the answer is a number: no thousands separators, no currency/percent symbols
unless the question asks for them.
4. If the answer is a list: comma-separated, in the order the question requests.
5. If the answer is a string: no articles (a, an, the) and no abbreviations unless
the question asks for them.
6. If a file path is given, read it with the read_local_file tool or with python code.
7. Be efficient: stop researching as soon as you have a reliable answer.
8. Finish by calling final_answer(...) with the answer only.
"""

# ---------------------------------------------------------------------------
# ์บ์‹œ (๋Š๊ฒจ๋„ ํ‘ผ ๋‹ต์€ ๋ณด์กด. ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์žฌ์‹œ์ž‘๋˜๋ฉด ์ดˆ๊ธฐํ™”๋จ)
# ---------------------------------------------------------------------------
_cache_lock = threading.Lock()


def load_cache() -> dict:
try:
with open(CACHE_PATH, "r", encoding="utf-8") as f:
return json.load(f)
except Exception: # noqa: BLE001
return {}


def save_cache_entry(task_id: str, answer: str):
with _cache_lock:
cache = load_cache()
cache[task_id] = answer
with open(CACHE_PATH, "w", encoding="utf-8") as f:
json.dump(cache, f, ensure_ascii=False)


# ---------------------------------------------------------------------------
# ๋„๊ตฌ
# ---------------------------------------------------------------------------
@tool
def read_local_file(path: str) -> str:
"""Reads a local file (txt, csv, json, py, md, xlsx) and returns its content as text.

Args:
path: Absolute path of the file to read.
"""
try:
lower = path.lower()
if lower.endswith((".xlsx", ".xls")):
sheets = pd.read_excel(path, sheet_name=None)
return "\n\n".join(
f"## Sheet: {n}\n{df.to_string(max_rows=200)}" for n, df in sheets.items()
)
if lower.endswith(".csv"):
return pd.read_csv(path).to_string(max_rows=200)
with open(path, "r", encoding="utf-8", errors="ignore") as f:
return f.read()[:20000]
except Exception as e: # noqa: BLE001
return f"Could not read file: {e}"


# ---------------------------------------------------------------------------
# ์—์ด์ „ํŠธ
# ---------------------------------------------------------------------------
def build_model():
gemini_key = os.getenv("GEMINI_API_KEY")
if gemini_key:
model_id = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
return LiteLLMModel(model_id=f"gemini/{model_id}", api_key=gemini_key)
openai_key = os.getenv("OPENAI_API_KEY")
if openai_key:
model_id = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
return LiteLLMModel(model_id=f"openai/{model_id}", api_key=openai_key)
model_id = os.getenv("HF_MODEL", "Qwen/Qwen2.5-Coder-32B-Instruct")
return InferenceClientModel(model_id=model_id, token=os.getenv("HF_TOKEN"))


def build_agent() -> CodeAgent:
# ์Šค๋ ˆ๋“œ ์•ˆ์ „์„ ์œ„ํ•ด ๋ฌธํ•ญ๋งˆ๋‹ค ์ƒˆ ์—์ด์ „ํŠธ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
return CodeAgent(
tools=[DuckDuckGoSearchTool(), VisitWebpageTool(), read_local_file],
model=build_model(),
additional_authorized_imports=[
"pandas", "numpy", "re", "json", "math", "datetime",
"collections", "itertools", "statistics", "requests", "bs4",
],
max_steps=MAX_STEPS,
verbosity_level=1,
)


def clean_answer(answer) -> str:
text = str(answer).strip()
text = re.sub(r"^\s*final answer\s*:\s*", "", text, flags=re.IGNORECASE)
return text.strip().rstrip(".").strip()


# ---------------------------------------------------------------------------
# ์ฑ„์  ์„œ๋ฒ„ ํ†ต์‹ 
# ---------------------------------------------------------------------------
def fetch_questions(api_url: str) -> list[dict]:
resp = requests.get(f"{api_url}/questions", timeout=30)
resp.raise_for_status()
data = resp.json()
if not data:
raise ValueError("์งˆ๋ฌธ ๋ชฉ๋ก์ด ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.")
return data


def download_task_file(api_url: str, task_id: str, file_name: str):
if not file_name:
return None
try:
resp = requests.get(f"{api_url}/files/{task_id}", timeout=60)
resp.raise_for_status()
path = os.path.join(FILES_DIR, f"{task_id}_{file_name}")
with open(path, "wb") as f:
f.write(resp.content)
return path
except Exception as e: # noqa: BLE001
print(f"ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ ({task_id}): {e}")
return None


# ---------------------------------------------------------------------------
# ๋ฌธํ•ญ ํ’€์ด (์บ์‹œ + ์‹œ๊ฐ„ ์ œํ•œ)
# ---------------------------------------------------------------------------
def solve_one(item: dict, api_url: str) -> str:
prompt = ANSWER_RULES + "\n\nQuestion:\n" + item["question"]
file_path = download_task_file(api_url, item["task_id"], item.get("file_name", ""))
if file_path:
prompt += f"\n\nAn attached file is available at: {file_path}"
return clean_answer(build_agent().run(prompt))


def solve_with_timeout(item: dict, api_url: str) -> tuple[str, bool]:
"""(๋‹ต๋ณ€, ์„ฑ๊ณต ์—ฌ๋ถ€)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์„ฑ๊ณตํ•œ ๋‹ต๋งŒ ์บ์‹œ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค."""
task_id = item["task_id"]
cached = load_cache().get(task_id)
if cached:
print(f"[cache] {task_id} -> {cached}")
return cached, True

box: dict = {}

def target():
try:
box["answer"] = solve_one(item, api_url)
except Exception as e: # noqa: BLE001
box["error"] = str(e)

print(f"[start] {task_id}")
t = threading.Thread(target=target, daemon=True)
t.start()
t.join(QUESTION_TIMEOUT)

if t.is_alive():
print(f"[timeout] {task_id}")
return "unknown", False
if "error" in box:
print(f"[error] {task_id}: {box['error']}")
return "unknown", False
save_cache_entry(task_id, box["answer"])
print(f"[done] {task_id} -> {box['answer']}")
return box["answer"], True


def iter_solve(questions: list[dict], api_url: str):
"""๋ฌธํ•ญ์„ ๋ณ‘๋ ฌ๋กœ ํ’€๋ฉด์„œ, ๋๋‚˜๋Š” ๋Œ€๋กœ (item, answer, ok)๋ฅผ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค."""
valid = [q for q in questions if q.get("task_id") and q.get("question") is not None]
with ThreadPoolExecutor(max_workers=WORKERS) as pool:
futures = {pool.submit(solve_with_timeout, q, api_url): q for q in valid}
for fut in as_completed(futures):
answer, ok = fut.result()
yield futures[fut], answer, ok


def make_df(rows: dict, order: list[str]) -> pd.DataFrame:
data = [rows[tid] for tid in order if tid in rows]
return pd.DataFrame(data, columns=["Task ID", "Question", "Submitted Answer", "OK"])


# ---------------------------------------------------------------------------
# Gradio ์ฝœ๋ฐฑ (์ œ๋„ˆ๋ ˆ์ดํ„ฐ: ์ง„ํ–‰ ์ƒํ™ฉ์ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™”๋ฉด์— ํ‘œ์‹œ๋จ)
# ---------------------------------------------------------------------------
def run_and_submit_all(profile: gr.OAuthProfile | None):
space_id = os.getenv("SPACE_ID")
if not profile:
yield "๋จผ์ € Hugging Face ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์œผ๋กœ ๋กœ๊ทธ์ธํ•ด ์ฃผ์„ธ์š”.", None
return
if not space_id:
yield "SPACE_ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. Space ์•ˆ์—์„œ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", None
return
username = profile.username
api_url = DEFAULT_API_URL
agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"

yield "์งˆ๋ฌธ์„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘...", None
try:
questions = fetch_questions(api_url)
except Exception as e: # noqa: BLE001
yield f"์งˆ๋ฌธ ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜: {e}", None
return

order = [q["task_id"] for q in questions if q.get("task_id")]
total = len(order)
rows: dict = {}
start = time.time()

for item, answer, ok in iter_solve(questions, api_url):
rows[item["task_id"]] = {
"Task ID": item["task_id"],
"Question": item["question"],
"Submitted Answer": answer,
"OK": "O" if ok else "X",
}
elapsed = int(time.time() - start)
yield f"์ง„ํ–‰ {len(rows)}/{total} ์™„๋ฃŒ (๊ฒฝ๊ณผ {elapsed}์ดˆ)", make_df(rows, order)

answers_payload = [
{"task_id": tid, "submitted_answer": rows[tid]["Submitted Answer"]}
for tid in order if tid in rows
]
failed = sum(1 for r in rows.values() if r["OK"] == "X")
yield f"ํ’€์ด ์™„๋ฃŒ (์‹คํŒจ/์‹œ๊ฐ„์ดˆ๊ณผ {failed}๊ฐœ). ์ œ์ถœ ์ค‘...", make_df(rows, order)

submission = {

Files changed (1) hide show
  1. app.py +180 -94
app.py CHANGED
@@ -1,7 +1,9 @@
1
- import spaces
2
  import os
3
  import re
 
4
  import time
 
5
 
6
  import gradio as gr
7
  import pandas as pd
@@ -15,17 +17,28 @@ from smolagents import (
15
  tool,
16
  )
17
 
 
 
 
 
 
 
 
 
 
 
18
  # ---------------------------------------------------------------------------
19
- # ์ƒ์ˆ˜
20
  # ---------------------------------------------------------------------------
21
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
22
  FILES_DIR = "/tmp/task_files"
 
23
  os.makedirs(FILES_DIR, exist_ok=True)
24
 
25
- # ์งˆ๋ฌธ ์‚ฌ์ด ๋Œ€๊ธฐ ์‹œ๊ฐ„(์ดˆ). ๋ฌด๋ฃŒ API์˜ rate limit ๋ฐฉ์ง€์šฉ
26
- SLEEP_BETWEEN_QUESTIONS = float(os.getenv("SLEEP_BETWEEN_QUESTIONS", "2"))
 
27
 
28
- # ์ฑ„์ ์ด '์ •ํ™•ํžˆ ์ผ์น˜(exact match)' ๋ฐฉ์‹์ด๋ฏ€๋กœ ๋‹ต๋ณ€ ํ˜•์‹ ๊ทœ์น™์ด ๋งค์šฐ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.
29
  ANSWER_RULES = """
30
  You are a careful research and reasoning agent solving GAIA-style questions.
31
  Rules:
@@ -39,9 +52,31 @@ Rules:
39
  5. If the answer is a string: no articles (a, an, the) and no abbreviations unless
40
  the question asks for them.
41
  6. If a file path is given, read it with the read_local_file tool or with python code.
42
- 7. Finish by calling final_answer(...) with the answer only.
 
43
  """
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  # ---------------------------------------------------------------------------
47
  # ๋„๊ตฌ
@@ -57,10 +92,9 @@ def read_local_file(path: str) -> str:
57
  lower = path.lower()
58
  if lower.endswith((".xlsx", ".xls")):
59
  sheets = pd.read_excel(path, sheet_name=None)
60
- parts = []
61
- for name, df in sheets.items():
62
- parts.append(f"## Sheet: {name}\n{df.to_string(max_rows=200)}")
63
- return "\n\n".join(parts)
64
  if lower.endswith(".csv"):
65
  return pd.read_csv(path).to_string(max_rows=200)
66
  with open(path, "r", encoding="utf-8", errors="ignore") as f:
@@ -73,57 +107,40 @@ def read_local_file(path: str) -> str:
73
  # ์—์ด์ „ํŠธ
74
  # ---------------------------------------------------------------------------
75
  def build_model():
76
- """ํ™˜๊ฒฝ๋ณ€์ˆ˜(Secrets)์— ๋”ฐ๋ผ ์‚ฌ์šฉํ•  LLM์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.
77
-
78
- ์šฐ์„ ์ˆœ์œ„: GEMINI_API_KEY > OPENAI_API_KEY > HF_TOKEN(๊ธฐ๋ณธ)
79
- """
80
  gemini_key = os.getenv("GEMINI_API_KEY")
81
  if gemini_key:
82
  model_id = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
83
- print(f"[model] Gemini: {model_id}")
84
  return LiteLLMModel(model_id=f"gemini/{model_id}", api_key=gemini_key)
85
-
86
  openai_key = os.getenv("OPENAI_API_KEY")
87
  if openai_key:
88
  model_id = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
89
- print(f"[model] OpenAI: {model_id}")
90
  return LiteLLMModel(model_id=f"openai/{model_id}", api_key=openai_key)
91
-
92
  model_id = os.getenv("HF_MODEL", "Qwen/Qwen2.5-Coder-32B-Instruct")
93
- print(f"[model] HF Inference: {model_id}")
94
  return InferenceClientModel(model_id=model_id, token=os.getenv("HF_TOKEN"))
95
 
96
 
97
- class BasicAgent:
98
- def __init__(self):
99
- print("BasicAgent ์ดˆ๊ธฐํ™” ์ค‘...")
100
- self.agent = CodeAgent(
101
- tools=[DuckDuckGoSearchTool(), VisitWebpageTool(), read_local_file],
102
- model=build_model(),
103
- additional_authorized_imports=[
104
- "pandas", "numpy", "re", "json", "math", "datetime",
105
- "collections", "itertools", "statistics", "requests", "bs4",
106
- ],
107
- max_steps=int(os.getenv("MAX_STEPS", "12")),
108
- verbosity_level=1,
109
- )
110
 
111
- @staticmethod
112
- def _clean(answer) -> str:
113
- text = str(answer).strip()
114
- text = re.sub(r"^\s*final answer\s*:\s*", "", text, flags=re.IGNORECASE)
115
- return text.strip().rstrip(".").strip()
116
 
117
- def __call__(self, question: str, file_path: str | None = None) -> str:
118
- prompt = ANSWER_RULES + "\n\nQuestion:\n" + question
119
- if file_path:
120
- prompt += f"\n\nAn attached file is available at: {file_path}"
121
- result = self.agent.run(prompt)
122
- return self._clean(result)
123
 
124
 
125
  # ---------------------------------------------------------------------------
126
- # ์ฑ„์  ์„œ๋ฒ„ ํ†ต์‹  ์œ ํ‹ธ
127
  # ---------------------------------------------------------------------------
128
  def fetch_questions(api_url: str) -> list[dict]:
129
  resp = requests.get(f"{api_url}/questions", timeout=30)
@@ -134,8 +151,7 @@ def fetch_questions(api_url: str) -> list[dict]:
134
  return data
135
 
136
 
137
- def download_task_file(api_url: str, task_id: str, file_name: str) -> str | None:
138
- """์งˆ๋ฌธ์— ์ฒจ๋ถ€ ํŒŒ์ผ์ด ์žˆ์œผ๋ฉด ๋‚ด๋ ค๋ฐ›์•„ ๊ฒฝ๋กœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
139
  if not file_name:
140
  return None
141
  try:
@@ -150,57 +166,107 @@ def download_task_file(api_url: str, task_id: str, file_name: str) -> str | None
150
  return None
151
 
152
 
153
- def run_questions(agent: BasicAgent, questions: list[dict], api_url: str):
154
- answers_payload, results_log = [], []
155
- for i, item in enumerate(questions, start=1):
156
- task_id = item.get("task_id")
157
- question_text = item.get("question")
158
- if not task_id or question_text is None:
159
- continue
160
- print(f"\n===== [{i}/{len(questions)}] task_id={task_id} =====")
161
- file_path = download_task_file(api_url, task_id, item.get("file_name", ""))
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  try:
163
- answer = agent(question_text, file_path)
164
  except Exception as e: # noqa: BLE001
165
- print(f"์—์ด์ „ํŠธ ์˜ค๋ฅ˜: {e}")
166
- answer = f"AGENT ERROR: {e}"
167
- answers_payload.append({"task_id": task_id, "submitted_answer": answer})
168
- results_log.append(
169
- {"Task ID": task_id, "Question": question_text, "Submitted Answer": answer}
170
- )
171
- time.sleep(SLEEP_BETWEEN_QUESTIONS)
172
- return answers_payload, results_log
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
 
175
  # ---------------------------------------------------------------------------
176
- # Gradio ์ฝœ๋ฐฑ
177
  # ---------------------------------------------------------------------------
178
  def run_and_submit_all(profile: gr.OAuthProfile | None):
179
- """์ „์ฒด ์งˆ๋ฌธ ์‹คํ–‰ -> ์ œ์ถœ -> ์ ์ˆ˜ ํ™•์ธ"""
180
  space_id = os.getenv("SPACE_ID")
181
  if not profile:
182
- return "๋จผ์ € Hugging Face ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์œผ๋กœ ๋กœ๊ทธ์ธํ•ด ์ฃผ์„ธ์š”.", None
 
 
 
 
183
  username = profile.username
184
  api_url = DEFAULT_API_URL
185
-
186
- if not space_id:
187
- return "SPACE_ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. Space ์•ˆ์—์„œ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", None
188
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
189
 
190
- try:
191
- agent = BasicAgent()
192
- except Exception as e: # noqa: BLE001
193
- return f"์—์ด์ „ํŠธ ์ดˆ๊ธฐํ™” ์˜ค๋ฅ˜: {e}", None
194
-
195
  try:
196
  questions = fetch_questions(api_url)
197
  except Exception as e: # noqa: BLE001
198
- return f"์งˆ๋ฌธ ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜: {e}", None
199
-
200
- answers_payload, results_log = run_questions(agent, questions, api_url)
201
- results_df = pd.DataFrame(results_log)
202
- if not answers_payload:
203
- return "์ œ์ถœํ•  ๋‹ต๋ณ€์ด ์—†์Šต๋‹ˆ๋‹ค.", results_df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
  submission = {
206
  "username": username.strip(),
@@ -218,24 +284,46 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
218
  f"({r.get('correct_count', '?')}/{r.get('total_attempted', '?')} ์ •๋‹ต)\n"
219
  f"๋ฉ”์‹œ์ง€: {r.get('message', '')}"
220
  )
221
- return status, results_df
222
  except requests.exceptions.HTTPError as e:
223
  detail = e.response.text[:500] if e.response is not None else str(e)
224
- return f"์ œ์ถœ ์‹คํŒจ: {detail}", results_df
225
  except Exception as e: # noqa: BLE001
226
- return f"์ œ์ถœ ์ค‘ ์˜ค๋ฅ˜: {e}", results_df
 
227
 
228
 
229
  def run_test_only(n_questions: int):
230
- """์ œ์ถœ ์—†์ด ์•ž์˜ N๊ฐœ ์งˆ๋ฌธ๋งŒ ์‹คํ–‰ํ•ด ๋ณด๋Š” ํ…Œ์ŠคํŠธ (์ ์ˆ˜์— ์˜ํ–ฅ ์—†์Œ)"""
231
  api_url = DEFAULT_API_URL
 
232
  try:
233
- agent = BasicAgent()
234
  questions = fetch_questions(api_url)[: int(n_questions)]
235
  except Exception as e: # noqa: BLE001
236
- return f"์ค€๋น„ ์ค‘ ์˜ค๋ฅ˜: {e}", None
237
- _, results_log = run_questions(agent, questions, api_url)
238
- return f"ํ…Œ์ŠคํŠธ ์™„๋ฃŒ: {len(results_log)}๊ฐœ ์งˆ๋ฌธ ์‹คํ–‰ (์ œ์ถœํ•˜์ง€ ์•Š์Œ)", pd.DataFrame(results_log)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
 
241
  # ---------------------------------------------------------------------------
@@ -256,6 +344,7 @@ with gr.Blocks() as demo:
256
  with gr.Row():
257
  test_n = gr.Number(value=2, label="ํ…Œ์ŠคํŠธํ•  ๋ฌธํ•ญ ์ˆ˜ (์ œ์ถœ ์•ˆ ํ•จ)", precision=0)
258
  test_button = gr.Button("ํ…Œ์ŠคํŠธ ์‹คํ–‰ (์ œ์ถœ ์•ˆ ํ•จ)")
 
259
  run_button = gr.Button("Run Evaluation & Submit All Answers")
260
 
261
  status_output = gr.Textbox(label="Run Status / Submission Result", lines=6, interactive=False)
@@ -263,11 +352,8 @@ with gr.Blocks() as demo:
263
 
264
  test_button.click(fn=run_test_only, inputs=[test_n], outputs=[status_output, results_table])
265
  run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])
266
-
267
- @spaces.GPU
268
- def dummy():
269
- return "ok"
270
 
271
  if __name__ == "__main__":
272
- demo.launch(debug=True, share=False)
273
 
 
1
+ import json
2
  import os
3
  import re
4
+ import threading
5
  import time
6
+ from concurrent.futures import ThreadPoolExecutor, as_completed
7
 
8
  import gradio as gr
9
  import pandas as pd
 
17
  tool,
18
  )
19
 
20
+ # ZeroGPU ํ•˜๋“œ์›จ์–ด์—์„œ ์‹œ์ž‘ ์˜ค๋ฅ˜๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•œ ๋”๋ฏธ ํ•จ์ˆ˜ (CPU ํ•˜๋“œ์›จ์–ด์—์„œ๋Š” ๋ฌด์‹œ๋จ)
21
+ try:
22
+ import spaces
23
+
24
+ @spaces.GPU
25
+ def _gpu_placeholder():
26
+ return "ok"
27
+ except ImportError:
28
+ pass
29
+
30
  # ---------------------------------------------------------------------------
31
+ # ์„ค์ • (Space์˜ Variables์—์„œ ์กฐ์ • ๊ฐ€๋Šฅ)
32
  # ---------------------------------------------------------------------------
33
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
34
  FILES_DIR = "/tmp/task_files"
35
+ CACHE_PATH = "/tmp/answers_cache.json"
36
  os.makedirs(FILES_DIR, exist_ok=True)
37
 
38
+ WORKERS = int(os.getenv("WORKERS", "3")) # ๋™์‹œ ์‹คํ–‰ ๋ฌธํ•ญ ์ˆ˜
39
+ QUESTION_TIMEOUT = int(os.getenv("QUESTION_TIMEOUT", "240")) # ๋ฌธํ•ญ๋‹น ์ œํ•œ ์‹œ๊ฐ„(์ดˆ)
40
+ MAX_STEPS = int(os.getenv("MAX_STEPS", "8")) # ์—์ด์ „ํŠธ ์ตœ๋Œ€ ๋‹จ๊ณ„
41
 
 
42
  ANSWER_RULES = """
43
  You are a careful research and reasoning agent solving GAIA-style questions.
44
  Rules:
 
52
  5. If the answer is a string: no articles (a, an, the) and no abbreviations unless
53
  the question asks for them.
54
  6. If a file path is given, read it with the read_local_file tool or with python code.
55
+ 7. Be efficient: stop researching as soon as you have a reliable answer.
56
+ 8. Finish by calling final_answer(...) with the answer only.
57
  """
58
 
59
+ # ---------------------------------------------------------------------------
60
+ # ์บ์‹œ (๋Š๊ฒจ๋„ ํ‘ผ ๋‹ต์€ ๋ณด์กด. ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์žฌ์‹œ์ž‘๋˜๋ฉด ์ดˆ๊ธฐํ™”๋จ)
61
+ # ---------------------------------------------------------------------------
62
+ _cache_lock = threading.Lock()
63
+
64
+
65
+ def load_cache() -> dict:
66
+ try:
67
+ with open(CACHE_PATH, "r", encoding="utf-8") as f:
68
+ return json.load(f)
69
+ except Exception: # noqa: BLE001
70
+ return {}
71
+
72
+
73
+ def save_cache_entry(task_id: str, answer: str):
74
+ with _cache_lock:
75
+ cache = load_cache()
76
+ cache[task_id] = answer
77
+ with open(CACHE_PATH, "w", encoding="utf-8") as f:
78
+ json.dump(cache, f, ensure_ascii=False)
79
+
80
 
81
  # ---------------------------------------------------------------------------
82
  # ๋„๊ตฌ
 
92
  lower = path.lower()
93
  if lower.endswith((".xlsx", ".xls")):
94
  sheets = pd.read_excel(path, sheet_name=None)
95
+ return "\n\n".join(
96
+ f"## Sheet: {n}\n{df.to_string(max_rows=200)}" for n, df in sheets.items()
97
+ )
 
98
  if lower.endswith(".csv"):
99
  return pd.read_csv(path).to_string(max_rows=200)
100
  with open(path, "r", encoding="utf-8", errors="ignore") as f:
 
107
  # ์—์ด์ „ํŠธ
108
  # ---------------------------------------------------------------------------
109
  def build_model():
 
 
 
 
110
  gemini_key = os.getenv("GEMINI_API_KEY")
111
  if gemini_key:
112
  model_id = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
 
113
  return LiteLLMModel(model_id=f"gemini/{model_id}", api_key=gemini_key)
 
114
  openai_key = os.getenv("OPENAI_API_KEY")
115
  if openai_key:
116
  model_id = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
 
117
  return LiteLLMModel(model_id=f"openai/{model_id}", api_key=openai_key)
 
118
  model_id = os.getenv("HF_MODEL", "Qwen/Qwen2.5-Coder-32B-Instruct")
 
119
  return InferenceClientModel(model_id=model_id, token=os.getenv("HF_TOKEN"))
120
 
121
 
122
+ def build_agent() -> CodeAgent:
123
+ # ์Šค๋ ˆ๋“œ ์•ˆ์ „์„ ์œ„ํ•ด ๋ฌธํ•ญ๋งˆ๋‹ค ์ƒˆ ์—์ด์ „ํŠธ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
124
+ return CodeAgent(
125
+ tools=[DuckDuckGoSearchTool(), VisitWebpageTool(), read_local_file],
126
+ model=build_model(),
127
+ additional_authorized_imports=[
128
+ "pandas", "numpy", "re", "json", "math", "datetime",
129
+ "collections", "itertools", "statistics", "requests", "bs4",
130
+ ],
131
+ max_steps=MAX_STEPS,
132
+ verbosity_level=1,
133
+ )
 
134
 
 
 
 
 
 
135
 
136
+ def clean_answer(answer) -> str:
137
+ text = str(answer).strip()
138
+ text = re.sub(r"^\s*final answer\s*:\s*", "", text, flags=re.IGNORECASE)
139
+ return text.strip().rstrip(".").strip()
 
 
140
 
141
 
142
  # ---------------------------------------------------------------------------
143
+ # ์ฑ„์  ์„œ๋ฒ„ ํ†ต์‹ 
144
  # ---------------------------------------------------------------------------
145
  def fetch_questions(api_url: str) -> list[dict]:
146
  resp = requests.get(f"{api_url}/questions", timeout=30)
 
151
  return data
152
 
153
 
154
+ def download_task_file(api_url: str, task_id: str, file_name: str):
 
155
  if not file_name:
156
  return None
157
  try:
 
166
  return None
167
 
168
 
169
+ # ---------------------------------------------------------------------------
170
+ # ๋ฌธํ•ญ ํ’€์ด (์บ์‹œ + ์‹œ๊ฐ„ ์ œํ•œ)
171
+ # ---------------------------------------------------------------------------
172
+ def solve_one(item: dict, api_url: str) -> str:
173
+ prompt = ANSWER_RULES + "\n\nQuestion:\n" + item["question"]
174
+ file_path = download_task_file(api_url, item["task_id"], item.get("file_name", ""))
175
+ if file_path:
176
+ prompt += f"\n\nAn attached file is available at: {file_path}"
177
+ return clean_answer(build_agent().run(prompt))
178
+
179
+
180
+ def solve_with_timeout(item: dict, api_url: str) -> tuple[str, bool]:
181
+ """(๋‹ต๋ณ€, ์„ฑ๊ณต ์—ฌ๋ถ€)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์„ฑ๊ณตํ•œ ๋‹ต๋งŒ ์บ์‹œ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค."""
182
+ task_id = item["task_id"]
183
+ cached = load_cache().get(task_id)
184
+ if cached:
185
+ print(f"[cache] {task_id} -> {cached}")
186
+ return cached, True
187
+
188
+ box: dict = {}
189
+
190
+ def target():
191
  try:
192
+ box["answer"] = solve_one(item, api_url)
193
  except Exception as e: # noqa: BLE001
194
+ box["error"] = str(e)
195
+
196
+ print(f"[start] {task_id}")
197
+ t = threading.Thread(target=target, daemon=True)
198
+ t.start()
199
+ t.join(QUESTION_TIMEOUT)
200
+
201
+ if t.is_alive():
202
+ print(f"[timeout] {task_id}")
203
+ return "unknown", False
204
+ if "error" in box:
205
+ print(f"[error] {task_id}: {box['error']}")
206
+ return "unknown", False
207
+ save_cache_entry(task_id, box["answer"])
208
+ print(f"[done] {task_id} -> {box['answer']}")
209
+ return box["answer"], True
210
+
211
+
212
+ def iter_solve(questions: list[dict], api_url: str):
213
+ """๋ฌธํ•ญ์„ ๋ณ‘๋ ฌ๋กœ ํ’€๋ฉด์„œ, ๋๋‚˜๋Š” ๋Œ€๋กœ (item, answer, ok)๋ฅผ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค."""
214
+ valid = [q for q in questions if q.get("task_id") and q.get("question") is not None]
215
+ with ThreadPoolExecutor(max_workers=WORKERS) as pool:
216
+ futures = {pool.submit(solve_with_timeout, q, api_url): q for q in valid}
217
+ for fut in as_completed(futures):
218
+ answer, ok = fut.result()
219
+ yield futures[fut], answer, ok
220
+
221
+
222
+ def make_df(rows: dict, order: list[str]) -> pd.DataFrame:
223
+ data = [rows[tid] for tid in order if tid in rows]
224
+ return pd.DataFrame(data, columns=["Task ID", "Question", "Submitted Answer", "OK"])
225
 
226
 
227
  # ---------------------------------------------------------------------------
228
+ # Gradio ์ฝœ๋ฐฑ (์ œ๋„ˆ๋ ˆ์ดํ„ฐ: ์ง„ํ–‰ ์ƒํ™ฉ์ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™”๋ฉด์— ํ‘œ์‹œ๋จ)
229
  # ---------------------------------------------------------------------------
230
  def run_and_submit_all(profile: gr.OAuthProfile | None):
 
231
  space_id = os.getenv("SPACE_ID")
232
  if not profile:
233
+ yield "๋จผ์ € Hugging Face ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์œผ๋กœ ๋กœ๊ทธ์ธํ•ด ์ฃผ์„ธ์š”.", None
234
+ return
235
+ if not space_id:
236
+ yield "SPACE_ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. Space ์•ˆ์—์„œ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", None
237
+ return
238
  username = profile.username
239
  api_url = DEFAULT_API_URL
 
 
 
240
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
241
 
242
+ yield "์งˆ๋ฌธ์„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘...", None
 
 
 
 
243
  try:
244
  questions = fetch_questions(api_url)
245
  except Exception as e: # noqa: BLE001
246
+ yield f"์งˆ๋ฌธ ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜: {e}", None
247
+ return
248
+
249
+ order = [q["task_id"] for q in questions if q.get("task_id")]
250
+ total = len(order)
251
+ rows: dict = {}
252
+ start = time.time()
253
+
254
+ for item, answer, ok in iter_solve(questions, api_url):
255
+ rows[item["task_id"]] = {
256
+ "Task ID": item["task_id"],
257
+ "Question": item["question"],
258
+ "Submitted Answer": answer,
259
+ "OK": "O" if ok else "X",
260
+ }
261
+ elapsed = int(time.time() - start)
262
+ yield f"์ง„ํ–‰ {len(rows)}/{total} ์™„๋ฃŒ (๊ฒฝ๊ณผ {elapsed}์ดˆ)", make_df(rows, order)
263
+
264
+ answers_payload = [
265
+ {"task_id": tid, "submitted_answer": rows[tid]["Submitted Answer"]}
266
+ for tid in order if tid in rows
267
+ ]
268
+ failed = sum(1 for r in rows.values() if r["OK"] == "X")
269
+ yield f"ํ’€์ด ์™„๋ฃŒ (์‹คํŒจ/์‹œ๊ฐ„์ดˆ๊ณผ {failed}๊ฐœ). ์ œ์ถœ ์ค‘...", make_df(rows, order)
270
 
271
  submission = {
272
  "username": username.strip(),
 
284
  f"({r.get('correct_count', '?')}/{r.get('total_attempted', '?')} ์ •๋‹ต)\n"
285
  f"๋ฉ”์‹œ์ง€: {r.get('message', '')}"
286
  )
 
287
  except requests.exceptions.HTTPError as e:
288
  detail = e.response.text[:500] if e.response is not None else str(e)
289
+ status = f"์ œ์ถœ ์‹คํŒจ: {detail}"
290
  except Exception as e: # noqa: BLE001
291
+ status = f"์ œ์ถœ ์ค‘ ์˜ค๋ฅ˜: {e}"
292
+ yield status, make_df(rows, order)
293
 
294
 
295
  def run_test_only(n_questions: int):
296
+ """์ œ์ถœ ์—†์ด ์•ž์˜ N๊ฐœ ๋ฌธํ•ญ๋งŒ ์‹คํ–‰ (์ ์ˆ˜์— ์˜ํ–ฅ ์—†์Œ)"""
297
  api_url = DEFAULT_API_URL
298
+ yield "์งˆ๋ฌธ์„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘...", None
299
  try:
 
300
  questions = fetch_questions(api_url)[: int(n_questions)]
301
  except Exception as e: # noqa: BLE001
302
+ yield f"์ค€๋น„ ์ค‘ ์˜ค๋ฅ˜: {e}", None
303
+ return
304
+ order = [q["task_id"] for q in questions if q.get("task_id")]
305
+ rows: dict = {}
306
+ start = time.time()
307
+ for item, answer, ok in iter_solve(questions, api_url):
308
+ rows[item["task_id"]] = {
309
+ "Task ID": item["task_id"],
310
+ "Question": item["question"],
311
+ "Submitted Answer": answer,
312
+ "OK": "O" if ok else "X",
313
+ }
314
+ yield (
315
+ f"ํ…Œ์ŠคํŠธ ์ง„ํ–‰ {len(rows)}/{len(order)} (๊ฒฝ๊ณผ {int(time.time() - start)}์ดˆ)",
316
+ make_df(rows, order),
317
+ )
318
+ yield f"ํ…Œ์ŠคํŠธ ์™„๋ฃŒ ({int(time.time() - start)}์ดˆ, ์ œ์ถœ ์•ˆ ํ•จ)", make_df(rows, order)
319
+
320
+
321
+ def clear_cache():
322
+ try:
323
+ os.remove(CACHE_PATH)
324
+ return "์บ์‹œ๋ฅผ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค."
325
+ except FileNotFoundError:
326
+ return "์‚ญ์ œํ•  ์บ์‹œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
327
 
328
 
329
  # ---------------------------------------------------------------------------
 
344
  with gr.Row():
345
  test_n = gr.Number(value=2, label="ํ…Œ์ŠคํŠธํ•  ๋ฌธํ•ญ ์ˆ˜ (์ œ์ถœ ์•ˆ ํ•จ)", precision=0)
346
  test_button = gr.Button("ํ…Œ์ŠคํŠธ ์‹คํ–‰ (์ œ์ถœ ์•ˆ ํ•จ)")
347
+ cache_button = gr.Button("์บ์‹œ ์ดˆ๊ธฐํ™”")
348
  run_button = gr.Button("Run Evaluation & Submit All Answers")
349
 
350
  status_output = gr.Textbox(label="Run Status / Submission Result", lines=6, interactive=False)
 
352
 
353
  test_button.click(fn=run_test_only, inputs=[test_n], outputs=[status_output, results_table])
354
  run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])
355
+ cache_button.click(fn=clear_cache, outputs=[status_output])
 
 
 
356
 
357
  if __name__ == "__main__":
358
+ demo.queue().launch(debug=True, share=False)
359