Spaces:
Sleeping
Sleeping
Download tools/attachments.py from jwlee-ai/Final_Assignment_Template: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/spaces/jwlee-ai/Final_Assignment_Template/resolve/main/tools/attachments.py
- Command line
-
hf download hf://spaces/jwlee-ai/Final_Assignment_Template/tools/attachments.py
-
curl -L -o attachments.py https://huggingface.co/spaces/jwlee-ai/Final_Assignment_Template/resolve/main/tools/attachments.py
12.7 kB
| """GAIA ์ฒจ๋ถ ํ์ผ ์ฒ๋ฆฌ + ์ง๋ฌธโtask_id ์ธ๋ฑ์ค. | |
| CodeAgent์ ์๊ทธ๋์ฒ ์ ์ฝ(__call__์ด question๋ง ๋ฐ์) ๋๋ฌธ์ task_id๋ฅผ ์ง์ | |
| ์ฃผ์ ํ ์ ์์ด, ๋ชจ๋ ์ ์ญ mutable ์ปจํ ์ด๋ + prefetch ์ธ๋ฑ์ค๋ก ์ฐํํ๋ค. | |
| ํ๋ฆ: | |
| 1) BasicAgent.__init__ ๋จ๊ณ์ prefetch_question_index() โ /questions 1ํ ํธ์ถ | |
| ํด์ {์ง๋ฌธ๋ณธ๋ฌธ: task_id} ์ฌ์ ์ ๋ง๋ค๊ณ set_question_index() ๋ก ๋ฑ๋ก. | |
| 2) BasicAgent.__call__ ์ง์ ์ set_current_task(question) ์ผ๋ก ํ์ฌ ๋ฌธ์ ์ | |
| task_id์ ์ง๋ฌธ ๋ณธ๋ฌธ์ _CURRENT_TASK ์ ์ธํ . | |
| 3) ์์ด์ ํธ๊ฐ get_attached_file() ์ ์ธ์ ์์ด ํธ์ถํ๋ฉด _CURRENT_TASK ์ | |
| task_id๋ก ์ฑ์ ์๋ฒ์์ ํ์ผ์ ๋ฐ์์ค๊ณ , ํ์ ๋ณ๋ก ์ฒ๋ฆฌ: | |
| - ํ ์คํธ/CSV/JSON/code: UTF-8 ๋์ฝ๋ฉ | |
| - Excel(.xlsx): ์ํธ๋ณ CSV | |
| - PDF: ํ์ด์ง๋ณ ํ ์คํธ ์ถ์ถ (pypdf) | |
| - ์ด๋ฏธ์ง: VLM(Qwen2.5-VL-7B)์ผ๋ก ํ์ฌ ์ง๋ฌธ ์ปจํ ์คํธ์ ๋ง์ถฐ ๋ถ์ | |
| - ์ค๋์ค: Whisper(large-v3) ์ ์ฌ | |
| """ | |
| import io | |
| import re | |
| import requests | |
| from smolagents import tool | |
| # ์ฑ์ ์๋ฒ URL์ ์ฌ๊ธฐ์๋ ํ ๋ฒ ์ ์ (app.py์ ๋์ผ ๊ฐ). | |
| # tools ๋ชจ๋์ ๋ ๋ฆฝ์ ์ผ๋ก ์ฌ์ฉํ๋๋ผ๋ ์๋ฏธ๊ฐ ํตํ๋๋ก ๋ถ๋ฆฌํด ๋๋ค. | |
| _DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space" | |
| # BasicAgent.__call__ ์ง์ ์ ๊ฐฑ์ ๋๋ mutable ์ปจํ ์ด๋. | |
| # question์ ์ด๋ฏธ์ง VLM ํธ์ถ ์ ์ปจํ ์คํธ(prompt)๋ก ์ฌ์ฉ๋๋ค. | |
| _CURRENT_TASK = {"id": None, "question": None} | |
| # question.strip() -> task_id ์ฌ์ . | |
| _QUESTION_INDEX: dict = {} | |
| def prefetch_question_index() -> dict: | |
| """์ฑ์ ์๋ฒ /questions ๋ฅผ ํ ๋ฒ ํธ์ถํด {์ง๋ฌธ๋ณธ๋ฌธ: task_id} ์ฌ์ ์ ๋น๋ํ๋ค. | |
| ์คํจํด๋ ๋น dict๋ฅผ ๋ฐํํด ์์ด์ ํธ๊ฐ ์ฒจ๋ถ ์๋ ๋ฌธ์ ๋ง์ด๋ผ๋ ํ ์ ์๊ฒ ํ๋ค.""" | |
| try: | |
| r = requests.get(f"{_DEFAULT_API_URL}/questions", timeout=15) | |
| r.raise_for_status() | |
| idx = {} | |
| for item in r.json(): | |
| qt = (item.get("question") or "").strip() | |
| tid = item.get("task_id") | |
| if qt and tid: | |
| if qt in idx and idx[qt] != tid: | |
| print( | |
| "Warning: duplicate question text in prefetch index โ " | |
| f"task_id {idx[qt]!r} will be overwritten by {tid!r}" | |
| ) | |
| idx[qt] = tid | |
| return idx | |
| except Exception as e: | |
| print(f"Warning: could not prefetch question index: {e}") | |
| return {} | |
| def set_question_index(idx: dict) -> None: | |
| """BasicAgent.__init__์์ prefetch ๊ฒฐ๊ณผ๋ฅผ ๋ชจ๋ ์ ์ญ์ ๋ฐ์์ฃผ๋ ์ธํฐ.""" | |
| global _QUESTION_INDEX | |
| _QUESTION_INDEX = idx | |
| def set_current_task(question: str): | |
| """BasicAgent.__call__ ์ง์ ์ ํ์ฌ ๋ฌธ์ ์ task_id์ ์ง๋ฌธ ๋ณธ๋ฌธ์ ๋ชจ๋ ์ ์ญ์ ์ธํ . | |
| ์ง๋ฌธ ๋ณธ๋ฌธ์ ์ด๋ฏธ์ง ์ฒจ๋ถ์ VLM ํธ์ถ์ prompt ์ปจํ ์คํธ๋ก ์ ๋ฌ๋๋ค. | |
| ๋งค์นญ ์คํจ ์ task_id๋ก None์ด ๋ค์ด๊ฐ์ง๋ง question์ ๊ทธ๋๋ก ์ ์ฅ๋๋ค.""" | |
| tid = _QUESTION_INDEX.get(question.strip()) | |
| _CURRENT_TASK["id"] = tid | |
| _CURRENT_TASK["question"] = question | |
| return tid | |
| # --- ํ์ผ ํ์ ๋ถ๊ธฐ ํฌํผ --- | |
| def _extract_filename(headers, url: str) -> str: | |
| """Content-Disposition ํค๋์์ filename์ ๋ฝ๊ฑฐ๋, URL ๋๋ถ๋ถ์ผ๋ก ํด๋ฐฑ. | |
| ์ฑ์ ์๋ฒ๊ฐ Content-Type์ octet-stream์ผ๋ก ์ค ๋ ํ์ฅ์๋ก ๋ณด๊ฐํ๊ธฐ ์ํจ.""" | |
| cd = headers.get("Content-Disposition", "") | |
| # filename* (RFC 5987) ์ filename= ์์ชฝ ๋ค ์ฒ๋ฆฌ. | |
| m = re.search(r'filename\*?=(?:UTF-8\'\')?"?([^";\r\n]+)"?', cd) | |
| if m: | |
| return m.group(1).strip().strip('"') | |
| return url.rsplit("/", 1)[-1] | |
| def _is_excel(content_type: str, ext: str) -> bool: | |
| if ext in ("xlsx", "xls"): | |
| return True | |
| ct = content_type.lower() | |
| return "spreadsheet" in ct or ct.endswith("xlsx") or ct.endswith("xls") or "excel" in ct | |
| def _is_pdf(content_type: str, ext: str) -> bool: | |
| return ext == "pdf" or "pdf" in content_type.lower() | |
| def _is_image(content_type: str, ext: str) -> bool: | |
| return ext in ("png", "jpg", "jpeg", "webp", "gif", "bmp") \ | |
| or content_type.lower().startswith("image/") | |
| def _is_audio(content_type: str, ext: str) -> bool: | |
| return ext in ("mp3", "wav", "m4a", "ogg", "flac") \ | |
| or content_type.lower().startswith("audio/") | |
| # --- ํ์ ๋ณ ํธ๋ค๋ฌ --- | |
| def _handle_excel(content: bytes, content_type: str) -> str: | |
| """xlsx โ ์ํธ๋ณ CSV๋ก ์ง๋ ฌํ. GAIA์ ๋งค์ถ/ํ๋งค ๋ฐ์ดํฐ ๋ฌธ์ ๊ฐ ์์ฃผ ๋์จ๋ค.""" | |
| try: | |
| import pandas as _pd | |
| bio = io.BytesIO(content) | |
| sheets = _pd.read_excel(bio, sheet_name=None) | |
| parts = [] | |
| for name, df in sheets.items(): | |
| parts.append(f"--- Sheet: {name} ---\n{df.to_csv(index=False)}") | |
| combined = "\n\n".join(parts) | |
| if len(combined) > 12000: | |
| combined = combined[:12000] + "\n...[truncated]" | |
| return f"[Content-Type: {content_type}]\n{combined}" | |
| except Exception as e: | |
| return f"Excel parse error: {e}" | |
| def _handle_pdf(content: bytes, content_type: str) -> str: | |
| """pypdf๋ก PDF ๋ณธ๋ฌธ ํ ์คํธ ์ถ์ถ. ํ์ด์ง๋ณ๋ก ๊ตฌ๋ถํด์ ๋ฐํ. | |
| ์ค์บ PDF(์ด๋ฏธ์ง๋ก ๋)๋ ํ ์คํธ๊ฐ ๋น๊ฑฐ๋ ๊นจ์ง ์ ์๋๋ฐ, ๊ทธ ๊ฒฝ์ฐ๋ | |
| LLM์ด ์ํค/์น๊ฒ์์ผ๋ก ํด๋ฐฑํ๋๋ก ์์คํ ํ๋กฌํํธ๊ฐ ์ ๋ํ๋ค.""" | |
| try: | |
| from pypdf import PdfReader | |
| bio = io.BytesIO(content) | |
| reader = PdfReader(bio) | |
| parts = [] | |
| for i, page in enumerate(reader.pages): | |
| try: | |
| txt = page.extract_text() or "" | |
| except Exception as pe: | |
| txt = f"(extraction failed: {pe})" | |
| parts.append(f"--- Page {i+1} ---\n{txt}") | |
| combined = "\n\n".join(parts) | |
| if len(combined) > 12000: | |
| combined = combined[:12000] + "\n...[truncated]" | |
| return f"[PDF, {len(reader.pages)} pages, Content-Type: {content_type}]\n{combined}" | |
| except Exception as e: | |
| return f"PDF parse error: {e}" | |
| def _handle_image(content: bytes, content_type: str) -> str: | |
| """VLM(Qwen2.5-VL-7B)์ผ๋ก ํ์ฌ ์ง๋ฌธ ์ปจํ ์คํธ์ ๋ง์ถฐ ์ด๋ฏธ์ง๋ฅผ ๋ถ์ํ๋ค. | |
| HF Inference API์ OpenAI ํธํ chat_completion์ผ๋ก base64 data URL์ ์ ์กํ๋ค. | |
| ์ง๋ฌธ ์ปจํ ์คํธ๊ฐ ์์ผ๋ฉด ๊ทธ๊ฑธ ๊ทธ๋๋ก prompt์ ๋ฐ์ ์ ๋ต์ ์ง์ ๋์์ด ๋๋ | |
| ๋ถ๋ถ๋ง ๋ฝ์๋ด๋๋ก ์ ๋(generic ์บก์ ์ ๋ํ ์ผ์ ๋์นจ). ํธ์ถ ์คํจ ์ ์๋ฌ | |
| ๋ฌธ์์ด์ ๋ฐํํด ์์ด์ ํธ๊ฐ ๋ค๋ฅธ ์ ๋ต์ผ๋ก ํด๋ฐฑํ ์ ์๊ฒ ํ๋ค. | |
| HF_TOKEN ํ๊ฒฝ๋ณ์๊ฐ ํ์ํ๋ค. Space ๋ฐฐํฌ ์์๋ Space secrets์ ๋ฑ๋กํด์ผ ํจ. | |
| """ | |
| try: | |
| import base64 | |
| from huggingface_hub import InferenceClient | |
| question = (_CURRENT_TASK.get("question") or "").strip() | |
| # ๋ฐ์ดํฐ URL ๊ตฌ์ฑ. content_type์ด image/* ๊ฐ ์๋ ์๋ ์์ด ์์ ํ๊ฒ ํด๋ฐฑ. | |
| mime = content_type.split(";")[0].strip() | |
| if not mime.startswith("image/"): | |
| mime = "image/png" | |
| b64 = base64.b64encode(content).decode("utf-8") | |
| data_url = f"data:{mime};base64,{b64}" | |
| if question: | |
| prompt = ( | |
| "Analyze the attached image and answer the following question. " | |
| "Read any text, numbers, or labels visible in the image. " | |
| "If it is a chart or table, extract the relevant data values precisely.\n\n" | |
| f"Question: {question}" | |
| ) | |
| else: | |
| prompt = ( | |
| "Describe the attached image in detail, including any visible text, " | |
| "numbers, or labels." | |
| ) | |
| client = InferenceClient(provider="auto") # HF_TOKEN ํ๊ฒฝ๋ณ์ ์ฌ์ฉ | |
| resp = client.chat_completion( | |
| model="Qwen/Qwen2.5-VL-7B-Instruct", | |
| messages=[ | |
| { | |
| "role": "user", | |
| "content": [ | |
| {"type": "text", "text": prompt}, | |
| {"type": "image_url", "image_url": {"url": data_url}}, | |
| ], | |
| } | |
| ], | |
| max_tokens=1024, | |
| ) | |
| analysis = resp.choices[0].message.content | |
| return ( | |
| f"[Image analysis (Content-Type: {content_type}, {len(content)} bytes)]\n" | |
| f"{analysis}" | |
| ) | |
| except Exception as e: | |
| return ( | |
| f"Image attached (Content-Type: {content_type}, {len(content)} bytes). " | |
| f"VLM analysis failed: {e}" | |
| ) | |
| def _handle_audio(content: bytes, content_type: str) -> str: | |
| """Whisper(large-v3)๋ก ์ค๋์ค ์ ์ฌ. GAIA ์ค๋์ค๋ ๋ณดํต ์งง์ ๋ฐํ๋ผ ํ ๋ฒ ํธ์ถ๋ก ์ถฉ๋ถ. | |
| HF_TOKEN ํ๊ฒฝ๋ณ์๊ฐ ํ์ํ๋ค. Space ๋ฐฐํฌ ์์๋ Space secrets์ ๋ฑ๋กํด์ผ ํจ. | |
| """ | |
| try: | |
| from huggingface_hub import InferenceClient | |
| client = InferenceClient(provider="auto") | |
| result = client.automatic_speech_recognition( | |
| audio=content, | |
| model="openai/whisper-large-v3", | |
| ) | |
| # huggingface_hub ๋ฒ์ ์ ๋ฐ๋ผ dict ๋๋ dataclass-like ๊ฐ์ฒด๋ก ๋ฐํ๋๋ฏ๋ก | |
| # ์์ชฝ ๋ชจ๋ ์ฒ๋ฆฌํ๋ค. | |
| if hasattr(result, "text"): | |
| transcription = result.text | |
| elif isinstance(result, dict): | |
| transcription = result.get("text", str(result)) | |
| else: | |
| transcription = str(result) | |
| return ( | |
| f"[Audio transcription (Content-Type: {content_type}, {len(content)} bytes)]\n" | |
| f"{transcription}" | |
| ) | |
| except Exception as e: | |
| return ( | |
| f"Audio attached (Content-Type: {content_type}, {len(content)} bytes). " | |
| f"Transcription failed: {e}" | |
| ) | |
| def get_attached_file() -> str: | |
| """Download the file attached to the CURRENT GAIA task and return its content. | |
| Takes no arguments โ the current task_id is auto-resolved from the question. | |
| Use this whenever the question references a file, spreadsheet, image, audio, PDF, code listing, | |
| CSV, or any external resource. Returns: | |
| - Text/CSV/JSON/code: the decoded text (truncated to ~12k chars). | |
| - Excel (.xlsx): each sheet rendered as CSV (truncated). | |
| - PDF: extracted text per page (truncated). | |
| - Image (PNG/JPEG/WEBP/GIF/BMP): a vision-language model analysis focused on the current question. | |
| - Audio (MP3/WAV/M4A/OGG/FLAC): a Whisper transcription. | |
| - Other binary: a metadata description (size + content-type). | |
| """ | |
| # ์๊ทธ๋์ฒ ์ ์ฝ ๋๋ฌธ์ task_id ์ธ์๋ฅผ ๋ฐ์ง ์๊ณ , ๋ชจ๋ ์ ์ญ _CURRENT_TASK ์์ ๊ฐ์ ธ์จ๋ค. | |
| # ์ด ๊ฐ์ BasicAgent.__call__ ์ง์ ์ set_current_task()๋ก ์ธํ ๋๋ค. | |
| task_id = _CURRENT_TASK.get("id") | |
| if not task_id: | |
| return "No task context available โ likely no file attached for this question." | |
| try: | |
| url = f"{_DEFAULT_API_URL}/files/{task_id}" | |
| r = requests.get(url, timeout=30) | |
| if r.status_code == 404: | |
| return "No file attached to this task." | |
| r.raise_for_status() | |
| content_type = r.headers.get("Content-Type", "") | |
| filename = _extract_filename(r.headers, url) | |
| ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else "" | |
| # 1) ๋ช ํํ ๋ฐ์ด๋๋ฆฌ ํ์ ์ ๋จผ์ ์ฒ๋ฆฌ. | |
| # ์ผ๋ถ PDF/SVG๋ UTF-8 decode๊ฐ ๋์ด๋ ์์ ํ ์คํธ๋ก ๋ฐํํ๋ฉด ํ์ง์ด ํฌ๊ฒ ๋จ์ด์ง๋ค. | |
| if _is_excel(content_type, ext): | |
| return _handle_excel(r.content, content_type) | |
| if _is_pdf(content_type, ext): | |
| return _handle_pdf(r.content, content_type) | |
| if _is_image(content_type, ext): | |
| return _handle_image(r.content, content_type) | |
| if _is_audio(content_type, ext): | |
| return _handle_audio(r.content, content_type) | |
| # 2) ํ ์คํธ ๊ณ์ด์ด๋ฉด UTF-8๋ก ๋ฐํ. | |
| try: | |
| text = r.content.decode("utf-8") | |
| if len(text) > 12000: | |
| text = text[:12000] + "\n...[truncated]" | |
| return f"[Content-Type: {content_type}]\n{text}" | |
| except UnicodeDecodeError: | |
| pass | |
| # 3) ์ ์ ์๋ ๋ฐ์ด๋๋ฆฌ โ ๋ฉํ๋ฐ์ดํฐ๋ง ๋ฐํ. | |
| return ( | |
| f"Binary file (Content-Type: {content_type}, " | |
| f"size: {len(r.content)} bytes). Cannot display as text. " | |
| f"URL: {url}" | |
| ) | |
| except Exception as e: | |
| return f"get_attached_file error: {e}" | |