KaiserShultz/Ankelodon_AI_Multi_task_agentic_system
1
1"""Ankelodon Agent Adapter for the Hugging Face Agents Course evaluator.2 3This module exposes a simple Gradio-powered wrapper around the4`ankelodon_multiagent_system` project. It follows the same high-level flow5as the official GAIA template provided in the course materials: fetch6evaluation questions from the GAIA API, run your agent to produce7responses, and submit those responses back to the leaderboard.8 9The key differences between this adapter and the GAIA template are:10 11 * It imports and uses your multi‑agent system defined in the `src`12 package (see `src/agent.py`) via the `build_workflow` function. This13 function returns a `langgraph` state machine capable of planning,14 reasoning and executing tools. The adapter calls into this workflow15 with a properly initialised `AgentState` and extracts the final16 answer from the resulting state.17 * It automatically downloads any file attachments associated with a18 task (via the `/files/{task_id}` endpoint exposed by the evaluation19 server) and saves them into a temporary directory. The local file20 paths are passed into the agent through the `files` field of the21 state. Your existing file handling logic (e.g. `preprocess_files`22 in `src/tools/tools.py`) will detect the file type and suggest23 appropriate tools.24 * It strips any leading ``Final answer:`` prefix from the agent's25 response. The evaluation server performs an exact string match26 against the ground truth answer【842261069842380†L108-L112】, so it is27 important that the returned text contains only the answer and28 nothing else.29 30Before running this script yourself, make sure all dependencies in31`requirements.txt` are installed. To use the Gradio interface locally,32run `python ankelodon_adapter.py` from the project root. When deploying33as a Hugging Face Space for leaderboard submission, ensure the34`SPACE_ID` environment variable is set by the platform; it is used to35construct a link back to your code for verification.36"""37 38from __future__ import annotations39 40import os41import tempfile42from typing import Optional, List, Dict, Any43 44import requests45import gradio as gr46import pandas as pd47 48try:49 # Import the multi‑agent system components. When running as a script50 # within the project root, Python's module search path should51 # already include the `src` directory. If you get import errors,52 # ensure that the working directory is the repository root or53 # append `src` to `sys.path` manually before these imports.54 from src.agent import build_workflow55 from src.config import config as WORKFLOW_CONFIG56 from src.state import AgentState57except Exception as import_err:58 raise RuntimeError(59 "Failed to import the Ankelodon multi-agent system. "60 "Make sure you are running this script from the repository root "61 "and that the project has been installed correctly."62 ) from import_err63 64DEFAULT_API_URL: str = "https://agents-course-unit4-scoring.hf.space"65 66 67class AnkelodonAgent:68 """Simple callable wrapper around the Ankelodon multi‑agent system.69 70 Instances of this class can be called directly with a natural71 language question and an optional task identifier. Under the hood it72 builds a `langgraph` workflow using ``build_workflow()``, prepares73 an initial state, fetches any file attachments associated with74 the task, and invokes the workflow to compute a final answer.75 """76 77 def __init__(self) -> None:78 # Initialise the workflow once per agent. Subsequent calls reuse79 # the compiled state machine, which is more efficient than80 # rebuilding it on every question.81 self.workflow = build_workflow()82 83 def _download_attachment(self, task_id: str) -> List[str]:84 """Download a file attachment for the given task ID.85 86 The evaluation API exposes a ``/files/{task_id}`` endpoint【842261069842380†L95-L107】.87 This helper downloads the content, infers a file extension88 from the HTTP ``Content-Type`` header and writes the bytes to a89 temporary file. It returns a list of file paths (zero or one90 element) to be included in the agent state.91 """92 files: List[str] = []93 url = f"{DEFAULT_API_URL}/files/{task_id}"94 try:95 resp = requests.get(url, timeout=15, allow_redirects=True)96 if resp.status_code == 200 and resp.content:97 # Map common MIME substrings to file extensions. The98 # multi‑agent system's file handling tools use the99 # extension to determine how to process the file.100 ctype = resp.headers.get("content-type", "").lower()101 ext_map = {102 "excel": ".xlsx",103 "sheet": ".xlsx",104 "csv": ".csv",105 "python": ".py",106 "audio": ".mp3",107 "image": ".jpg",108 }109 extension = ""110 for key, val in ext_map.items():111 if key in ctype:112 extension = val113 break114 tmp_dir = tempfile.mkdtemp(prefix="ankelodon_task_")115 filename = f"attachment{extension}"116 path = os.path.join(tmp_dir, filename)117 with open(path, "wb") as fh:118 fh.write(resp.content)119 files.append(path)120 except Exception as e:121 # Log the error to console but don't fail the entire task.122 print(f"[WARNING] Failed to fetch attachment for task {task_id}: {e}")123 return files124 125 def __call__(self, question: str, task_id: Optional[str] = None) -> str:126 """Run the multi‑agent system to answer a question.127 128 Parameters129 ----------130 question: str131 The natural language query to answer.132 task_id: Optional[str]133 If provided, the ID used to fetch any associated file134 attachment from the evaluation API. Attachments are stored135 locally and passed into the agent via the ``files`` field.136 137 Returns138 -------139 str140 The final answer produced by the agent, with any "final141 answer" prefix removed. If no answer is produced the empty142 string is returned.143 """144 # Build the initial agent state. The AgentState type defines145 # numerous fields, many of which the workflow populates146 # internally. We set only the essentials here. Unrecognised147 # keys are ignored by the underlying state machine.148 state: Dict[str, Any] = {149 "query": question,150 "final_answer": "",151 "plan": None,152 "complexity_assessment": None,153 "current_step": 0,154 "reasoning_done": False,155 "messages": [],156 "files": [],157 "file_contents": {},158 "critique_feedback": None,159 "iteration_count": 0,160 "max_iterations": 3,161 "execution_report": None,162 "previous_tool_results": {},163 "critic_replan" : False,164 }165 166 # If a task ID is provided, attempt to download its attachment.167 if task_id:168 attachment_paths = self._download_attachment(task_id)169 if attachment_paths:170 state["files"] = attachment_paths171 172 # Invoke the workflow. The `config` parameter defines runtime173 # options such as recursion limits and thread identifiers. It is174 # imported from `src.config`.175 try:176 result_state = self.workflow.invoke(state, config=WORKFLOW_CONFIG)177 except Exception as e:178 print(f"[ERROR] Failed to run workflow: {e}")179 return ""180 181 # Extract the final answer. Depending on the branch taken,182 # either the ``final_answer`` key or a generic ``answer`` key may183 # be present. Use whichever exists. Some nodes may prepend184 # "final answer:"; remove it for exact match scoring【842261069842380†L108-L112】.185 answer = ""186 if isinstance(result_state, dict):187 answer = result_state.get("execution_report") or ""188 answer = answer.final_answer189 if answer:190 answer = answer.replace("Final answer:", "").replace("final answer:", "").strip()191 return answer192 193 194def run_and_submit_all(profile: Optional[gr.OAuthProfile]) -> tuple[str, pd.DataFrame | None]:195 """Fetch all questions, run the agent, and submit the answers.196 197 This function replicates the behaviour of the GAIA template's198 ``run_and_submit_all`` function【566837548679297†L247-L306】 but uses the199 ``AnkelodonAgent`` class defined above. It is bound to a Gradio200 button in the UI. On success it returns a status message and a201 DataFrame of results; on failure it returns an error message and202 ``None`` or an empty DataFrame.203 """204 # Require the user to be logged in so we can report the username.205 if not profile:206 return "Please Login to Hugging Face with the button.", None207 username = getattr(profile, "username", "").strip()208 209 api_url = DEFAULT_API_URL210 questions_url = f"{api_url}/questions"211 submit_url = f"{api_url}/submit"212 213 # Instantiate the agent once.214 try:215 agent = AnkelodonAgent()216 print("Ankelodon agent initialised successfully")217 except Exception as e:218 err_msg = f"Error initialising agent: {e}"219 print(err_msg)220 return err_msg, None221 222 # Fetch questions from the evaluation API.【566837548679297†L247-L268】223 try:224 print(f"Fetching questions from: {questions_url}")225 resp = requests.get(questions_url, timeout=15)226 resp.raise_for_status()227 questions_data = resp.json()228 if not questions_data:229 return "Fetched questions list is empty or invalid format.", None230 print(f"Fetched {len(questions_data)} questions.")231 except Exception as e:232 err_msg = f"Error fetching questions: {e}"233 print(err_msg)234 return err_msg, None235 236 # Run the agent on each question.237 results_log: List[Dict[str, Any]] = []238 answers_payload: List[Dict[str, str]] = []239 print(f"Running agent on {len(questions_data)} questions…")240 for number, item in enumerate(questions_data):241 task_id = item.get("task_id")242 question_text = item.get("question")243 if not task_id or question_text is None:244 print(f"Skipping item with missing task_id or question: {item}")245 continue246 try:247 print(f"===== QUESTION {number + 1}/{len(questions_data)} (ID: {task_id}): {question_text} ==== ")248 answer = agent(question_text, task_id)249 answers_payload.append({"task_id": task_id, "submitted_answer": answer})250 results_log.append({251 "Task ID": task_id,252 "Question": question_text,253 "Submitted Answer": answer,254 })255 except Exception as e:256 print(f"Error running agent on task {task_id}: {e}")257 results_log.append({258 "Task ID": task_id,259 "Question": question_text,260 "Submitted Answer": f"AGENT ERROR: {e}",261 })262 263 if not answers_payload:264 return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)265 266 # Prepare submission payload. The leaderboard displays a link to your267 # code; this is constructed from the SPACE_ID environment variable.268 space_id = os.getenv("SPACE_ID", "")269 agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main" if space_id else ""270 submission_data = {271 "username": username,272 "agent_code": agent_code,273 "answers": answers_payload,274 }275 276 print(f"Submitting {len(answers_payload)} answers to: {submit_url}")277 try:278 submission_resp = requests.post(submit_url, json=submission_data, timeout=60)279 submission_resp.raise_for_status()280 result_data = submission_resp.json()281 final_status = (282 f"Submission Successful!\n"283 f"User: {result_data.get('username')}\n"284 f"Overall Score: {result_data.get('score', 'N/A')}% "285 f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"286 f"Message: {result_data.get('message', 'No message received.')}"287 )288 print("Submission successful.")289 return final_status, pd.DataFrame(results_log)290 except Exception as e:291 err_msg = f"Submission Failed: {e}"292 print(err_msg)293 return err_msg, pd.DataFrame(results_log)294 295 296# Build the Gradio interface. This interface resembles the official297# GAIA template【566837548679297†L372-L401】 but runs your Ankelodon agent.298with gr.Blocks() as demo:299 gr.Markdown("# Ankelodon Agent Evaluation Runner")300 gr.Markdown(301 """302 **Instructions**303 304 1. Clone this repository or duplicate the associated Hugging Face Space.305 2. Log in to your Hugging Face account using the button below. Your HF306 username is used to attribute your submission on the leaderboard.307 3. Click **Run Evaluation & Submit All Answers** to fetch the questions,308 run the Ankelodon agent on each one, submit your answers, and display309 the resulting score and answers.310 311 ---312 This template is intentionally lightweight. Feel free to customise it –313 add caching, parallel execution or additional logging as you see fit.314 """315 )316 gr.LoginButton()317 run_button = gr.Button("Run Evaluation & Submit All Answers")318 status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)319 results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)320 run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])321 322 323if __name__ == "__main__":324 # When running locally, print some information about the environment.325 print("\n" + "-" * 30 + " Ankelodon Adapter Starting " + "-" * 30)326 space_host_startup = os.getenv("SPACE_HOST")327 space_id_startup = os.getenv("SPACE_ID")328 if space_host_startup:329 print(f"✅ SPACE_HOST found: {space_host_startup}")330 print(f" Runtime URL should be: https://{space_host_startup}.hf.space")331 else:332 print("ℹ️ SPACE_HOST environment variable not found (running locally?).")333 if space_id_startup:334 print(f"✅ SPACE_ID found: {space_id_startup}")335 print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")336 print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")337 else:338 print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")339 print("-" * (60 + len(" Ankelodon Adapter Starting ")) + "\n")340 # Launch the Gradio app.341 demo.launch(debug=True, share=False)