CoolFace
Apppublic

KaiserShultz/Ankelodon_AI_Multi_task_agentic_system

sourceHugging Facemitupdated 1y agoView on Hugging Face
1likes
main.py338 linesDownload Raw Back to src
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 agent import build_workflow55    from config import config as WORKFLOW_CONFIG56    from 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        }164 165        # If a task ID is provided, attempt to download its attachment.166        if task_id:167            attachment_paths = self._download_attachment(task_id)168            if attachment_paths:169                state["files"] = attachment_paths170 171        # Invoke the workflow. The `config` parameter defines runtime172        # options such as recursion limits and thread identifiers. It is173        # imported from `src.config`.174        try:175            result_state = self.workflow.invoke(state, config=WORKFLOW_CONFIG)176        except Exception as e:177            print(f"[ERROR] Failed to run workflow: {e}")178            return ""179 180        # Extract the final answer. Depending on the branch taken,181        # either the ``final_answer`` key or a generic ``answer`` key may182        # be present. Use whichever exists. Some nodes may prepend183        # "final answer:"; remove it for exact match scoring【842261069842380†L108-L112】.184        answer = ""185        if isinstance(result_state, dict):186            answer = result_state.get("final_answer") or result_state.get("answer") or ""187        if answer:188            answer = answer.replace("Final answer:", "").replace("final answer:", "").strip()189        return answer190 191 192def run_and_submit_all(profile: Optional[gr.OAuthProfile]) -> tuple[str, pd.DataFrame | None]:193    """Fetch all questions, run the agent, and submit the answers.194 195    This function replicates the behaviour of the GAIA template's196    ``run_and_submit_all`` function【566837548679297†L247-L306】 but uses the197    ``AnkelodonAgent`` class defined above. It is bound to a Gradio198    button in the UI. On success it returns a status message and a199    DataFrame of results; on failure it returns an error message and200    ``None`` or an empty DataFrame.201    """202    # Require the user to be logged in so we can report the username.203    if not profile:204        return "Please Login to Hugging Face with the button.", None205    username = getattr(profile, "username", "").strip()206 207    api_url = DEFAULT_API_URL208    questions_url = f"{api_url}/questions"209    submit_url = f"{api_url}/submit"210 211    # Instantiate the agent once.212    try:213        agent = AnkelodonAgent()214        print("Ankelodon agent initialised successfully")215    except Exception as e:216        err_msg = f"Error initialising agent: {e}"217        print(err_msg)218        return err_msg, None219 220    # Fetch questions from the evaluation API.【566837548679297†L247-L268】221    try:222        print(f"Fetching questions from: {questions_url}")223        resp = requests.get(questions_url, timeout=15)224        resp.raise_for_status()225        questions_data = resp.json()226        if not questions_data:227            return "Fetched questions list is empty or invalid format.", None228        print(f"Fetched {len(questions_data)} questions.")229    except Exception as e:230        err_msg = f"Error fetching questions: {e}"231        print(err_msg)232        return err_msg, None233 234    # Run the agent on each question.235    results_log: List[Dict[str, Any]] = []236    answers_payload: List[Dict[str, str]] = []237    print(f"Running agent on {len(questions_data)} questions…")238    for item in questions_data:239        task_id = item.get("task_id")240        question_text = item.get("question")241        if not task_id or question_text is None:242            print(f"Skipping item with missing task_id or question: {item}")243            continue244        try:245            answer = agent(question_text, task_id)246            answers_payload.append({"task_id": task_id, "submitted_answer": answer})247            results_log.append({248                "Task ID": task_id,249                "Question": question_text,250                "Submitted Answer": answer,251            })252        except Exception as e:253            print(f"Error running agent on task {task_id}: {e}")254            results_log.append({255                "Task ID": task_id,256                "Question": question_text,257                "Submitted Answer": f"AGENT ERROR: {e}",258            })259 260    if not answers_payload:261        return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)262 263    # Prepare submission payload. The leaderboard displays a link to your264    # code; this is constructed from the SPACE_ID environment variable.265    space_id = os.getenv("SPACE_ID", "")266    agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main" if space_id else ""267    submission_data = {268        "username": username,269        "agent_code": agent_code,270        "answers": answers_payload,271    }272 273    print(f"Submitting {len(answers_payload)} answers to: {submit_url}")274    try:275        submission_resp = requests.post(submit_url, json=submission_data, timeout=60)276        submission_resp.raise_for_status()277        result_data = submission_resp.json()278        final_status = (279            f"Submission Successful!\n"280            f"User: {result_data.get('username')}\n"281            f"Overall Score: {result_data.get('score', 'N/A')}% "282            f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"283            f"Message: {result_data.get('message', 'No message received.')}"284        )285        print("Submission successful.")286        return final_status, pd.DataFrame(results_log)287    except Exception as e:288        err_msg = f"Submission Failed: {e}"289        print(err_msg)290        return err_msg, pd.DataFrame(results_log)291 292 293# Build the Gradio interface. This interface resembles the official294# GAIA template【566837548679297†L372-L401】 but runs your Ankelodon agent.295with gr.Blocks() as demo:296    gr.Markdown("# Ankelodon Agent Evaluation Runner")297    gr.Markdown(298        """299        **Instructions**300        301        1. Clone this repository or duplicate the associated Hugging Face Space.302        2. Log in to your Hugging Face account using the button below. Your HF303           username is used to attribute your submission on the leaderboard.304        3. Click **Run Evaluation & Submit All Answers** to fetch the questions,305           run the Ankelodon agent on each one, submit your answers, and display306           the resulting score and answers.307        308        ---309        This template is intentionally lightweight. Feel free to customise it –310        add caching, parallel execution or additional logging as you see fit.311        """312    )313    gr.LoginButton()314    run_button = gr.Button("Run Evaluation & Submit All Answers")315    status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)316    results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)317    run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])318 319 320if __name__ == "__main__":321    # When running locally, print some information about the environment.322    print("\n" + "-" * 30 + " Ankelodon Adapter Starting " + "-" * 30)323    space_host_startup = os.getenv("SPACE_HOST")324    space_id_startup = os.getenv("SPACE_ID")325    if space_host_startup:326        print(f"✅ SPACE_HOST found: {space_host_startup}")327        print(f"   Runtime URL should be: https://{space_host_startup}.hf.space")328    else:329        print("ℹ️  SPACE_HOST environment variable not found (running locally?).")330    if space_id_startup:331        print(f"✅ SPACE_ID found: {space_id_startup}")332        print(f"   Repo URL: https://huggingface.co/spaces/{space_id_startup}")333        print(f"   Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")334    else:335        print("ℹ️  SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")336    print("-" * (60 + len(" Ankelodon Adapter Starting ")) + "\n")337    # Launch the Gradio app.338    demo.launch(debug=True, share=False)