omega21 commited on
Commit
43846ee
ยท
verified ยท
1 Parent(s): 7bda50e

Update app.py

Browse files

"""HF Agents Course ์ตœ์ข… ๊ณผ์ œ - Gemini ๋˜๋Š” Hugging Face ์„ ํƒํ˜• ๊ฒฝ๋Ÿ‰ ๋ฒ„์ „

์„ค๊ณ„ ์›์น™: ๋ฌธํ•ญ๋‹น LLM ํ˜ธ์ถœ 1ํšŒ.
์›น ๊ฒ€์ƒ‰(ํ˜ธ์ถœ ์—†์Œ) -> ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ + ์ฒจ๋ถ€ ํŒŒ์ผ์„ ํ•œ ๋ฒˆ์— Gemini์— ์ „๋‹ฌ -> ์ตœ์ข… ๋‹ต๋งŒ ๋ฐ›์Œ
"""
import base64
import json
import mimetypes
import os
import re
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"
FILES_DIR = "/tmp/task_files"
CACHE_PATH = "/tmp/answers_cache.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 "2")) # LLM ํ˜ธ์ถœ ๊ฐ„ ์ตœ์†Œ ๊ฐ„๊ฒฉ(์ดˆ)
HF_ROUTER = "https://router.huggingface.co/v1"
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()]
MAX_RETRY = int(os.getenv("MAX_RETRY", "3")) # 429 ์žฌ์‹œ๋„ ํšŸ์ˆ˜
FETCH_PAGES = int(os.getenv("FETCH_PAGES", "2")) # ๋ณธ๋ฌธ์„ ์ฝ์„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ˆ˜
PAGE_CHARS = int(os.getenv("PAGE_CHARS", "5000")) # ํŽ˜์ด์ง€๋‹น ์ตœ๋Œ€ ๊ธ€์ž ์ˆ˜
LLM_QUERY = os.getenv("LLM_QUERY", "0") == "1" # 1์ด๋ฉด ๊ฒ€์ƒ‰์–ด๋„ Gemini๋กœ ์ƒ์„ฑ(ํ˜ธ์ถœ 2๋ฐฐ)
GEMINI_MODELS = os.getenv("GEMINI_MODELS", "") # ์„ ํƒ: ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ํ•œ ๋ชจ๋ธ ์ด๋ฆ„ ๋ชฉ๋ก

PROMPT = """You are answering a GAIA-style benchmark question. The answer is graded by EXACT MATCH.

Rules:
- Use the provided web context and attached file. If the context is insufficient, use your own knowledge and reason carefully.
- Give 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), no abbreviations unless asked.
- End your reply with one final line exactly in the form:
FINAL ANSWER: <answer>

Question:
{question}

{extra}"""

# ---------------------------------------------------------------------------
# ๋กœ๊ทธ
# ---------------------------------------------------------------------------
_events = deque(maxlen=12)


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)


# ---------------------------------------------------------------------------
# Gemini REST ํ˜ธ์ถœ (๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด requests๋งŒ ์‚ฌ์šฉ)
# ---------------------------------------------------------------------------
class LLMUnavailable(Exception):
pass


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


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()
names = []
for m in r.json().get("models", []):
if "generateContent" in m.get("supportedGenerationMethods", []):
names.append(m["name"].replace("models/", ""))
return names


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


def candidate_models() -> list[str]:
"""GEMINI_MODELS๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ ๋ชฉ๋ก, ์—†์œผ๋ฉด ํ‚ค๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ๋ชจ๋ธ์—์„œ ์ž๋™ ์„ ํƒ."""
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"์ž๋™ ์„ ํƒ๋œ ๋ชจ๋ธ ํ›„๋ณด: {_candidates}")
return _candidates


_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 call_gemini(parts: list[dict]) -> tuple[str, str]:
"""(์‘๋‹ต ํ…์ŠคํŠธ, ์‚ฌ์šฉํ•œ ๋ชจ๋ธ)์„ ๋ฐ˜ํ™˜. ๋ชจ๋“  ํ›„๋ณด๊ฐ€ ์‹คํŒจํ•˜๋ฉด LLMUnavailable."""
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:
# ์ผ์ผ ํ•œ๋„ ์†Œ์ง„ ๋˜๋Š” ํ•œ๋„ 0์ด๋ฉด ์žฌ์‹œ๋„ํ•ด๋„ ์†Œ์šฉ์—†์œผ๋ฏ€๋กœ ๋‹ค์Œ ๋ชจ๋ธ๋กœ
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 # 400, 403, 404 ๋“ฑ์€ ์ด ๋ชจ๋ธ ํฌ๊ธฐ
raise LLMUnavailable(last_err or "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋ธ์ด ์—†์Šต๋‹ˆ๋‹ค.")


# ---------------------------------------------------------------------------
# Hugging Face Inference Providers (OpenAI ํ˜ธํ™˜ ๋ผ์šฐํ„ฐ)
# ---------------------------------------------------------------------------
def hf_token() -> str:
return os.getenv("HF_TOKEN", "")


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": 1024, "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
last_err = f"{model}: ๋นˆ ์‘๋‹ต"
break
body = r.text
last_err = f"{model}: HTTP {r.status_code} {body[:180].replace(chr(10), ' ')}"
if r.status_code == 402:
raise LLMUnavailable("HF ๋ฌด๋ฃŒ ํฌ๋ ˆ๋”ง ์†Œ์ง„(402). " + body[:150])

Files changed (1) hide show
  1. app.py +429 -319
app.py CHANGED
@@ -1,32 +1,19 @@
1
- import glob
 
 
 
 
 
2
  import json
 
3
  import os
4
  import re
5
- import threading
6
  import time
7
  from collections import deque
8
- from concurrent.futures import ThreadPoolExecutor, wait, FIRST_COMPLETED
9
-
10
- # litellm์˜ ์žฌ์‹œ๋„ ๊ธฐ๋Šฅ์— ํ•„์š”ํ•œ tenacity๊ฐ€ ์—†์œผ๋ฉด ์‹œ์ž‘ ์‹œ ์ž๋™ ์„ค์น˜
11
- try:
12
- import tenacity # noqa: F401
13
- except ImportError:
14
- import subprocess
15
- import sys
16
-
17
- subprocess.run([sys.executable, "-m", "pip", "install", "tenacity"], check=False)
18
 
19
  import gradio as gr
20
  import pandas as pd
21
  import requests
22
- from smolagents import (
23
- CodeAgent,
24
- DuckDuckGoSearchTool,
25
- InferenceClientModel,
26
- LiteLLMModel,
27
- VisitWebpageTool,
28
- tool,
29
- )
30
 
31
  # ZeroGPU ํ•˜๋“œ์›จ์–ด์—์„œ ์‹œ์ž‘ ์˜ค๋ฅ˜๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•œ ๋”๋ฏธ ํ•จ์ˆ˜ (CPU ํ•˜๋“œ์›จ์–ด์—์„œ๋Š” ๋ฌด์‹œ๋จ)
32
  try:
@@ -38,61 +25,68 @@ try:
38
  except ImportError:
39
  pass
40
 
 
 
 
 
 
 
 
 
41
  # ---------------------------------------------------------------------------
42
- # ์„ค์ • (Space์˜ Variables์—์„œ ์กฐ์ • ๊ฐ€๋Šฅ)
43
  # ---------------------------------------------------------------------------
44
- DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
 
45
  FILES_DIR = "/tmp/task_files"
46
  CACHE_PATH = "/tmp/answers_cache.json"
47
  os.makedirs(FILES_DIR, exist_ok=True)
48
 
49
- WORKERS = int(os.getenv("WORKERS", "1")) # ๋™์‹œ ์‹คํ–‰ ๋ฌธํ•ญ ์ˆ˜
50
- QUESTION_TIMEOUT = int(os.getenv("QUESTION_TIMEOUT", "300")) # ๋ฌธํ•ญ๋‹น ์ œํ•œ ์‹œ๊ฐ„(์ดˆ)
51
- MAX_STEPS = int(os.getenv("MAX_STEPS", "8")) # ์—์ด์ „ํŠธ ์ตœ๋Œ€ ๋‹จ๊ณ„
52
- MODEL_TIMEOUT = int(os.getenv("MODEL_TIMEOUT", "90")) # LLM 1ํšŒ ํ˜ธ์ถœ ์ œํ•œ(์ดˆ)
53
- HEARTBEAT = 3 # ํ™”๋ฉด ๊ฐฑ์‹  ์ฃผ๊ธฐ(์ดˆ)
 
 
 
 
 
 
54
 
55
- ANSWER_RULES = """You are a careful research and reasoning agent solving GAIA-style questions.
56
  Rules:
57
- 1. Use the tools (web search, webpage visit, file reading, python code) whenever the
58
- question needs facts you are not 100% sure about. Do not guess.
59
- 2. The final answer is graded by EXACT MATCH. Return ONLY the answer value, with no
60
- explanation, no units unless asked, no trailing period, no "FINAL ANSWER:" prefix.
61
- 3. If the answer is a number: no thousands separators, no currency/percent symbols
62
- unless the question asks for them.
63
- 4. If the answer is a list: comma-separated, in the order the question requests.
64
- 5. If the answer is a string: no articles (a, an, the) and no abbreviations unless
65
- the question asks for them.
66
- 6. If a file path is given, read it with the read_local_file tool or with python code.
67
- 7. Be efficient: stop researching as soon as you have a reliable answer.
68
- 8. Finish by calling final_answer(...) with the answer only."""
69
 
70
  # ---------------------------------------------------------------------------
71
- # ์ด๋ฒคํŠธ ๋กœ๊ทธ (ํ™”๋ฉด ์ƒํƒœ์ฐฝ์— ์ตœ๊ทผ ๊ธฐ๋ก์„ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•จ)
72
  # ---------------------------------------------------------------------------
73
- _events = deque(maxlen=10)
74
- _active: dict = {} # task_id -> ์‹œ์ž‘ ์‹œ๊ฐ
75
- _state_lock = threading.Lock()
76
 
77
 
78
  def log(msg: str):
79
  line = f"[{time.strftime('%H:%M:%S')}] {msg}"
80
  print(line, flush=True)
81
- with _state_lock:
82
- _events.append(line)
83
 
84
 
85
  def recent_log() -> str:
86
- with _state_lock:
87
- return "\n".join(_events)
88
 
89
 
90
  # ---------------------------------------------------------------------------
91
  # ์บ์‹œ
92
  # ---------------------------------------------------------------------------
93
- _cache_lock = threading.Lock()
94
-
95
-
96
  def load_cache() -> dict:
97
  try:
98
  with open(CACHE_PATH, "r", encoding="utf-8") as f:
@@ -101,224 +95,367 @@ def load_cache() -> dict:
101
  return {}
102
 
103
 
104
- def save_cache_entry(task_id: str, answer: str):
105
- with _cache_lock:
106
- cache = load_cache()
107
- cache[task_id] = answer
108
- with open(CACHE_PATH, "w", encoding="utf-8") as f:
109
- json.dump(cache, f, ensure_ascii=False)
110
 
111
 
112
  # ---------------------------------------------------------------------------
113
- # ๋„๊ตฌ
114
  # ---------------------------------------------------------------------------
115
- @tool
116
- def read_local_file(path: str) -> str:
117
- """Reads a local file (txt, csv, json, py, md, xlsx) and returns its content as text.
118
 
119
- Args:
120
- path: Absolute or relative path of the file to read.
121
- """
122
- try:
123
- if not os.path.exists(path):
124
- basename = os.path.basename(path)
125
- matches = glob.glob(os.path.join(FILES_DIR, f"*{basename}"))
126
- if matches:
127
- path = matches[0]
128
- lower = path.lower()
129
- if lower.endswith((".xlsx", ".xls")):
130
- sheets = pd.read_excel(path, sheet_name=None)
131
- return "\n\n".join(
132
- f"## Sheet: {n}\n{df.to_string(max_rows=200)}" for n, df in sheets.items()
133
- )
134
- if lower.endswith(".csv"):
135
- return pd.read_csv(path).to_string(max_rows=200)
136
- with open(path, "r", encoding="utf-8", errors="ignore") as f:
137
- return f.read()[:20000]
138
- except Exception as e: # noqa: BLE001
139
- return f"Could not read file: {e}"
140
 
 
 
141
 
142
- # ---------------------------------------------------------------------------
143
- # ๋ชจ๋ธ / ์—์ด์ „ํŠธ
144
- # ---------------------------------------------------------------------------
145
- def gemini_model_id() -> str:
146
- return os.getenv("GEMINI_MODEL", "gemini-3.8-flash").replace("gemini/", "")
147
-
148
-
149
- def build_model():
150
- gemini_key = os.getenv("GEMINI_API_KEY")
151
- if gemini_key:
152
- return LiteLLMModel(
153
- model_id=f"gemini/{gemini_model_id()}",
154
- api_key=gemini_key,
155
- timeout=MODEL_TIMEOUT, # LLM ํ˜ธ์ถœ์ด ๋ฌดํ•œ์ • ๋ฉˆ์ถ”์ง€ ์•Š๋„๋ก ์ œํ•œ
156
- num_retries=2,
157
- )
158
- openai_key = os.getenv("OPENAI_API_KEY")
159
- if openai_key:
160
- return LiteLLMModel(
161
- model_id=f"openai/{os.getenv('OPENAI_MODEL', 'gpt-4o-mini')}",
162
- api_key=openai_key,
163
- timeout=MODEL_TIMEOUT,
164
- num_retries=2,
165
- )
166
- return InferenceClientModel(
167
- model_id=os.getenv("HF_MODEL", "Qwen/Qwen2.5-Coder-32B-Instruct"),
168
- token=os.getenv("HF_TOKEN"),
169
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
- def build_agent() -> CodeAgent:
173
- return CodeAgent(
174
- tools=[DuckDuckGoSearchTool(), VisitWebpageTool(), read_local_file],
175
- model=build_model(),
176
- additional_authorized_imports=[
177
- "pandas", "numpy", "re", "json", "math", "datetime",
178
- "collections", "itertools", "statistics", "requests", "bs4",
179
- ],
180
- max_steps=MAX_STEPS,
181
- verbosity_level=1,
182
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
 
185
- def clean_answer(answer) -> str:
186
- text = str(answer).strip()
187
- text = re.sub(r"^\s*final answer\s*:\s*", "", text, flags=re.IGNORECASE)
188
- return text.strip().rstrip(".").strip()
189
 
190
 
191
  # ---------------------------------------------------------------------------
192
- # ์ฑ„์  ์„œ๋ฒ„ ํ†ต์‹ 
193
  # ---------------------------------------------------------------------------
194
- def fetch_questions(api_url: str) -> list[dict]:
195
- resp = requests.get(f"{api_url}/questions", timeout=30)
196
- resp.raise_for_status()
197
- data = resp.json()
198
- if not data:
199
- raise ValueError("์งˆ๋ฌธ ๋ชฉ๋ก์ด ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.")
200
- return data
201
 
202
 
203
- def download_task_file(api_url: str, task_id: str, file_name: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  if not file_name:
205
  return None
206
  try:
207
- resp = requests.get(f"{api_url}/files/{task_id}", timeout=60)
208
- resp.raise_for_status()
209
  path = os.path.join(FILES_DIR, f"{task_id}_{file_name}")
210
  with open(path, "wb") as f:
211
- f.write(resp.content)
212
  return path
213
  except Exception as e: # noqa: BLE001
214
- log(f"ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ {task_id[:8]}: {e}")
215
  return None
216
 
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  # ---------------------------------------------------------------------------
219
- # ๋ฌธํ•ญ ํ’€์ด (์บ์‹œ + ์‹œ๊ฐ„ ์ œํ•œ)
220
  # ---------------------------------------------------------------------------
221
- def solve_one(item: dict, api_url: str) -> str:
222
- prompt = ANSWER_RULES + "\n\nQuestion:\n" + item["question"]
223
- file_path = download_task_file(api_url, item["task_id"], item.get("file_name", ""))
224
- if file_path:
225
- prompt += f"\n\nAn attached file is available at: {file_path}"
226
- return clean_answer(build_agent().run(prompt))
227
-
228
-
229
- def solve_with_timeout(item: dict, api_url: str):
230
- """(๋‹ต๋ณ€, ์„ฑ๊ณต ์—ฌ๋ถ€, ๋น„๊ณ )๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
231
- task_id = item["task_id"]
232
- short = task_id[:8]
233
- cached = load_cache().get(task_id)
234
- if cached:
235
- log(f"์บ์‹œ ์‚ฌ์šฉ {short} -> {cached}")
236
- return cached, True, "cache"
237
-
238
- box: dict = {}
239
-
240
- def target():
241
- try:
242
- box["answer"] = solve_one(item, api_url)
243
- except Exception as e: # noqa: BLE001
244
- box["error"] = f"{type(e).__name__}: {e}"[:300]
245
-
246
- log(f"์‹œ์ž‘ {short}")
247
- with _state_lock:
248
- _active[task_id] = time.time()
249
- t = threading.Thread(target=target, daemon=True)
250
- t.start()
251
- t.join(QUESTION_TIMEOUT)
252
- with _state_lock:
253
- _active.pop(task_id, None)
254
-
255
- if t.is_alive():
256
- log(f"์‹œ๊ฐ„ ์ดˆ๊ณผ {short} ({QUESTION_TIMEOUT}์ดˆ)")
257
- return "unknown", False, f"timeout {QUESTION_TIMEOUT}s"
258
- if "error" in box:
259
- log(f"์˜ค๋ฅ˜ {short}: {box['error']}")
260
- return "unknown", False, box["error"]
261
-
262
- save_cache_entry(task_id, box["answer"])
263
- log(f"์™„๋ฃŒ {short} -> {box['answer']}")
264
- return box["answer"], True, ""
265
-
266
-
267
- def iter_solve(questions: list[dict], api_url: str):
268
- """๋๋‚œ ๋ฌธํ•ญ์€ (item, answer, ok, note)๋กœ, ๋Œ€๊ธฐ ์ค‘์—๋Š” ์ฃผ๊ธฐ์ ์œผ๋กœ None์„ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค."""
269
- valid = [q for q in questions if q.get("task_id") and q.get("question") is not None]
270
- pool = ThreadPoolExecutor(max_workers=WORKERS)
271
  try:
272
- pending = {pool.submit(solve_with_timeout, q, api_url): q for q in valid}
273
- while pending:
274
- done, _ = wait(list(pending), timeout=HEARTBEAT, return_when=FIRST_COMPLETED)
275
- if not done:
276
- yield None
277
- continue
278
- for fut in done:
279
- item = pending.pop(fut)
280
- answer, ok, note = fut.result()
281
- yield item, answer, ok, note
282
- finally:
283
- pool.shutdown(wait=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
 
286
  def make_df(rows: dict, order: list[str]) -> pd.DataFrame:
287
- data = [rows[tid] for tid in order if tid in rows]
288
- return pd.DataFrame(data, columns=["Task ID", "Question", "Submitted Answer", "OK", "Note"])
289
-
290
-
291
- def status_text(label: str, done: int, total: int, start: float) -> str:
292
- with _state_lock:
293
- running = len(_active)
294
- oldest = int(time.time() - min(_active.values())) if _active else 0
295
- return (
296
- f"{label}: {done}/{total} ์™„๋ฃŒ | ์‹คํ–‰ ์ค‘ {running}๊ฐœ "
297
- f"(๊ฐ€์žฅ ์˜ค๋ž˜๋œ ๋ฌธํ•ญ {oldest}๏ฟฝ๏ฟฝ๏ฟฝ) | ์ด ๊ฒฝ๊ณผ {int(time.time() - start)}์ดˆ\n"
298
- f"--- ์ตœ๊ทผ ๊ธฐ๋ก ---\n{recent_log()}"
299
  )
300
 
301
 
302
- def run_batch(questions: list[dict], api_url: str, label: str):
303
- """์ง„ํ–‰ ์ƒํ™ฉ์„ ํ™”๋ฉด์— ๋‚ด๋ณด๋‚ด๊ณ , ๋งˆ์ง€๋ง‰์— (rows, order)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
304
  order = [q["task_id"] for q in questions if q.get("task_id")]
305
  rows: dict = {}
 
306
  start = time.time()
307
- log(f"{label} ์‹œ์ž‘: ์งˆ๋ฌธ {len(order)}๊ฐœ, WORKERS={WORKERS}, ๋ชจ๋ธ={gemini_model_id()}")
308
- yield status_text(label, 0, len(order), start), None
309
-
310
- for event in iter_solve(questions, api_url):
311
- if event is not None:
312
- item, answer, ok, note = event
313
- rows[item["task_id"]] = {
314
- "Task ID": item["task_id"],
315
- "Question": item["question"],
316
- "Submitted Answer": answer,
317
- "OK": "O" if ok else "X",
318
- "Note": note,
319
- }
320
- yield status_text(label, len(rows), len(order), start), make_df(rows, order)
321
- return rows, order
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
 
323
 
324
  # ---------------------------------------------------------------------------
@@ -332,144 +469,117 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
332
  if not space_id:
333
  yield "SPACE_ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. Space ์•ˆ์—์„œ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", None
334
  return
335
-
336
- username = profile.username
337
- api_url = DEFAULT_API_URL
338
- agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
339
-
340
- yield "์งˆ๋ฌธ์„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘... (์ตœ๋Œ€ 30์ดˆ)", None
341
  try:
342
- questions = fetch_questions(api_url)
343
  except Exception as e: # noqa: BLE001
344
  yield f"์งˆ๋ฌธ ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜: {e}", None
345
  return
346
 
347
- rows, order = yield from run_batch(questions, api_url, "๋ณธ ์‹คํ–‰")
 
 
 
 
 
348
 
349
- answers_payload = [
350
- {"task_id": tid, "submitted_answer": rows[tid]["Submitted Answer"]}
351
- for tid in order if tid in rows
352
- ]
353
  failed = sum(1 for r in rows.values() if r["OK"] == "X")
354
- yield f"ํ’€์ด ์™„๋ฃŒ (์‹คํŒจ/์‹œ๊ฐ„์ดˆ๊ณผ {failed}๊ฐœ). ์ œ์ถœ ์ค‘...", make_df(rows, order)
355
-
356
  submission = {
357
- "username": username.strip(),
358
- "agent_code": agent_code,
359
- "answers": answers_payload,
360
  }
361
  try:
362
- resp = requests.post(f"{api_url}/submit", json=submission, timeout=120)
363
- resp.raise_for_status()
364
- r = resp.json()
365
- status = (
366
- "์ œ์ถœ ์„ฑ๊ณต!\n"
367
- f"์‚ฌ์šฉ์ž: {r.get('username')}\n"
368
- f"์ ์ˆ˜: {r.get('score', 'N/A')}% "
369
- f"({r.get('correct_count', '?')}/{r.get('total_attempted', '?')} ์ •๋‹ต)\n"
370
- f"๋ฉ”์‹œ์ง€: {r.get('message', '')}"
371
- )
372
  except requests.exceptions.HTTPError as e:
373
- detail = e.response.text[:500] if e.response is not None else str(e)
374
- status = f"์ œ์ถœ ์‹คํŒจ: {detail}"
375
  except Exception as e: # noqa: BLE001
376
  status = f"์ œ์ถœ ์ค‘ ์˜ค๋ฅ˜: {e}"
377
  yield status, make_df(rows, order)
378
 
379
 
380
  def run_test_only(n_questions: int):
381
- api_url = DEFAULT_API_URL
382
- yield "์งˆ๋ฌธ์„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘... (์ตœ๋Œ€ 30์ดˆ)", None
383
  try:
384
- questions = fetch_questions(api_url)[: int(n_questions)]
385
  except Exception as e: # noqa: BLE001
386
  yield f"์ค€๋น„ ์ค‘ ์˜ค๋ฅ˜: {e}", None
387
  return
388
- rows, order = yield from run_batch(questions, api_url, "ํ…Œ์ŠคํŠธ")
389
- yield f"ํ…Œ์ŠคํŠธ ์™„๋ฃŒ (์ œ์ถœ ์•ˆ ํ•จ)\n--- ์ตœ๊ทผ ๊ธฐ๋ก ---\n{recent_log()}", make_df(rows, order)
390
-
391
 
392
- def _call_with_timeout(fn, seconds: int):
393
- box: dict = {}
394
 
395
- def target():
 
 
 
 
396
  try:
397
- box["r"] = fn()
 
 
 
 
398
  except Exception as e: # noqa: BLE001
399
- box["e"] = f"{type(e).__name__}: {e}"[:300]
400
-
401
- th = threading.Thread(target=target, daemon=True)
402
- th.start()
403
- th.join(seconds)
404
- if th.is_alive():
405
- return False, f"{seconds}์ดˆ ์•ˆ์— ์‘๋‹ต ์—†์Œ"
406
- if "e" in box:
407
- return False, box["e"]
408
- return True, str(box.get("r"))[:150]
409
-
410
-
411
- def diagnose():
412
- """์ฑ„์  ์„œ๋ฒ„ / Gemini / ์›น ๊ฒ€์ƒ‰ ์—ฐ๊ฒฐ์„ ๊ฐ๊ฐ ์ ๊ฒ€ํ•ฉ๋‹ˆ๋‹ค."""
413
- lines = [
414
- f"์„ค์ •: ๋ชจ๋ธ={gemini_model_id()}, GEMINI_API_KEY={'์žˆ์Œ' if os.getenv('GEMINI_API_KEY') else '์—†์Œ'}, "
415
- f"WORKERS={WORKERS}, MAX_STEPS={MAX_STEPS}, QUESTION_TIMEOUT={QUESTION_TIMEOUT}"
416
- ]
417
- yield "\n".join(lines + ["", "1/3 ์ฑ„์  ์„œ๋ฒ„ ํ™•์ธ ์ค‘..."])
418
-
419
- ok, msg = _call_with_timeout(lambda: len(fetch_questions(DEFAULT_API_URL)), 35)
420
- lines.append(f"1) ์ฑ„์  ์„œ๋ฒ„ ์งˆ๋ฌธ ๋ชฉ๋ก: {'์ •์ƒ' if ok else '์‹คํŒจ'} - {msg}")
421
- yield "\n".join(lines + ["", "2/3 Gemini ํ˜ธ์ถœ ํ™•์ธ ์ค‘..."])
422
-
423
- key = os.getenv("GEMINI_API_KEY")
424
- if key:
425
- def ping():
426
- import litellm
427
- r = litellm.completion(
428
- model=f"gemini/{gemini_model_id()}",
429
- api_key=key,
430
- messages=[{"role": "user", "content": "Reply with the single word OK."}],
431
- timeout=45,
432
- )
433
- return r.choices[0].message.content
434
-
435
- ok, msg = _call_with_timeout(ping, 60)
436
- lines.append(f"2) Gemini ํ˜ธ์ถœ: {'์ •์ƒ' if ok else '์‹คํŒจ'} - {msg}")
437
  else:
438
- lines.append("2) Gemini ํ˜ธ์ถœ: GEMINI_API_KEY๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•„ ๊ฑด๋„ˆ๋œ€")
439
- yield "\n".join(lines + ["", "3/3 ์›น ๊ฒ€์ƒ‰ ํ™•์ธ ์ค‘..."])
440
-
441
- ok, msg = _call_with_timeout(lambda: DuckDuckGoSearchTool()("Hugging Face"), 40)
442
- lines.append(f"3) ์›น ๊ฒ€์ƒ‰(DuckDuckGo): {'์ •์ƒ' if ok else '์‹คํŒจ'} - {msg}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  yield "\n".join(lines)
444
 
445
 
446
  def clear_cache():
 
 
447
  try:
448
  os.remove(CACHE_PATH)
449
- return "์บ์‹œ๋ฅผ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค."
450
  except FileNotFoundError:
451
- return "์‚ญ์ œํ•  ์บ์‹œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
452
 
453
 
454
  # ---------------------------------------------------------------------------
455
  # UI
456
  # ---------------------------------------------------------------------------
457
  with gr.Blocks() as demo:
458
- gr.Markdown("# Basic Agent Evaluation Runner (Gemini 3.8 Flash)")
459
  gr.Markdown(
460
- """**Instructions:**
461
- 1. ๋จผ์ € '์—ฐ๊ฒฐ ์ง„๋‹จ'์œผ๋กœ ์ฑ„์  ์„œ๋ฒ„, Gemini, ์›น ๊ฒ€์ƒ‰์ด ์ •์ƒ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
462
- 2. ๋กœ๊ทธ์ธ ํ›„ 'ํ…Œ์ŠคํŠธ ์‹คํ–‰'์œผ๋กœ ์†Œ์ˆ˜ ๋ฌธํ•ญ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. (์ œ์ถœ ์•ˆ ํ•จ)
463
  3. ๋ฌธ์ œ์—†์œผ๋ฉด 'Run Evaluation & Submit All Answers'๋ฅผ ๋ˆ„๋ฆ…๋‹ˆ๋‹ค."""
464
  )
465
  gr.LoginButton()
466
-
467
  with gr.Row():
468
  diag_button = gr.Button("์—ฐ๊ฒฐ ์ง„๋‹จ")
469
  test_n = gr.Number(value=2, label="ํ…Œ์ŠคํŠธํ•  ๋ฌธํ•ญ ์ˆ˜ (์ œ์ถœ ์•ˆ ํ•จ)", precision=0)
470
  test_button = gr.Button("ํ…Œ์ŠคํŠธ ์‹คํ–‰ (์ œ์ถœ ์•ˆ ํ•จ)")
471
  cache_button = gr.Button("์บ์‹œ ์ดˆ๊ธฐํ™”")
472
-
473
  run_button = gr.Button("Run Evaluation & Submit All Answers")
474
  status_output = gr.Textbox(label="Run Status / Submission Result", lines=14, interactive=False)
475
  results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
 
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
 
14
  import gradio as gr
15
  import pandas as pd
16
  import requests
 
 
 
 
 
 
 
 
17
 
18
  # ZeroGPU ํ•˜๋“œ์›จ์–ด์—์„œ ์‹œ์ž‘ ์˜ค๋ฅ˜๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•œ ๋”๋ฏธ ํ•จ์ˆ˜ (CPU ํ•˜๋“œ์›จ์–ด์—์„œ๋Š” ๋ฌด์‹œ๋จ)
19
  try:
 
25
  except ImportError:
26
  pass
27
 
28
+ try:
29
+ from ddgs import DDGS
30
+ except ImportError:
31
+ try:
32
+ from duckduckgo_search import DDGS
33
+ except ImportError:
34
+ DDGS = None
35
+
36
  # ---------------------------------------------------------------------------
37
+ # ์„ค์ • (Space Variables/Secrets๋กœ ์กฐ์ •)
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):
78
  line = f"[{time.strftime('%H:%M:%S')}] {msg}"
79
  print(line, flush=True)
80
+ _events.append(line)
 
81
 
82
 
83
  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:
 
95
  return {}
96
 
97
 
98
+ def save_cache(task_id: str, answer: str):
99
+ cache = load_cache()
100
+ cache[task_id] = answer
101
+ with open(CACHE_PATH, "w", encoding="utf-8") as f:
102
+ json.dump(cache, f, ensure_ascii=False)
 
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")
132
+ _candidates: list[str] = []
133
+
134
+
135
+ def candidate_models() -> list[str]:
136
+ """GEMINI_MODELS๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ ๋ชฉ๋ก, ์—†์œผ๋ฉด ํ‚ค๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ๋ชจ๋ธ์—์„œ ์ž๋™ ์„ ํƒ."""
137
+ global _candidates
138
+ if _candidates:
139
+ return _candidates
140
+ if GEMINI_MODELS.strip():
141
+ _candidates = [m.strip().replace("models/", "") for m in GEMINI_MODELS.split(",") if m.strip()]
142
+ return _candidates
143
+ names = [n for n in list_models() if n.startswith("gemini") and not any(b in n for b in _BAD)]
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 = ""
167
+ for model in candidate_models():
168
+ for attempt in range(MAX_RETRY + 1):
169
+ throttle()
170
+ try:
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
204
+ if attempt == MAX_RETRY:
205
+ break
206
+ m = re.search(r'"retryDelay":\s*"(\d+(?:\.\d+)?)s"', body)
207
+ wait = min(float(m.group(1)) + 1, 60) if m else 15 * (attempt + 1)
208
+ log(f"{model}: 429, {int(wait)}์ดˆ ํ›„ ์žฌ์‹œ๋„ ({attempt + 1}/{MAX_RETRY})")
209
+ time.sleep(wait)
210
+ continue
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):
236
+ throttle()
237
+ try:
238
+ r = requests.post(
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:
246
+ last_err = f"{model}: ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ {e}"
247
+ time.sleep(5)
248
+ continue
249
+ if r.status_code == 200:
250
+ content = (r.json().get("choices") or [{}])[0].get("message", {}).get("content") or ""
251
+ content = re.sub(r"(?s)<think>.*?</think>", "", content).strip()
252
+ if content:
253
+ return content, model
254
+ last_err = f"{model}: ๋นˆ ์‘๋‹ต"
255
+ break
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
265
+ wait = 15 * (attempt + 1)
266
+ log(f"{model}: 429, {wait}์ดˆ ํ›„ ์žฌ์‹œ๋„ ({attempt + 1}/{MAX_RETRY})")
267
+ time.sleep(wait)
268
+ continue
269
+ if r.status_code >= 500:
270
+ time.sleep(5)
271
+ continue
272
+ break # 400, 404 ๋“ฑ์€ ์ด ๋ชจ๋ธ ํฌ๊ธฐ
273
+ raise LLMUnavailable(last_err or "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋ธ์ด ์—†์Šต๋‹ˆ๋‹ค.")
274
 
275
 
276
+ def call_llm(parts: list[dict]) -> tuple[str, str]:
277
+ return call_hf(parts) if PROVIDER == "hf" else call_gemini(parts)
 
 
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
+ # ---------------------------------------------------------------------------
317
+ def download_file(task_id: str, file_name: str):
318
  if not file_name:
319
  return None
320
  try:
321
+ r = requests.get(f"{SCORING_API}/files/{task_id}", timeout=60)
322
+ r.raise_for_status()
323
  path = os.path.join(FILES_DIR, f"{task_id}_{file_name}")
324
  with open(path, "wb") as f:
325
+ f.write(r.content)
326
  return path
327
  except Exception as e: # noqa: BLE001
328
+ log(f"ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ: {e}")
329
  return None
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
+ # ---------------------------------------------------------------------------
397
+ # ์ฑ„์  ์„œ๋ฒ„
398
+ # ---------------------------------------------------------------------------
399
+ def fetch_questions() -> list[dict]:
400
+ r = requests.get(f"{SCORING_API}/questions", timeout=30)
401
+ r.raise_for_status()
402
+ data = r.json()
403
+ if not data:
404
+ raise ValueError("์งˆ๋ฌธ ๋ชฉ๋ก์ด ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.")
405
+ return data
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ํšŒ ์‹คํŒจํ•˜์—ฌ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค. (ํ•œ๋„/ํฌ๋ ˆ๋”ง ๋ฌธ์ œ ๊ฐ€๋Šฅ์„ฑ)")
455
+ break
456
+
457
+ yield f"{label} ์ข…๋ฃŒ (๊ฒฝ๊ณผ {int(time.time() - start)}์ดˆ)\n--- ์ตœ๊ทผ ๊ธฐ๋ก ---\n{recent_log()}", make_df(rows, order)
458
+ return rows, order, aborted
459
 
460
 
461
  # ---------------------------------------------------------------------------
 
469
  if not space_id:
470
  yield "SPACE_ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. Space ์•ˆ์—์„œ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", None
471
  return
472
+ yield "์งˆ๋ฌธ์„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘...", None
 
 
 
 
 
473
  try:
474
+ questions = fetch_questions()
475
  except Exception as e: # noqa: BLE001
476
  yield f"์งˆ๋ฌธ ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜: {e}", None
477
  return
478
 
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()
497
+ res = r.json()
498
+ status = (f"์ œ์ถœ ์„ฑ๊ณต!\n์‚ฌ์šฉ์ž: {res.get('username')}\n"
499
+ f"์ ์ˆ˜: {res.get('score', 'N/A')}% ({res.get('correct_count', '?')}/{res.get('total_attempted', '?')} ์ •๋‹ต)\n"
500
+ f"๋ฉ”์‹œ์ง€: {res.get('message', '')}")
 
 
 
 
501
  except requests.exceptions.HTTPError as e:
502
+ status = f"์ œ์ถœ ์‹คํŒจ: {e.response.text[:500] if e.response is not None else e}"
 
503
  except Exception as e: # noqa: BLE001
504
  status = f"์ œ์ถœ ์ค‘ ์˜ค๋ฅ˜: {e}"
505
  yield status, make_df(rows, order)
506
 
507
 
508
  def run_test_only(n_questions: int):
509
+ yield "์งˆ๋ฌธ์„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘...", None
 
510
  try:
511
+ questions = fetch_questions()[: int(n_questions)]
512
  except Exception as e: # noqa: BLE001
513
  yield f"์ค€๋น„ ์ค‘ ์˜ค๋ฅ˜: {e}", None
514
  return
515
+ yield from run_batch(questions, "ํ…Œ์ŠคํŠธ")
 
 
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]}")
539
+ yield "\n".join(lines)
540
+ return
541
+ yield "\n".join(lines + ["", "LLM ํ˜ธ์ถœ ํ…Œ์ŠคํŠธ ์ค‘..."])
542
+ try:
543
+ text, model = call_llm([{"text": "Reply with the single word OK."}])
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
 
556
  def clear_cache():
557
+ global _candidates
558
+ _candidates = []
559
  try:
560
  os.remove(CACHE_PATH)
561
+ return "์บ์‹œ์™€ ๋ชจ๋ธ ์„ ํƒ ์ •๋ณด๋ฅผ ์ดˆ๊ธฐํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค."
562
  except FileNotFoundError:
563
+ return "์‚ญ์ œํ•  ์บ์‹œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. (๋ชจ๋ธ ์„ ํƒ ์ •๋ณด๋Š” ์ดˆ๊ธฐํ™”๋จ)"
564
 
565
 
566
  # ---------------------------------------------------------------------------
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():
579
  diag_button = gr.Button("์—ฐ๊ฒฐ ์ง„๋‹จ")
580
  test_n = gr.Number(value=2, label="ํ…Œ์ŠคํŠธํ•  ๋ฌธํ•ญ ์ˆ˜ (์ œ์ถœ ์•ˆ ํ•จ)", precision=0)
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)