Spaces:
Runtime error
Runtime error
| import os | |
| import gradio as gr | |
| import requests | |
| import inspect | |
| import pandas as pd | |
| import spaces | |
| from langchain_openai import ChatOpenAI | |
| from langchain.agents import create_agent | |
| from langchain.agents.middleware import ToolCallLimitMiddleware | |
| from langchain_tavily import TavilySearch | |
| from langchain_core.messages import SystemMessage | |
| # (Keep Constants as is) | |
| # --- Constants --- | |
| DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space" | |
| def zerogpu_function(): | |
| return "ok" | |
| # --- Basic Agent Definition --- | |
| # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------ | |
| # class BasicAgent: | |
| # def __init__(self): | |
| # print("BasicAgent initialized.") | |
| # def __call__(self, question: str) -> str: | |
| # print(f"Agent received question (first 50 chars): {question[:50]}...") | |
| # fixed_answer = "This is a default answer." | |
| # print(f"Agent returning fixed answer: {fixed_answer}") | |
| # return fixed_answer | |
| from datasets import load_dataset | |
| from huggingface_hub import hf_hub_download | |
| from langchain.tools import tool | |
| from pathlib import Path | |
| import base64 | |
| from langfuse import get_client | |
| from langfuse.langchain import CallbackHandler | |
| from openai import OpenAI | |
| langfuse = get_client() | |
| langfuse_handler = CallbackHandler() | |
| gaia_dataset = load_dataset( | |
| "gaia-benchmark/GAIA", | |
| "2023_all", | |
| split="validation", | |
| ) | |
| task_map = { | |
| item["task_id"]: item | |
| for item in gaia_dataset | |
| } | |
| omni_client = OpenAI( | |
| api_key=os.environ["DASHSCOPE_API_KEY"], | |
| base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", | |
| ) | |
| def read_text_file(file_path: str) -> str: | |
| """Read a local text or Python file and return its contents.""" | |
| with open(file_path, "r", encoding="utf-8") as f: | |
| return f.read() | |
| def download_attachment(task_id: str, file_name: str) -> str: | |
| """ | |
| Download the attachment associated with a GAIA task. | |
| Use this tool when the question has an attachment and you need | |
| the local file path in order to inspect the file. | |
| Args: | |
| task_id: The GAIA task ID. | |
| file_name: The name of the file to download. | |
| Returns: | |
| The local path of the downloaded attachment. | |
| """ | |
| item = task_map.get(task_id) | |
| if item is None: | |
| return f"ERROR: task_id {task_id} was not found. Do not call this tool again." | |
| if not file_name or not file_name.strip(): | |
| return "ERROR: This task has no attachment. Do not call this tool again." | |
| file_name = item.get("file_name") | |
| repo_file_path = item.get("file_path") | |
| if not file_name or not repo_file_path: | |
| return "ERROR: This task has no attachment. Do not call this tool again." | |
| local_path = hf_hub_download( | |
| repo_id="gaia-benchmark/GAIA", | |
| repo_type="dataset", | |
| filename=repo_file_path, | |
| ) | |
| return local_path | |
| from pypdf import PdfReader | |
| def read_pdf(file_path: str) -> str: | |
| """ | |
| Read a local PDF file and return its extracted text. | |
| Use this tool for .pdf attachments after download_attachment | |
| has returned the local file path. | |
| """ | |
| reader = PdfReader(file_path) | |
| pages = [] | |
| for page in reader.pages: | |
| text = page.extract_text() | |
| if text: | |
| pages.append(text) | |
| return "\n\n".join(pages) | |
| def read_excel(file_path: str) -> str: | |
| """Read an Excel spreadsheet and return its contents.""" | |
| import pandas as pd | |
| df = pd.read_excel(file_path) | |
| return df.to_string() | |
| def analyze_media(file_path: str) -> str: | |
| """ | |
| Inspect an image or audio attachment and return the factual | |
| information contained in it. | |
| This tool is for perception only. | |
| For images: | |
| - Describe visible objects, text, labels, numbers, positions, | |
| tables, diagrams, or other observable details accurately. | |
| - Preserve exact text and numbers when possible. | |
| - Do NOT solve the user's question. | |
| - Do NOT infer the final answer. | |
| - Do NOT perform domain reasoning beyond what is necessary | |
| to describe the media. | |
| For audio: | |
| - Transcribe the spoken content accurately. | |
| - Identify clearly observable speakers or sound events when useful. | |
| - Do NOT solve the user's question. | |
| - Do NOT infer the final answer. | |
| Args: | |
| file_path: Local path of the media file. | |
| Returns: | |
| A factual description or transcription of the media. | |
| """ | |
| path = Path(file_path) | |
| if not path.exists(): | |
| return f"ERROR: File does not exist: {file_path}" | |
| suffix = path.suffix.lower() | |
| # 读取文件 → Base64 | |
| with open(path, "rb") as f: | |
| file_base64 = base64.b64encode(f.read()).decode("utf-8") | |
| # ---------- 图片 ---------- | |
| if suffix in {".png", ".jpg", ".jpeg"}: | |
| media_prompt = """ | |
| Inspect this image carefully. | |
| Return only the factual information that is directly observable | |
| in the image. | |
| Preserve exact text, labels, numbers, symbols, spatial relationships, | |
| and positions when relevant. | |
| Do not answer any external question. | |
| Do not solve the task. | |
| Do not infer a final answer. | |
| Your job is only to convert the visual information into an accurate | |
| text representation for another reasoning agent. | |
| """ | |
| mime_type = { | |
| ".png": "image/png", | |
| ".jpg": "image/jpeg", | |
| ".jpeg": "image/jpeg", | |
| }[suffix] | |
| content = [ | |
| { | |
| "type": "image_url", | |
| "image_url": { | |
| "url": f"data:{mime_type};base64,{file_base64}" | |
| }, | |
| }, | |
| { | |
| "type": "text", | |
| "text": media_prompt.strip(), | |
| }, | |
| ] | |
| # ---------- 音频 ---------- | |
| elif suffix in {".mp3", ".wav"}: | |
| audio_format = suffix[1:] # ".mp3" -> "mp3" | |
| media_prompt = """ | |
| Listen to this audio carefully. | |
| Produce an accurate transcription of the spoken content. | |
| Preserve names, numbers, dates, and other important details. | |
| Do not answer questions about the audio. | |
| Do not solve any task. | |
| Your job is only to convert the audio information into text for | |
| another reasoning agent. | |
| """ | |
| content = [ | |
| { | |
| "type": "input_audio", | |
| "input_audio": { | |
| "data": f"data:;base64,{file_base64}", | |
| "format": audio_format, | |
| }, | |
| }, | |
| { | |
| "type": "text", | |
| "text": media_prompt.strip(), | |
| }, | |
| ] | |
| else: | |
| return f"ERROR: Unsupported media type: {suffix}" | |
| # ---------- 调 Qwen Omni ---------- | |
| completion = omni_client.chat.completions.create( | |
| model="qwen3.5-omni-flash", | |
| messages=[ | |
| { | |
| "role": "user", | |
| "content": content, | |
| } | |
| ], | |
| modalities=["text"], | |
| stream=True, | |
| timeout=20, | |
| ) | |
| # Omni 是 streaming,把文字拼起来 | |
| result = "" | |
| for chunk in completion: | |
| if ( | |
| chunk.choices | |
| and chunk.choices[0].delta.content | |
| ): | |
| result += chunk.choices[0].delta.content | |
| return result.strip() | |
| search_tool = TavilySearch( | |
| max_results=3, | |
| topic="general", | |
| search_depth="basic", | |
| # include_domains=["wikipedia.org"], | |
| ) | |
| model = ChatOpenAI( | |
| model="qwen3.5-plus", | |
| api_key=os.environ["DASHSCOPE_API_KEY"], | |
| base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", | |
| temperature=0, | |
| extra_body={"enable_thinking": False}, | |
| ) | |
| from pydantic import BaseModel, Field | |
| from langchain.agents.structured_output import ToolStrategy | |
| class FinalAnswer(BaseModel): | |
| answer: str = Field( | |
| description=( | |
| "Only the exact final answer requested by the question. " | |
| "No reasoning, no explanation, no prefix such as " | |
| "'Final answer:', and no extra commentary." | |
| ) | |
| ) | |
| class BasicAgent: | |
| def __init__(self): | |
| print("BasicAgent initialized.") | |
| agent_model = model | |
| self.search_calls = 0 | |
| def limited_search_tool(query: str) -> str: | |
| """Search Wikipedia for facts needed to answer the current question.""" | |
| if self.search_calls >= 3: | |
| return ( | |
| "SEARCH_LIMIT_REACHED: You have already used all three allowed " | |
| "web searches for this question. Answer using the information " | |
| "already available." | |
| ) | |
| self.search_calls += 1 | |
| return str(search_tool.invoke({"query": query})) | |
| self.limited_search_tool = limited_search_tool | |
| self.agent = create_agent( | |
| model=agent_model, | |
| tools=[ | |
| download_attachment, | |
| read_text_file, | |
| analyze_media, | |
| read_excel, | |
| read_pdf, | |
| self.limited_search_tool, | |
| ], | |
| middleware=[ | |
| ToolCallLimitMiddleware( | |
| tool_name="web_search", | |
| run_limit=3, | |
| exit_behavior="end", | |
| ) | |
| ], | |
| response_format=ToolStrategy(FinalAnswer), | |
| system_prompt=""" | |
| You are an agent solving GAIA benchmark questions. | |
| Follow this decision process strictly. | |
| 1. ATTACHMENT CHECK | |
| Only enter the attachment workflow if the user message exactly includes "There is an attachment for this question.". | |
| - Use download_attachment with the provided task_id. | |
| - After obtaining the local file path, choose the appropriate tool: | |
| - Text or source-code file -> read_text_file | |
| - Excel file -> read_excel | |
| - PDF file -> read_pdf | |
| - Image or audio file -> analyze_media | |
| - Use the information returned by the file tool to answer the original question. | |
| - Do not invent or guess the contents of the attachment. | |
| - Inspect the attachment before answering if the question depends on it. | |
| 2. NO ATTACHMENT | |
| If the user message does NOT explicitly state that an attachment is present: | |
| - Do NOT call download_attachment. | |
| - Do NOT call read_text_file. | |
| - Do NOT call read_excel. | |
| - Do NOT call read_pdf. | |
| - Do NOT call analyze_media. | |
| Then determine whether the question requires web search or external information. | |
| Use web_search only when external or up-to-date information is necessary. | |
| Prefer answering from the available attachment or your own knowledge. | |
| Use at most three web searches per question, and use fewer when possible. | |
| Make each query precise and Wikipedia-focused. | |
| Only make a follow-up search when the earlier Wikipedia results are insufficient. | |
| Do not repeat equivalent or overly broad searches. | |
| 3. FINAL ANSWER FORMAT | |
| Return only the exact final answer requested by the question. | |
| Do not include explanations. | |
| Do not include reasoning. | |
| Do not include prefixes such as "Final answer:". | |
| Follow the exact answer format requested by the question. | |
| """ | |
| ) | |
| def __call__( | |
| self, | |
| question: str, | |
| task_id: str = "", | |
| file_name: str = "" | |
| ) -> str: | |
| self.search_calls = 0 | |
| content = question | |
| if file_name: | |
| content += f""" | |
| There is an attachment for this question. | |
| File name: {file_name} | |
| Task ID: {task_id} | |
| Use an appropriate file tool if you need to inspect the attachment. | |
| """ | |
| print(f"Agent received question: {content[:100]}...") | |
| result = self.agent.invoke({ | |
| "messages": [ | |
| { | |
| "role": "user", | |
| "content": content | |
| } | |
| ], | |
| }, | |
| config={ | |
| "callbacks": [langfuse_handler] | |
| } | |
| ) | |
| structured_response = result.get("structured_response") | |
| if structured_response is None: | |
| print("Web-search limit reached; generating a final answer without tools.") | |
| final_message = model.invoke( | |
| [ | |
| SystemMessage( | |
| content=( | |
| "The research phase is complete because the web-search " | |
| "limit was reached. Do not call tools or browse. Answer " | |
| "the original question using the search results already in " | |
| "this conversation and your own knowledge.\n\n" | |
| "FINAL ANSWER FORMAT — mandatory:\n" | |
| "- Return only the exact final answer requested by the original question.\n" | |
| "- Do not include reasoning, explanations, sources, or prefixes.\n" | |
| "- Follow the exact requested format, including units, dates, " | |
| "capitalization, ordering, and number of items." | |
| ) | |
| ), | |
| *result["messages"], | |
| ] | |
| ) | |
| answer = str(final_message.content).strip() | |
| print(f"Agent answer after search limit: {answer}") | |
| return answer or "GIVE UP" | |
| answer = structured_response.answer.strip() | |
| print(f"Agent answer: {answer}") | |
| return answer | |
| def run_and_submit_all( profile: gr.OAuthProfile | None): | |
| """ | |
| Fetches all questions, runs the BasicAgent on them, submits all answers, | |
| and displays the results. | |
| """ | |
| # --- Determine HF Space Runtime URL and Repo URL --- | |
| space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code | |
| if profile: | |
| username= f"{profile.username}" | |
| print(f"User logged in: {username}") | |
| else: | |
| print("User not logged in.") | |
| return "Please Login to Hugging Face with the button.", None | |
| api_url = DEFAULT_API_URL | |
| questions_url = f"{api_url}/questions" | |
| submit_url = f"{api_url}/submit" | |
| # 1. Instantiate Agent ( modify this part to create your agent) | |
| try: | |
| agent = BasicAgent() | |
| except Exception as e: | |
| print(f"Error instantiating agent: {e}") | |
| return f"Error initializing agent: {e}", None | |
| # In the case of an app running as a hugging Face space, this link points toward your codebase ( usefull for others so please keep it public) | |
| agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main" | |
| print(agent_code) | |
| # 2. Fetch Questions | |
| print(f"Fetching questions from: {questions_url}") | |
| try: | |
| response = requests.get(questions_url, timeout=15) | |
| response.raise_for_status() | |
| questions_data = response.json() | |
| if not questions_data: | |
| print("Fetched questions list is empty.") | |
| return "Fetched questions list is empty or invalid format.", None | |
| print(f"Fetched {len(questions_data)} questions.") | |
| except requests.exceptions.RequestException as e: | |
| print(f"Error fetching questions: {e}") | |
| return f"Error fetching questions: {e}", None | |
| except requests.exceptions.JSONDecodeError as e: | |
| print(f"Error decoding JSON response from questions endpoint: {e}") | |
| print(f"Response text: {response.text[:500]}") | |
| return f"Error decoding server response for questions: {e}", None | |
| except Exception as e: | |
| print(f"An unexpected error occurred fetching questions: {e}") | |
| return f"An unexpected error occurred fetching questions: {e}", None | |
| # 3. Run your Agent | |
| results_log = [] | |
| answers_payload = [] | |
| print(f"Running agent on {len(questions_data)} questions...") | |
| for item in questions_data: | |
| task_id = item.get("task_id") | |
| question_text = item.get("question") | |
| if not task_id or question_text is None: | |
| print(f"Skipping item with missing task_id or question: {item}") | |
| continue | |
| try: | |
| submitted_answer = agent(question_text, task_id=task_id, file_name=item.get("file_name", "")) | |
| answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer}) | |
| results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer}) | |
| except Exception as e: | |
| print(f"Error running agent on task {task_id}: {e}") | |
| results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"}) | |
| if not answers_payload: | |
| print("Agent did not produce any answers to submit.") | |
| return "Agent did not produce any answers to submit.", pd.DataFrame(results_log) | |
| # 4. Prepare Submission | |
| submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload} | |
| status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..." | |
| print(status_update) | |
| # 5. Submit | |
| print(f"Submitting {len(answers_payload)} answers to: {submit_url}") | |
| try: | |
| response = requests.post(submit_url, json=submission_data, timeout=60) | |
| response.raise_for_status() | |
| result_data = response.json() | |
| final_status = ( | |
| f"Submission Successful!\n" | |
| f"User: {result_data.get('username')}\n" | |
| f"Overall Score: {result_data.get('score', 'N/A')}% " | |
| f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n" | |
| f"Message: {result_data.get('message', 'No message received.')}" | |
| ) | |
| print("Submission successful.") | |
| results_df = pd.DataFrame(results_log) | |
| return final_status, results_df | |
| except requests.exceptions.HTTPError as e: | |
| error_detail = f"Server responded with status {e.response.status_code}." | |
| try: | |
| error_json = e.response.json() | |
| error_detail += f" Detail: {error_json.get('detail', e.response.text)}" | |
| except requests.exceptions.JSONDecodeError: | |
| error_detail += f" Response: {e.response.text[:500]}" | |
| status_message = f"Submission Failed: {error_detail}" | |
| print(status_message) | |
| results_df = pd.DataFrame(results_log) | |
| return status_message, results_df | |
| except requests.exceptions.Timeout: | |
| status_message = "Submission Failed: The request timed out." | |
| print(status_message) | |
| results_df = pd.DataFrame(results_log) | |
| return status_message, results_df | |
| except requests.exceptions.RequestException as e: | |
| status_message = f"Submission Failed: Network error - {e}" | |
| print(status_message) | |
| results_df = pd.DataFrame(results_log) | |
| return status_message, results_df | |
| except Exception as e: | |
| status_message = f"An unexpected error occurred during submission: {e}" | |
| print(status_message) | |
| results_df = pd.DataFrame(results_log) | |
| return status_message, results_df | |
| # --- Build Gradio Interface using Blocks --- | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# Basic Agent Evaluation Runner") | |
| gr.Markdown( | |
| """ | |
| **Instructions:** | |
| 1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ... | |
| 2. Log in to your Hugging Face account using the button below. This uses your HF username for submission. | |
| 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score. | |
| --- | |
| **Disclaimers:** | |
| Once clicking on the "submit button, it can take quite some time ( this is the time for the agent to go through all the questions). | |
| This space provides a basic setup and is intentionally sub-optimal to encourage you to develop your own, more robust solution. For instance for the delay process of the submit button, a solution could be to cache the answers and submit in a seperate action or even to answer the questions in async. | |
| """ | |
| ) | |
| gr.LoginButton() | |
| run_button = gr.Button("Run Evaluation & Submit All Answers") | |
| status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False) | |
| # Removed max_rows=10 from DataFrame constructor | |
| results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True) | |
| run_button.click( | |
| fn=run_and_submit_all, | |
| outputs=[status_output, results_table] | |
| ) | |
| if __name__ == "__main__": | |
| print("\n" + "-"*30 + " App Starting " + "-"*30) | |
| # Check for SPACE_HOST and SPACE_ID at startup for information | |
| space_host_startup = os.getenv("SPACE_HOST") | |
| space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup | |
| if space_host_startup: | |
| print(f"✅ SPACE_HOST found: {space_host_startup}") | |
| print(f" Runtime URL should be: https://{space_host_startup}.hf.space") | |
| else: | |
| print("ℹ️ SPACE_HOST environment variable not found (running locally?).") | |
| if space_id_startup: # Print repo URLs if SPACE_ID is found | |
| print(f"✅ SPACE_ID found: {space_id_startup}") | |
| print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}") | |
| print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main") | |
| else: | |
| print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.") | |
| print("-"*(60 + len(" App Starting ")) + "\n") | |
| print("Launching Gradio Interface for Basic Agent Evaluation...") | |
| demo.launch(debug=True, share=False) | |