omega21 commited on
Commit
9564d9d
ยท
verified ยท
1 Parent(s): aacc1dd

Update app.py

Browse files

import glob
import json
import os
import re
import threading
import time
from collections import deque
from concurrent.futures import ThreadPoolExecutor, wait, FIRST_COMPLETED

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

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

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

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

WORKERS = int(os.getenv("WORKERS", "2")) # ๋™์‹œ ์‹คํ–‰ ๋ฌธํ•ญ ์ˆ˜
QUESTION_TIMEOUT = int(os.getenv("QUESTION_TIMEOUT", "300")) # ๋ฌธํ•ญ๋‹น ์ œํ•œ ์‹œ๊ฐ„(์ดˆ)
MAX_STEPS = int(os.getenv("MAX_STEPS", "8")) # ์—์ด์ „ํŠธ ์ตœ๋Œ€ ๋‹จ๊ณ„
MODEL_TIMEOUT = int(os.getenv("MODEL_TIMEOUT", "90")) # LLM 1ํšŒ ํ˜ธ์ถœ ์ œํ•œ(์ดˆ)
HEARTBEAT = 3 # ํ™”๋ฉด ๊ฐฑ์‹  ์ฃผ๊ธฐ(์ดˆ)

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

# ---------------------------------------------------------------------------
# ์ด๋ฒคํŠธ ๋กœ๊ทธ (ํ™”๋ฉด ์ƒํƒœ์ฐฝ์— ์ตœ๊ทผ ๊ธฐ๋ก์„ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•จ)
# ---------------------------------------------------------------------------
_events = deque(maxlen=10)
_active: dict = {} # task_id -> ์‹œ์ž‘ ์‹œ๊ฐ
_state_lock = threading.Lock()


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


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


# ---------------------------------------------------------------------------
# ์บ์‹œ
# ---------------------------------------------------------------------------
_cache_lock = threading.Lock()


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


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


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

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


# ---------------------------------------------------------------------------
# ๋ชจ๋ธ / ์—์ด์ „ํŠธ
# ---------------------------------------------------------------------------
def gemini_model_id() -> str:
return os.getenv("GEMINI_MODEL", "gemini-3.8-flash").replace("gemini/", "")


def build_model():
gemini_key = os.getenv("GEMINI_API_KEY")
if gemini_key:
return LiteLLMModel(
model_id=f"gemini/{gemini_model_id()}",
api_key=gemini_key,
timeout=MODEL_TIMEOUT, # LLM ํ˜ธ์ถœ์ด ๋ฌดํ•œ์ • ๋ฉˆ์ถ”์ง€ ์•Š๋„๋ก ์ œํ•œ
num_retries=2,
)
openai_key = os.getenv("OPENAI_API_KEY")
if openai_key:
return LiteLLMModel(
model_id=f"openai/{os.getenv('OPENAI_MODEL', 'gpt-4o-mini')}",
api_key=openai_key,
timeout=MODEL_TIMEOUT,
num_retries=2,
)
return InferenceClientModel(
model_id=os.getenv("HF_MODEL", "Qwen/Qwen2.5-Coder-32B-Instruct"),
token=os.getenv("HF_TOKEN"),
)


def build_agent() -> CodeAgent:
return CodeAgent(
tools=[DuckDuckGoSearchTool(), VisitWebpageTool(), read_local_file],
model=build_model(),
additional_authorized_imports=[
"pandas", "numpy", "re", "json", "math", "datetime",
"collections", "itertools", "statistics", "requests", "bs4",
],
max_steps=MAX_STEPS,
verbosity_level=1,
)


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


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


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


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


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

box: dict = {}

def target():
try:
box["answer"] = solve_one(item, api_url)
except Exception as e: # noqa: BLE001
box["error"] = f"{type(e).__name__}: {e}"[:300]

log(f"์‹œ์ž‘ {short}")
with _state_lock:
_active[task_id] = time.time()
t = threading.Thread(target=target, daemon=True)
t.start()
t.join(QUESTION_TIMEOUT)
with _state_lock:
_active.pop(task_id, None)

if t.is_alive():
log(f"์‹œ๊ฐ„ ์ดˆ๊ณผ {short} ({QUESTION_TIMEOUT}์ดˆ)")
return "unknown", False, f"timeout {QUESTION_TIMEOUT}s"
if "error" in box:
log(f"์˜ค๋ฅ˜ {short}: {box['error']}")
return "unknown", False, box["error"]

save_cache_entry(task_id, box["answer"])
log(f"์™„๋ฃŒ {short} -> {box['answer']}")
return box["answer"], True, ""


def iter_solve(questions: list[dict], api_url: str):
"""๋๋‚œ ๋ฌธํ•ญ์€ (item, answer, ok, note)๋กœ, ๋Œ€๊ธฐ ์ค‘์—๋Š” ์ฃผ๊ธฐ์ ์œผ๋กœ None์„ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค."""
valid = [q for q in questions if q.get("task_id") and q.get("question") is not None]
pool = ThreadPoolExecutor(max_workers=WORKERS)
try:
pending = {pool.submit(solve_with_timeout, q, api_url): q for q in valid}
while pending:
done, _ = wait(list(pending), timeout=HEARTBEAT, return_when=FIRST_COMPLETED)
if not done:
yield None
continue
for fut in done:
item = pending.pop(fut)
answer, ok, note = fut.result()
yield item, answer, ok, note
finally:
pool.shutdown(wait=False)


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


def status_text(label: str, done: int, total: int, start: float) -> str:
with _state_lock:
running = len(_active)
oldest = int(time.time() - min

Files changed (1) hide show
  1. app.py +212 -90
app.py CHANGED
@@ -1,9 +1,12 @@
 
1
  import json
2
  import os
3
  import re
4
  import threading
5
  import time
6
- from concurrent.futures import ThreadPoolExecutor, as_completed
 
 
7
  import gradio as gr
8
  import pandas as pd
9
  import requests
@@ -16,9 +19,10 @@ from smolagents import (
16
  tool,
17
  )
18
 
19
- # ZeroGPU ํ•˜๋“œ์›จ์–ด์—์„œ ์‹œ์ž‘ ์˜ค๋ฅ˜๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•œ ์•ˆ์ „ํ•œ ๋”๋ฏธ ํ•จ์ˆ˜
20
  try:
21
  import spaces
 
22
  @spaces.GPU
23
  def _gpu_placeholder():
24
  return "ok"
@@ -26,16 +30,18 @@ except ImportError:
26
  pass
27
 
28
  # ---------------------------------------------------------------------------
29
- # ์„ค์ • (Space์˜ Variables ๋˜๋Š” Secrets์—์„œ ์กฐ์ • ๊ฐ€๋Šฅ)
30
  # ---------------------------------------------------------------------------
31
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
32
  FILES_DIR = "/tmp/task_files"
33
  CACHE_PATH = "/tmp/answers_cache.json"
34
  os.makedirs(FILES_DIR, exist_ok=True)
35
 
36
- WORKERS = int(os.getenv("WORKERS", "3")) # ๋™์‹œ ์‹คํ–‰ ๋ฌธํ•ญ ์ˆ˜
37
- QUESTION_TIMEOUT = int(os.getenv("QUESTION_TIMEOUT", "240")) # ๋ฌธํ•ญ๋‹น ์ œํ•œ ์‹œ๊ฐ„(์ดˆ)
38
- MAX_STEPS = int(os.getenv("MAX_STEPS", "12")) # ์—์ด์ „ํŠธ ์ตœ๋Œ€ ๋‹จ๊ณ„
 
 
39
 
40
  ANSWER_RULES = """You are a careful research and reasoning agent solving GAIA-style questions.
41
  Rules:
@@ -53,10 +59,31 @@ Rules:
53
  8. Finish by calling final_answer(...) with the answer only."""
54
 
55
  # ---------------------------------------------------------------------------
56
- # ์บ์‹œ ๊ด€๋ฆฌ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  # ---------------------------------------------------------------------------
58
  _cache_lock = threading.Lock()
59
 
 
60
  def load_cache() -> dict:
61
  try:
62
  with open(CACHE_PATH, "r", encoding="utf-8") as f:
@@ -64,6 +91,7 @@ def load_cache() -> dict:
64
  except Exception: # noqa: BLE001
65
  return {}
66
 
 
67
  def save_cache_entry(task_id: str, answer: str):
68
  with _cache_lock:
69
  cache = load_cache()
@@ -71,23 +99,23 @@ def save_cache_entry(task_id: str, answer: str):
71
  with open(CACHE_PATH, "w", encoding="utf-8") as f:
72
  json.dump(cache, f, ensure_ascii=False)
73
 
 
74
  # ---------------------------------------------------------------------------
75
- # ๋„๊ตฌ (ํŒŒ์ผ ๊ฒฝ๋กœ ๋ณด์™„ ๊ธฐ๋Šฅ ํฌํ•จ)
76
  # ---------------------------------------------------------------------------
77
  @tool
78
  def read_local_file(path: str) -> str:
79
  """Reads a local file (txt, csv, json, py, md, xlsx) and returns its content as text.
 
80
  Args:
81
  path: Absolute or relative path of the file to read.
82
  """
83
  try:
84
- # ์—์ด์ „ํŠธ๊ฐ€ ํŒŒ์ผ๋ช…๋งŒ ์ž…๋ ฅํ•˜๊ฑฐ๋‚˜ ๊ฒฝ๋กœ๊ฐ€ ์ž˜๋ชป๋œ ๊ฒฝ์šฐ FILES_DIR์—์„œ ํƒ์ƒ‰
85
  if not os.path.exists(path):
86
  basename = os.path.basename(path)
87
- candidate = os.path.join(FILES_DIR, basename)
88
- if os.path.exists(candidate):
89
- path = candidate
90
-
91
  lower = path.lower()
92
  if lower.endswith((".xlsx", ".xls")):
93
  sheets = pd.read_excel(path, sheet_name=None)
@@ -101,28 +129,38 @@ def read_local_file(path: str) -> str:
101
  except Exception as e: # noqa: BLE001
102
  return f"Could not read file: {e}"
103
 
 
104
  # ---------------------------------------------------------------------------
105
- # ์—์ด์ „ํŠธ ๋ฐ ๋ชจ๋ธ ์„ค์ • (gemini-3.8-flash ๋ฐ˜์˜)
106
  # ---------------------------------------------------------------------------
 
 
 
 
107
  def build_model():
108
  gemini_key = os.getenv("GEMINI_API_KEY")
109
  if gemini_key:
110
- model_id = os.getenv("GEMINI_MODEL", "gemini-3.8-flash")
111
- print(f"[model] Gemini: {model_id}")
112
- return LiteLLMModel(model_id=f"gemini/{model_id}", api_key=gemini_key)
113
-
 
 
114
  openai_key = os.getenv("OPENAI_API_KEY")
115
  if openai_key:
116
- model_id = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
117
- print(f"[model] OpenAI: {model_id}")
118
- return LiteLLMModel(model_id=f"openai/{model_id}", api_key=openai_key)
119
-
120
- model_id = os.getenv("HF_MODEL", "Qwen/Qwen2.5-Coder-32B-Instruct")
121
- print(f"[model] HF Inference: {model_id}")
122
- return InferenceClientModel(model_id=model_id, token=os.getenv("HF_TOKEN"))
 
 
 
 
123
 
124
  def build_agent() -> CodeAgent:
125
- # ์Šค๋ ˆ๋“œ ์•ˆ์ „ ๋ฐ ๋ฌธํ•ญ๋ณ„ ๋…๋ฆฝ ์ปจํ…์ŠคํŠธ ์œ ์ง€๋ฅผ ์œ„ํ•ด ๋งค๋ฒˆ ์ƒˆ ์—์ด์ „ํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
126
  return CodeAgent(
127
  tools=[DuckDuckGoSearchTool(), VisitWebpageTool(), read_local_file],
128
  model=build_model(),
@@ -134,13 +172,15 @@ def build_agent() -> CodeAgent:
134
  verbosity_level=1,
135
  )
136
 
 
137
  def clean_answer(answer) -> str:
138
  text = str(answer).strip()
139
  text = re.sub(r"^\s*final answer\s*:\s*", "", text, flags=re.IGNORECASE)
140
  return text.strip().rstrip(".").strip()
141
 
 
142
  # ---------------------------------------------------------------------------
143
- # ์ฑ„์  ์„œ๋ฒ„ ํ†ต์‹  ๋ฐ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ
144
  # ---------------------------------------------------------------------------
145
  def fetch_questions(api_url: str) -> list[dict]:
146
  resp = requests.get(f"{api_url}/questions", timeout=30)
@@ -150,6 +190,7 @@ def fetch_questions(api_url: str) -> list[dict]:
150
  raise ValueError("์งˆ๋ฌธ ๋ชฉ๋ก์ด ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.")
151
  return data
152
 
 
153
  def download_task_file(api_url: str, task_id: str, file_name: str):
154
  if not file_name:
155
  return None
@@ -161,66 +202,118 @@ def download_task_file(api_url: str, task_id: str, file_name: str):
161
  f.write(resp.content)
162
  return path
163
  except Exception as e: # noqa: BLE001
164
- print(f"ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ ({task_id}): {e}")
165
  return None
166
 
 
167
  # ---------------------------------------------------------------------------
168
- # ๊ฐœ๋ณ„ ๋ฌธํ•ญ ํ’€์ด (์บ์‹œ + ํƒ€์ž„์•„์›ƒ)
169
  # ---------------------------------------------------------------------------
170
  def solve_one(item: dict, api_url: str) -> str:
171
  prompt = ANSWER_RULES + "\n\nQuestion:\n" + item["question"]
172
  file_path = download_task_file(api_url, item["task_id"], item.get("file_name", ""))
173
  if file_path:
174
  prompt += f"\n\nAn attached file is available at: {file_path}"
175
-
176
- agent = build_agent()
177
- result = agent.run(prompt)
178
- return clean_answer(result)
179
 
180
- def solve_with_timeout(item: dict, api_url: str) -> tuple[str, bool]:
 
 
181
  task_id = item["task_id"]
 
182
  cached = load_cache().get(task_id)
183
  if cached:
184
- print(f"[cache] {task_id} -> {cached}")
185
- return cached, True
186
 
187
  box: dict = {}
 
188
  def target():
189
  try:
190
  box["answer"] = solve_one(item, api_url)
191
  except Exception as e: # noqa: BLE001
192
- box["error"] = str(e)
193
 
194
- print(f"[start] {task_id}")
 
 
195
  t = threading.Thread(target=target, daemon=True)
196
  t.start()
197
  t.join(QUESTION_TIMEOUT)
 
 
198
 
199
  if t.is_alive():
200
- print(f"[timeout] {task_id}")
201
- return "unknown", False
202
  if "error" in box:
203
- print(f"[error] {task_id}: {box['error']}")
204
- return "unknown", False
205
 
206
  save_cache_entry(task_id, box["answer"])
207
- print(f"[done] {task_id} -> {box['answer']}")
208
- return box["answer"], True
 
209
 
210
  def iter_solve(questions: list[dict], api_url: str):
 
211
  valid = [q for q in questions if q.get("task_id") and q.get("question") is not None]
212
- with ThreadPoolExecutor(max_workers=WORKERS) as pool:
213
- futures = {pool.submit(solve_with_timeout, q, api_url): q for q in valid}
214
- for fut in as_completed(futures):
215
- answer, ok = fut.result()
216
- yield futures[fut], answer, ok
 
 
 
 
 
 
 
 
 
 
217
 
218
  def make_df(rows: dict, order: list[str]) -> pd.DataFrame:
219
  data = [rows[tid] for tid in order if tid in rows]
220
- return pd.DataFrame(data, columns=["Task ID", "Question", "Submitted Answer", "OK"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
  # ---------------------------------------------------------------------------
223
- # Gradio ์ฝœ๋ฐฑ (์‹ค์‹œ๊ฐ„ ์ง„ํ–‰ ์ƒํ™ฉ ์ œ๋„ˆ๋ ˆ์ดํ„ฐ ๋ฐ˜์˜)
224
  # ---------------------------------------------------------------------------
225
  def run_and_submit_all(profile: gr.OAuthProfile | None):
226
  space_id = os.getenv("SPACE_ID")
@@ -235,27 +328,14 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
235
  api_url = DEFAULT_API_URL
236
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
237
 
238
- yield "์งˆ๋ฌธ์„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘...", None
239
  try:
240
  questions = fetch_questions(api_url)
241
  except Exception as e: # noqa: BLE001
242
  yield f"์งˆ๋ฌธ ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜: {e}", None
243
  return
244
 
245
- order = [q["task_id"] for q in questions if q.get("task_id")]
246
- total = len(order)
247
- rows: dict = {}
248
- start = time.time()
249
-
250
- for item, answer, ok in iter_solve(questions, api_url):
251
- rows[item["task_id"]] = {
252
- "Task ID": item["task_id"],
253
- "Question": item["question"],
254
- "Submitted Answer": answer,
255
- "OK": "O" if ok else "X",
256
- }
257
- elapsed = int(time.time() - start)
258
- yield f"์ง„ํ–‰ {len(rows)}/{total} ์™„๋ฃŒ (๊ฒฝ๊ณผ {elapsed}์ดˆ)", make_df(rows, order)
259
 
260
  answers_payload = [
261
  {"task_id": tid, "submitted_answer": rows[tid]["Submitted Answer"]}
@@ -285,35 +365,74 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
285
  status = f"์ œ์ถœ ์‹คํŒจ: {detail}"
286
  except Exception as e: # noqa: BLE001
287
  status = f"์ œ์ถœ ์ค‘ ์˜ค๋ฅ˜: {e}"
288
-
289
  yield status, make_df(rows, order)
290
 
 
291
  def run_test_only(n_questions: int):
292
  api_url = DEFAULT_API_URL
293
- yield "์งˆ๋ฌธ์„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘...", None
294
  try:
295
  questions = fetch_questions(api_url)[: int(n_questions)]
296
  except Exception as e: # noqa: BLE001
297
  yield f"์ค€๋น„ ์ค‘ ์˜ค๋ฅ˜: {e}", None
298
  return
 
 
299
 
300
- order = [q["task_id"] for q in questions if q.get("task_id")]
301
- rows: dict = {}
302
- start = time.time()
303
 
304
- for item, answer, ok in iter_solve(questions, api_url):
305
- rows[item["task_id"]] = {
306
- "Task ID": item["task_id"],
307
- "Question": item["question"],
308
- "Submitted Answer": answer,
309
- "OK": "O" if ok else "X",
310
- }
311
- yield (
312
- f"ํ…Œ์ŠคํŠธ ์ง„ํ–‰ {len(rows)}/{len(order)} (๊ฒฝ๊ณผ {int(time.time() - start)}์ดˆ)",
313
- make_df(rows, order),
314
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
 
316
- yield f"ํ…Œ์ŠคํŠธ ์™„๋ฃŒ ({int(time.time() - start)}์ดˆ, ์ œ์ถœ ์•ˆ ํ•จ)", make_df(rows, order)
317
 
318
  def clear_cache():
319
  try:
@@ -322,28 +441,31 @@ def clear_cache():
322
  except FileNotFoundError:
323
  return "์‚ญ์ œํ•  ์บ์‹œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
324
 
 
325
  # ---------------------------------------------------------------------------
326
- # Gradio UI ๊ตฌ์„ฑ
327
  # ---------------------------------------------------------------------------
328
  with gr.Blocks() as demo:
329
  gr.Markdown("# Basic Agent Evaluation Runner (Gemini 3.8 Flash)")
330
  gr.Markdown(
331
  """**Instructions:**
332
- 1. ์ด Space๋ฅผ ๋ณต์ œ(Duplicate)ํ•œ ๋’ค ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ด ์—์ด์ „ํŠธ ๋กœ์ง์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
333
- 2. ์•„๋ž˜ ๋ฒ„ํŠผ์œผ๋กœ Hugging Face์— ๋กœ๊ทธ์ธํ•ฉ๋‹ˆ๋‹ค. (HF ์‚ฌ์šฉ์ž๋ช…์œผ๋กœ ์ œ์ถœ๋ฉ๋‹ˆ๋‹ค)
334
- 3. ๋จผ์ € 'ํ…Œ์ŠคํŠธ ์‹คํ–‰'์œผ๋กœ ์†Œ์ˆ˜ ๋ฌธํ•ญ์„ ํ™•์ธํ•˜๊ณ , ๋ฌธ์ œ์—†์œผ๋ฉด 'Run Evaluation & Submit All Answers'๋ฅผ ๋ˆ„๋ฆ…๋‹ˆ๋‹ค."""
335
  )
336
  gr.LoginButton()
337
-
338
  with gr.Row():
 
339
  test_n = gr.Number(value=2, label="ํ…Œ์ŠคํŠธํ•  ๋ฌธํ•ญ ์ˆ˜ (์ œ์ถœ ์•ˆ ํ•จ)", precision=0)
340
  test_button = gr.Button("ํ…Œ์ŠคํŠธ ์‹คํ–‰ (์ œ์ถœ ์•ˆ ํ•จ)")
341
  cache_button = gr.Button("์บ์‹œ ์ดˆ๊ธฐํ™”")
342
-
343
  run_button = gr.Button("Run Evaluation & Submit All Answers")
344
- status_output = gr.Textbox(label="Run Status / Submission Result", lines=6, interactive=False)
345
  results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
346
 
 
347
  test_button.click(fn=run_test_only, inputs=[test_n], outputs=[status_output, results_table])
348
  run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])
349
  cache_button.click(fn=clear_cache, outputs=[status_output])
 
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
  import gradio as gr
11
  import pandas as pd
12
  import requests
 
19
  tool,
20
  )
21
 
22
+ # ZeroGPU ํ•˜๋“œ์›จ์–ด์—์„œ ์‹œ์ž‘ ์˜ค๋ฅ˜๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•œ ๋”๋ฏธ ํ•จ์ˆ˜ (CPU ํ•˜๋“œ์›จ์–ด์—์„œ๋Š” ๋ฌด์‹œ๋จ)
23
  try:
24
  import spaces
25
+
26
  @spaces.GPU
27
  def _gpu_placeholder():
28
  return "ok"
 
30
  pass
31
 
32
  # ---------------------------------------------------------------------------
33
+ # ์„ค์ • (Space์˜ Variables์—์„œ ์กฐ์ • ๊ฐ€๋Šฅ)
34
  # ---------------------------------------------------------------------------
35
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
36
  FILES_DIR = "/tmp/task_files"
37
  CACHE_PATH = "/tmp/answers_cache.json"
38
  os.makedirs(FILES_DIR, exist_ok=True)
39
 
40
+ WORKERS = int(os.getenv("WORKERS", "2")) # ๋™์‹œ ์‹คํ–‰ ๋ฌธํ•ญ ์ˆ˜
41
+ QUESTION_TIMEOUT = int(os.getenv("QUESTION_TIMEOUT", "300")) # ๋ฌธํ•ญ๋‹น ์ œํ•œ ์‹œ๊ฐ„(์ดˆ)
42
+ MAX_STEPS = int(os.getenv("MAX_STEPS", "8")) # ์—์ด์ „ํŠธ ์ตœ๋Œ€ ๋‹จ๊ณ„
43
+ MODEL_TIMEOUT = int(os.getenv("MODEL_TIMEOUT", "90")) # LLM 1ํšŒ ํ˜ธ์ถœ ์ œํ•œ(์ดˆ)
44
+ HEARTBEAT = 3 # ํ™”๋ฉด ๊ฐฑ์‹  ์ฃผ๊ธฐ(์ดˆ)
45
 
46
  ANSWER_RULES = """You are a careful research and reasoning agent solving GAIA-style questions.
47
  Rules:
 
59
  8. Finish by calling final_answer(...) with the answer only."""
60
 
61
  # ---------------------------------------------------------------------------
62
+ # ์ด๋ฒคํŠธ ๋กœ๊ทธ (ํ™”๋ฉด ์ƒํƒœ์ฐฝ์— ์ตœ๊ทผ ๊ธฐ๋ก์„ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•จ)
63
+ # ---------------------------------------------------------------------------
64
+ _events = deque(maxlen=10)
65
+ _active: dict = {} # task_id -> ์‹œ์ž‘ ์‹œ๊ฐ
66
+ _state_lock = threading.Lock()
67
+
68
+
69
+ def log(msg: str):
70
+ line = f"[{time.strftime('%H:%M:%S')}] {msg}"
71
+ print(line, flush=True)
72
+ with _state_lock:
73
+ _events.append(line)
74
+
75
+
76
+ def recent_log() -> str:
77
+ with _state_lock:
78
+ return "\n".join(_events)
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # ์บ์‹œ
83
  # ---------------------------------------------------------------------------
84
  _cache_lock = threading.Lock()
85
 
86
+
87
  def load_cache() -> dict:
88
  try:
89
  with open(CACHE_PATH, "r", encoding="utf-8") as f:
 
91
  except Exception: # noqa: BLE001
92
  return {}
93
 
94
+
95
  def save_cache_entry(task_id: str, answer: str):
96
  with _cache_lock:
97
  cache = load_cache()
 
99
  with open(CACHE_PATH, "w", encoding="utf-8") as f:
100
  json.dump(cache, f, ensure_ascii=False)
101
 
102
+
103
  # ---------------------------------------------------------------------------
104
+ # ๋„๊ตฌ
105
  # ---------------------------------------------------------------------------
106
  @tool
107
  def read_local_file(path: str) -> str:
108
  """Reads a local file (txt, csv, json, py, md, xlsx) and returns its content as text.
109
+
110
  Args:
111
  path: Absolute or relative path of the file to read.
112
  """
113
  try:
 
114
  if not os.path.exists(path):
115
  basename = os.path.basename(path)
116
+ matches = glob.glob(os.path.join(FILES_DIR, f"*{basename}"))
117
+ if matches:
118
+ path = matches[0]
 
119
  lower = path.lower()
120
  if lower.endswith((".xlsx", ".xls")):
121
  sheets = pd.read_excel(path, sheet_name=None)
 
129
  except Exception as e: # noqa: BLE001
130
  return f"Could not read file: {e}"
131
 
132
+
133
  # ---------------------------------------------------------------------------
134
+ # ๋ชจ๋ธ / ์—์ด์ „ํŠธ
135
  # ---------------------------------------------------------------------------
136
+ def gemini_model_id() -> str:
137
+ return os.getenv("GEMINI_MODEL", "gemini-3.8-flash").replace("gemini/", "")
138
+
139
+
140
  def build_model():
141
  gemini_key = os.getenv("GEMINI_API_KEY")
142
  if gemini_key:
143
+ return LiteLLMModel(
144
+ model_id=f"gemini/{gemini_model_id()}",
145
+ api_key=gemini_key,
146
+ timeout=MODEL_TIMEOUT, # LLM ํ˜ธ์ถœ์ด ๋ฌดํ•œ์ • ๋ฉˆ์ถ”์ง€ ์•Š๋„๋ก ์ œํ•œ
147
+ num_retries=2,
148
+ )
149
  openai_key = os.getenv("OPENAI_API_KEY")
150
  if openai_key:
151
+ return LiteLLMModel(
152
+ model_id=f"openai/{os.getenv('OPENAI_MODEL', 'gpt-4o-mini')}",
153
+ api_key=openai_key,
154
+ timeout=MODEL_TIMEOUT,
155
+ num_retries=2,
156
+ )
157
+ return InferenceClientModel(
158
+ model_id=os.getenv("HF_MODEL", "Qwen/Qwen2.5-Coder-32B-Instruct"),
159
+ token=os.getenv("HF_TOKEN"),
160
+ )
161
+
162
 
163
  def build_agent() -> CodeAgent:
 
164
  return CodeAgent(
165
  tools=[DuckDuckGoSearchTool(), VisitWebpageTool(), read_local_file],
166
  model=build_model(),
 
172
  verbosity_level=1,
173
  )
174
 
175
+
176
  def clean_answer(answer) -> str:
177
  text = str(answer).strip()
178
  text = re.sub(r"^\s*final answer\s*:\s*", "", text, flags=re.IGNORECASE)
179
  return text.strip().rstrip(".").strip()
180
 
181
+
182
  # ---------------------------------------------------------------------------
183
+ # ์ฑ„์  ์„œ๋ฒ„ ํ†ต์‹ 
184
  # ---------------------------------------------------------------------------
185
  def fetch_questions(api_url: str) -> list[dict]:
186
  resp = requests.get(f"{api_url}/questions", timeout=30)
 
190
  raise ValueError("์งˆ๋ฌธ ๋ชฉ๋ก์ด ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.")
191
  return data
192
 
193
+
194
  def download_task_file(api_url: str, task_id: str, file_name: str):
195
  if not file_name:
196
  return None
 
202
  f.write(resp.content)
203
  return path
204
  except Exception as e: # noqa: BLE001
205
+ log(f"ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ {task_id[:8]}: {e}")
206
  return None
207
 
208
+
209
  # ---------------------------------------------------------------------------
210
+ # ๋ฌธํ•ญ ํ’€์ด (์บ์‹œ + ์‹œ๊ฐ„ ์ œํ•œ)
211
  # ---------------------------------------------------------------------------
212
  def solve_one(item: dict, api_url: str) -> str:
213
  prompt = ANSWER_RULES + "\n\nQuestion:\n" + item["question"]
214
  file_path = download_task_file(api_url, item["task_id"], item.get("file_name", ""))
215
  if file_path:
216
  prompt += f"\n\nAn attached file is available at: {file_path}"
217
+ return clean_answer(build_agent().run(prompt))
 
 
 
218
 
219
+
220
+ def solve_with_timeout(item: dict, api_url: str):
221
+ """(๋‹ต๋ณ€, ์„ฑ๊ณต ์—ฌ๋ถ€, ๋น„๊ณ )๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
222
  task_id = item["task_id"]
223
+ short = task_id[:8]
224
  cached = load_cache().get(task_id)
225
  if cached:
226
+ log(f"์บ์‹œ ์‚ฌ์šฉ {short} -> {cached}")
227
+ return cached, True, "cache"
228
 
229
  box: dict = {}
230
+
231
  def target():
232
  try:
233
  box["answer"] = solve_one(item, api_url)
234
  except Exception as e: # noqa: BLE001
235
+ box["error"] = f"{type(e).__name__}: {e}"[:300]
236
 
237
+ log(f"์‹œ์ž‘ {short}")
238
+ with _state_lock:
239
+ _active[task_id] = time.time()
240
  t = threading.Thread(target=target, daemon=True)
241
  t.start()
242
  t.join(QUESTION_TIMEOUT)
243
+ with _state_lock:
244
+ _active.pop(task_id, None)
245
 
246
  if t.is_alive():
247
+ log(f"์‹œ๊ฐ„ ์ดˆ๊ณผ {short} ({QUESTION_TIMEOUT}์ดˆ)")
248
+ return "unknown", False, f"timeout {QUESTION_TIMEOUT}s"
249
  if "error" in box:
250
+ log(f"์˜ค๋ฅ˜ {short}: {box['error']}")
251
+ return "unknown", False, box["error"]
252
 
253
  save_cache_entry(task_id, box["answer"])
254
+ log(f"์™„๋ฃŒ {short} -> {box['answer']}")
255
+ return box["answer"], True, ""
256
+
257
 
258
  def iter_solve(questions: list[dict], api_url: str):
259
+ """๋๋‚œ ๋ฌธํ•ญ์€ (item, answer, ok, note)๋กœ, ๋Œ€๊ธฐ ์ค‘์—๋Š” ์ฃผ๊ธฐ์ ์œผ๋กœ None์„ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค."""
260
  valid = [q for q in questions if q.get("task_id") and q.get("question") is not None]
261
+ pool = ThreadPoolExecutor(max_workers=WORKERS)
262
+ try:
263
+ pending = {pool.submit(solve_with_timeout, q, api_url): q for q in valid}
264
+ while pending:
265
+ done, _ = wait(list(pending), timeout=HEARTBEAT, return_when=FIRST_COMPLETED)
266
+ if not done:
267
+ yield None
268
+ continue
269
+ for fut in done:
270
+ item = pending.pop(fut)
271
+ answer, ok, note = fut.result()
272
+ yield item, answer, ok, note
273
+ finally:
274
+ pool.shutdown(wait=False)
275
+
276
 
277
  def make_df(rows: dict, order: list[str]) -> pd.DataFrame:
278
  data = [rows[tid] for tid in order if tid in rows]
279
+ return pd.DataFrame(data, columns=["Task ID", "Question", "Submitted Answer", "OK", "Note"])
280
+
281
+
282
+ def status_text(label: str, done: int, total: int, start: float) -> str:
283
+ with _state_lock:
284
+ running = len(_active)
285
+ oldest = int(time.time() - min(_active.values())) if _active else 0
286
+ return (
287
+ f"{label}: {done}/{total} ์™„๋ฃŒ | ์‹คํ–‰ ์ค‘ {running}๊ฐœ "
288
+ f"(๊ฐ€์žฅ ์˜ค๋ž˜๋œ ๋ฌธํ•ญ {oldest}์ดˆ) | ์ด ๊ฒฝ๊ณผ {int(time.time() - start)}์ดˆ\n"
289
+ f"--- ์ตœ๊ทผ ๊ธฐ๋ก ---\n{recent_log()}"
290
+ )
291
+
292
+
293
+ def run_batch(questions: list[dict], api_url: str, label: str):
294
+ """์ง„ํ–‰ ์ƒํ™ฉ์„ ํ™”๋ฉด์— ๋‚ด๋ณด๋‚ด๊ณ , ๋งˆ์ง€๋ง‰์— (rows, order)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
295
+ order = [q["task_id"] for q in questions if q.get("task_id")]
296
+ rows: dict = {}
297
+ start = time.time()
298
+ log(f"{label} ์‹œ์ž‘: ์งˆ๋ฌธ {len(order)}๊ฐœ, WORKERS={WORKERS}, ๋ชจ๋ธ={gemini_model_id()}")
299
+ yield status_text(label, 0, len(order), start), None
300
+
301
+ for event in iter_solve(questions, api_url):
302
+ if event is not None:
303
+ item, answer, ok, note = event
304
+ rows[item["task_id"]] = {
305
+ "Task ID": item["task_id"],
306
+ "Question": item["question"],
307
+ "Submitted Answer": answer,
308
+ "OK": "O" if ok else "X",
309
+ "Note": note,
310
+ }
311
+ yield status_text(label, len(rows), len(order), start), make_df(rows, order)
312
+ return rows, order
313
+
314
 
315
  # ---------------------------------------------------------------------------
316
+ # Gradio ์ฝœ๋ฐฑ
317
  # ---------------------------------------------------------------------------
318
  def run_and_submit_all(profile: gr.OAuthProfile | None):
319
  space_id = os.getenv("SPACE_ID")
 
328
  api_url = DEFAULT_API_URL
329
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
330
 
331
+ yield "์งˆ๋ฌธ์„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘... (์ตœ๋Œ€ 30์ดˆ)", None
332
  try:
333
  questions = fetch_questions(api_url)
334
  except Exception as e: # noqa: BLE001
335
  yield f"์งˆ๋ฌธ ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜: {e}", None
336
  return
337
 
338
+ rows, order = yield from run_batch(questions, api_url, "๋ณธ ์‹คํ–‰")
 
 
 
 
 
 
 
 
 
 
 
 
 
339
 
340
  answers_payload = [
341
  {"task_id": tid, "submitted_answer": rows[tid]["Submitted Answer"]}
 
365
  status = f"์ œ์ถœ ์‹คํŒจ: {detail}"
366
  except Exception as e: # noqa: BLE001
367
  status = f"์ œ์ถœ ์ค‘ ์˜ค๋ฅ˜: {e}"
 
368
  yield status, make_df(rows, order)
369
 
370
+
371
  def run_test_only(n_questions: int):
372
  api_url = DEFAULT_API_URL
373
+ yield "์งˆ๋ฌธ์„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘... (์ตœ๋Œ€ 30์ดˆ)", None
374
  try:
375
  questions = fetch_questions(api_url)[: int(n_questions)]
376
  except Exception as e: # noqa: BLE001
377
  yield f"์ค€๋น„ ์ค‘ ์˜ค๋ฅ˜: {e}", None
378
  return
379
+ rows, order = yield from run_batch(questions, api_url, "ํ…Œ์ŠคํŠธ")
380
+ yield f"ํ…Œ์ŠคํŠธ ์™„๋ฃŒ (์ œ์ถœ ์•ˆ ํ•จ)\n--- ์ตœ๊ทผ ๊ธฐ๋ก ---\n{recent_log()}", make_df(rows, order)
381
 
 
 
 
382
 
383
+ def _call_with_timeout(fn, seconds: int):
384
+ box: dict = {}
385
+
386
+ def target():
387
+ try:
388
+ box["r"] = fn()
389
+ except Exception as e: # noqa: BLE001
390
+ box["e"] = f"{type(e).__name__}: {e}"[:300]
391
+
392
+ th = threading.Thread(target=target, daemon=True)
393
+ th.start()
394
+ th.join(seconds)
395
+ if th.is_alive():
396
+ return False, f"{seconds}์ดˆ ์•ˆ์— ์‘๋‹ต ์—†์Œ"
397
+ if "e" in box:
398
+ return False, box["e"]
399
+ return True, str(box.get("r"))[:150]
400
+
401
+
402
+ def diagnose():
403
+ """์ฑ„์  ์„œ๋ฒ„ / Gemini / ์›น ๊ฒ€์ƒ‰ ์—ฐ๊ฒฐ์„ ๊ฐ๊ฐ ์ ๊ฒ€ํ•ฉ๋‹ˆ๋‹ค."""
404
+ lines = [
405
+ f"์„ค์ •: ๋ชจ๋ธ={gemini_model_id()}, GEMINI_API_KEY={'์žˆ์Œ' if os.getenv('GEMINI_API_KEY') else '์—†์Œ'}, "
406
+ f"WORKERS={WORKERS}, MAX_STEPS={MAX_STEPS}, QUESTION_TIMEOUT={QUESTION_TIMEOUT}"
407
+ ]
408
+ yield "\n".join(lines + ["", "1/3 ์ฑ„์  ์„œ๋ฒ„ ํ™•์ธ ์ค‘..."])
409
+
410
+ ok, msg = _call_with_timeout(lambda: len(fetch_questions(DEFAULT_API_URL)), 35)
411
+ lines.append(f"1) ์ฑ„์  ์„œ๋ฒ„ ์งˆ๋ฌธ ๋ชฉ๋ก: {'์ •์ƒ' if ok else '์‹คํŒจ'} - {msg}")
412
+ yield "\n".join(lines + ["", "2/3 Gemini ํ˜ธ์ถœ ํ™•์ธ ์ค‘..."])
413
+
414
+ key = os.getenv("GEMINI_API_KEY")
415
+ if key:
416
+ def ping():
417
+ import litellm
418
+ r = litellm.completion(
419
+ model=f"gemini/{gemini_model_id()}",
420
+ api_key=key,
421
+ messages=[{"role": "user", "content": "Reply with the single word OK."}],
422
+ timeout=45,
423
+ )
424
+ return r.choices[0].message.content
425
+
426
+ ok, msg = _call_with_timeout(ping, 60)
427
+ lines.append(f"2) Gemini ํ˜ธ์ถœ: {'์ •์ƒ' if ok else '์‹คํŒจ'} - {msg}")
428
+ else:
429
+ lines.append("2) Gemini ํ˜ธ์ถœ: GEMINI_API_KEY๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•„ ๊ฑด๋„ˆ๋œ€")
430
+ yield "\n".join(lines + ["", "3/3 ์›น ๊ฒ€์ƒ‰ ํ™•์ธ ์ค‘..."])
431
+
432
+ ok, msg = _call_with_timeout(lambda: DuckDuckGoSearchTool()("Hugging Face"), 40)
433
+ lines.append(f"3) ์›น ๊ฒ€์ƒ‰(DuckDuckGo): {'์ •์ƒ' if ok else '์‹คํŒจ'} - {msg}")
434
+ yield "\n".join(lines)
435
 
 
436
 
437
  def clear_cache():
438
  try:
 
441
  except FileNotFoundError:
442
  return "์‚ญ์ œํ•  ์บ์‹œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
443
 
444
+
445
  # ---------------------------------------------------------------------------
446
+ # UI
447
  # ---------------------------------------------------------------------------
448
  with gr.Blocks() as demo:
449
  gr.Markdown("# Basic Agent Evaluation Runner (Gemini 3.8 Flash)")
450
  gr.Markdown(
451
  """**Instructions:**
452
+ 1. ๋จผ์ € '์—ฐ๊ฒฐ ์ง„๋‹จ'์œผ๋กœ ์ฑ„์  ์„œ๋ฒ„, Gemini, ์›น ๊ฒ€์ƒ‰์ด ์ •์ƒ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
453
+ 2. ๋กœ๊ทธ์ธ ํ›„ 'ํ…Œ์ŠคํŠธ ์‹คํ–‰'์œผ๋กœ ์†Œ์ˆ˜ ๋ฌธํ•ญ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. (์ œ์ถœ ์•ˆ ํ•จ)
454
+ 3. ๋ฌธ์ œ์—†์œผ๋ฉด 'Run Evaluation & Submit All Answers'๋ฅผ ๋ˆ„๋ฆ…๋‹ˆ๋‹ค."""
455
  )
456
  gr.LoginButton()
457
+
458
  with gr.Row():
459
+ diag_button = gr.Button("์—ฐ๊ฒฐ ์ง„๋‹จ")
460
  test_n = gr.Number(value=2, label="ํ…Œ์ŠคํŠธํ•  ๋ฌธํ•ญ ์ˆ˜ (์ œ์ถœ ์•ˆ ํ•จ)", precision=0)
461
  test_button = gr.Button("ํ…Œ์ŠคํŠธ ์‹คํ–‰ (์ œ์ถœ ์•ˆ ํ•จ)")
462
  cache_button = gr.Button("์บ์‹œ ์ดˆ๊ธฐํ™”")
463
+
464
  run_button = gr.Button("Run Evaluation & Submit All Answers")
465
+ status_output = gr.Textbox(label="Run Status / Submission Result", lines=14, interactive=False)
466
  results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
467
 
468
+ diag_button.click(fn=diagnose, outputs=[status_output])
469
  test_button.click(fn=run_test_only, inputs=[test_n], outputs=[status_output, results_table])
470
  run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])
471
  cache_button.click(fn=clear_cache, outputs=[status_output])