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