CoolFace
Apppublic

KaiserShultz/Ankelodon_AI_Multi_task_agentic_system

sourceHugging Facemitupdated 1y agoView on Hugging Face
1likes
app.py341 linesDownload Raw Back to root
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)