omega21 commited on
Commit
a212dc5
ยท
verified ยท
1 Parent(s): ceb10f4

Update app.py

Browse files

"""HF Agents Course ์ตœ์ข… ๊ณผ์ œ - ๋‹ค๋‹จ๊ณ„ ์—์ด์ „ํŠธ ๋ฒ„์ „ (Gemini ๋˜๋Š” Hugging Face)

LLM์ด ํ•œ ๋‹จ๊ณ„์— ํ•˜๋‚˜์˜ ๋„๊ตฌ๋ฅผ ๊ณจ๋ผ ์“ฐ๊ณ (๊ฒ€์ƒ‰/์œ„ํ‚ค/์›นํŽ˜์ด์ง€/ํŒŒ์ด์ฌ/์œ ํŠœ๋ธŒ ์ž๋ง‰), ๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ณ  ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ์ •ํ•ฉ๋‹ˆ๋‹ค.
์ตœ๋Œ€ MAX_STEPS ๋‹จ๊ณ„ ์•ˆ์— ์ตœ์ข… ๋‹ต์„ ๋‚ด๋„๋ก ์ œํ•œํ•ด ๋น„์šฉ์„ ํ†ต์ œํ•ฉ๋‹ˆ๋‹ค.
"""
import base64
import json
import mimetypes
import os
import re
import subprocess
import sys
import tempfile
import threading
import time
from collections import deque

import gradio as gr
import pandas as pd
import requests

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

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

try:
from ddgs import DDGS
except ImportError:
try:
from duckduckgo_search import DDGS
except ImportError:
DDGS = None

# ---------------------------------------------------------------------------
# ์„ค์ • (Space Variables/Secrets๋กœ ์กฐ์ •)
# ---------------------------------------------------------------------------
SCORING_API = "https://agents-course-unit4-scoring.hf.space"
GEMINI_API = "https://generativelanguage.googleapis.com/v1beta"
HF_ROUTER = "https://router.huggingface.co/v1"
FILES_DIR = "/tmp/task_files"
CACHE_PATH = "/tmp/answers_cache_v4.json"
os.makedirs(FILES_DIR, exist_ok=True)

PROVIDER = (os.getenv("PROVIDER", "") or ("gemini" if os.getenv("GEMINI_API_KEY") else "hf")).lower() # gemini | hf
MIN_INTERVAL = float(os.getenv("MIN_INTERVAL", "7" if PROVIDER == "gemini" else "1")) # LLM ํ˜ธ์ถœ ๊ฐ„ ์ตœ์†Œ ๊ฐ„๊ฒฉ(์ดˆ)
MAX_RETRY = int(os.getenv("MAX_RETRY", "3")) # 429 ์žฌ์‹œ๋„ ํšŸ์ˆ˜
MAX_STEPS = int(os.getenv("MAX_STEPS", "6")) # ๋ฌธํ•ญ๋‹น ์ตœ๋Œ€ ๋‹จ๊ณ„(=์ตœ๋Œ€ LLM ํ˜ธ์ถœ ์ˆ˜)
QUESTION_TIMEOUT = int(os.getenv("QUESTION_TIMEOUT", "240")) # ๋ฌธํ•ญ๋‹น ๋ชฉํ‘œ ์ œํ•œ ์‹œ๊ฐ„(์ดˆ). ๋„˜์œผ๋ฉด ๋ฐ”๋กœ ๋‹ต์„ ๋‚ด๊ฒŒ ํ•จ
OBS_CHARS = int(os.getenv("OBS_CHARS", "6000")) # ๋„๊ตฌ ๊ฒฐ๊ณผ 1ํšŒ ์ตœ๋Œ€ ๊ธ€์ž ์ˆ˜
PY_TIMEOUT = int(os.getenv("PY_TIMEOUT", "25")) # ํŒŒ์ด์ฌ ์‹คํ–‰ ์ œํ•œ ์‹œ๊ฐ„(์ดˆ)
GEMINI_MODELS = os.getenv("GEMINI_MODELS", "") # ์„ ํƒ: ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ํ•œ Gemini ๋ชจ๋ธ ๋ชฉ๋ก
HF_MODELS = [m.strip() for m in (os.getenv("HF_MODELS") or os.getenv("HF_MODEL") or "Qwen/Qwen2.5-72B-Instruct").split(",") if m.strip()]
UA = {"User-Agent": "Mozilla/5.0 (agents-course-runner)"}

AGENT_PROMPT = """You are a careful research agent solving a GAIA-style benchmark question. The final answer is graded by EXACT MATCH.

You work in steps. In each turn reply with EXACTLY ONE action, in this format and nothing else:

Thought: <short reasoning>
Action: <search|wiki|fetch|python|youtube|final>
Input: <input for the action>

Actions:
- search: Input = a web search query. Returns titles, URLs and snippets.
- wiki: Input = "Page Title" or "Page Title | keyword". Returns the English Wikipedia article text including tables; with a keyword you also get the passages around it.
- fetch: Input = a URL, or "URL | keyword". Returns the page text.
- python: Input = Python code (may span several lines). Use print() to output results. pandas, openpyxl, re, json and math are available. Use it for ANY arithmetic, counting, sorting, string manipulation, and for reading attached files.
- youtube: Input = a YouTube URL. Returns the transcript if available.
- final: Input = ONLY the final answer value.

Rules for the final answer:
- Output only the answer value: no explanation, no trailing period, no units unless asked.
- Numbers: digits only, no thousands separators, no currency or percent symbols unless asked.
- Lists: comma-separated, in the order the question asks.
- Strings: no leading articles (a, an, the) and no abbreviations unless asked.
- Follow any formatting instruction inside the question exactly.

Be efficient: you have at most {max_steps} steps. Prefer verifying facts with tools over guessing. If a tool result is not helpful, change your approach instead of repeating it. If you cannot find the answer, give your best reasoned answer rather than leaving it empty."""

# ---------------------------------------------------------------------------
# ๋กœ๊ทธ / ์บ์‹œ
# ---------------------------------------------------------------------------
_events = deque(maxlen=14)


def log(msg: str):
line = f"[{time.strftime('%H:%M:%S')}] {msg}"
print(line, flush=True)
_events.append(line)


def recent_log() -> str:
return "\n".join(_events)


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(task_id: str, answer: str):
cache = load_cache()
cache[task_id] = answer
with open(CACHE_PATH, "w", encoding="utf-8") as f:
json.dump(cache, f, ensure_ascii=False)


# ---------------------------------------------------------------------------
# LLM ํ˜ธ์ถœ (Gemini REST / Hugging Face ๋ผ์šฐํ„ฐ)
# ---------------------------------------------------------------------------
class LLMUnavailable(Exception):
pass


_last_call = 0.0


def throttle():
global _last_call
wait = MIN_INTERVAL - (time.time() - _last_call)
if wait > 0:
time.sleep(wait)
_last_call = time.time()


def api_key() -> str:
return os.getenv("GEMINI_API_KEY", "")


def hf_token() -> str:
return os.getenv("HF_TOKEN", "")


def list_models() -> list[str]:
r = requests.get(f"{GEMINI_API}/models", headers={"x-goog-api-key": api_key()}, params={"pageSize": 200}, timeout=30)
r.raise_for_status()
return [m["name"].replace("models/", "") for m in r.json().get("models", [])
if "generateContent" in m.get("supportedGenerationMethods", [])]


_BAD = ("image", "tts", "audio", "live", "embed", "vision", "robotics", "computer", "imagen", "veo")
_candidates: list[str] = []


def candidate_models() -> list[str]:
global _candidates
if _candidates:
return _candidates
if GEMINI_MODELS.strip():
_candidates = [m.strip().replace("models/", "") for m in GEMINI_MODELS.split(",") if m.strip()]
return _candidates
names = [n for n in list_models() if n.startswith("gemini") and not any(b in n for b in _BAD)]
lite = [n for n in names if "flash-lite" in n]
flash = [n for n in names if "flash" in n and "flash-lite" not in n]
_candidates = (lite[:2] + flash[:2]) or names[:3]
log(f"์ž๋™ ์„ ํƒ๋œ Gemini ๋ชจ๋ธ ํ›„๋ณด: {_candidates}")
return _candidates


def call_gemini(parts: list[dict]) -> tuple[str, str]:
if not api_key():
raise LLMUnavailable("GEMINI_API_KEY๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
last_err = ""
for model in candidate_models():
for attempt in range(MAX_RETRY + 1):
throttle()
try:
r = requests.post(
f"{GEMINI_API}/models/{model}:generateContent",
headers={"x-goog-api-key": api_key(), "Content-Type": "application/json"},
json={"contents": [{"role": "user", "parts": parts}],
"generationConfig": {"temperature": 0, "maxOutputTokens": 2048}},
timeout=120,
)
except requests.RequestException as e:
last_err = f"{model}: ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ {e}"
time.sleep(5)
continue
if r.status_code == 200:
cand = (r.json().get("candidates") or [{}])[0]
text = "".join(p.get("text", "") for p in cand.get("content", {}).get("parts", []) if not p.get("thought"))
if text.strip():
return text, model
last_err = f"{model}: ๋นˆ ์‘๋‹ต({cand.get('finishReason')})"
break
body = r.text
last_err = f"{model}: HTTP {r.status_code} {body[:180].replace(chr(10), ' ')}"
if r.status_code == 429:
if "PerDay" in body or "limit: 0" in body:
log(f"{model}: ์ผ์ผ ํ•œ๋„ ์†Œ์ง„ ๋˜๋Š” ํ•œ๋„ 0 -> ๋‹ค์Œ ๋ชจ๋ธ")
break
if attempt == MAX_RETRY:
break
m = re.search(r'"retryDelay":\s*"(\d+(?:\.\d+)?)s"', body)
wait = min(float(m.group(1)) + 1, 60) if m else 15 * (attempt + 1)
log(f"{model}: 429, {int(wait)}์ดˆ ํ›„ ์žฌ์‹œ๋„ ({attempt + 1}/{MAX_RETRY})")
time.sleep(wait)
continue
if r.status_code >= 500:
time.sleep(5)
continue
break
raise LLMUnavailable(last_err or "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋ธ์ด ์—†์Šต๋‹ˆ๋‹ค.")


def call_hf(parts: list[dict]) -> tuple[str, str]:
if not hf_token():
raise LLMUnavailable("HF_TOKEN์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
text = "\n".join(p["text"] for p in parts if "text" in p)
if any("inline_data" in p for p in parts):
text = "NOTE: An image/audio attachment exists but you cannot view it; answer from the other information.\n\n" + text
last_err = ""
for model in HF_MODELS:
for attempt in range(MAX_RETRY + 1):
throttle()
try:
r = requests.post(
f"{HF_ROUTER}/chat/completions",
headers={"Authorization": f"Bearer {hf_token()}", "Content-Type": "application/json"},
json={"model": model, "messages": [{"role": "user", "content": text}],
"max_tokens": 1500, "temperature": 0},
timeout=120,
)
except requests.RequestException as e:
last_err = f"{model}: ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ {e}"
time.sleep(5)
continue
if r.status_code == 200:
content = (r.json().get("choices") or [{}])[0].get("message", {}).get("content") or ""
content = re.sub(r"(?s)<think>.*?</think>", "", content).strip()
if content:
return content, model

Files changed (1) hide show
  1. app.py +306 -176
app.py CHANGED
@@ -1,13 +1,17 @@
1
- """HF Agents Course ์ตœ์ข… ๊ณผ์ œ - Gemini ๋˜๋Š” Hugging Face ์„ ํƒํ˜• ๊ฒฝ๋Ÿ‰ ๋ฒ„์ „
2
 
3
- ์„ค๊ณ„ ์›์น™: ๋ฌธํ•ญ๋‹น LLM ํ˜ธ์ถœ 1ํšŒ.
4
- ์›น ๊ฒ€์ƒ‰(ํ˜ธ์ถœ ์—†์Œ) -> ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ + ์ฒจ๋ถ€ ํŒŒ์ผ์„ ํ•œ ๋ฒˆ์— Gemini์— ์ „๋‹ฌ -> ์ตœ์ข… ๋‹ต๋งŒ ๋ฐ›์Œ
5
  """
6
  import base64
7
  import json
8
  import mimetypes
9
  import os
10
  import re
 
 
 
 
11
  import time
12
  from collections import deque
13
 
@@ -38,40 +42,51 @@ except ImportError:
38
  # ---------------------------------------------------------------------------
39
  SCORING_API = "https://agents-course-unit4-scoring.hf.space"
40
  GEMINI_API = "https://generativelanguage.googleapis.com/v1beta"
 
41
  FILES_DIR = "/tmp/task_files"
42
- CACHE_PATH = "/tmp/answers_cache.json"
43
  os.makedirs(FILES_DIR, exist_ok=True)
44
 
45
  PROVIDER = (os.getenv("PROVIDER", "") or ("gemini" if os.getenv("GEMINI_API_KEY") else "hf")).lower() # gemini | hf
46
- MIN_INTERVAL = float(os.getenv("MIN_INTERVAL", "7" if PROVIDER == "gemini" else "2")) # LLM ํ˜ธ์ถœ ๊ฐ„ ์ตœ์†Œ ๊ฐ„๊ฒฉ(์ดˆ)
47
- HF_ROUTER = "https://router.huggingface.co/v1"
 
 
 
 
 
48
  HF_MODELS = [m.strip() for m in (os.getenv("HF_MODELS") or os.getenv("HF_MODEL") or "Qwen/Qwen2.5-72B-Instruct").split(",") if m.strip()]
49
- MAX_RETRY = int(os.getenv("MAX_RETRY", "3")) # 429 ์žฌ์‹œ๋„ ํšŸ์ˆ˜
50
- FETCH_PAGES = int(os.getenv("FETCH_PAGES", "2")) # ๋ณธ๋ฌธ์„ ์ฝ์„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ˆ˜
51
- PAGE_CHARS = int(os.getenv("PAGE_CHARS", "5000")) # ํŽ˜์ด์ง€๋‹น ์ตœ๋Œ€ ๊ธ€์ž ์ˆ˜
52
- LLM_QUERY = os.getenv("LLM_QUERY", "0") == "1" # 1์ด๋ฉด ๊ฒ€์ƒ‰์–ด๋„ Gemini๋กœ ์ƒ์„ฑ(ํ˜ธ์ถœ 2๋ฐฐ)
53
- GEMINI_MODELS = os.getenv("GEMINI_MODELS", "") # ์„ ํƒ: ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ํ•œ ๋ชจ๋ธ ์ด๋ฆ„ ๋ชฉ๋ก
 
 
 
 
54
 
55
- PROMPT = """You are answering a GAIA-style benchmark question. The answer is graded by EXACT MATCH.
 
 
 
 
 
 
56
 
57
- Rules:
58
- - Use the provided web context and attached file. If the context is insufficient, use your own knowledge and reason carefully.
59
- - Give ONLY the answer value: no explanation, no trailing period, no units unless asked.
60
  - Numbers: digits only, no thousands separators, no currency or percent symbols unless asked.
61
  - Lists: comma-separated, in the order the question asks.
62
- - Strings: no leading articles (a, an, the), no abbreviations unless asked.
63
- - End your reply with one final line exactly in the form:
64
- FINAL ANSWER: <answer>
65
 
66
- Question:
67
- {question}
68
-
69
- {extra}"""
70
 
71
  # ---------------------------------------------------------------------------
72
- # ๋กœ๊ทธ
73
  # ---------------------------------------------------------------------------
74
- _events = deque(maxlen=12)
75
 
76
 
77
  def log(msg: str):
@@ -84,9 +99,6 @@ def recent_log() -> str:
84
  return "\n".join(_events)
85
 
86
 
87
- # ---------------------------------------------------------------------------
88
- # ์บ์‹œ
89
- # ---------------------------------------------------------------------------
90
  def load_cache() -> dict:
91
  try:
92
  with open(CACHE_PATH, "r", encoding="utf-8") as f:
@@ -103,29 +115,36 @@ def save_cache(task_id: str, answer: str):
103
 
104
 
105
  # ---------------------------------------------------------------------------
106
- # Gemini REST ํ˜ธ์ถœ (๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด requests๋งŒ ์‚ฌ์šฉ)
107
  # ---------------------------------------------------------------------------
108
  class LLMUnavailable(Exception):
109
  pass
110
 
111
 
 
 
 
 
 
 
 
 
 
 
 
112
  def api_key() -> str:
113
  return os.getenv("GEMINI_API_KEY", "")
114
 
115
 
 
 
 
 
116
  def list_models() -> list[str]:
117
- r = requests.get(
118
- f"{GEMINI_API}/models",
119
- headers={"x-goog-api-key": api_key()},
120
- params={"pageSize": 200},
121
- timeout=30,
122
- )
123
  r.raise_for_status()
124
- names = []
125
- for m in r.json().get("models", []):
126
- if "generateContent" in m.get("supportedGenerationMethods", []):
127
- names.append(m["name"].replace("models/", ""))
128
- return names
129
 
130
 
131
  _BAD = ("image", "tts", "audio", "live", "embed", "vision", "robotics", "computer", "imagen", "veo")
@@ -133,7 +152,6 @@ _candidates: list[str] = []
133
 
134
 
135
  def candidate_models() -> list[str]:
136
- """GEMINI_MODELS๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ ๋ชฉ๋ก, ์—†์œผ๋ฉด ํ‚ค๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ๋ชจ๋ธ์—์„œ ์ž๋™ ์„ ํƒ."""
137
  global _candidates
138
  if _candidates:
139
  return _candidates
@@ -144,23 +162,11 @@ def candidate_models() -> list[str]:
144
  lite = [n for n in names if "flash-lite" in n]
145
  flash = [n for n in names if "flash" in n and "flash-lite" not in n]
146
  _candidates = (lite[:2] + flash[:2]) or names[:3]
147
- log(f"์ž๋™ ์„ ํƒ๋œ ๋ชจ๋ธ ํ›„๋ณด: {_candidates}")
148
  return _candidates
149
 
150
 
151
- _last_call = 0.0
152
-
153
-
154
- def throttle():
155
- global _last_call
156
- wait = MIN_INTERVAL - (time.time() - _last_call)
157
- if wait > 0:
158
- time.sleep(wait)
159
- _last_call = time.time()
160
-
161
-
162
  def call_gemini(parts: list[dict]) -> tuple[str, str]:
163
- """(์‘๋‹ต ํ…์ŠคํŠธ, ์‚ฌ์šฉํ•œ ๋ชจ๋ธ)์„ ๋ฐ˜ํ™˜. ๋ชจ๋“  ํ›„๋ณด๊ฐ€ ์‹คํŒจํ•˜๋ฉด LLMUnavailable."""
164
  if not api_key():
165
  raise LLMUnavailable("GEMINI_API_KEY๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
166
  last_err = ""
@@ -171,33 +177,24 @@ def call_gemini(parts: list[dict]) -> tuple[str, str]:
171
  r = requests.post(
172
  f"{GEMINI_API}/models/{model}:generateContent",
173
  headers={"x-goog-api-key": api_key(), "Content-Type": "application/json"},
174
- json={
175
- "contents": [{"role": "user", "parts": parts}],
176
- "generationConfig": {"temperature": 0, "maxOutputTokens": 2048},
177
- },
178
  timeout=120,
179
  )
180
  except requests.RequestException as e:
181
  last_err = f"{model}: ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ {e}"
182
  time.sleep(5)
183
  continue
184
-
185
  if r.status_code == 200:
186
  cand = (r.json().get("candidates") or [{}])[0]
187
- text = "".join(
188
- p.get("text", "")
189
- for p in cand.get("content", {}).get("parts", [])
190
- if not p.get("thought")
191
- )
192
  if text.strip():
193
  return text, model
194
  last_err = f"{model}: ๋นˆ ์‘๋‹ต({cand.get('finishReason')})"
195
  break
196
-
197
  body = r.text
198
  last_err = f"{model}: HTTP {r.status_code} {body[:180].replace(chr(10), ' ')}"
199
  if r.status_code == 429:
200
- # ๏ฟฝ๏ฟฝ์ผ ํ•œ๋„ ์†Œ์ง„ ๋˜๋Š” ํ•œ๋„ 0์ด๋ฉด ์žฌ์‹œ๋„ํ•ด๋„ ์†Œ์šฉ์—†์œผ๋ฏ€๋กœ ๋‹ค์Œ ๋ชจ๋ธ๋กœ
201
  if "PerDay" in body or "limit: 0" in body:
202
  log(f"{model}: ์ผ์ผ ํ•œ๋„ ์†Œ์ง„ ๋˜๋Š” ํ•œ๋„ 0 -> ๋‹ค์Œ ๋ชจ๋ธ")
203
  break
@@ -211,25 +208,16 @@ def call_gemini(parts: list[dict]) -> tuple[str, str]:
211
  if r.status_code >= 500:
212
  time.sleep(5)
213
  continue
214
- break # 400, 403, 404 ๋“ฑ์€ ์ด ๋ชจ๋ธ ํฌ๊ธฐ
215
  raise LLMUnavailable(last_err or "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋ธ์ด ์—†์Šต๋‹ˆ๋‹ค.")
216
 
217
 
218
- # ---------------------------------------------------------------------------
219
- # Hugging Face Inference Providers (OpenAI ํ˜ธํ™˜ ๋ผ์šฐํ„ฐ)
220
- # ---------------------------------------------------------------------------
221
- def hf_token() -> str:
222
- return os.getenv("HF_TOKEN", "")
223
-
224
-
225
  def call_hf(parts: list[dict]) -> tuple[str, str]:
226
- """(์‘๋‹ต ํ…์ŠคํŠธ, ์‚ฌ์šฉํ•œ ๋ชจ๋ธ)์„ ๋ฐ˜ํ™˜. ํ…์ŠคํŠธ ์ „์šฉ์ด๋ฏ€๋กœ ์ด๋ฏธ์ง€/์˜ค๋””์˜ค ํŒŒํŠธ๋Š” ์ „๋‹ฌํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค."""
227
  if not hf_token():
228
  raise LLMUnavailable("HF_TOKEN์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
229
  text = "\n".join(p["text"] for p in parts if "text" in p)
230
  if any("inline_data" in p for p in parts):
231
- text = ("NOTE: An image/audio attachment exists but you cannot view it; "
232
- "answer from the other information.\n\n" + text)
233
  last_err = ""
234
  for model in HF_MODELS:
235
  for attempt in range(MAX_RETRY + 1):
@@ -239,7 +227,7 @@ def call_hf(parts: list[dict]) -> tuple[str, str]:
239
  f"{HF_ROUTER}/chat/completions",
240
  headers={"Authorization": f"Bearer {hf_token()}", "Content-Type": "application/json"},
241
  json={"model": model, "messages": [{"role": "user", "content": text}],
242
- "max_tokens": 1024, "temperature": 0},
243
  timeout=120,
244
  )
245
  except requests.RequestException as e:
@@ -256,9 +244,9 @@ def call_hf(parts: list[dict]) -> tuple[str, str]:
256
  body = r.text
257
  last_err = f"{model}: HTTP {r.status_code} {body[:180].replace(chr(10), ' ')}"
258
  if r.status_code == 402:
259
- raise LLMUnavailable("HF ๋ฌด๋ฃŒ ํฌ๋ ˆ๋”ง ์†Œ์ง„(402). " + body[:150])
260
  if r.status_code in (401, 403):
261
- raise LLMUnavailable(f"HF_TOKEN ์ธ์ฆ/๊ถŒํ•œ ์˜ค๋ฅ˜({r.status_code}). ํ† ํฐ์˜ Inference Providers ๊ถŒํ•œ์„ ํ™•์ธํ•˜์„ธ์š”.")
262
  if r.status_code == 429:
263
  if attempt == MAX_RETRY:
264
  break
@@ -269,7 +257,7 @@ def call_hf(parts: list[dict]) -> tuple[str, str]:
269
  if r.status_code >= 500:
270
  time.sleep(5)
271
  continue
272
- break # 400, 404 ๋“ฑ์€ ์ด ๋ชจ๋ธ ํฌ๊ธฐ
273
  raise LLMUnavailable(last_err or "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋ธ์ด ์—†์Šต๋‹ˆ๋‹ค.")
274
 
275
 
@@ -278,39 +266,148 @@ def call_llm(parts: list[dict]) -> tuple[str, str]:
278
 
279
 
280
  # ---------------------------------------------------------------------------
281
- # ์›น ๊ฒ€์ƒ‰ + ํŽ˜์ด์ง€ ์ฝ๊ธฐ (Gemini ํ˜ธ์ถœ ์—†์Œ)
282
  # ---------------------------------------------------------------------------
283
  def html_to_text(html: str) -> str:
284
  html = re.sub(r"(?is)<(script|style|noscript|svg|header|footer|nav)[^>]*>.*?</\1>", " ", html)
 
 
 
285
  text = re.sub(r"(?s)<[^>]+>", " ", html)
286
- text = re.sub(r"&nbsp;|&#160;", " ", text)
287
- return re.sub(r"\s+", " ", text).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
 
290
- def web_context(query: str) -> str:
291
  if DDGS is None:
292
- return ""
293
  try:
294
  results = list(DDGS().text(query, max_results=6))
295
  except Exception as e: # noqa: BLE001
296
- log(f"๊ฒ€์ƒ‰ ์‹คํŒจ: {type(e).__name__}")
297
- return ""
298
- chunks = []
299
- for i, res in enumerate(results, start=1):
300
- chunks.append(f"[{i}] {res.get('title', '')}\n{res.get('href', '')}\n{res.get('body', '')}")
301
- for res in results[:FETCH_PAGES]:
302
- url = res.get("href")
303
- if not url or url.lower().endswith((".pdf", ".zip", ".mp4")):
304
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  try:
306
- page = requests.get(url, timeout=15, headers={"User-Agent": "Mozilla/5.0"})
307
- if page.ok and "text/html" in page.headers.get("content-type", ""):
308
- chunks.append(f"--- PAGE: {url} ---\n{html_to_text(page.text)[:PAGE_CHARS]}")
309
- except requests.RequestException:
310
- continue
311
- return "\n\n".join(chunks)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
 
 
 
314
  # ---------------------------------------------------------------------------
315
  # ์ฒจ๋ถ€ ํŒŒ์ผ
316
  # ---------------------------------------------------------------------------
@@ -330,67 +427,88 @@ def download_file(task_id: str, file_name: str):
330
 
331
 
332
  def file_to_parts(path: str) -> tuple[str, list[dict]]:
333
- """(ํ”„๋กฌํ”„ํŠธ์— ๋ถ™์ผ ํ…์ŠคํŠธ, Gemini์— ์ง์ ‘ ๋„ฃ์„ inline_data ํŒŒํŠธ๋“ค)"""
334
  lower = path.lower()
335
  try:
336
  if lower.endswith((".png", ".jpg", ".jpeg", ".webp", ".mp3", ".wav", ".m4a")):
337
  if os.path.getsize(path) > 15 * 1024 * 1024:
338
- return "(์ฒจ๋ถ€ ํŒŒ์ผ์ด ๋„ˆ๋ฌด ์ปค์„œ ์ƒ๋žต๋จ)", []
339
- mime = mimetypes.guess_type(path)[0] or "application/octet-stream"
340
- if lower.endswith(".mp3"):
341
- mime = "audio/mp3"
342
  with open(path, "rb") as f:
343
  data = base64.b64encode(f.read()).decode()
344
- return "(์ฒจ๋ถ€ ํŒŒ์ผ์€ ํ•จ๊ป˜ ์ „๋‹ฌ๋จ)", [{"inline_data": {"mime_type": mime, "data": data}}]
345
  if lower.endswith((".xlsx", ".xls")):
346
  sheets = pd.read_excel(path, sheet_name=None)
347
- return "\n\n".join(f"## Sheet {n}\n{df.to_string(max_rows=300)}" for n, df in sheets.items())[:30000], []
348
  if lower.endswith(".csv"):
349
- return pd.read_csv(path).to_string(max_rows=300)[:30000], []
 
350
  with open(path, "r", encoding="utf-8", errors="ignore") as f:
351
- return f.read()[:30000], []
352
  except Exception as e: # noqa: BLE001
353
  return f"(ํŒŒ์ผ ์ฝ๊ธฐ ์‹คํŒจ: {e})", []
354
 
355
 
356
  # ---------------------------------------------------------------------------
357
- # ํ•œ ๋ฌธํ•ญ ํ’€์ด
358
  # ---------------------------------------------------------------------------
359
- def parse_final(text: str) -> str:
360
- m = re.findall(r"FINAL ANSWER:\s*(.+)", text, flags=re.IGNORECASE)
361
- ans = m[-1] if m else (text.strip().splitlines() or [""])[-1]
362
- return ans.strip().strip("`").strip().rstrip(".").strip()
363
 
364
 
365
- def make_search_query(question: str) -> str:
366
- if not LLM_QUERY:
367
- return question[:300]
368
- try:
369
- text, _ = call_llm([{"text": "Write ONE concise web search query (max 12 words) that would help answer "
370
- "this question. Reply with the query only.\n\n" + question}])
371
- return text.strip().splitlines()[0][:200]
372
- except LLMUnavailable:
373
- return question[:300]
374
 
375
 
376
  def solve(item: dict) -> tuple[str, str]:
377
- """(๋‹ต๋ณ€, ์‚ฌ์šฉํ•œ ๋ชจ๋ธ)"""
378
  question = item["question"]
379
  extra_parts: list[dict] = []
380
- extra_text = ""
381
-
382
  path = download_file(item["task_id"], item.get("file_name", ""))
383
  if path:
384
- file_text, extra_parts = file_to_parts(path)
385
- extra_text += f"ATTACHED FILE ({os.path.basename(path)}):\n{file_text}\n\n"
 
386
 
387
- ctx = web_context(make_search_query(question))
388
- if ctx:
389
- extra_text += f"WEB CONTEXT (may be incomplete or irrelevant):\n{ctx}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
- prompt = PROMPT.format(question=question, extra=extra_text[:60000])
392
- text, model = call_llm([{"text": prompt}] + extra_parts)
393
- return parse_final(text), model
 
394
 
395
 
396
  # ---------------------------------------------------------------------------
@@ -406,49 +524,64 @@ def fetch_questions() -> list[dict]:
406
 
407
 
408
  def make_df(rows: dict, order: list[str]) -> pd.DataFrame:
409
- return pd.DataFrame(
410
- [rows[t] for t in order if t in rows],
411
- columns=["Task ID", "Question", "Submitted Answer", "OK", "Note"],
412
- )
413
 
414
 
415
  def run_batch(questions: list[dict], label: str):
416
- """๋ฌธํ•ญ์„ ์ˆœ์„œ๋Œ€๋กœ ํ’€๋ฉฐ ์ง„ํ–‰ ์ƒํ™ฉ์„ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค. ๋งˆ์ง€๋ง‰์— (rows, order, aborted)๋ฅผ ๋ฐ˜ํ™˜."""
417
  order = [q["task_id"] for q in questions if q.get("task_id")]
418
  rows: dict = {}
419
  cache = load_cache()
420
  start = time.time()
421
- fails = 0
422
- aborted = False
423
- log(f"{label} ์‹œ์ž‘: {len(order)}๋ฌธํ•ญ (๋ฌธํ•ญ๋‹น LLM ํ˜ธ์ถœ {2 if LLM_QUERY else 1}ํšŒ)")
 
 
 
424
 
425
  for i, item in enumerate(questions, start=1):
426
  tid = item.get("task_id")
427
  if not tid or item.get("question") is None:
428
  continue
429
- head = f"{label}: {i - 1}/{len(order)} ์™„๋ฃŒ, {i}๋ฒˆ์งธ ๋ฌธํ•ญ ์ฒ˜๋ฆฌ ์ค‘ (๊ฒฝ๊ณผ {int(time.time() - start)}์ดˆ)"
430
- yield f"{head}\n--- ์ตœ๊ทผ ๊ธฐ๋ก ---\n{recent_log()}", make_df(rows, order)
431
 
432
  if tid in cache:
433
  answer, ok, note = cache[tid], True, "cache"
434
  log(f"์บ์‹œ ์‚ฌ์šฉ {tid[:8]}")
435
  else:
436
- try:
437
- answer, model = solve(item)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  save_cache(tid, answer)
439
- ok, note = True, model
440
- fails = 0
441
  log(f"์™„๋ฃŒ {tid[:8]} -> {answer}")
442
- except LLMUnavailable as e:
443
- answer, ok, note = "unknown", False, str(e)[:250]
444
  fails += 1
445
  log(f"LLM ์‹คํŒจ {tid[:8]}: {note}")
446
- except Exception as e: # noqa: BLE001
447
- answer, ok, note = "unknown", False, f"{type(e).__name__}: {e}"[:250]
448
  log(f"์˜ค๋ฅ˜ {tid[:8]}: {note}")
449
 
450
- rows[tid] = {"Task ID": tid, "Question": item["question"],
451
- "Submitted Answer": answer, "OK": "O" if ok else "X", "Note": note}
452
  if fails >= 2:
453
  aborted = True
454
  log("LLM์ด ์—ฐ์† 2ํšŒ ์‹คํŒจํ•˜์—ฌ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค. (ํ•œ๋„/ํฌ๋ ˆ๋”ง ๋ฌธ์ œ ๊ฐ€๋Šฅ์„ฑ)")
@@ -479,18 +612,16 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
479
  rows, order, aborted = yield from run_batch(questions, "๋ณธ ์‹คํ–‰")
480
  if aborted:
481
  yield ("LLM ํ•œ๋„/ํฌ๋ ˆ๋”ง ๋ฌธ์ œ๋กœ ์ค‘๋‹จํ–ˆ์œผ๋ฏ€๋กœ ์ œ์ถœํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.\n"
482
- "ํ‘ผ ๋‹ต์€ ์ €์žฅ๋˜์–ด ์žˆ์–ด, ํ•œ๋„๊ฐ€ ํšŒ๋ณต๋œ ๋’ค ๋‹ค์‹œ ๋ˆ„๋ฅด๋ฉด ์ด์–ด์„œ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. "
483
  "(Space๊ฐ€ ์žฌ์‹œ์ž‘๋˜๋ฉด ์ €์žฅ๋œ ๋‹ต์€ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.)\n\n" + recent_log()), make_df(rows, order)
484
  return
485
 
486
  payload = [{"task_id": t, "submitted_answer": rows[t]["Submitted Answer"]} for t in order if t in rows]
487
  failed = sum(1 for r in rows.values() if r["OK"] == "X")
488
  yield f"ํ’€์ด ์™„๋ฃŒ (์‹คํŒจ {failed}๊ฐœ). ์ œ์ถœ ์ค‘...", make_df(rows, order)
489
- submission = {
490
- "username": profile.username.strip(),
491
- "agent_code": f"https://huggingface.co/spaces/{space_id}/tree/main",
492
- "answers": payload,
493
- }
494
  try:
495
  r = requests.post(f"{SCORING_API}/submit", json=submission, timeout=120)
496
  r.raise_for_status()
@@ -516,23 +647,21 @@ def run_test_only(n_questions: int):
516
 
517
 
518
  def diagnose():
519
- lines = [f"PROVIDER={PROVIDER}, MIN_INTERVAL={MIN_INTERVAL}์ดˆ, LLM_QUERY={LLM_QUERY}"]
520
  yield "\n".join(lines + ["", "๋ชจ๋ธ ํ™•์ธ ์ค‘..."])
521
  if PROVIDER == "hf":
522
- lines.append(f"HF_TOKEN: {'์žˆ์Œ' if hf_token() else '์—†์Œ'}, ์‚ฌ์šฉํ•  ๋ชจ๋ธ: {HF_MODELS}")
523
  try:
524
  r = requests.get(f"{HF_ROUTER}/models", headers={"Authorization": f"Bearer {hf_token()}"}, timeout=30)
525
  r.raise_for_status()
526
  ids = [m.get("id") for m in r.json().get("data", [])]
527
- check = ", ".join(f"{m}={'๋ชฉ๋ก์— ์žˆ์Œ' if m in ids else '๋ชฉ๋ก์— ์—†์Œ'}" for m in HF_MODELS)
528
- lines.append(f"1) ๋ผ์šฐํ„ฐ ์ œ๊ณต ๋ชจ๋ธ {len(ids)}๊ฐœ / {check}")
529
  except Exception as e: # noqa: BLE001
530
  lines.append(f"1) ๋ชจ๋ธ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ(์ง„ํ–‰์€ ๊ณ„์†): {type(e).__name__}: {str(e)[:150]}")
531
  else:
532
  lines.append(f"GEMINI_API_KEY: {'์žˆ์Œ' if api_key() else '์—†์Œ'}")
533
  try:
534
- names = [n for n in list_models() if n.startswith("gemini")][:25]
535
- lines.append("1) ์ด ํ‚ค๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋ธ(์ผ๋ถ€): " + ", ".join(names))
536
  lines.append(f" ์‚ฌ์šฉํ•  ํ›„๋ณด ์ˆœ์„œ: {candidate_models()}")
537
  except Exception as e: # noqa: BLE001
538
  lines.append(f"1) ๋ชจ๋ธ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ: {type(e).__name__}: {str(e)[:200]}")
@@ -544,12 +673,13 @@ def diagnose():
544
  lines.append(f"2) LLM ํ˜ธ์ถœ: ์ •์ƒ (๋ชจ๋ธ {model}, ์‘๋‹ต '{text.strip()[:30]}')")
545
  except LLMUnavailable as e:
546
  lines.append(f"2) LLM ํ˜ธ์ถœ: ์‹คํŒจ - {str(e)[:300]}")
547
- yield "\n".join(lines + ["", "์›น ๊ฒ€์ƒ‰ ํ…Œ์ŠคํŠธ ์ค‘..."])
548
- try:
549
- ctx = web_context("Hugging Face")
550
- lines.append(f"3) ์›น ๊ฒ€์ƒ‰: {'์ •์ƒ' if ctx else '๊ฒฐ๊ณผ ์—†์Œ'} ({len(ctx)}์ž)")
551
- except Exception as e: # noqa: BLE001
552
- lines.append(f"3) ์›น ๊ฒ€์ƒ‰: ์‹คํŒจ - {e}")
 
553
  yield "\n".join(lines)
554
 
555
 
@@ -567,12 +697,12 @@ def clear_cache():
567
  # UI
568
  # ---------------------------------------------------------------------------
569
  with gr.Blocks() as demo:
570
- gr.Markdown("# Basic Agent Evaluation Runner (Gemini / Hugging Face ์„ ํƒํ˜• ๊ฒฝ๋Ÿ‰ ๋ฒ„์ „)")
571
  gr.Markdown(
572
  """**์‚ฌ์šฉ ์ˆœ์„œ**
573
- 1. '์—ฐ๊ฒฐ ์ง„๋‹จ'์œผ๋กœ ๋ชจ๋ธ๊ณผ ํ˜ธ์ถœ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
574
  2. ๋กœ๊ทธ์ธ ํ›„ 'ํ…Œ์ŠคํŠธ ์‹คํ–‰'(1~2๋ฌธํ•ญ)์œผ๋กœ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. (์ œ์ถœ ์•ˆ ํ•จ)
575
- 3. ๋ฌธ์ œ์—†์œผ๋ฉด 'Run Evaluation & Submit All Answers'๋ฅผ ๋ˆ„๋ฆ…๋‹ˆ๋‹ค."""
576
  )
577
  gr.LoginButton()
578
  with gr.Row():
@@ -581,7 +711,7 @@ with gr.Blocks() as demo:
581
  test_button = gr.Button("ํ…Œ์ŠคํŠธ ์‹คํ–‰ (์ œ์ถœ ์•ˆ ํ•จ)")
582
  cache_button = gr.Button("์บ์‹œ ์ดˆ๊ธฐํ™”")
583
  run_button = gr.Button("Run Evaluation & Submit All Answers")
584
- status_output = gr.Textbox(label="Run Status / Submission Result", lines=14, interactive=False)
585
  results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
586
 
587
  diag_button.click(fn=diagnose, outputs=[status_output])
 
1
+ """HF Agents Course ์ตœ์ข… ๊ณผ์ œ - ๋‹ค๋‹จ๊ณ„ ์—์ด์ „ํŠธ ๋ฒ„์ „ (Gemini ๋˜๋Š” Hugging Face)
2
 
3
+ LLM์ด ํ•œ ๋‹จ๊ณ„์— ํ•˜๋‚˜์˜ ๋„๊ตฌ๋ฅผ ๊ณจ๋ผ ์“ฐ๊ณ (๊ฒ€์ƒ‰/์œ„ํ‚ค/์›นํŽ˜์ด์ง€/ํŒŒ์ด์ฌ/์œ ํŠœ๋ธŒ ์ž๋ง‰), ๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ณ  ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ์ •ํ•ฉ๋‹ˆ๋‹ค.
4
+ ์ตœ๋Œ€ MAX_STEPS ๋‹จ๊ณ„ ์•ˆ์— ์ตœ์ข… ๋‹ต์„ ๋‚ด๋„๋ก ์ œํ•œํ•ด ๋น„์šฉ์„ ํ†ต์ œํ•ฉ๋‹ˆ๋‹ค.
5
  """
6
  import base64
7
  import json
8
  import mimetypes
9
  import os
10
  import re
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+ import threading
15
  import time
16
  from collections import deque
17
 
 
42
  # ---------------------------------------------------------------------------
43
  SCORING_API = "https://agents-course-unit4-scoring.hf.space"
44
  GEMINI_API = "https://generativelanguage.googleapis.com/v1beta"
45
+ HF_ROUTER = "https://router.huggingface.co/v1"
46
  FILES_DIR = "/tmp/task_files"
47
+ CACHE_PATH = "/tmp/answers_cache_v4.json"
48
  os.makedirs(FILES_DIR, exist_ok=True)
49
 
50
  PROVIDER = (os.getenv("PROVIDER", "") or ("gemini" if os.getenv("GEMINI_API_KEY") else "hf")).lower() # gemini | hf
51
+ MIN_INTERVAL = float(os.getenv("MIN_INTERVAL", "7" if PROVIDER == "gemini" else "1")) # LLM ํ˜ธ์ถœ ๊ฐ„ ์ตœ์†Œ ๊ฐ„๊ฒฉ(์ดˆ)
52
+ MAX_RETRY = int(os.getenv("MAX_RETRY", "3")) # 429 ์žฌ์‹œ๋„ ํšŸ์ˆ˜
53
+ MAX_STEPS = int(os.getenv("MAX_STEPS", "6")) # ๋ฌธํ•ญ๋‹น ์ตœ๋Œ€ ๋‹จ๊ณ„(=์ตœ๋Œ€ LLM ํ˜ธ์ถœ ์ˆ˜)
54
+ QUESTION_TIMEOUT = int(os.getenv("QUESTION_TIMEOUT", "240")) # ๋ฌธํ•ญ๋‹น ๋ชฉํ‘œ ์ œํ•œ ์‹œ๊ฐ„(์ดˆ). ๋„˜์œผ๋ฉด ๋ฐ”๋กœ ๋‹ต์„ ๋‚ด๊ฒŒ ํ•จ
55
+ OBS_CHARS = int(os.getenv("OBS_CHARS", "6000")) # ๋„๊ตฌ ๊ฒฐ๊ณผ 1ํšŒ ์ตœ๋Œ€ ๊ธ€์ž ์ˆ˜
56
+ PY_TIMEOUT = int(os.getenv("PY_TIMEOUT", "25")) # ํŒŒ์ด์ฌ ์‹คํ–‰ ์ œํ•œ ์‹œ๊ฐ„(์ดˆ)
57
+ GEMINI_MODELS = os.getenv("GEMINI_MODELS", "") # ์„ ํƒ: ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ํ•œ Gemini ๋ชจ๋ธ ๋ชฉ๋ก
58
  HF_MODELS = [m.strip() for m in (os.getenv("HF_MODELS") or os.getenv("HF_MODEL") or "Qwen/Qwen2.5-72B-Instruct").split(",") if m.strip()]
59
+ UA = {"User-Agent": "Mozilla/5.0 (agents-course-runner)"}
60
+
61
+ AGENT_PROMPT = """You are a careful research agent solving a GAIA-style benchmark question. The final answer is graded by EXACT MATCH.
62
+
63
+ You work in steps. In each turn reply with EXACTLY ONE action, in this format and nothing else:
64
+
65
+ Thought: <short reasoning>
66
+ Action: <search|wiki|fetch|python|youtube|final>
67
+ Input: <input for the action>
68
 
69
+ Actions:
70
+ - search: Input = a web search query. Returns titles, URLs and snippets.
71
+ - wiki: Input = "Page Title" or "Page Title | keyword". Returns the English Wikipedia article text including tables; with a keyword you also get the passages around it.
72
+ - fetch: Input = a URL, or "URL | keyword". Returns the page text.
73
+ - python: Input = Python code (may span several lines). Use print() to output results. pandas, openpyxl, re, json and math are available. Use it for ANY arithmetic, counting, sorting, string manipulation, and for reading attached files.
74
+ - youtube: Input = a YouTube URL. Returns the transcript if available.
75
+ - final: Input = ONLY the final answer value.
76
 
77
+ Rules for the final answer:
78
+ - Output only the answer value: no explanation, no trailing period, no units unless asked.
 
79
  - Numbers: digits only, no thousands separators, no currency or percent symbols unless asked.
80
  - Lists: comma-separated, in the order the question asks.
81
+ - Strings: no leading articles (a, an, the) and no abbreviations unless asked.
82
+ - Follow any formatting instruction inside the question exactly.
 
83
 
84
+ Be efficient: you have at most {max_steps} steps. Prefer verifying facts with tools over guessing. If a tool result is not helpful, change your approach instead of repeating it. If you cannot find the answer, give your best reasoned answer rather than leaving it empty."""
 
 
 
85
 
86
  # ---------------------------------------------------------------------------
87
+ # ๋กœ๊ทธ / ์บ์‹œ
88
  # ---------------------------------------------------------------------------
89
+ _events = deque(maxlen=14)
90
 
91
 
92
  def log(msg: str):
 
99
  return "\n".join(_events)
100
 
101
 
 
 
 
102
  def load_cache() -> dict:
103
  try:
104
  with open(CACHE_PATH, "r", encoding="utf-8") as f:
 
115
 
116
 
117
  # ---------------------------------------------------------------------------
118
+ # LLM ํ˜ธ์ถœ (Gemini REST / Hugging Face ๋ผ์šฐํ„ฐ)
119
  # ---------------------------------------------------------------------------
120
  class LLMUnavailable(Exception):
121
  pass
122
 
123
 
124
+ _last_call = 0.0
125
+
126
+
127
+ def throttle():
128
+ global _last_call
129
+ wait = MIN_INTERVAL - (time.time() - _last_call)
130
+ if wait > 0:
131
+ time.sleep(wait)
132
+ _last_call = time.time()
133
+
134
+
135
  def api_key() -> str:
136
  return os.getenv("GEMINI_API_KEY", "")
137
 
138
 
139
+ def hf_token() -> str:
140
+ return os.getenv("HF_TOKEN", "")
141
+
142
+
143
  def list_models() -> list[str]:
144
+ r = requests.get(f"{GEMINI_API}/models", headers={"x-goog-api-key": api_key()}, params={"pageSize": 200}, timeout=30)
 
 
 
 
 
145
  r.raise_for_status()
146
+ return [m["name"].replace("models/", "") for m in r.json().get("models", [])
147
+ if "generateContent" in m.get("supportedGenerationMethods", [])]
 
 
 
148
 
149
 
150
  _BAD = ("image", "tts", "audio", "live", "embed", "vision", "robotics", "computer", "imagen", "veo")
 
152
 
153
 
154
  def candidate_models() -> list[str]:
 
155
  global _candidates
156
  if _candidates:
157
  return _candidates
 
162
  lite = [n for n in names if "flash-lite" in n]
163
  flash = [n for n in names if "flash" in n and "flash-lite" not in n]
164
  _candidates = (lite[:2] + flash[:2]) or names[:3]
165
+ log(f"์ž๋™ ์„ ํƒ๋œ Gemini ๋ชจ๋ธ ํ›„๋ณด: {_candidates}")
166
  return _candidates
167
 
168
 
 
 
 
 
 
 
 
 
 
 
 
169
  def call_gemini(parts: list[dict]) -> tuple[str, str]:
 
170
  if not api_key():
171
  raise LLMUnavailable("GEMINI_API_KEY๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
172
  last_err = ""
 
177
  r = requests.post(
178
  f"{GEMINI_API}/models/{model}:generateContent",
179
  headers={"x-goog-api-key": api_key(), "Content-Type": "application/json"},
180
+ json={"contents": [{"role": "user", "parts": parts}],
181
+ "generationConfig": {"temperature": 0, "maxOutputTokens": 2048}},
 
 
182
  timeout=120,
183
  )
184
  except requests.RequestException as e:
185
  last_err = f"{model}: ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ {e}"
186
  time.sleep(5)
187
  continue
 
188
  if r.status_code == 200:
189
  cand = (r.json().get("candidates") or [{}])[0]
190
+ text = "".join(p.get("text", "") for p in cand.get("content", {}).get("parts", []) if not p.get("thought"))
 
 
 
 
191
  if text.strip():
192
  return text, model
193
  last_err = f"{model}: ๋นˆ ์‘๋‹ต({cand.get('finishReason')})"
194
  break
 
195
  body = r.text
196
  last_err = f"{model}: HTTP {r.status_code} {body[:180].replace(chr(10), ' ')}"
197
  if r.status_code == 429:
 
198
  if "PerDay" in body or "limit: 0" in body:
199
  log(f"{model}: ์ผ์ผ ํ•œ๋„ ์†Œ์ง„ ๋˜๋Š” ํ•œ๋„ 0 -> ๋‹ค์Œ ๋ชจ๋ธ")
200
  break
 
208
  if r.status_code >= 500:
209
  time.sleep(5)
210
  continue
211
+ break
212
  raise LLMUnavailable(last_err or "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋ธ์ด ์—†์Šต๋‹ˆ๋‹ค.")
213
 
214
 
 
 
 
 
 
 
 
215
  def call_hf(parts: list[dict]) -> tuple[str, str]:
 
216
  if not hf_token():
217
  raise LLMUnavailable("HF_TOKEN์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
218
  text = "\n".join(p["text"] for p in parts if "text" in p)
219
  if any("inline_data" in p for p in parts):
220
+ text = "NOTE: An image/audio attachment exists but you cannot view it; answer from the other information.\n\n" + text
 
221
  last_err = ""
222
  for model in HF_MODELS:
223
  for attempt in range(MAX_RETRY + 1):
 
227
  f"{HF_ROUTER}/chat/completions",
228
  headers={"Authorization": f"Bearer {hf_token()}", "Content-Type": "application/json"},
229
  json={"model": model, "messages": [{"role": "user", "content": text}],
230
+ "max_tokens": 1500, "temperature": 0},
231
  timeout=120,
232
  )
233
  except requests.RequestException as e:
 
244
  body = r.text
245
  last_err = f"{model}: HTTP {r.status_code} {body[:180].replace(chr(10), ' ')}"
246
  if r.status_code == 402:
247
+ raise LLMUnavailable("HF ํฌ๋ ˆ๋”ง ์†Œ์ง„(402). " + body[:150])
248
  if r.status_code in (401, 403):
249
+ raise LLMUnavailable(f"HF_TOKEN ์ธ์ฆ/๊ถŒํ•œ ์˜ค๋ฅ˜({r.status_code}). Inference Providers ๊ถŒํ•œ์„ ํ™•์ธํ•˜์„ธ์š”.")
250
  if r.status_code == 429:
251
  if attempt == MAX_RETRY:
252
  break
 
257
  if r.status_code >= 500:
258
  time.sleep(5)
259
  continue
260
+ break
261
  raise LLMUnavailable(last_err or "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋ธ์ด ์—†์Šต๋‹ˆ๋‹ค.")
262
 
263
 
 
266
 
267
 
268
  # ---------------------------------------------------------------------------
269
+ # ๋„๊ตฌ ๊ตฌํ˜„
270
  # ---------------------------------------------------------------------------
271
  def html_to_text(html: str) -> str:
272
  html = re.sub(r"(?is)<(script|style|noscript|svg|header|footer|nav)[^>]*>.*?</\1>", " ", html)
273
+ html = re.sub(r"(?is)<sup[^>]*class=\"[^\"]*reference[^\"]*\"[^>]*>.*?</sup>", "", html)
274
+ html = re.sub(r"(?i)</t[dh]>", " | ", html)
275
+ html = re.sub(r"(?i)</tr>|<br\s*/?>|</p>|</li>|</h[1-6]>|</caption>", "\n", html)
276
  text = re.sub(r"(?s)<[^>]+>", " ", html)
277
+ text = (text.replace("&nbsp;", " ").replace("&#160;", " ").replace("&amp;", "&")
278
+ .replace("&lt;", "<").replace("&gt;", ">").replace("&quot;", '"').replace("&#39;", "'"))
279
+ text = re.sub(r"[ \t\u00a0]+", " ", text)
280
+ text = re.sub(r"(\s*\|\s*)+\n", "\n", text)
281
+ text = re.sub(r"\n\s*\n+", "\n", text)
282
+ return text.strip()
283
+
284
+
285
+ def windows(text: str, keyword: str, head: int = 1500, win: int = 1600, max_hits: int = 3) -> str:
286
+ """ํ‚ค์›Œ๋“œ ์ฃผ๋ณ€ ๋ฌธ๋‹จ๋งŒ ์ž˜๋ผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ํ‚ค์›Œ๋“œ๊ฐ€ ์—†์œผ๋ฉด ์•ž๋ถ€๋ถ„์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค."""
287
+ if not keyword:
288
+ return text[:OBS_CHARS]
289
+ low, key = text.lower(), keyword.lower()
290
+ spans, pos = [], 0
291
+ while len(spans) < max_hits:
292
+ i = low.find(key, pos)
293
+ if i < 0:
294
+ break
295
+ spans.append((max(0, i - win // 4), min(len(text), i + win)))
296
+ pos = i + win
297
+ if not spans:
298
+ return f"(ํ‚ค์›Œ๋“œ '{keyword}'๋ฅผ ์ฐพ์ง€ ๋ชปํ•จ. ๋ฌธ์„œ ์•ž๋ถ€๋ถ„:)\n" + text[:OBS_CHARS]
299
+ out = [text[:head]] + [f"...\n{text[a:b]}" for a, b in spans]
300
+ return "\n".join(out)[: OBS_CHARS * 2]
301
 
302
 
303
+ def tool_search(query: str) -> str:
304
  if DDGS is None:
305
+ return "๊ฒ€์ƒ‰ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
306
  try:
307
  results = list(DDGS().text(query, max_results=6))
308
  except Exception as e: # noqa: BLE001
309
+ return f"๊ฒ€์ƒ‰ ์‹คํŒจ: {type(e).__name__}: {str(e)[:150]}"
310
+ if not results:
311
+ return "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ"
312
+ return "\n\n".join(f"[{i}] {r.get('title', '')}\n{r.get('href', '')}\n{r.get('body', '')}"
313
+ for i, r in enumerate(results, 1))[:OBS_CHARS]
314
+
315
+
316
+ def _wiki_parse(title: str):
317
+ r = requests.get("https://en.wikipedia.org/w/api.php", headers=UA, timeout=25,
318
+ params={"action": "parse", "page": title, "prop": "text", "format": "json",
319
+ "redirects": 1, "formatversion": 2})
320
+ j = r.json()
321
+ if "parse" in j:
322
+ return j["parse"]["text"], j["parse"]["title"]
323
+ return None
324
+
325
+
326
+ def tool_wiki(inp: str) -> str:
327
+ title, _, kw = inp.partition("|")
328
+ title, kw = title.strip(), kw.strip()
329
+ try:
330
+ got = _wiki_parse(title)
331
+ if not got:
332
+ r = requests.get("https://en.wikipedia.org/w/api.php", headers=UA, timeout=25,
333
+ params={"action": "query", "list": "search", "srsearch": title,
334
+ "format": "json", "srlimit": 1})
335
+ hits = r.json().get("query", {}).get("search", [])
336
+ got = _wiki_parse(hits[0]["title"]) if hits else None
337
+ if not got:
338
+ return f"Wikipedia ๋ฌธ์„œ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค: {title}"
339
+ html, real = got
340
+ return f"[Wikipedia: {real}]\n" + windows(html_to_text(html), kw)
341
+ except Exception as e: # noqa: BLE001
342
+ return f"Wikipedia ์กฐํšŒ ์‹คํŒจ: {type(e).__name__}: {str(e)[:150]}"
343
+
344
+
345
+ def tool_fetch(inp: str) -> str:
346
+ url, _, kw = inp.partition("|")
347
+ url, kw = url.strip(), kw.strip()
348
+ if not url.startswith("http"):
349
+ return "URL์€ http๋กœ ์‹œ์ž‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."
350
+ try:
351
+ r = requests.get(url, headers=UA, timeout=25)
352
+ ctype = r.headers.get("content-type", "")
353
+ if "pdf" in ctype or url.lower().endswith(".pdf"):
354
+ return "PDF๋Š” ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."
355
+ if "html" not in ctype and "text" not in ctype and "json" not in ctype:
356
+ return f"์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ˜•์‹์ž…๋‹ˆ๋‹ค: {ctype}"
357
+ text = html_to_text(r.text) if "html" in ctype else r.text
358
+ return windows(text, kw)
359
+ except Exception as e: # noqa: BLE001
360
+ return f"ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค: {type(e).__name__}: {str(e)[:150]}"
361
+
362
+
363
+ def tool_python(code: str) -> str:
364
+ code = re.sub(r"^```(?:python)?\s*\n?", "", code.strip())
365
+ code = re.sub(r"\n?```\s*$", "", code)
366
+ with tempfile.NamedTemporaryFile("w", suffix=".py", dir="/tmp", delete=False, encoding="utf-8") as f:
367
+ f.write(code)
368
+ path = f.name
369
+ # ํ† ํฐ ๋“ฑ ๋น„๋ฐ€ ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์ฝ”๋“œ์— ๋…ธ์ถœ๋˜์ง€ ์•Š๋„๋ก ์ตœ์†Œ ํ™˜๊ฒฝ๋งŒ ์ „๋‹ฌ
370
+ env = {"PATH": os.environ.get("PATH", ""), "HOME": "/tmp", "PYTHONIOENCODING": "utf-8"}
371
+ try:
372
+ r = subprocess.run([sys.executable, path], capture_output=True, text=True,
373
+ timeout=PY_TIMEOUT, cwd=FILES_DIR, env=env)
374
+ out = r.stdout or ""
375
+ if r.stderr:
376
+ out += "\n[stderr]\n" + r.stderr[-1500:]
377
+ except subprocess.TimeoutExpired:
378
+ out = f"(์‹คํ–‰ ์‹œ๊ฐ„ ์ดˆ๊ณผ {PY_TIMEOUT}์ดˆ)"
379
+ except Exception as e: # noqa: BLE001
380
+ out = f"์‹คํ–‰ ์˜ค๋ฅ˜: {e}"
381
+ finally:
382
  try:
383
+ os.remove(path)
384
+ except OSError:
385
+ pass
386
+ return out[:OBS_CHARS] if out.strip() else "(์ถœ๋ ฅ ์—†์Œ. ๊ฒฐ๊ณผ๋Š” print()๋กœ ์ถœ๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.)"
387
+
388
+
389
+ def tool_youtube(url: str) -> str:
390
+ m = re.search(r"(?:v=|youtu\.be/)([\w-]{11})", url)
391
+ if not m:
392
+ return "์œ ํšจํ•œ YouTube URL์ด ์•„๋‹™๋‹ˆ๋‹ค."
393
+ try:
394
+ from youtube_transcript_api import YouTubeTranscriptApi
395
+ except ImportError:
396
+ return "youtube-transcript-api๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
397
+ vid = m.group(1)
398
+ try:
399
+ try:
400
+ items = YouTubeTranscriptApi.get_transcript(vid)
401
+ text = " ".join(i.get("text", "") for i in items)
402
+ except AttributeError:
403
+ text = " ".join(getattr(s, "text", "") for s in YouTubeTranscriptApi().fetch(vid))
404
+ return text[: OBS_CHARS * 2] or "์ž๋ง‰์ด ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค."
405
+ except Exception as e: # noqa: BLE001
406
+ return f"์ž๋ง‰์„ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค(์˜์ƒ ๋‚ด์šฉ์€ ๋ณผ ์ˆ˜ ์—†์Œ): {type(e).__name__}: {str(e)[:150]}"
407
 
408
 
409
+ TOOLS = {"search": tool_search, "wiki": tool_wiki, "fetch": tool_fetch, "python": tool_python, "youtube": tool_youtube}
410
+
411
  # ---------------------------------------------------------------------------
412
  # ์ฒจ๋ถ€ ํŒŒ์ผ
413
  # ---------------------------------------------------------------------------
 
427
 
428
 
429
  def file_to_parts(path: str) -> tuple[str, list[dict]]:
430
+ """(๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ…์ŠคํŠธ, Gemini์— ์ง์ ‘ ๋„ฃ์„ inline_data ํŒŒํŠธ๋“ค)"""
431
  lower = path.lower()
432
  try:
433
  if lower.endswith((".png", ".jpg", ".jpeg", ".webp", ".mp3", ".wav", ".m4a")):
434
  if os.path.getsize(path) > 15 * 1024 * 1024:
435
+ return "(ํŒŒ์ผ์ด ๋„ˆ๋ฌด ์ปค์„œ ์ƒ๋žต๋จ)", []
436
+ mime = "audio/mp3" if lower.endswith(".mp3") else (mimetypes.guess_type(path)[0] or "application/octet-stream")
 
 
437
  with open(path, "rb") as f:
438
  data = base64.b64encode(f.read()).decode()
439
+ return "(์ด๋ฏธ์ง€/์˜ค๋””์˜ค ํŒŒ์ผ. Gemini์ผ ๋•Œ๋งŒ ๋‚ด์šฉ์„ ๋ณผ ์ˆ˜ ์žˆ์Œ)", [{"inline_data": {"mime_type": mime, "data": data}}]
440
  if lower.endswith((".xlsx", ".xls")):
441
  sheets = pd.read_excel(path, sheet_name=None)
442
+ return "\n\n".join(f"## Sheet {n} (shape {df.shape})\n{df.head(25).to_string()}" for n, df in sheets.items()), []
443
  if lower.endswith(".csv"):
444
+ df = pd.read_csv(path)
445
+ return f"shape {df.shape}\n{df.head(25).to_string()}", []
446
  with open(path, "r", encoding="utf-8", errors="ignore") as f:
447
+ return f.read()[:12000], []
448
  except Exception as e: # noqa: BLE001
449
  return f"(ํŒŒ์ผ ์ฝ๊ธฐ ์‹คํŒจ: {e})", []
450
 
451
 
452
  # ---------------------------------------------------------------------------
453
+ # ์—์ด์ „ํŠธ ๋ฃจํ”„
454
  # ---------------------------------------------------------------------------
455
+ def clean_answer(ans: str) -> str:
456
+ ans = (ans.strip().splitlines() or [""])[0]
457
+ return ans.strip().strip("`").strip().strip('"').strip().rstrip(".").strip()
 
458
 
459
 
460
+ def parse_action(text: str):
461
+ m = re.search(r"Action:\s*([A-Za-z_]+)\s*\n?\s*Input:\s*(.*)", text, flags=re.S | re.I)
462
+ if m:
463
+ inp = re.split(r"\n\s*Observation:", m.group(2))[0].strip()
464
+ return m.group(1).lower(), inp
465
+ m = re.findall(r"FINAL ANSWER:\s*(.+)", text, flags=re.I)
466
+ if m:
467
+ return "final", m[-1].strip()
468
+ return None, ""
469
 
470
 
471
  def solve(item: dict) -> tuple[str, str]:
472
+ """(๋‹ต๋ณ€, ๋น„๊ณ )๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
473
  question = item["question"]
474
  extra_parts: list[dict] = []
475
+ file_note = ""
 
476
  path = download_file(item["task_id"], item.get("file_name", ""))
477
  if path:
478
+ preview, extra_parts = file_to_parts(path)
479
+ file_note = (f"ATTACHED FILE path: {path}\nPreview:\n{preview[:6000]}\n"
480
+ "(The file can be read by the python action at that exact path.)\n\n")
481
 
482
+ head = AGENT_PROMPT.replace("{max_steps}", str(MAX_STEPS)) + "\n\nQUESTION:\n" + question + "\n\n" + file_note
483
+ history: list[str] = []
484
+ start = time.time()
485
+ model_used, last_text = "", ""
486
+
487
+ for step in range(1, MAX_STEPS + 1):
488
+ last = step == MAX_STEPS or (time.time() - start) > QUESTION_TIMEOUT
489
+ prompt = head + "\n".join(history)
490
+ prompt += ("\n\nThis is your LAST step: reply now with Action: final." if last
491
+ else "\n\nReply with your next step (one action).")
492
+ text, model_used = call_llm([{"text": prompt}] + extra_parts)
493
+ last_text = text
494
+ action, inp = parse_action(text)
495
+ log(f" [{question[:28]}โ€ฆ] ๋‹จ๊ณ„ {step}: {action or 'ํ˜•์‹์˜ค๋ฅ˜'} {inp[:50].replace(chr(10), ' ')}")
496
+
497
+ if action == "final":
498
+ return clean_answer(inp), f"{model_used} / {step}๋‹จ๊ณ„"
499
+ if last:
500
+ break
501
+ if action not in TOOLS:
502
+ history.append(f"[Step {step}]\n{text[:800]}\nObservation: Invalid format or action. Reply exactly with "
503
+ "Thought/Action/Input using one of: search, wiki, fetch, python, youtube, final.")
504
+ continue
505
+ obs = TOOLS[action](inp)
506
+ history.append(f"[Step {step}]\nThought/Action: {action}\nInput: {inp[:1500]}\nObservation: {obs[:OBS_CHARS]}")
507
 
508
+ # ๋งˆ์ง€๋ง‰ ์‘๋‹ต์—์„œ ๋‹ต์„ ์ตœ๋Œ€ํ•œ ๊ฑด์ ธ๋ƒ„
509
+ m = re.findall(r"FINAL ANSWER:\s*(.+)", last_text, flags=re.I)
510
+ fallback = clean_answer(m[-1]) if m else clean_answer(last_text.splitlines()[-1] if last_text.strip() else "unknown")
511
+ return fallback or "unknown", f"{model_used} / ๋‹จ๊ณ„์ดˆ๊ณผ"
512
 
513
 
514
  # ---------------------------------------------------------------------------
 
524
 
525
 
526
  def make_df(rows: dict, order: list[str]) -> pd.DataFrame:
527
+ return pd.DataFrame([rows[t] for t in order if t in rows],
528
+ columns=["Task ID", "Question", "Submitted Answer", "OK", "Note"])
 
 
529
 
530
 
531
  def run_batch(questions: list[dict], label: str):
532
+ """๋ฌธํ•ญ์„ ์ˆœ์„œ๋Œ€๋กœ ํ’€๋ฉฐ ์ง„ํ–‰ ์ƒํ™ฉ์„ ๊ณ„์† ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค. ๋งˆ์ง€๋ง‰์— (rows, order, aborted)๋ฅผ ๋ฐ˜ํ™˜."""
533
  order = [q["task_id"] for q in questions if q.get("task_id")]
534
  rows: dict = {}
535
  cache = load_cache()
536
  start = time.time()
537
+ fails, aborted = 0, False
538
+ log(f"{label} ์‹œ์ž‘: {len(order)}๋ฌธํ•ญ, ๋ชจ๋ธ ์ œ๊ณต์ž={PROVIDER}, ๋ฌธํ•ญ๋‹น ์ตœ๋Œ€ {MAX_STEPS}๋‹จ๊ณ„")
539
+
540
+ def status(i: int) -> str:
541
+ return (f"{label}: {len(rows)}/{len(order)} ์™„๋ฃŒ, {i}๋ฒˆ์งธ ๋ฌธํ•ญ ์ฒ˜๋ฆฌ ์ค‘ (๊ฒฝ๊ณผ {int(time.time() - start)}์ดˆ)\n"
542
+ f"--- ์ตœ๊ทผ ๊ธฐ๋ก ---\n{recent_log()}")
543
 
544
  for i, item in enumerate(questions, start=1):
545
  tid = item.get("task_id")
546
  if not tid or item.get("question") is None:
547
  continue
548
+ yield status(i), make_df(rows, order)
 
549
 
550
  if tid in cache:
551
  answer, ok, note = cache[tid], True, "cache"
552
  log(f"์บ์‹œ ์‚ฌ์šฉ {tid[:8]}")
553
  else:
554
+ box: dict = {}
555
+
556
+ def work(it=item):
557
+ try:
558
+ box["r"] = solve(it)
559
+ except LLMUnavailable as e:
560
+ box["llm"] = str(e)
561
+ except Exception as e: # noqa: BLE001
562
+ box["err"] = f"{type(e).__name__}: {e}"
563
+
564
+ th = threading.Thread(target=work, daemon=True)
565
+ th.start()
566
+ while th.is_alive():
567
+ th.join(2)
568
+ yield status(i), make_df(rows, order)
569
+
570
+ if "r" in box:
571
+ answer, note = box["r"]
572
+ ok, fails = True, 0
573
  save_cache(tid, answer)
 
 
574
  log(f"์™„๋ฃŒ {tid[:8]} -> {answer}")
575
+ elif "llm" in box:
576
+ answer, ok, note = "unknown", False, box["llm"][:250]
577
  fails += 1
578
  log(f"LLM ์‹คํŒจ {tid[:8]}: {note}")
579
+ else:
580
+ answer, ok, note = "unknown", False, box.get("err", "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜")[:250]
581
  log(f"์˜ค๋ฅ˜ {tid[:8]}: {note}")
582
 
583
+ rows[tid] = {"Task ID": tid, "Question": item["question"], "Submitted Answer": answer,
584
+ "OK": "O" if ok else "X", "Note": note}
585
  if fails >= 2:
586
  aborted = True
587
  log("LLM์ด ์—ฐ์† 2ํšŒ ์‹คํŒจํ•˜์—ฌ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค. (ํ•œ๋„/ํฌ๋ ˆ๋”ง ๋ฌธ์ œ ๊ฐ€๋Šฅ์„ฑ)")
 
612
  rows, order, aborted = yield from run_batch(questions, "๋ณธ ์‹คํ–‰")
613
  if aborted:
614
  yield ("LLM ํ•œ๋„/ํฌ๋ ˆ๋”ง ๋ฌธ์ œ๋กœ ์ค‘๋‹จํ–ˆ์œผ๋ฏ€๋กœ ์ œ์ถœํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.\n"
615
+ "ํ‘ผ ๋‹ต์€ ์ €์žฅ๋˜์–ด ์žˆ์–ด ํ•ด๊ฒฐ ํ›„ ๋‹ค์‹œ ๋ˆ„๋ฅด๋ฉด ์ด์–ด์„œ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. "
616
  "(Space๊ฐ€ ์žฌ์‹œ์ž‘๋˜๋ฉด ์ €์žฅ๋œ ๋‹ต์€ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.)\n\n" + recent_log()), make_df(rows, order)
617
  return
618
 
619
  payload = [{"task_id": t, "submitted_answer": rows[t]["Submitted Answer"]} for t in order if t in rows]
620
  failed = sum(1 for r in rows.values() if r["OK"] == "X")
621
  yield f"ํ’€์ด ์™„๋ฃŒ (์‹คํŒจ {failed}๊ฐœ). ์ œ์ถœ ์ค‘...", make_df(rows, order)
622
+ submission = {"username": profile.username.strip(),
623
+ "agent_code": f"https://huggingface.co/spaces/{space_id}/tree/main",
624
+ "answers": payload}
 
 
625
  try:
626
  r = requests.post(f"{SCORING_API}/submit", json=submission, timeout=120)
627
  r.raise_for_status()
 
647
 
648
 
649
  def diagnose():
650
+ lines = [f"PROVIDER={PROVIDER}, MAX_STEPS={MAX_STEPS}, MIN_INTERVAL={MIN_INTERVAL}์ดˆ, QUESTION_TIMEOUT={QUESTION_TIMEOUT}์ดˆ"]
651
  yield "\n".join(lines + ["", "๋ชจ๋ธ ํ™•์ธ ์ค‘..."])
652
  if PROVIDER == "hf":
653
+ lines.append(f"HF_TOKEN: {'์žˆ์Œ' if hf_token() else '์—†์Œ'}, ๋ชจ๋ธ: {HF_MODELS}")
654
  try:
655
  r = requests.get(f"{HF_ROUTER}/models", headers={"Authorization": f"Bearer {hf_token()}"}, timeout=30)
656
  r.raise_for_status()
657
  ids = [m.get("id") for m in r.json().get("data", [])]
658
+ lines.append("1) ๋ชจ๋ธ ๋ชฉ๋ก: " + ", ".join(f"{m}={'์žˆ์Œ' if m in ids else '๋ชฉ๋ก์— ์—†์Œ'}" for m in HF_MODELS))
 
659
  except Exception as e: # noqa: BLE001
660
  lines.append(f"1) ๋ชจ๋ธ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ(์ง„ํ–‰์€ ๊ณ„์†): {type(e).__name__}: {str(e)[:150]}")
661
  else:
662
  lines.append(f"GEMINI_API_KEY: {'์žˆ์Œ' if api_key() else '์—†์Œ'}")
663
  try:
664
+ lines.append("1) ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Gemini ๋ชจ๋ธ(์ผ๋ถ€): " + ", ".join([n for n in list_models() if n.startswith('gemini')][:25]))
 
665
  lines.append(f" ์‚ฌ์šฉํ•  ํ›„๋ณด ์ˆœ์„œ: {candidate_models()}")
666
  except Exception as e: # noqa: BLE001
667
  lines.append(f"1) ๋ชจ๋ธ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ: {type(e).__name__}: {str(e)[:200]}")
 
673
  lines.append(f"2) LLM ํ˜ธ์ถœ: ์ •์ƒ (๋ชจ๋ธ {model}, ์‘๋‹ต '{text.strip()[:30]}')")
674
  except LLMUnavailable as e:
675
  lines.append(f"2) LLM ํ˜ธ์ถœ: ์‹คํŒจ - {str(e)[:300]}")
676
+ yield "\n".join(lines + ["", "๋„๊ตฌ ํ…Œ์ŠคํŠธ ์ค‘..."])
677
+ s = tool_search("Hugging Face")
678
+ lines.append(f"3) ์›น ๊ฒ€์ƒ‰: {'์ •์ƒ' if len(s) > 100 else '์‹คํŒจ/๊ฒฐ๊ณผ ๋ถ€์กฑ'} ({len(s)}์ž)")
679
+ w = tool_wiki("Mercedes Sosa | studio album")
680
+ lines.append(f"4) ์œ„ํ‚ค๋ฐฑ๊ณผ: {'์ •์ƒ' if 'Wikipedia:' in w[:30] else '์‹คํŒจ'} ({len(w)}์ž) {'' if 'Wikipedia:' in w[:30] else w[:100]}")
681
+ p = tool_python("print(sum(range(1, 11)))")
682
+ lines.append(f"5) ํŒŒ์ด์ฌ ์‹คํ–‰: {'์ •์ƒ' if p.strip() == '55' else '์ด์ƒ - ' + p[:100]}")
683
  yield "\n".join(lines)
684
 
685
 
 
697
  # UI
698
  # ---------------------------------------------------------------------------
699
  with gr.Blocks() as demo:
700
+ gr.Markdown("# Basic Agent Evaluation Runner (๋‹ค๋‹จ๊ณ„ ์—์ด์ „ํŠธ ๋ฒ„์ „)")
701
  gr.Markdown(
702
  """**์‚ฌ์šฉ ์ˆœ์„œ**
703
+ 1. '์—ฐ๊ฒฐ ์ง„๋‹จ'์œผ๋กœ ๋ชจ๋ธ๊ณผ ๋„๊ตฌ(๊ฒ€์ƒ‰, ์œ„ํ‚ค, ํŒŒ์ด์ฌ)๊ฐ€ ์ •์ƒ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
704
  2. ๋กœ๊ทธ์ธ ํ›„ 'ํ…Œ์ŠคํŠธ ์‹คํ–‰'(1~2๋ฌธํ•ญ)์œผ๋กœ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. (์ œ์ถœ ์•ˆ ํ•จ)
705
+ 3. ๋ฌธ์ œ์—†์œผ๋ฉด '์บ์‹œ ์ดˆ๊ธฐํ™”' ํ›„ 'Run Evaluation & Submit All Answers'๋ฅผ ๋ˆ„๋ฆ…๋‹ˆ๋‹ค."""
706
  )
707
  gr.LoginButton()
708
  with gr.Row():
 
711
  test_button = gr.Button("ํ…Œ์ŠคํŠธ ์‹คํ–‰ (์ œ์ถœ ์•ˆ ํ•จ)")
712
  cache_button = gr.Button("์บ์‹œ ์ดˆ๊ธฐํ™”")
713
  run_button = gr.Button("Run Evaluation & Submit All Answers")
714
+ status_output = gr.Textbox(label="Run Status / Submission Result", lines=16, interactive=False)
715
  results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
716
 
717
  diag_button.click(fn=diagnose, outputs=[status_output])