diff --git a/.gitignore b/.gitignore index b18528112..c8b7382aa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,21 @@ logs tmp wandb +make_test_log.xml +runs/ +runs_old/ +runs_v0/ +output/ + +#Test gaia +wb/ +pdb5wb7.ent +downloads_folder/ +model_performance_comparison.png +langfuse_test.py # Data data -outputs data/ # Apple @@ -148,8 +159,11 @@ interpreter_workspace/ # Archive archive/ savedir/ -output/ +#output/ tool_output/ # Gradio runtime -.gradio/ \ No newline at end of file +.gradio/ + +#Other cache +.ruff_cache/ \ No newline at end of file diff --git a/Makefile b/Makefile index 602248712..98cc19564 100644 --- a/Makefile +++ b/Makefile @@ -14,4 +14,4 @@ style: # Run smolagents tests test: - pytest ./tests/ \ No newline at end of file + pytest ./tests/ --junitxml=make_test_log.xml \ No newline at end of file diff --git a/README.md b/README.md index dac23c04b..61e58c643 100644 --- a/README.md +++ b/README.md @@ -30,56 +30,98 @@ limitations under the License. -`smolagents` is a library that enables you to run powerful agents in a few lines of code. It offers: +`smolagents` is a lightweight library for building autonomous agents that solve tasks using code and tools. Agents run as independent processes and communicate directly via message queues using the `SendMessageTool` and `ReceiveMessagesTool`. Each agent maintains its own queue in a shared dictionary for decentralized task processing. -✨ **Simplicity**: the logic for agents fits in ~1,000 lines of code (see [agents.py](https://github.com/huggingface/smolagents/blob/main/src/smolagents/agents.py)). We kept abstractions to their minimal shape above raw code! -πŸ§‘β€πŸ’» **First-class support for Code Agents**. Our [`CodeAgent`](https://huggingface.co/docs/smolagents/reference/agents#smolagents.CodeAgent) writes its actions in code (as opposed to "agents being used to write code"). To make it secure, we support executing in sandboxed environments via [E2B](https://e2b.dev/), [Modal](https://modal.com/), Docker, or Pyodide+Deno WebAssembly sandbox. +✨ **Simplicity**: The core logic fits in ~1,000 lines of code (see [agents.py](https://github.com/huggingface/smolagents/blob/main/src/smolagents/agents.py)), keeping abstractions minimal. +πŸ§‘β€πŸ’» **First-class support for Code Agents**. Our [`CodeAgent`](https://huggingface.co/docs/smolagents/reference/agents#smolagents.CodeAgent) writes its actions in code (as opposed to "agents being used to write code"). To make it secure, we support executing in sandboxed environments via [E2B](https://e2b.dev/), Docker, or Pyodide+Deno WebAssembly sandbox. -πŸ€— **Hub integrations**: you can [share/pull tools or agents to/from the Hub](https://huggingface.co/docs/smolagents/reference/tools#smolagents.Tool.from_hub) for instant sharing of the most efficient agents! -🌐 **Model-agnostic**: smolagents supports any LLM. It can be a local `transformers` or `ollama` model, one of [many providers on the Hub](https://huggingface.co/blog/inference-providers), or any model from OpenAI, Anthropic and many others via our [LiteLLM](https://www.litellm.ai/) integration. +πŸ€— **Hub integrations**: Share or pull tools and agents to/from the Hub for instant collaboration (see [Tool.from_hub](https://huggingface.co/docs/smolagents/reference/tools#smolagents.Tool.from_hub)). -πŸ‘οΈ **Modality-agnostic**: Agents support text, vision, video, even audio inputs! Cf [this tutorial](https://huggingface.co/docs/smolagents/examples/web_browser) for vision. +🌐 **Model-agnostic**: Supports any LLM, including local `transformers` or `ollama` models, [HF inference providers](https://huggingface.co/blog/inference-providers), or models from OpenAI, Anthropic, and others via [LiteLLM](https://www.litellm.ai/). -πŸ› οΈ **Tool-agnostic**: you can use tools from any [MCP server](https://huggingface.co/docs/smolagents/reference/tools#smolagents.ToolCollection.from_mcp), from [LangChain](https://huggingface.co/docs/smolagents/reference/tools#smolagents.Tool.from_langchain), you can even use a [Hub Space](https://huggingface.co/docs/smolagents/reference/tools#smolagents.Tool.from_space) as a tool. +πŸ‘οΈ **Modality-agnostic**: Agents handle text, vision, video, and audio inputs (see [vision tutorial](https://huggingface.co/docs/smolagents/examples/web_browser)). -Full documentation can be found [here](https://huggingface.co/docs/smolagents/index). +πŸ› οΈ **Tool-agnostic**: Use tools from [MCP servers](https://huggingface.co/docs/smolagents/reference/tools#smolagents.ToolCollection.from_mcp), [LangChain](https://huggingface.co/docs/smolagents/reference/tools#smolagents.Tool.from_langchain), or [Hub Spaces](https://huggingface.co/docs/smolagents/reference/tools#smolagents.Tool.from_space). + +Full documentation is available [here](https://huggingface.co/docs/smolagents/index). > [!NOTE] -> Check the our [launch blog post](https://huggingface.co/blog/smolagents) to learn more about `smolagents`! +> Check our [launch blog post](https://huggingface.co/blog/smolagents) to learn more about `smolagents`! + +## Quick Demo -## Quick demo +Install the package with default tools: -First install the package with a default set of tools: ```bash pip install "smolagents[toolkit]" ``` -Then define your agent, give it the tools it needs and run it! -```py -from smolagents import CodeAgent, WebSearchTool, InferenceClientModel -model = InferenceClientModel() -agent = CodeAgent(tools=[WebSearchTool()], model=model, stream_outputs=True) +Set your Hugging Face API key: -agent.run("How many seconds would it take for a leopard at full speed to run through Pont des Arts?") +```bash +export HF_TOKEN=your_huggingface_api_key_here ``` -https://github.com/user-attachments/assets/84b149b4-246c-40c9-a48d-ba013b08e600 +Run multiple agents to solve a task collaboratively: -You can even share your agent to the Hub, as a Space repository: -```py -agent.push_to_hub("m-ric/my_agent") +```python +from multiprocessing import Manager, Process +from smolagents import ( + CodeAgent, + InferenceClientModel, + WebSearchTool, + SendMessageTool, + ReceiveMessagesTool, +) +def start_agent(agent_id, queue_dict, task=None): + model = InferenceClientModel() + tools = [ + WebSearchTool(), + SendMessageTool(queue_dict, agent_id), + ReceiveMessagesTool(queue_dict, agent_id), + ] + agent = CodeAgent( + tools=tools, + model=model, + additional_authorized_imports=["numpy", "pandas"], + ) + agent.run(task=task) + +if __name__ == "__main__": + task = "How many seconds would it take for a leopard at full speed to run through Pont des Arts?" + num_agents = 2 + with Manager() as manager: + queue_dict = manager.dict() + for i in range(num_agents): + queue_dict[i] = manager.Queue() + processes = [ + Process(target=start_agent, args=(i, queue_dict, task if i == 0 else None)) + for i in range(num_agents) + ] + for p in processes: + p.start() + for p in processes: + p.join() +``` + +This launches two `CodeAgent` instances. Agent 0 processes the task and may send subtasks (e.g., code or search results) to Agent 1 via message queues using `SendMessageTool` and `ReceiveMessagesTool`. Agents use tools like `web_search` or `python_interpreter` and return results with `final_answer()`. + +You can share your agent to the Hub as a Space repository: + +```python +agent.push_to_hub("m-ric/my_agent") # agent.from_hub("m-ric/my_agent") to load an agent from Hub ``` -Our library is LLM-agnostic: you could switch the example above to any inference provider. +`smolagents` is LLM-agnostic. Switch the model as needed:
- InferenceClientModel, gateway for all inference providers supported on HF + InferenceClientModel (HF inference providers) -```py +```python from smolagents import InferenceClientModel model = InferenceClientModel( @@ -89,9 +131,9 @@ model = InferenceClientModel( ```
- LiteLLM to access 100+ LLMs + LiteLLM (100+ LLMs) -```py +```python from smolagents import LiteLLMModel model = LiteLLMModel( @@ -104,36 +146,35 @@ model = LiteLLMModel(
OpenAI-compatible servers: Together AI -```py +```python import os from smolagents import OpenAIModel model = OpenAIModel( model_id="deepseek-ai/DeepSeek-R1", - api_base="https://api.together.xyz/v1/", # Leave this blank to query OpenAI servers. - api_key=os.environ["TOGETHER_API_KEY"], # Switch to the API key for the server you're targeting. + api_base="https://api.together.xyz/v1/", + api_key=os.environ["TOGETHER_API_KEY"], ) ```
OpenAI-compatible servers: OpenRouter -```py +```python import os from smolagents import OpenAIModel model = OpenAIModel( model_id="openai/gpt-4o", - api_base="https://openrouter.ai/api/v1", # Leave this blank to query OpenAI servers. - api_key=os.environ["OPENROUTER_API_KEY"], # Switch to the API key for the server you're targeting. + api_base="https://openrouter.ai/api/v1", + api_key=os.environ["OPENROUTER_API_KEY"], ) ``` -
Local `transformers` model -```py +```python from smolagents import TransformersModel model = TransformersModel( @@ -146,126 +187,96 @@ model = TransformersModel(
Azure models -```py +```python import os from smolagents import AzureOpenAIModel + model = AzureOpenAIModel( model_id = os.environ.get("AZURE_OPENAI_MODEL"), + azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"), api_key=os.environ.get("AZURE_OPENAI_API_KEY"), - api_version=os.environ.get("OPENAI_API_VERSION") + api_version=os.environ.get("OPENAI_API_VERSION") ) ```
Amazon Bedrock models -```py +```python import os from smolagents import AmazonBedrockModel + model = AmazonBedrockModel( model_id = os.environ.get("AMAZON_BEDROCK_MODEL_ID") + ) ```
## CLI -You can run agents from CLI using two commands: `smolagent` and `webagent`. +Run agents from the CLI using `smolagent` or `webagent`. -`smolagent` is a generalist command to run a multi-step `CodeAgent` that can be equipped with various tools. +`smolagent` runs multiple `CodeAgent` or `ToolCallingAgent` instances that collaborate via message queues: ```bash + smolagent "Plan a trip to Tokyo, Kyoto and Osaka between Mar 28 and Apr 7." --model-type "InferenceClientModel" --model-id "Qwen/Qwen3-Next-80B-A3B-Thinking" --imports pandas numpy --tools web_search ``` -Meanwhile `webagent`Β is a specific web-browsing agent using [helium](https://github.com/mherrmann/helium) (read more [here](https://github.com/huggingface/smolagents/blob/main/src/smolagents/vision_web_browser.py)). +`webagent` is a specific web-browsing agent using [helium](https://github.com/mherrmann/helium) (read more [here](https://github.com/huggingface/smolagents/blob/main/src/smolagents/vision_web_browser.py)): -For instance: ```bash + webagent "go to xyz.com/men, get to sale section, click the first clothing item you see. Get the product details, and the price, return them. note that I'm shopping from France" --model-type "LiteLLMModel" --model-id "gpt-5" ``` -## How do Code agents work? - -Our [`CodeAgent`](https://huggingface.co/docs/smolagents/reference/agents#smolagents.CodeAgent) works mostly like classical ReAct agents - the exception being that the LLM engine writes its actions as Python code snippets. - -```mermaid -flowchart TB - Task[User Task] - Memory[agent.memory] - Generate[Generate from agent.model] - Execute[Execute Code action - Tool calls are written as functions] - Answer[Return the argument given to 'final_answer'] - - Task -->|Add task to agent.memory| Memory - - subgraph ReAct[ReAct loop] - Memory -->|Memory as chat messages| Generate - Generate -->|Parse output to extract code action| Execute - Execute -->|No call to 'final_answer' tool => Store execution logs in memory and keep running| Memory - end - - Execute -->|Call to 'final_answer' tool| Answer - - %% Styling - classDef default fill:#d4b702,stroke:#8b7701,color:#ffffff - classDef io fill:#4a5568,stroke:#2d3748,color:#ffffff - - class Task,Answer io -``` +## How Do Agents Work? -Actions are now Python code snippets. Hence, tool calls will be performed as Python function calls. For instance, here is how the agent can perform web search over several websites in one single action: -```py -requests_to_search = ["gulf of mexico america", "greenland denmark", "tariffs"] -for request in requests_to_search: - print(f"Here are the search results for {request}:", web_search(request)) -``` +Agents in `smolagents` run as independent processes. Each agent has a queue inside a shared dictionary, and they communicate by sending messages with `SendMessageTool` and retrieving them with `ReceiveMessagesTool`. This decentralized approach eliminates the need for a centralized ReAct loop. -Writing actions as code snippets is demonstrated to work better than the current industry practice of letting the LLM output a dictionary of the tools it wants to call: [uses 30% fewer steps](https://huggingface.co/papers/2402.01030) (thus 30% fewer LLM calls) and [reaches higher performance on difficult benchmarks](https://huggingface.co/papers/2411.01747). Head to [our high-level intro to agents](https://huggingface.co/docs/smolagents/conceptual_guides/intro_agents) to learn more on that. +[`CodeAgent`](https://huggingface.co/docs/smolagents/reference/agents#smolagents.CodeAgent) writes actions as Python code snippets, executed securely in sandboxed environments (e.g., [E2B](https://e2b.dev/) or Docker). Code-based actions [use 30% fewer steps](https://huggingface.co/papers/2402.01030) and [achieve higher performance](https://huggingface.co/papers/2411.01747) compared to traditional tool-calling methods. -Especially, since code execution can be a security concern (arbitrary code execution!), we provide options at runtime: - - a secure python interpreter to run code more safely in your environment (more secure than raw code execution but still risky) - - a sandboxed environment using [E2B](https://e2b.dev/) or Docker (removes the risk to your own system). +[`ToolCallingAgent`](https://huggingface.co/docs/smolagents/reference/agents#smolagents.ToolCallingAgent) writes actions as JSON blobs, suitable for tasks requiring structured tool calls. Both agent types support collaborative workflows via message queues. -Alongside [`CodeAgent`](https://huggingface.co/docs/smolagents/reference/agents#smolagents.CodeAgent), we also provide the standard [`ToolCallingAgent`](https://huggingface.co/docs/smolagents/reference/agents#smolagents.ToolCallingAgent) which writes actions as JSON/text blobs. You can pick whichever style best suits your use case. +Example workflow for two agents solving "Compute 5 + 3": -## How smol is this library? +- **Agent 0**: Generates code (`result = 5 + 3; print(result)`) and sends it to Agent 1. +- **Agent 1**: Receives the code, executes it using `python_interpreter`, and returns the result with `final_answer()`. -We strived to keep abstractions to a strict minimum: the main code in `agents.py` has <1,000 lines of code. -Still, we implement several types of agents: `CodeAgent` writes its actions as Python code snippets, and the more classic `ToolCallingAgent` leverages built-in tool calling methods. We also have multi-agent hierarchies, import from tool collections, remote code execution, vision models... +See [our intro to agents](https://huggingface.co/docs/smolagents/conceptual_guides/intro_agents) for more details. -By the way, why use a framework at all? Well, because a big part of this stuff is non-trivial. For instance, the code agent has to keep a consistent format for code throughout its system prompt, its parser, the execution. So our framework handles this complexity for you. But of course we still encourage you to hack into the source code and use only the bits that you need, to the exclusion of everything else! +## How Smol Is This Library? -## How strong are open models for agentic workflows? +The core logic in `agents.py` is <1,000 lines, minimizing abstractions. We support `CodeAgent` (Python code actions), `ToolCallingAgent` (JSON actions), multi-agent collaboration, tool collections, remote code execution, and vision models. The framework handles complex tasks like consistent code formatting, parsing, and secure execution, but you can hack into the source code to use only what you need. -We've created [`CodeAgent`](https://huggingface.co/docs/smolagents/reference/agents#smolagents.CodeAgent) instances with some leading models, and compared them on [this benchmark](https://huggingface.co/datasets/m-ric/agents_medium_benchmark_2) that gathers questions from a few different benchmarks to propose a varied blend of challenges. +## How Strong Are Open Models for Agentic Workflows? -[Find the benchmarking code here](https://github.com/huggingface/smolagents/blob/main/examples/smolagents_benchmark/run.py) for more detail on the agentic setup used, and see a comparison of using LLMs code agents compared to vanilla (spoilers: code agents works better). +We’ve benchmarked [`CodeAgent`](https://huggingface.co/docs/smolagents/reference/agents#smolagents.CodeAgent) with leading models on [this benchmark](https://huggingface.co/datasets/m-ric/agents_medium_benchmark_2), combining varied challenges. [See the benchmarking code](https://github.com/huggingface/smolagents/blob/main/examples/smolagents_benchmark/run.py) for details. Open-source models like DeepSeek-R1 often outperform closed-source models in agentic tasks.

benchmark of different models on agentic workflows. Open model DeepSeek-R1 beats closed-source models.

-This comparison shows that open-source models can now take on the best closed models! - ## Security + Security is a critical consideration when working with code-executing agents. Our library provides: - Sandboxed execution options using [E2B](https://e2b.dev/), [Modal](https://modal.com/), Docker, or Pyodide+Deno WebAssembly sandbox - Best practices for running agent code securely -For security policies, vulnerability reporting, and more information on secure agent execution, please see our [Security Policy](SECURITY.md). +See our [Security Policy](SECURITY.md) for vulnerability reporting and secure execution guidelines. ## Contribute -Everyone is welcome to contribute, get started with our [contribution guide](https://github.com/huggingface/smolagents/blob/main/CONTRIBUTING.md). +Everyone is welcome to contribute. See our [contribution guide](https://github.com/huggingface/smolagents/blob/main/CONTRIBUTING.md). ## Cite smolagents -If you use `smolagents` in your publication, please cite it by using the following BibTeX entry. +If you use `smolagents` in your publication, please cite it: ```bibtex @Misc{smolagents, @@ -274,4 +285,4 @@ If you use `smolagents` in your publication, please cite it by using the followi howpublished = {\url{https://github.com/huggingface/smolagents}}, year = {2025} } -``` +``` \ No newline at end of file diff --git a/examples/decentralized_smolagents_benchmark/README.md b/examples/decentralized_smolagents_benchmark/README.md new file mode 100644 index 000000000..3b021f877 --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/README.md @@ -0,0 +1,186 @@ +# Decentralized smolagents Benchmark + +This folder contains a decentralized multi-agent system implementation for benchmarking against the smolagents benchmark dataset. The system coordinates multiple specialized agents working collaboratively to solve complex problems. + +## Overview + +The decentralized approach distributes problem-solving across multiple specialized agents that communicate and coordinate through a message-passing system with consensus mechanisms. This contrasts with the centralized approach where a single agent has access to all tools. + +### Architecture + +The system consists of: + +- **4 Specialized Agents**: + - **CodeAgent**: Handles code execution and computational tasks + - **WebSearchAgent**: Performs web searches and information retrieval + - **DeepResearchAgent**: Conducts in-depth research using web browsing + - **DocumentReaderAgent**: Reads and analyzes various document formats + +- **Message Store**: Central communication hub for agent coordination +- **Consensus Protocol**: Voting mechanism for final answer agreement + +## Files + +- **`decentralized_agent.py`**: Main entry point for running a single question through the decentralized agent team +- **`run.py`**: Benchmark runner that evaluates the decentralized system across the entire benchmark dataset +- **`run_centralized.py`**: Comparison implementation using a centralized agent approach +- **`requirements.txt`**: Python dependencies required for the project +- **`scripts/`**: Supporting modules for agents, tools, communication, and utilities + +### Key Scripts + +- `scripts/agents.py`: Agent definitions and team coordination logic +- `scripts/message_store.py`: Message-passing infrastructure for agent communication +- `scripts/consensus_protocol.py`: Voting mechanism for reaching consensus on answers +- `scripts/decentralized_tools.py`: Custom tools for decentralized agent communication +- `scripts/text_web_browser.py`: Text-based web browsing tools +- `scripts/text_inspector_tool.py`: Document reading and analysis tools +- `scripts/visual_qa.py`: Visual question answering capabilities +- `scripts/html_renderer.py`: HTML visualization of agent runs +- `scripts/convert_messages_to_html.py`: Convert message logs to HTML format +- `scripts/gaia_scorer.py`: Scoring utilities for GAIA benchmark format + +## Installation + +1. Install the required dependencies: +```bash +pip install -r requirements.txt +``` + +2. Set up your environment variables in a `.env` file: +```bash +# API Keys +OPENAI_API_KEY=your_openai_key #You can replace it by whatever model you want to use +ANTHROPIC_API_KEY=your_anthropic_key #You can replace it by whatever model you want to use +SERPAPI_API_KEY=your_serpapi_key # For web search functionality +LANGFUSE_PUBLIC_KEY=your_langfuse_public_key # Optional: for tracing +LANGFUSE_SECRET_KEY=your_langfuse_secret_key # Optional: for tracing +LANGFUSE_HOST=your_langfuse_host # Optional: for tracing +``` + +## Usage + +### Running a Single Question + +Use `decentralized_agent.py` to run a single question through the decentralized team: + +```bash +python decentralized_agent.py \ + --model-type LiteLLMModel \ + --model-id gpt-4o \ #or another model + --provider openai \ #or another provider + "What is the half of the speed of a Leopard?" +``` + +**Arguments:** +- `--model-type`: Model type to use (e.g., `LiteLLMModel`) +- `--model-id`: Specific model identifier (e.g., `gpt-4o`, `claude-3-5-sonnet-20241022`) +- `--provider`: Model provider (e.g., `openai`, `anthropic`, `hf-inference`) +- `question`: The question to answer (positional argument) + +**Output:** +- Creates a `runs/{run_id}/` directory with: + - `run.log`: JSON-formatted execution logs + - Agent interaction traces and message history + +### Running the Full Benchmark + +Use `run.py` to evaluate across the entire benchmark dataset: + +```bash +python run.py \ + --model-type LiteLLMModel \ + --model-id gpt-4o \ #or another model + --provider openai \ #or another provider + --parallel-workers 4 +``` + +**Arguments:** +- `--date`: Date string for the evaluation (default: current date) +- `--eval-dataset`: Dataset to evaluate on (default: `smolagents/benchmark-v1`) +- `--model-type`: Model type to use +- `--model-id`: Specific model identifier +- `--provider`: Model provider +- `--parallel-workers`: Number of concurrent benchmark runs (default: 4) +- `--num-examples`: Limit examples per task for testing (optional) +- `--push-answers-to-hub`: Push results to HuggingFace Hub +- `--answers-dataset`: Dataset name for answers (default: `smolagents/answers`) + +**Output:** +- `output/results_{date}_{model_id}.csv`: Benchmark results +- `output/answers_{date}_{model_id}.json`: Generated answers +- Individual run directories under `runs/` + +### Running the Centralized Baseline + +For comparison, run the centralized agent: + +```bash +python run_centralized.py \ + --model-type LiteLLMModel \ + --model-id gpt-4o \ #or another model + --provider openai \ #or another provider + --parallel-workers 4 +``` + +Uses the same arguments as `run.py`. + +## Features + +### Decentralized Coordination + +- **Message-Based Communication**: Agents communicate through a shared message store +- **Consensus Protocol**: Multiple agents must agree on the final answer through voting +- **Specialized Roles**: Each agent has specific capabilities and responsibilities +- **Parallel Execution**: Agents can work concurrently on different aspects of the problem + +### Monitoring & Observability + +- **Langfuse Integration**: Optional tracing and monitoring of agent interactions +- **JSON Logging**: Structured logs for debugging and analysis +- **HTML Visualization**: Convert message logs to interactive HTML reports +- **Run Tracking**: Unique run IDs for tracking individual executions + +### Tool Capabilities + +The agents have access to various tools: +- Python code execution +- Google search +- Web browsing (text-based) +- Document reading (PDF, DOCX, PPTX, etc.) +- Visual question answering +- File downloads +- Archive searching + +## Project Structure + +``` +decentralized_smolagents_benchmark/ +β”œβ”€β”€ decentralized_agent.py # Single question entry point +β”œβ”€β”€ run.py # Benchmark runner (decentralized) +β”œβ”€β”€ run_centralized.py # Benchmark runner (centralized baseline) +β”œβ”€β”€ requirements.txt # Dependencies +β”œβ”€β”€ scripts/ # Supporting modules +β”‚ β”œβ”€β”€ agents.py # Agent definitions +β”‚ β”œβ”€β”€ message_store.py # Communication infrastructure +β”‚ β”œβ”€β”€ consensus_protocol.py # Voting mechanism +β”‚ β”œβ”€β”€ decentralized_tools.py # Communication tools +β”‚ β”œβ”€β”€ text_web_browser.py # Web browsing tools +β”‚ β”œβ”€β”€ text_inspector_tool.py # Document tools +β”‚ β”œβ”€β”€ visual_qa.py # Visual QA +β”‚ β”œβ”€β”€ html_renderer.py # HTML visualization +β”‚ └── ... # Other utilities +β”œβ”€β”€ runs/ # Individual run outputs (created at runtime) +└── output/ # Benchmark results (created at runtime) +``` + +## Contributing + +When contributing to this project, please follow the guidelines in the root-level `AGENTS.md`: +- Follow OOP principles +- Be Pythonic: follow Python best practices and idiomatic patterns +- Write unit tests for new functionality + +## License + +This project is part of the smolagents repository. Please refer to the root LICENSE file for licensing information. diff --git a/examples/decentralized_smolagents_benchmark/decentralized_agent.py b/examples/decentralized_smolagents_benchmark/decentralized_agent.py new file mode 100644 index 000000000..64d6affcd --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/decentralized_agent.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +# Example run: python examples/decentralized_smolagents_benchmark/decentralized_agent.py --model-type LiteLLMModel --model-id gpt-4o --provider openai "What is the half of the speed of a Leopard?" +"""Entry point for decentralized agent team execution.""" + +import argparse +import json +import logging +import sys +import uuid +from pathlib import Path + +from scripts.agents import DecentralizedAgents +from scripts.message_store import MessageStore + + +QUESTION_ADDON = """It is critical to respect the format of the answer when it is asked. """ + + +# Langfuse instrumentation setup +try: + from dotenv import load_dotenv + + load_dotenv() + + from langfuse import Langfuse + from openinference.instrumentation.smolagents import SmolagentsInstrumentor + + # Initialize Langfuse client + langfuse_client = Langfuse() + if langfuse_client.auth_check(): + print("βœ… Langfuse client authenticated successfully") + SmolagentsInstrumentor().instrument() + print("βœ… SmolagentsInstrumentor enabled") + else: + print("⚠️ Langfuse authentication failed - tracing disabled") + langfuse_client = None +except ImportError as e: + print(f"⚠️ Langfuse not available: {e}") + langfuse_client = None +except Exception as e: + print(f"⚠️ Langfuse setup error: {e}") + langfuse_client = None + + +def setup_logging(run_dir: Path) -> None: + """Setup JSON logging to file.""" + log_file = run_dir / "run.log" + + # Clear existing handlers to avoid duplication + logger = logging.getLogger() + logger.handlers.clear() + + # Create formatters + json_formatter = logging.Formatter('{"timestamp":"%(asctime)s", "level":"%(levelname)s", "message":%(message)s}') + # console_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + + # File handler with JSON format + file_handler = logging.FileHandler(log_file, encoding="utf-8") + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(json_formatter) + + # Console handler with readable format (optional, for debugging) + # Uncomment the next 4 lines if you want console logging too + # console_handler = logging.StreamHandler(sys.stdout) + # console_handler.setLevel(logging.INFO) + # console_handler.setFormatter(console_formatter) + # logger.addHandler(console_handler) + + # Add handlers to logger + logger.addHandler(file_handler) + logger.setLevel(logging.INFO) + + +def main(args: argparse.Namespace) -> int: + """Main entry point - simplified execution.""" + print(f"πŸš€ Starting decentralized agent team for: {args.question}") + + # Create message store with proper agent names for correct voting thresholds + run_id = str(uuid.uuid4())[:8] # Short run ID + agent_names = ["CodeAgent", "WebSearchAgent", "DeepResearchAgent", "DocumentReaderAgent"] + message_store = MessageStore(run_id, agent_names=agent_names) + + # Handle the case where __file__ might not be defined + try: + script_dir = Path(__file__).parent + except NameError: + # Fallback if __file__ is not defined + script_dir = Path(sys.argv[0]).parent.absolute() if sys.argv[0] else Path.cwd() + + run_dir = script_dir / "runs" / run_id + run_dir.mkdir(parents=True, exist_ok=True) + + # Setup logging + setup_logging(run_dir) + logging.info(json.dumps({"event": "run_started", "run_id": run_id, "args": vars(args)})) + + try: + # Create the decentralized agent team + logging.info(json.dumps({"event": "creating_team", "run_id": run_id})) + decentralized_team = DecentralizedAgents( + message_store=message_store, + model_type=args.model_type, + model_id=args.model_id, + provider=args.provider, + run_id=run_id, + ) + + # Run the team on the task with enhanced collaboration instructions + enhanced_task = f"{args.question}" + logging.info(json.dumps({"event": "starting_execution", "run_id": run_id, "question": args.question})) + result = decentralized_team.run(enhanced_task) + + logging.info( + json.dumps( + { + "event": "execution_completed", + "run_id": run_id, + "status": result.get("status", "unknown"), + "has_answer": "answer" in result, + } + ) + ) + + # Output the result + if result["status"] in ["success", "success_early", "success_fallback"]: + print(json.dumps({"answer": result["answer"]})) + return 0 + else: + error_msg = result.get("error", "No valid results") + logging.error( + json.dumps({"event": "execution_failed", "run_id": run_id, "error": error_msg, "result": result}) + ) + print(f"\n❌ Team execution failed: {error_msg}") + return 1 + + except Exception as e: + # Catch any unexpected errors and log them with full context + logging.error( + json.dumps( + { + "event": "unexpected_error", + "run_id": run_id, + "error_type": type(e).__name__, + "error_message": str(e), + "question": args.question, + } + ) + ) + + # Also log the full stack trace for debugging + import traceback + + logging.error(json.dumps({"event": "error_traceback", "run_id": run_id, "traceback": traceback.format_exc()})) + + print(f"\n❌ Unexpected error: {e}") + return 1 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Run decentralized agent team") + parser.add_argument("--model-type", required=True, help="Model type to use") + parser.add_argument("--model-id", required=True, help="Model ID to use") + parser.add_argument("--provider", help="Model provider") + parser.add_argument("question", help="Question to answer") + + args = parser.parse_args() + sys.exit(main(args)) diff --git a/examples/decentralized_smolagents_benchmark/model_performance_comparison.png b/examples/decentralized_smolagents_benchmark/model_performance_comparison.png new file mode 100644 index 000000000..30cb23506 Binary files /dev/null and b/examples/decentralized_smolagents_benchmark/model_performance_comparison.png differ diff --git a/examples/decentralized_smolagents_benchmark/requirements.txt b/examples/decentralized_smolagents_benchmark/requirements.txt new file mode 100644 index 000000000..29c868a34 --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/requirements.txt @@ -0,0 +1,38 @@ +anthropic>=0.37.1 +audioop-lts<1.0; python_version >= "3.13" # required to use pydub in Python >=3.13; LTS port of the removed Python builtin module audioop +beautifulsoup4>=4.12.3 +datasets>=2.21.0 +google_search_results>=2.4.2 +huggingface_hub>=0.23.4 +mammoth>=1.8.0 +markdownify>=0.13.1 +numexpr>=2.10.1 +numpy>=2.1.2 +openai>=1.52.2 +openpyxl +pandas>=2.2.3 +pathvalidate>=3.2.1 +pdfminer.six>=20240706 +Pillow>=11.0.0 +puremagic>=1.28 +pypdf>=5.1.0 +python-dotenv>=1.0.1 +python_pptx>=1.0.2 +Requests>=2.32.3 +tqdm>=4.66.4 +torch>=2.2.2 +torchvision>=0.17.2 +transformers>=4.46.0 +youtube_transcript_api>=0.6.2 +chess +sympy +pubchempy +Bio +scikit-learn +scipy +pydub +PyPDF2 +python-pptx +torch +xlrd +SpeechRecognition diff --git a/examples/decentralized_smolagents_benchmark/run.py b/examples/decentralized_smolagents_benchmark/run.py new file mode 100644 index 000000000..4c586fc8c --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/run.py @@ -0,0 +1,520 @@ +#!/usr/bin/env python +# Example usage: python run.py --model-type LiteLLMModel --model-id gpt-4o --provider openai +"""Benchmarking script for decentralized agent implementation.""" + +import argparse +import datetime +import json +import os +import re +import subprocess +import threading +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path + +import datasets +import pandas as pd +from dotenv import load_dotenv +from tqdm import tqdm + + +# Langfuse instrumentation setup +try: + from dotenv import load_dotenv + + load_dotenv() + + from langfuse import Langfuse + from openinference.instrumentation.smolagents import SmolagentsInstrumentor + + # Initialize Langfuse client + langfuse_client = Langfuse() + if langfuse_client.auth_check(): + print("βœ… Langfuse client authenticated successfully") + SmolagentsInstrumentor().instrument() + print("βœ… SmolagentsInstrumentor enabled") + else: + print("⚠️ Langfuse authentication failed - tracing disabled") + langfuse_client = None +except ImportError as e: + print(f"⚠️ Langfuse not available: {e}") + langfuse_client = None +except Exception as e: + print(f"⚠️ Langfuse setup error: {e}") + langfuse_client = None + + +script_dir = Path(__file__).parent +output_path = script_dir / "output" +os.makedirs(output_path, exist_ok=True) + + +APPEND_ANSWER_LOCK = threading.Lock() + + +def parse_arguments(): + parser = argparse.ArgumentParser(description="Runs decentralized agent team on smolagent benchmark.") + parser.add_argument( + "--date", + type=str, + default=None, + help="The date for the evaluation.", + ) + parser.add_argument( + "--eval-dataset", + type=str, + default="smolagents/benchmark-v1", + ) + parser.add_argument( + "--model-type", + type=str, + required=True, + help="The model type to use for the decentralized agents", + ) + parser.add_argument( + "--model-id", + type=str, + required=True, + help="The model ID to use for the specified model type", + ) + parser.add_argument( + "--provider", + type=str, + default="hf-inference", + help="The provider for the model", + ) + parser.add_argument( + "--parallel-workers", + type=int, + default=4, + help="The number of concurrent benchmark runs", + ) + parser.add_argument( + "--num-examples", + type=int, + default=None, + help="Limit the number of examples per task (useful for testing)", + ) + parser.add_argument( + "--push-answers-to-hub", + action="store_true", + help="Push the answers to the hub", + ) + parser.add_argument( + "--answers-dataset", + type=str, + default="smolagents/answers", + ) + return parser.parse_args() + + +def load_eval_dataset(eval_dataset, num_examples=None): + """Load the evaluation dataset.""" + # Get all available tasks + tasks = datasets.get_dataset_config_names(eval_dataset) + print(f"Available tasks: {tasks}") + + # Load each task's dataset + all_data = [] + for task in tasks: + dataset = datasets.load_dataset(eval_dataset, task, split="test") + data_list = list(dataset) + print(f"Loaded {len(data_list)} examples for {task}") + + # Add task information to each item and create unique IDs + for i, item in enumerate(data_list): + item["task"] = task + item["source"] = task # Add source field to match original format + item["id"] = f"{task}_{i}" # Create unique ID for each item + + all_data.extend(data_list) + + df = pd.DataFrame(all_data) + + if num_examples is not None: + # Sample num_examples from each task + df = ( + df.groupby("task", group_keys=False) + .apply(lambda x: x.sample(n=min(num_examples, len(x)), random_state=42)) + .reset_index(drop=True) + ) + + return df + + +def run_decentralized_agent(row, args): + """Run decentralized agent on a single benchmark example.""" + start_time = time.time() + + # Get the date for file naming + date = args.date if hasattr(args, "date") and args.date else datetime.datetime.now().strftime("%Y-%m-%d") + + try: + # Prepare question from the dataset row + question = row["question"] + if row.get("context"): + question = f"Context: {row['context']}\n\nQuestion: {question}" + + # Run the decentralized agent process + script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "decentralized_agent.py") + cmd = [ + "python", + script_path, + "--model-type", + args.model_type, + "--model-id", + args.model_id, + "--provider", + args.provider, + question, + ] + + # Run the process and capture output + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + # Parse the final answer and run_id from JSON output + final_answer = None + run_id = None + + # First, check stderr for run_id from logging output + if result.stderr: + for line in result.stderr.split("\n"): + line = line.strip() + if '"run_id"' in line and '"event"' in line: + try: + # Parse the JSON structure + parsed = json.loads(line) + if "message" in parsed and isinstance(parsed["message"], dict): + if "run_id" in parsed["message"]: + run_id = parsed["message"]["run_id"] + break + except json.JSONDecodeError: + # Try regex as fallback + import re + + match = re.search(r'"run_id":\s*"([^"]+)"', line) + if match: + run_id = match.group(1) + break + + # Parse final answer from stdout + for line in result.stdout.split("\n"): + line = line.strip() + if line: + try: + parsed = json.loads(line) + if "answer" in parsed: + final_answer = parsed["answer"] + break + except json.JSONDecodeError: + continue + + if not final_answer: + # Also check stderr for error messages + error_msg = result.stderr.strip() if result.stderr else "No final answer reached" + raise Exception(error_msg) + + # Calculate metrics + duration = time.time() - start_time + success = True + error = None + + except subprocess.TimeoutExpired as e: + duration = time.time() - start_time + success = False + error = f"Process timed out after duration {duration}, error: {str(e)}" + final_answer = None + run_id = None # Set run_id to None for timeout cases + except subprocess.CalledProcessError as e: + duration = time.time() - start_time + success = False + error_output = e.stderr.strip() if e.stderr else "No stderr output" + stdout_output = e.stdout.strip() if e.stdout else "No stdout output" + error = f"Process failed with exit code {e.returncode}. Stderr: {error_output}. Stdout: {stdout_output}" + final_answer = None + run_id = None # Set run_id to None for process error cases + except Exception as e: + duration = time.time() - start_time + success = False + error = str(e) + final_answer = None + run_id = None # Set run_id to None for error cases + + # Prepare result dictionary matching the expected format + model_id = f"decentralized-{args.model_type}-{args.model_id}" + action_type = "decentralized-consensus" + + result = { + "task": row["task"], + "question_id": row["id"], + "success": success, + "error": error, + "duration": duration, + "answer": final_answer, + "model_type": args.model_type, + "model_id": args.model_id, + "provider": args.provider, + "run_id": run_id, # Add the extracted run_id + "timestamp": datetime.datetime.now().isoformat(), + } + + # Also save in the format expected by the scoring system + scoring_result = { + "model_id": model_id, + "agent_action_type": action_type, + "question": row["question"], + "original_question": row["question"], + "answer": final_answer, + "true_answer": row.get("true_answer", ""), + "source": row["task"], + "start_time": start_time, + "end_time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "duration": duration, + "run_id": run_id, # Add run_id here too + } + + # Save result to general output file + output_file = Path(output_path) / f"results_{date}.jsonl" + with APPEND_ANSWER_LOCK: + output_file.parent.mkdir(parents=True, exist_ok=True) + with open(output_file, "a") as f: + json.dump(result, f) + f.write("\n") + + # Save result to task-specific file for scoring + task_output_file = Path(output_path) / f"{model_id.replace('/', '__')}__{action_type}__{row['task']}__{date}.jsonl" + with APPEND_ANSWER_LOCK: + task_output_file.parent.mkdir(parents=True, exist_ok=True) + with open(task_output_file, "a") as f: + json.dump(scoring_result, f) + f.write("\n") + + return result + + +def normalize_answer(answer): + """ + Normalize answer for comparison. + + Removes extra whitespace, converts to lowercase, and strips punctuation + to enable more flexible answer matching. + """ + if answer is None: + return "" + answer = str(answer).strip().lower() + # Remove extra whitespace + answer = re.sub(r"\s+", " ", answer) + # Remove common punctuation at the end + answer = re.sub(r"[.!?;,]+$", "", answer) + return answer + + +def calculate_exact_match_score(predicted_answer, true_answer): + """ + Calculate exact match score (1.0 for perfect match, 0.0 otherwise). + + This is the strictest scoring metric. + """ + return 1.0 if normalize_answer(predicted_answer) == normalize_answer(true_answer) else 0.0 + + +def calculate_contains_score(predicted_answer, true_answer): + """Calculate score based on whether the predicted answer contains the true answer.""" + normalized_pred = normalize_answer(predicted_answer) + normalized_true = normalize_answer(true_answer) + + if not normalized_true: + return 0.0 + + return 1.0 if normalized_true in normalized_pred else 0.0 + + +def calculate_benchmark_scores(jsonl_file_path): + """Calculate scores for a benchmark result file.""" + if not os.path.exists(jsonl_file_path): + return {"error": "File not found"} + + total_questions = 0 + exact_matches = 0 + contains_matches = 0 + + with open(jsonl_file_path, "r", encoding="utf-8") as f: + for line in f: + try: + data = json.loads(line.strip()) + if not data: + continue + + predicted = data.get("answer", "") + true_answer = data.get("true_answer", "") + + total_questions += 1 + exact_matches += calculate_exact_match_score(predicted, true_answer) + contains_matches += calculate_contains_score(predicted, true_answer) + + except json.JSONDecodeError: + continue + + if total_questions == 0: + return {"error": "No valid questions found"} + + return { + "total_questions": total_questions, + "exact_match_score": exact_matches / total_questions, + "contains_score": contains_matches / total_questions, + "exact_matches": exact_matches, + "contains_matches": contains_matches, + } + + +def save_benchmark_scores(output_dir, model_id, action_type, date, eval_ds): + """Calculate and save scores for all benchmarks.""" + scores_file = f"{output_dir}/benchmark_scores_{model_id.replace('/', '__')}__{action_type}__{date}.json" + + all_scores = { + "model_id": model_id, + "action_type": action_type, + "date": date, + "timestamp": datetime.datetime.now().isoformat(), + "benchmarks": {}, + } + + total_questions_all = 0 + total_exact_matches_all = 0 + total_contains_matches_all = 0 + + print("\nπŸ“Š Calculating benchmark scores...") + + for task in eval_ds: + jsonl_file = f"{output_dir}/{model_id.replace('/', '__')}__{action_type}__{task}__{date}.jsonl" + scores = calculate_benchmark_scores(jsonl_file) + + if "error" not in scores: + all_scores["benchmarks"][task] = scores + total_questions_all += scores["total_questions"] + total_exact_matches_all += scores["exact_matches"] + total_contains_matches_all += scores["contains_matches"] + + print(f" πŸ“ˆ {task.upper()}:") + print(f" Questions: {scores['total_questions']}") + print( + f" Exact Match: {scores['exact_match_score']:.1%} ({scores['exact_matches']}/{scores['total_questions']})" + ) + print( + f" Contains: {scores['contains_score']:.1%} ({scores['contains_matches']}/{scores['total_questions']})" + ) + else: + print(f" ❌ {task.upper()}: {scores['error']}") + + # Overall scores + if total_questions_all > 0: + all_scores["overall"] = { + "total_questions": total_questions_all, + "exact_match_score": total_exact_matches_all / total_questions_all, + "contains_score": total_contains_matches_all / total_questions_all, + "exact_matches": total_exact_matches_all, + "contains_matches": total_contains_matches_all, + } + + print("\n🎯 OVERALL SCORES:") + print(f" Questions: {total_questions_all}") + print( + f" Exact Match: {all_scores['overall']['exact_match_score']:.1%} ({total_exact_matches_all}/{total_questions_all})" + ) + print( + f" Contains: {all_scores['overall']['contains_score']:.1%} ({total_contains_matches_all}/{total_questions_all})" + ) + + # Save scores to file + with open(scores_file, "w", encoding="utf-8") as f: + json.dump(all_scores, f, indent=2, ensure_ascii=False) + + print(f"\nπŸ’Ύ Scores saved to: {scores_file}") + return all_scores + + +def main(): + """Main benchmarking function.""" + args = parse_arguments() + + # Set date if not provided + if args.date is None: + args.date = datetime.datetime.now().strftime("%Y-%m-%d") + + # Load dataset + print(f"Loading dataset {args.eval_dataset}...") + df = load_eval_dataset(args.eval_dataset, args.num_examples) + + # If num_examples is specified, sample from each task + if args.num_examples is not None: + df = ( + df.groupby("task", group_keys=False) + .apply(lambda x: x.sample(n=min(args.num_examples, len(x)), random_state=42)) + .reset_index(drop=True) + ) + + print(f"\nLoaded {len(df)} examples total:") + for task in df["task"].unique(): + task_count = len(df[df["task"] == task]) + print(f"- {task}: {task_count} examples") + + # Run benchmark + results = [] + with ThreadPoolExecutor(max_workers=args.parallel_workers) as executor: + futures = [] + + # Group examples by task for better progress tracking + for task in df["task"].unique(): + task_df = df[df["task"] == task] + print(f"\nπŸš€ Starting benchmark for {task} with {len(task_df)} examples...") + + for _, row in task_df.iterrows(): + future = executor.submit(run_decentralized_agent, row, args) + futures.append(future) + + # Process results with progress bar + for future in tqdm(as_completed(futures), total=len(futures), desc="Processing examples"): + try: + result = future.result() + results.append(result) + except Exception as e: + print(f"Error processing example: {str(e)}") + + # Calculate and print summary statistics per task + print("\nπŸ“Š Results Summary:") + for task in df["task"].unique(): + task_results = [r for r in results if r["task"] == task] + task_success = sum(1 for r in task_results if r["success"]) + task_duration = sum(r["duration"] for r in task_results) / len(task_results) if task_results else 0 + + print(f"\n{task}:") + print(f" Total examples: {len(task_results)}") + print(f" Success rate: {task_success / len(task_results):.2%}") + print(f" Average duration: {task_duration:.2f}s") + + # Overall statistics + total_success = sum(1 for r in results if r["success"]) + avg_duration = sum(r["duration"] for r in results) / len(results) if results else 0 + + print("\nπŸ“ˆ Overall Statistics:") + print(f" Total examples: {len(results)}") + print(f" Success rate: {total_success / len(results):.2%}") + print(f" Average duration: {avg_duration:.2f}s") + + # Push results to hub if requested + if args.push_answers_to_hub: + print("\nπŸš€ Pushing results to hub not yet implemented") + + # Calculate and save benchmark scores with proper variables + output_dir = output_path + model_id = f"decentralized-{args.model_type}-{args.model_id}" + action_type = "decentralized-consensus" + date = args.date + eval_ds = df["task"].unique() + + save_benchmark_scores(output_dir, model_id, action_type, date, eval_ds) + + +if __name__ == "__main__": + main() diff --git a/examples/decentralized_smolagents_benchmark/run_centralized.py b/examples/decentralized_smolagents_benchmark/run_centralized.py new file mode 100644 index 000000000..f762c3075 --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/run_centralized.py @@ -0,0 +1,718 @@ +#!/usr/bin/env python +# Example usage: python run_centralized.py --model-type LiteLLMModel --model-id gpt-4o --provider openai +"""Benchmarking script for centralized agent implementation.""" + +import argparse +import datetime +import json +import os +import re +import threading +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path +from typing import List + +import datasets +import pandas as pd +from dotenv import load_dotenv +from scripts.text_inspector_tool import FileReaderTool, TextInspectorTool +from scripts.text_web_browser import ( + ArchiveSearchTool, + DownloadTool, + FinderTool, + FindNextTool, + PageDownTool, + PageUpTool, + SimpleTextBrowser, + VisitTool, +) +from scripts.visual_qa import visualizer +from tqdm import tqdm + +from smolagents import ( + CodeAgent, + GoogleSearchTool, + LiteLLMModel, + Model, + ToolCallingAgent, +) +from smolagents.default_tools import PythonInterpreterTool +from smolagents.tools import Tool + + +# Langfuse instrumentation setup +try: + from dotenv import load_dotenv + + load_dotenv() + + from langfuse import Langfuse + from openinference.instrumentation.smolagents import SmolagentsInstrumentor + + # Initialize Langfuse client + langfuse_client = Langfuse() + if langfuse_client.auth_check(): + print("βœ… Langfuse client authenticated successfully") + SmolagentsInstrumentor().instrument() + print("βœ… SmolagentsInstrumentor enabled") + else: + print("⚠️ Langfuse authentication failed - tracing disabled") + langfuse_client = None +except ImportError as e: + print(f"⚠️ Langfuse not available: {e}") + langfuse_client = None +except Exception as e: + print(f"⚠️ Langfuse setup error: {e}") + langfuse_client = None + + +script_dir = Path(__file__).parent +output_path = script_dir / "output" +os.makedirs(output_path, exist_ok=True) + +custom_role_conversions = {"tool-call": "assistant", "tool-response": "user"} + + +user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0" + +BROWSER_CONFIG = { + "viewport_size": 1024 * 5, + "downloads_folder": "downloads_folder", + "request_kwargs": { + "headers": {"User-Agent": user_agent}, + "timeout": 300, + }, + "serpapi_key": os.getenv("SERPAPI_API_KEY"), +} + +os.makedirs(f"./{BROWSER_CONFIG['downloads_folder']}", exist_ok=True) + +# Base prompt for team charter +TEAM_CHARTER = """You are part of an expert AI agent team working collaboratively to solve complex problems. + +TEAM COORDINATION PRINCIPLES: +- Work systematically and build on each other's findings +- Communicate clearly about your role, findings, and next steps +- Verify important information through multiple approaches when possible +- Focus on accuracy and provide evidence for your conclusions +- Coordinate efforts to avoid duplication and ensure comprehensive coverage + +RESPONSE FORMAT: +Always structure your responses with clear sections and evidence-based conclusions.""" + +APPEND_ANSWER_LOCK = threading.Lock() + + +def parse_arguments(): + parser = argparse.ArgumentParser(description="Runs centralized agent team on smolagent benchmark.") + parser.add_argument( + "--date", + type=str, + default=None, + help="The date for the evaluation.", + ) + parser.add_argument( + "--eval-dataset", + type=str, + default="smolagents/benchmark-v1", + ) + parser.add_argument( + "--model-type", + type=str, + required=True, + help="The model type to use for centralized multi-agent system", + ) + parser.add_argument( + "--model-id", + type=str, + required=True, + help="The model ID to use for the specified model type", + ) + parser.add_argument( + "--provider", + type=str, + default="hf-inference", + help="The provider for the model", + ) + parser.add_argument( + "--parallel-workers", + type=int, + default=4, + help="The number of concurrent benchmark runs", + ) + parser.add_argument( + "--num-examples", + type=int, + default=None, + help="Limit the number of examples per task (useful for testing)", + ) + parser.add_argument( + "--push-answers-to-hub", + action="store_true", + help="Push the answers to the hub", + ) + parser.add_argument( + "--answers-dataset", + type=str, + default="smolagents/answers", + ) + return parser.parse_args() + + +def load_eval_dataset(eval_dataset, num_examples=None): + """Load the evaluation dataset.""" + # Get all available tasks + tasks = datasets.get_dataset_config_names(eval_dataset) + print(f"Available tasks: {tasks}") + + # Load each task's dataset + all_data = [] + for task in tasks: + dataset = datasets.load_dataset(eval_dataset, task, split="test") + data_list = list(dataset) + print(f"Loaded {len(data_list)} examples for {task}") + + # Add task information to each item and create unique IDs + for i, item in enumerate(data_list): + item["task"] = task + item["source"] = task # Add source field to match original format + item["id"] = f"{task}_{i}" # Create unique ID for each item + + all_data.extend(data_list) + + df = pd.DataFrame(all_data) + + if num_examples is not None: + # Sample num_examples from each task + df = ( + df.groupby("task", group_keys=False) + .apply(lambda x: x.sample(n=min(num_examples, len(x)), random_state=42)) + .reset_index(drop=True) + ) + + return df + + +def run_centralized_agents(row, args, model): + """Run centralized agent on a single benchmark example.""" + start_time = time.time() + + # Get the date for file naming + date = args.date if hasattr(args, "date") and args.date else datetime.datetime.now().strftime("%Y-%m-%d") + + try: + # Create the centralized agent + agent = create_agent_team(model) + + # Prepare question from the dataset row + question = row["question"] + if row.get("context"): + question = f"Context: {row['context']}\n\nQuestion: {question}" + + # Run the agent + print(f"Running agent on question: {question[:100]}...") + result = agent.run(question) + print(f"Agent result type: {type(result)}") + print(f"Agent result: {str(result)[:200]}") + + # Extract final answer from result + if hasattr(result, "content"): + final_answer = result.content + elif hasattr(result, "final_answer"): + final_answer = result.final_answer + elif isinstance(result, str): + final_answer = result + else: + final_answer = str(result) + + print(f"Final answer: {final_answer}") + + # Calculate metrics + duration = time.time() - start_time + success = True + error = None + run_id = f"centralized_{int(time.time() * 1000)}" # Generate a simple run ID + + except Exception as e: + duration = time.time() - start_time + success = False + error = str(e) + final_answer = None + run_id = None + + # Prepare result dictionary matching the expected format + model_id = f"centralized-{args.model_type}-{args.model_id}" + action_type = "centralized-agents" + + result = { + "task": row["task"], + "question_id": row["id"], + "success": success, + "error": error, + "duration": duration, + "answer": final_answer, + "model_type": args.model_type, + "model_id": args.model_id, + "provider": args.provider, + "run_id": run_id, + "timestamp": datetime.datetime.now().isoformat(), + } + + # Also save in the format expected by the scoring system + scoring_result = { + "model_id": model_id, + "agent_action_type": action_type, + "question": row["question"], + "original_question": row["question"], + "answer": final_answer, + "true_answer": row.get("true_answer", ""), + "source": row["task"], + "start_time": start_time, + "end_time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "duration": duration, + "run_id": run_id, + } + + # Save result to general output file + output_file = Path(output_path) / f"centralized_results_{date}.jsonl" + with APPEND_ANSWER_LOCK: + output_file.parent.mkdir(parents=True, exist_ok=True) + with open(output_file, "a") as f: + json.dump(result, f) + f.write("\n") + + # Save result to task-specific file for scoring + task_output_file = Path(output_path) / f"{model_id.replace('/', '__')}__{action_type}__{row['task']}__{date}.jsonl" + with APPEND_ANSWER_LOCK: + task_output_file.parent.mkdir(parents=True, exist_ok=True) + with open(task_output_file, "a") as f: + json.dump(scoring_result, f) + f.write("\n") + + return result + + +def normalize_answer(answer): + """ + Normalize answer for comparison. + + Removes extra whitespace, converts to lowercase, and strips punctuation + to enable more flexible answer matching. + """ + if answer is None: + return "" + answer = str(answer).strip().lower() + # Remove extra whitespace + answer = re.sub(r"\s+", " ", answer) + # Remove common punctuation at the end + answer = re.sub(r"[.!?;,]+$", "", answer) + return answer + + +def calculate_exact_match_score(predicted_answer, true_answer): + """ + Calculate exact match score (1.0 for perfect match, 0.0 otherwise). + + This is the strictest scoring metric. + """ + return 1.0 if normalize_answer(predicted_answer) == normalize_answer(true_answer) else 0.0 + + +def calculate_contains_score(predicted_answer, true_answer): + """Calculate score based on whether the predicted answer contains the true answer.""" + normalized_pred = normalize_answer(predicted_answer) + normalized_true = normalize_answer(true_answer) + + if not normalized_true: + return 0.0 + + return 1.0 if normalized_true in normalized_pred else 0.0 + + +def calculate_benchmark_scores(jsonl_file_path): + """Calculate scores for a benchmark result file.""" + if not os.path.exists(jsonl_file_path): + return {"error": "File not found"} + + total_questions = 0 + exact_matches = 0 + contains_matches = 0 + + with open(jsonl_file_path, "r", encoding="utf-8") as f: + for line in f: + try: + data = json.loads(line.strip()) + if not data: + continue + + predicted = data.get("answer", "") + true_answer = data.get("true_answer", "") + + total_questions += 1 + exact_matches += calculate_exact_match_score(predicted, true_answer) + contains_matches += calculate_contains_score(predicted, true_answer) + + except json.JSONDecodeError: + continue + + if total_questions == 0: + return {"error": "No valid questions found"} + + return { + "total_questions": total_questions, + "exact_match_score": exact_matches / total_questions, + "contains_score": contains_matches / total_questions, + "exact_matches": exact_matches, + "contains_matches": contains_matches, + } + + +def save_benchmark_scores(output_dir, model_id, action_type, date, eval_ds): + """Calculate and save scores for all benchmarks.""" + scores_file = f"{output_dir}/benchmark_scores_{model_id.replace('/', '__')}__{action_type}__{date}.json" + + all_scores = { + "model_id": model_id, + "action_type": action_type, + "date": date, + "timestamp": datetime.datetime.now().isoformat(), + "benchmarks": {}, + } + + total_questions_all = 0 + total_exact_matches_all = 0 + total_contains_matches_all = 0 + + print("\nπŸ“Š Calculating benchmark scores...") + + for task in eval_ds: + jsonl_file = f"{output_dir}/{model_id.replace('/', '__')}__{action_type}__{task}__{date}.jsonl" + scores = calculate_benchmark_scores(jsonl_file) + + if "error" not in scores: + all_scores["benchmarks"][task] = scores + total_questions_all += scores["total_questions"] + total_exact_matches_all += scores["exact_matches"] + total_contains_matches_all += scores["contains_matches"] + + print(f" πŸ“ˆ {task.upper()}:") + print(f" Questions: {scores['total_questions']}") + print( + f" Exact Match: {scores['exact_match_score']:.1%} ({scores['exact_matches']}/{scores['total_questions']})" + ) + print( + f" Contains: {scores['contains_score']:.1%} ({scores['contains_matches']}/{scores['total_questions']})" + ) + else: + print(f" ❌ {task.upper()}: {scores['error']}") + + # Overall scores + if total_questions_all > 0: + all_scores["overall"] = { + "total_questions": total_questions_all, + "exact_match_score": total_exact_matches_all / total_questions_all, + "contains_score": total_contains_matches_all / total_questions_all, + "exact_matches": total_exact_matches_all, + "contains_matches": total_contains_matches_all, + } + + print("\n🎯 OVERALL SCORES:") + print(f" Questions: {total_questions_all}") + print( + f" Exact Match: {all_scores['overall']['exact_match_score']:.1%} ({total_exact_matches_all}/{total_questions_all})" + ) + print( + f" Contains: {all_scores['overall']['contains_score']:.1%} ({total_contains_matches_all}/{total_questions_all})" + ) + + # Save scores to file + with open(scores_file, "w", encoding="utf-8") as f: + json.dump(all_scores, f, indent=2, ensure_ascii=False) + + print(f"\nπŸ’Ύ Scores saved to: {scores_file}") + return all_scores + + +def create_agent_team(model: Model): + """Create a centralized multi-agent system with a manager and 4 specialized agents.""" + text_limit = 100000 + ti_tool = TextInspectorTool(model, text_limit) + + browser = SimpleTextBrowser(**BROWSER_CONFIG) + + # Create base tools that are shared between agents + # shared_tools = [visualizer, ti_tool] + shared_tools = [] + + # Define tool sets for different agent types + web_tools: List[Tool] = [ + GoogleSearchTool(provider="serper"), + VisitTool(browser), + DownloadTool(browser), + PageUpTool(browser), + PageDownTool(browser), + FinderTool(browser), + FindNextTool(browser), + ArchiveSearchTool(browser), + ] + + code_tools: List[Tool] = [PythonInterpreterTool()] + reader_tools: List[Tool] = [FileReaderTool(), visualizer, ti_tool] + + # Add shared tools to each tool set + web_tools.extend(shared_tools) + code_tools.extend(shared_tools) + reader_tools.extend(shared_tools) + + # Create 4 specialized agents with detailed descriptions + + # Agent 1: Python Code Execution and Algorithm Implementation Specialist + code_agent = CodeAgent( + model=model, + tools=code_tools, + max_steps=15, + verbosity_level=1, + additional_authorized_imports=["*"], + planning_interval=3, + name="CodeAgent", + description="""Python Code Execution and Algorithm Implementation Specialist. + +Responsibilities: +- Write, test, and execute Python code with proper error handling and validation +- Create modular, testable functions with comprehensive docstrings and documentation +- Implement mathematical algorithms, computational solutions, and numerical analysis +- Run validation tests and smoke tests on code changes to ensure reliability +- Optimize code performance, debug complex issues, and handle edge cases +- Handle numerical computations, data processing tasks, and statistical analysis +- Extract precise numerical results for mathematical problems (e.g., "7", "12.5", "3/4") +- Validate results through multiple calculation methods when possible + +Special focus on mathematical problem solving: +- Use Python to calculate exact results with proper precision +- Show all calculations, code, and reasoning processes +- Follow specific format requirements (decimal, fraction, etc.) +- Provide executable demonstrations of solutions""", + ) + + # Agent 2: Fast Web Research and Information Gathering Specialist + web_research_agent = ToolCallingAgent( + model=model, + tools=web_tools, + max_steps=15, + verbosity_level=1, + name="WebResearchAgent", + description="""Fast Web Research and Information Gathering Specialist. + +Responsibilities: +- Conduct rapid, targeted web searches for relevant and up-to-date information +- Evaluate source credibility, authority, and reliability of information sources +- Perform thorough fact-checking and information verification from multiple sources +- Gather real-time data, current information, and recent developments +- Identify trending topics, breaking news, and emerging information patterns +- Extract key facts, summarize findings concisely, and present structured results +- Cross-reference findings against multiple independent sources for accuracy +- Provide source URLs, publication dates, and credibility assessments + +Research methodology: +- Always verify information from multiple independent, authoritative sources +- Prioritize recent, peer-reviewed, and official sources over outdated information +- Include comprehensive source attribution with URLs and publication dates +- Flag conflicting information and present different perspectives objectively +- Focus on factual accuracy over speed when sources present contradictory claims""", + ) + + # Agent 3: Document Analysis and Technical Specification Specialist + document_agent = ToolCallingAgent( + model=model, + tools=reader_tools, + max_steps=10, + verbosity_level=1, + name="DocumentAgent", + description="""Document Analysis and Technical Specification Specialist. + +Responsibilities: +- Analyze and extract critical information from technical documents, PDFs, and specifications +- Maintain precise citations with page numbers, section references, and source attribution +- Structure complex documentation into digestible, actionable summaries and insights +- Validate technical specifications against implementation requirements and standards +- Cross-reference multiple documents for consistency, completeness, and accuracy +- Identify critical details, potential implementation considerations, and technical constraints +- Extract both explicit information and implied requirements from documentation +- Highlight contradictions, ambiguities, and gaps found in documents + +Documentation standards: +- Always include precise citations with page numbers or section references +- Maintain clear separation between documented facts and personal interpretations +- Focus on actionable information that directly impacts problem-solving and decision-making +- Cross-reference claims against other available documentation and external sources +- Present structured analysis with clear organization and logical flow""", + ) + + # Agent 4: Comprehensive Analysis and Advanced Research Specialist + analysis_tools = list(set(web_tools + code_tools)) + analysis_agent = CodeAgent( + model=model, + tools=analysis_tools, + max_steps=20, + verbosity_level=1, + additional_authorized_imports=["*"], + planning_interval=4, + name="AnalysisAgent", + description="""Comprehensive Analysis and Advanced Research Specialist. + +Responsibilities: +- Conduct thorough, multi-layered investigations and comprehensive analysis of complex problems +- Develop, test, and rigorously validate complex hypotheses and theoretical frameworks +- Synthesize information from diverse sources into coherent, actionable insights and conclusions +- Perform advanced reasoning, logical validation, and critical evaluation of findings +- Design and execute comprehensive research methodologies with systematic approaches +- Validate findings through multiple analytical approaches and cross-verification methods +- Integrate quantitative computational analysis with qualitative research insights +- Challenge assumptions, test edge cases, and explore alternative explanations + +Advanced analytical framework: +- Apply systematic analytical frameworks and methodologies to complex, multi-faceted problems +- Present multiple perspectives and consider alternative explanations and interpretations +- Use both web research and code execution capabilities to validate hypotheses comprehensively +- Document reasoning processes, analytical methodologies, and decision-making criteria clearly +- Integrate diverse analytical approaches including statistical, computational, and theoretical methods +- Maintain objectivity while exploring creative and innovative solution approaches""", + ) + + # Create the manager agent with comprehensive coordination capabilities + manager_agent = CodeAgent( + model=model, + tools=shared_tools, # Manager has minimal tools, delegates to agents + max_steps=25, + verbosity_level=2, + additional_authorized_imports=["*"], + planning_interval=5, + managed_agents=[code_agent, web_research_agent, document_agent, analysis_agent], + name="ManagerAgent", + description="""Centralized Manager and Team Coordination Specialist. + +Primary Role: +- Orchestrate and coordinate the work of 4 specialized agents to solve complex, multi-faceted problems +- Strategically delegate tasks to the most appropriate agent based on their expertise and capabilities +- Synthesize results from multiple agents into comprehensive, well-reasoned final answers +- Ensure quality control and consistency across all agent contributions and outputs +- Manage workflow optimization, resource allocation, and inter-agent communication +- Don't hesitate to ask all agents for their input and expertise. + +Coordination Responsibilities: +- CodeAgent: Delegate mathematical computations, algorithm implementation, and code execution tasks +- WebResearchAgent: Assign web research, fact-checking, and real-time information gathering tasks +- DocumentAgent: Direct document analysis, technical specification review, and citation extraction +- AnalysisAgent: Coordinate comprehensive analysis, hypothesis testing, and multi-source synthesis + +Management Strategy: +- Break down complex problems into specialized subtasks aligned with agent expertise +- Ensure comprehensive coverage of all problem aspects through strategic task distribution +- Validate and cross-check results from multiple agents for accuracy and consistency +- Integrate diverse perspectives and methodologies into cohesive, evidence-based solutions +- Maintain high standards for accuracy, completeness, and methodological rigor""", + ) + + return manager_agent + + +def main(): + """Main benchmarking function.""" + args = parse_arguments() + + # Set date if not provided + if args.date is None: + args.date = datetime.datetime.now().strftime("%Y-%m-%d") + + # Initialize the model + if args.model_type == "LiteLLMModel": + if args.provider == "openai": + # For OpenAI, use the model_id directly + model = LiteLLMModel(model_id=args.model_id) + else: + # For other providers, include provider in model_id + model_id = f"{args.provider}/{args.model_id}" if args.provider != "hf-inference" else args.model_id + model = LiteLLMModel(model_id=model_id) + else: + raise ValueError(f"Unsupported model type: {args.model_type}") + + # Create downloads folder for browser + os.makedirs(BROWSER_CONFIG["downloads_folder"], exist_ok=True) + + # Load dataset + print(f"Loading dataset {args.eval_dataset}...") + df = load_eval_dataset(args.eval_dataset, args.num_examples) + + # If num_examples is specified, sample from each task + if args.num_examples is not None: + df = ( + df.groupby("task", group_keys=False) + .apply(lambda x: x.sample(n=min(args.num_examples, len(x)), random_state=42)) + .reset_index(drop=True) + ) + + print(f"\nLoaded {len(df)} examples total:") + for task in df["task"].unique(): + task_count = len(df[df["task"] == task]) + print(f"- {task}: {task_count} examples") + + # Run benchmark + results = [] + with ThreadPoolExecutor(max_workers=args.parallel_workers) as executor: + futures = [] + + # Group examples by task for better progress tracking + for task in df["task"].unique(): + task_df = df[df["task"] == task] + print(f"\nπŸš€ Starting benchmark for {task} with {len(task_df)} examples...") + + for _, row in task_df.iterrows(): + future = executor.submit(run_centralized_agents, row, args, model) + futures.append(future) + + # Process results with progress bar + for future in tqdm(as_completed(futures), total=len(futures), desc="Processing examples"): + try: + result = future.result() + results.append(result) + except Exception as e: + print(f"Error processing example: {str(e)}") + + # Calculate and print summary statistics per task + print("\nπŸ“Š Results Summary:") + for task in df["task"].unique(): + task_results = [r for r in results if r["task"] == task] + task_success = sum(1 for r in task_results if r["success"]) + task_duration = sum(r["duration"] for r in task_results) / len(task_results) if task_results else 0 + + print(f"\n{task}:") + print(f" Total examples: {len(task_results)}") + print(f" Success rate: {task_success / len(task_results):.2%}") + print(f" Average duration: {task_duration:.2f}s") + + # Overall statistics + total_success = sum(1 for r in results if r["success"]) + avg_duration = sum(r["duration"] for r in results) / len(results) if results else 0 + + print("\nπŸ“ˆ Overall Statistics:") + print(f" Total examples: {len(results)}") + print(f" Success rate: {total_success / len(results):.2%}") + print(f" Average duration: {avg_duration:.2f}s") + + # Push results to hub if requested + if args.push_answers_to_hub: + print("\nπŸš€ Pushing results to hub not yet implemented") + + # Calculate and save benchmark scores with proper variables + output_dir = output_path + model_id = f"centralized-{args.model_type}-{args.model_id}" + action_type = "centralized-agents" + date = args.date + eval_ds = df["task"].unique() + + save_benchmark_scores(output_dir, model_id, action_type, date, eval_ds) + + +if __name__ == "__main__": + main() diff --git a/examples/decentralized_smolagents_benchmark/scripts/__init__.py b/examples/decentralized_smolagents_benchmark/scripts/__init__.py new file mode 100644 index 000000000..b618e8995 --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/__init__.py @@ -0,0 +1 @@ +"""Decentralized agent scripts package.""" diff --git a/examples/decentralized_smolagents_benchmark/scripts/agents.py b/examples/decentralized_smolagents_benchmark/scripts/agents.py new file mode 100644 index 000000000..d9b77dead --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/agents.py @@ -0,0 +1,1051 @@ +"""Agent definitions and utilities for decentralized team.""" + +import json +import logging +import os +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional + + +try: + from PIL import Image as PIL +except ImportError: + PIL = None + +from scripts.text_inspector_tool import FileReaderTool, TextInspectorTool +from scripts.text_web_browser import ( + ArchiveSearchTool, + DownloadTool, + FinderTool, + FindNextTool, + PageDownTool, + PageUpTool, + SimpleTextBrowser, + VisitTool, +) +from scripts.visual_qa import visualizer +from smolagents import CodeAgent, GoogleSearchTool, LiteLLMModel, ToolCallingAgent +from smolagents.default_tools import PythonInterpreterTool +from smolagents.tools import Tool + +from .decentralized_tools import create_decentralized_tools +from .message_store import MessageStore + + +# evaluation roles +custom_role_conversions = {"tool-call": "assistant", "tool-response": "user"} + +script_dir = Path(__file__).parent.parent + +# Browser configuration +user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0" +BROWSER_CONFIG = { + "viewport_size": 1024 * 5, + "downloads_folder": script_dir / "downloads_folder", + "request_kwargs": { + "headers": {"User-Agent": user_agent}, + "timeout": 300, + }, + "serpapi_key": os.getenv("SERPAPI_API_KEY"), +} + +# ------------------------ Prompts (Team Charter) --------------------------- + +TEAM_CHARTER = """ +You are a specialized agent working as part of a decentralized team composed of 4 agents collaborating to solve problems. +CRITICAL: You must regularly use the communication tools provided to stay connected with your team. Check messages and notifications frequently! + +COMMUNICATION TOOLS AVAILABLE: +1. send_message_to_agent: Send private messages to specific agents +2. send_message_to_channel: Post messages to team channels/threads +3. read_messages: Check all your messages and mentions +4. read_notifications: Check notifications including polls needing votes +5. search_messages: Search through message history +6. create_general_poll: Create polls for team decisions +7. create_final_answer_poll: Propose final answers for consensus +8. vote_on_poll: Vote on active polls +9. view_active_polls: See what polls are currently active + +FILE HANDLING WORKFLOW: +CRITICAL: When working with files from URLs, follow this exact sequence: +1. For .docx, .xlsx, .pptx, .wav, .mp3, .m4a, .png files from URLs: + - FIRST: Use 'download_file' tool to download the file locally + - THEN: Use 'inspect_file_as_text' with the downloaded file path for analysis + - For .png files: Use visualizer tool after downloading + +2. For .pdf, .txt, .htm files from URLs: + - Use 'visit_page' tool directly (do NOT use download_file) + +3. For already local files (existing paths): + - Use 'inspect_file_as_text' or 'file_reader' directly + +NEVER try to use text inspection tools on URLs directly - this will cause errors! + +COMMUNICATION CHANNELS: +1. Public Messages (Message Store): + - Post in the main thread or create topic-specific threads (#topic) + - Mention others with @name to get their attention + - Use @all for team-wide announcements + +2. Direct Messages: + - Use send_message tool for private agent-to-agent communication. You are encouraged to communicate with other agents to get help from them. + - Valid target_ids: 0=CodeAgent, 1=WebSearchAgent, 2=DeepResearchAgent, 3=DocumentReaderAgent + - Use receive_messages tool to check your private messages + - Useful for detailed discussions or sharing sensitive information + +3. Threads: + - Default thread is #main + - Create new threads for subtopics (#analysis, #research, etc.) + - Follow relevant threads to stay updated + +COLLABORATION PROTOCOL: +1. When receiving a task: + - FIRST: Check notifications and messages to see team status + - Share your initial thoughts in #main channel + - Break down complex problems into subtasks + - Create relevant threads for different aspects + +2. During Investigation: + - Check messages frequently to see team updates + - Share findings and progress regularly via messaging + - Ask questions and request help when needed + - Combine private and public messages strategically + - Offer assistance to teammates + - Respond to messages and mentions promptly + +3. For Proposals: + - Use create_general_poll to propose intermediate solutions + - Use create_final_answer_poll to propose final answers that would be sent to the user + - Multiple polls can be active simultaneously - you can create additional polls if needed + - When voting, specify the poll_id if multiple polls are active + - Polls with 2+ NO votes are automatically deleted to allow new proposals + - Always include evidence and reasoning in proposals + - Required votes for consensus: floor(N/2)+1 = 3 + +4. Voting Guidelines: + - Check notifications regularly for polls needing votes + - Vote honestly based on your expertise using vote_on_poll + - When multiple polls are active, specify poll_id parameter when voting + - Include confidence level (scoring from 0.0 to 1.0) and rationale + - Suggest improvements if voting NO + +5. Best Practices: + - Keep messages concise and cite sources + - Use thread-specific discussions + - Be proactive in seeking consensus. Therefore, answer to polls promptly. + - Use tools deliberately; include only minimal outputs and sources. + - Use messaging tools proactively - the team relies on communication! + - Combine expertise with other agents through discussion + - Be responsive to team communications + - Never leak secrets or tokens. Sandbox untrusted code/data. + - CRITICAL: Always check for and vote on active polls using read_notifications and vote_on_poll + - You can create multiple polls and vote on all relevant polls + +COMMUNICATION APPROACH: +- Use send_message_to_channel to share deep analysis in #analysis threads +- Use send_message_to_agent for complex hypothesis discussions +- Collaborate with all agents to develop comprehensive understanding +- Lead consensus-building through thoughtful poll creation and voting +- Start each task by checking your notifications with read_notifications +- Use read_messages regularly to stay updated on team discussions, after having looked at notifications +- Send messages to collaborate: use send_message_to_agent for private discussions, send_message_to_channel for team communication (with context and more than one agent, but not cessarely all the agents) +- When you have findings or need input, share them through messaging +- Before making decisions, you can create polls using create_general_poll +- Vote on polls promptly when you see them in notifications +- For final answers, use create_final_answer_poll to get team consensus + +KEY COMMUNICATION PATTERNS: +1. Use read_notifications to gather context from other agents' work +2. Share comprehensive analysis with send_message_to_channel in #analysis +3. Use send_message_to_agent for deep technical and hypothesis discussions +4. Create well-reasoned polls with create_general_poll for major findings +5. Vote thoughtfully on polls using vote_on_poll with detailed rationale + +Remember: A solution is only accepted when a majority agrees (3/4 agents). Work together to reach consensus! + +CRITICAL: make sure that you respect the required format of answer, when you propose one. +MUCH CRITICAL: Never call the FinalAnswerTool, use create_final_answer_poll instead.to propose final answers. + +ANSWER FORMAT REQUIREMENTS: +- For MATH problems: final_answer must be ONLY the number, expression, or result (e.g., "7", "12.5", "$100", "x = 3") +- For FACTUAL questions: final_answer must be ONLY the specific fact requested (e.g., "1925", "John Smith", "Paris", "Blue") +- For YES/NO questions: final_answer must be ONLY "Yes" or "No" +- Do NOT include phrases like "The answer is...", "approximately...", "roughly..." in final_answer +- Do NOT include units unless specifically requested (e.g., if asked "how many", answer "5" not "5 items") +- Put ALL explanations, reasoning, and context in supporting_evidence, NOT in final_answer +- The final_answer field will be returned directly to the user, so keep it clean and minimal + +Your success depends on active communication. Use the messaging and notification tools regularly to coordinate with your team! +""".strip() + + +def _generate_agent_addon(agent_config: dict, all_agents: List[dict]) -> str: + """Generate dynamic agent addon based on configuration and team composition.""" + + # Get other agents for collaboration patterns + other_agents = [a for a in all_agents if a["name"] != agent_config["name"]] + + collaboration_patterns = [] + for i, other_agent in enumerate(other_agents, 1): + pattern = f"{i}. With {other_agent['full_role']} (@{other_agent['name']}):\n" + pattern += f" - {other_agent['collaboration_with'][agent_config['name']]}" + collaboration_patterns.append(pattern) + + addon = f""" +ROLE: {agent_config["full_role"]} +Primary Responsibilities: +{chr(10).join(f"- {resp}" for resp in agent_config["responsibilities"])} + +COLLABORATION PATTERNS: +{chr(10).join(collaboration_patterns)} + +COMMUNICATION: +{chr(10).join(f"- {comm}" for comm in agent_config["communication_patterns"])} + +{agent_config.get("special_instructions", "")} +""".strip() + + return addon + + +# TODO: mettre les descriptions des tools comme dans chacun des autres tools dans default_tools. +# Mettre les notifications +# Post and send messages sous la forme de tools. +# Ou crΓ©er des conversations ids. Et envoyer Γ  la conversation, avec SendMessageTools. 2 tools: SendMessageToChannel (liste agent id + main channel + titre channel) et SendMessageToAgent. +# Utiliser un poll diffΓ©rent pour les pols et ne pas hΓ©riter de FinalAnswerTool. Ne plus l'utiliser et arrΓͺter les agents lorsque c'est fini. Le but est aussi d'avoir une trace complΓ¨te. + +# ---------------End of prompts---------------- + + +class DecentralizedToolCallingAgent(ToolCallingAgent): + """ToolCallingAgent that creates polls for final answers.""" + + def __init__(self, message_store: MessageStore, agent_name: str, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message_store = message_store + self.agent_name = agent_name + + +class DecentralizedCodeAgent(CodeAgent): + """CodeAgent that creates polls for final answers.""" + + def __init__(self, message_store: MessageStore, agent_name: str, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message_store = message_store + self.agent_name = agent_name + + +@dataclass +class AgentConfig: + name: str + role: str + tools: List[Tool] + system_prompt: str + model: Any # Model instance + max_turns: int = 20 + keywords: List[str] = field(default_factory=list) + agent_type: str = "tool_calling" # "tool_calling" or "code" + + +class DecentralizedAgent: + """Minimal wrapper around vanilla smolagents that provides decentralized tools.""" + + def __init__( + self, + config: "AgentConfig", + message_store: MessageStore, + model_type: str, + model_id: str, + provider: Optional[str] = None, + ): + self.config = config + self.message_store = message_store + + # Create the appropriate vanilla smolagents agent + # The LLM will decide when and how to use the communication tools + if config.agent_type == "code": + self.agent = CodeAgent( + tools=config.tools, + model=config.model, + instructions=config.system_prompt, + max_steps=20, + ) + else: + self.agent = ToolCallingAgent( + tools=config.tools, + model=config.model, + instructions=config.system_prompt, + max_steps=20, + ) + + def run(self, task: str, **kwargs): + """Run the agent on a task - delegate to the vanilla smolagents.""" + return self.agent.run(task, **kwargs) + + def step(self) -> List[Dict[str, Any]]: + """Single step interface for compatibility.""" + return [] + + +class DecentralizedAgents: + """Main orchestrator class that combines all the created agents of the team.""" + + def __init__( + self, + message_store: MessageStore, + model_type: str, + model_id: str, + provider: Optional[str] = None, + run_id: Optional[str] = None, + ): + """Initialize the decentralized agent team. + + Args: + message_store: The message store for agent communication + model_type: Type of model to use (e.g., "LiteLLMModel") + model_id: Model ID to use (e.g., "gpt-4o") + provider: Model provider (e.g., "openai") + run_id: Optional run ID for tracking + """ + self.message_store = message_store + self.model_type = model_type + self.model_id = model_id + self.provider = provider + self.run_id = run_id or "default" + + # Create the team of agents + self.agents = self._create_decentralized_team() + + # Track execution state + self.results = [] + self.consensus_reached = False + self.final_answer = None + + # Log team creation + agent_info = [] + for agent in self.agents: + agent_details = {"name": agent.config.name, "role": agent.config.role} + agent_info.append(agent_details) + + logging.info( + json.dumps( + { + "event": "decentralized_team_created", + "run_id": self.run_id, + "agent_count": len(self.agents), + "agents": agent_info, + } + ) + ) + + def _create_decentralized_team(self) -> List[DecentralizedAgent]: + """Create the team of specialized agents.""" + base_prompt = TEAM_CHARTER + + # Initialize shared components + text_limit = 100000 + browser = SimpleTextBrowser(**BROWSER_CONFIG) + + # Initialize model + model_params: dict[str, Any] = { + "model_id": self.model_id, + "custom_role_conversions": custom_role_conversions, + } + + model_params["max_tokens"] = 8192 + + model = LiteLLMModel(**model_params) + + ti_tool = TextInspectorTool(model, text_limit) + + # Create directory for browser downloads if needed + os.makedirs(BROWSER_CONFIG["downloads_folder"], exist_ok=True) + + # Create base tools that are shared between code and web tools + shared_tools = [visualizer, ti_tool] + + # Base web tools + web_tools = [ + GoogleSearchTool(provider="serper"), + VisitTool(browser), + DownloadTool(browser), + PageUpTool(browser), + PageDownTool(browser), + FinderTool(browser), + FindNextTool(browser), + ArchiveSearchTool(browser), + ] + + # Base code tools + code_tools: List[Tool] = [PythonInterpreterTool()] + + # Use simple file reader tool + reader_tools: List[Tool] = [FileReaderTool()] + + # Add shared tools to both collections + code_tools.extend(shared_tools) + web_tools.extend(shared_tools) + reader_tools.extend(shared_tools) + + # Define agent configurations with enhanced role descriptions + agent_definitions = [ + { + "name": "CodeAgent", + "full_role": "Python Code Execution and Algorithm Implementation Specialist", + "short_role": "Python code execution specialist", + "responsibilities": [ + "Write, test, and execute Python code with proper error handling", + "Create modular, testable functions with comprehensive docstrings", + "Implement mathematical algorithms and computational solutions", + "Run validation tests and smoke tests on code changes", + "Optimize code performance and debug complex issues", + "Handle numerical computations and data processing tasks", + ], + "communication_patterns": [ + "Create #implementation threads for coding tasks and technical discussions", + "Use private messages for detailed algorithmic discussions", + "Share code execution results and outputs in public threads for team review", + "Tag @all for major implementation decisions requiring team input", + "Provide code examples and executable demonstrations of solutions", + ], + "special_instructions": """MATH PROBLEM FORMAT: +When solving math problems and creating final answer polls: +- Use Python to calculate exact results with proper precision +- Extract ONLY the numerical result for final_answer (e.g., "7", "12.5", "3/4") +- Do NOT include "The answer is..." or explanations in final_answer +- Show all calculations, code, and reasoning in supporting_evidence +- Follow the specific format requested by the question (decimal, fraction, etc.) +- Validate results through multiple calculation methods when possible""", + "tools": code_tools, + "agent_type": "code", + "keywords": ["code", "python", "execution", "algorithm", "computation"], + }, + { + "name": "WebSearchAgent", + "full_role": "Fast Web Research and Information Gathering Specialist", + "short_role": "Fast web research specialist", + "responsibilities": [ + "Conduct rapid, targeted web searches for relevant information", + "Evaluate source credibility and cross-reference findings", + "Perform initial fact-checking and information triage", + "Gather real-time data and current information from multiple sources", + "Identify trending topics and recent developments", + "Extract key facts and summarize findings concisely", + ], + "communication_patterns": [ + "Create #research threads for new investigation topics", + "Share quick findings and preliminary results in #main channel", + "Use private messages for hypothesis formation and validation discussions", + "Tag @all for significant discoveries that impact the entire team", + "Provide source links and credibility assessments with all findings", + ], + "special_instructions": """RESEARCH METHODOLOGY: +- Always verify information from multiple independent sources +- Prioritize recent, authoritative sources over outdated information +- Include source URLs and publication dates in all research findings +- Flag conflicting information and present different perspectives +- Focus on factual accuracy over speed when sources conflict""", + "tools": web_tools, + "agent_type": "tool_calling", + "keywords": ["research", "web", "search", "facts", "verification"], + }, + { + "name": "DeepResearchAgent", + "full_role": "Comprehensive Analysis and Advanced Research Specialist", + "short_role": "Deep analysis and advanced research specialist", + "responsibilities": [ + "Conduct thorough, multi-layered investigations and analysis", + "Develop and rigorously test complex hypotheses and theories", + "Synthesize information from diverse sources into coherent insights", + "Perform advanced reasoning and logical validation of conclusions", + "Design and execute comprehensive research methodologies", + "Validate findings through multiple analytical approaches", + ], + "communication_patterns": [ + "Maintain #analysis thread for deep analytical discussions", + "Create hypothesis-specific threads for focused investigation", + "Use private messages for complex theoretical discussions", + "Tag @all for major research breakthroughs and validated findings", + "Present comprehensive analysis with supporting evidence and methodology", + ], + "special_instructions": """ANALYSIS FRAMEWORK: +- Apply systematic analytical frameworks to complex problems +- Present multiple perspectives and consider alternative explanations +- Use both web research and code execution to validate hypotheses +- Document reasoning processes and analytical methodologies clearly +- Integrate quantitative and qualitative analysis approaches +- Challenge assumptions and test edge cases thoroughly""", + "tools": list(set(web_tools + code_tools)), + "agent_type": "code", + "keywords": ["research", "deep", "analysis", "hypothesis", "validation", "synthesis"], + }, + { + "name": "DocumentReaderAgent", + "full_role": "Document Analysis and Technical Specification Specialist", + "short_role": "Document analysis and technical specification specialist", + "responsibilities": [ + "Analyze and extract key information from technical documents", + "Maintain precise citations and track information sources", + "Structure complex documentation into digestible summaries", + "Validate technical specifications against implementation requirements", + "Cross-reference multiple documents for consistency and completeness", + "Identify critical details and potential implementation considerations", + ], + "communication_patterns": [ + "Maintain #documentation thread for document-related discussions", + "Create topic-specific threads for major document analyses", + "Use private messages for detailed technical specification reviews", + "Tag @all for critical documentation updates affecting team decisions", + "Provide structured summaries with precise page/section references", + ], + "special_instructions": """DOCUMENTATION STANDARDS: +- Always include precise citations with page numbers or section references +- Highlight contradictions or ambiguities found in documents +- Extract both explicit information and implied requirements +- Cross-reference claims against other available documentation +- Focus on actionable information that impacts problem-solving +- Maintain clear separation between documented facts and interpretations""", + "tools": reader_tools, + "agent_type": "tool_calling", + "keywords": ["document", "pdf", "extract", "page", "specification", "analysis"], + }, + ] + + # Define collaboration patterns between agents + collaboration_matrix = { + "CodeAgent": { + "WebSearchAgent": "Receive algorithm suggestions, implementation requirements, and real-world examples to guide development", + "DocumentReaderAgent": "Get technical specifications, API documentation, and implementation patterns to follow standards", + "DeepResearchAgent": "Implement complex algorithms, run validation experiments, and execute computational analyses", + }, + "WebSearchAgent": { + "CodeAgent": "Share algorithm ideas, provide implementation examples, and verify technical information through search", + "DocumentReaderAgent": "Cross-reference web findings with official documentation and validate external claims", + "DeepResearchAgent": "Provide initial research foundation for deeper analysis and hypothesis development", + }, + "DeepResearchAgent": { + "CodeAgent": "Design computational experiments, request algorithm implementations, and validate results mathematically", + "WebSearchAgent": "Expand on initial findings, request targeted searches, and cross-validate information sources", + "DocumentReaderAgent": "Deep dive into technical documentation, analyze architectural decisions, and validate against specifications", + }, + "DocumentReaderAgent": { + "CodeAgent": "Share technical specifications, validate implementations against documentation, and track API requirements", + "WebSearchAgent": "Compare documentation with external sources, validate technical claims, and share relevant sections", + "DeepResearchAgent": "Provide detailed technical background, support hypothesis validation with documented evidence", + }, + } + + # Add collaboration information to agent definitions + for agent_def in agent_definitions: + agent_def["collaboration_with"] = collaboration_matrix[agent_def["name"]] + + # Add decentralized tools to each agent's tool set and create configs + configs = [] + for agent_def in agent_definitions: + # Add decentralized communication tools + agent_tools = agent_def["tools"] + create_decentralized_tools(self.message_store, agent_def["name"]) + + # Generate dynamic addon + agent_addon = _generate_agent_addon(agent_def, agent_definitions) + + config = AgentConfig( + name=agent_def["name"], + role=agent_def["short_role"], + tools=agent_tools, + model=model, + system_prompt=base_prompt + "\n" + agent_addon, + keywords=agent_def["keywords"], + agent_type=agent_def["agent_type"], + ) + configs.append(config) + + return [ + DecentralizedAgent( + config=config, + message_store=self.message_store, + model_type=self.model_type, + model_id=self.model_id, + provider=self.provider, + ) + for config in configs + ] + + def run(self, task: str) -> Dict[str, Any]: + """Run the entire decentralized agent team on a task. + + Args: + task: The task/question to solve + + Returns: + Dict containing the result with status, answer, and metadata + """ + print(f"πŸš€ Starting decentralized agent team for: {task}") + logging.info(json.dumps({"event": "team_execution_started", "run_id": self.run_id, "task": task})) + + # Post initial task to message store + initial_msg = self.message_store.append_message( + sender="system", content=task, thread_id="main", msg_type="task" + ) + print("πŸ“ Posted initial task to message store") + logging.info( + json.dumps( + {"event": "task_posted", "run_id": self.run_id, "message_id": initial_msg.get("id"), "task": task} + ) + ) + + # Print team info + print(f"πŸ‘₯ Created team of {len(self.agents)} agents:") + for agent in self.agents: + print(f" - {agent.config.name} ({agent.config.role})") + + print("\nπŸ’¬ Running agents in parallel with collaborative approach...") + + # Prepare collaborative task prompt with enhanced instructions + QUESTION_ADDON = """ +IMPORTANT: Before answering, please: +1. Use read_notifications to check if there are any ongoing team discussions or polls +2. Use view_active_polls to see if there are any polls you should vote on +3. Use read_messages to see what other agents have contributed +4. If there's an active poll about the final answer, vote on it using vote_on_poll +5. If no poll exists yet and you're confident in an answer, create a final answer poll using create_final_answer_poll + +Work collaboratively with your team!""" + + collaborative_task = f"{task}\n\n{QUESTION_ADDON}" + + # Run all agents in parallel + self.results = [] + start_time = time.time() + + with ThreadPoolExecutor(max_workers=len(self.agents)) as executor: + # Submit all agent tasks + future_to_agent = { + executor.submit(self._run_single_agent, agent, collaborative_task, i): agent + for i, agent in enumerate(self.agents) + } + + # Process completed agents and check for early consensus + for future in as_completed(future_to_agent): + agent = future_to_agent[future] + try: + agent_result = future.result() + self.results.append(agent_result) + + print(f"βœ… {agent.config.name} completed ({len(self.results)}/{len(self.agents)} done)") + + # Check for early consensus after each completion + if len(self.results) >= 2: # Need at least 2 agents for meaningful consensus + consensus_result = self._check_for_consensus() + if consensus_result: + print(f"\n🎯 Consensus reached early after {agent.config.name}: {consensus_result}") + logging.info( + json.dumps( + { + "event": "consensus_reached_early", + "run_id": self.run_id, + "after_agent": agent.config.name, + "completed_agents": len(self.results), + "final_answer": str(consensus_result)[:200], + } + ) + ) + + # Cancel remaining tasks (best effort) + for remaining_future in future_to_agent: + if not remaining_future.done(): + remaining_future.cancel() + + self.consensus_reached = True + self.final_answer = consensus_result + total_duration = time.time() - start_time + return self._create_result_summary("success_early", total_duration) + + except Exception as e: + print(f"❌ {agent.config.name} failed: {e}") + logging.error( + json.dumps( + { + "event": "agent_failed_in_parallel", + "run_id": self.run_id, + "agent_name": agent.config.name, + "error": str(e), + } + ) + ) + # Continue with other agents + + # Final consensus check + logging.info(json.dumps({"event": "final_consensus_check_started", "run_id": self.run_id})) + final_consensus = self._check_for_consensus() + + total_duration = time.time() - start_time + + if final_consensus: + print(f"\n🎯 Final consensus reached: {final_consensus}") + self.consensus_reached = True + self.final_answer = final_consensus + return self._create_result_summary("success", total_duration) + else: + # Fallback to last valid result + return self._create_fallback_result(total_duration) + + def _run_single_agent(self, agent: DecentralizedAgent, task: str, index: int) -> Dict[str, Any]: + """Run a single agent and return result metadata.""" + print(f"\nπŸ€– Running {agent.config.name}...") + logging.info( + json.dumps( + { + "event": "agent_started", + "run_id": self.run_id, + "agent_name": agent.config.name, + "agent_index": index, + } + ) + ) + + start_time = time.time() + + try: + result = agent.run(task) + end_time = time.time() + duration = end_time - start_time + + print(f"βœ… {agent.config.name} completed") + logging.info( + json.dumps( + { + "event": "agent_completed", + "run_id": self.run_id, + "agent_name": agent.config.name, + "duration_seconds": round(duration, 2), + "result_type": type(result).__name__, + "result_preview": str(result)[:200] if result else None, + } + ) + ) + + return { + "agent_name": agent.config.name, + "status": "success", + "result": result, + "duration": duration, + "error": None, + } + + except Exception as e: + end_time = time.time() + duration = end_time - start_time + + # Special handling for the "int is not iterable" error we're hunting + error_str = str(e) + if "not iterable" in error_str: + import traceback + + logging.error( + json.dumps( + { + "event": "type_iteration_error_caught", + "run_id": self.run_id, + "agent_name": agent.config.name, + "error": error_str, + "error_type": type(e).__name__, + "duration_seconds": round(duration, 2), + "full_traceback": traceback.format_exc(), + "task_preview": task[:300] + "..." if len(task) > 300 else task, + } + ) + ) + print(f"πŸ” FOUND TYPE ERROR in {agent.config.name}: {error_str}") + else: + logging.error( + json.dumps( + { + "event": "agent_failed", + "run_id": self.run_id, + "agent_name": agent.config.name, + "error": error_str, + "error_type": type(e).__name__, + "duration_seconds": round(duration, 2), + } + ) + ) + + print(f"❌ {agent.config.name} failed: {e}") + + return { + "agent_name": agent.config.name, + "status": "error", + "result": f"Error: {e}", + "duration": duration, + "error": str(e), + } + + def _check_for_consensus(self) -> Optional[str]: + """Check if agents reached consensus through polling. + + IMPORTANT: This method ensures that when multiple polls reach consensus, + the answer from the FIRST poll (chronologically) is returned, not just + any successful poll. This maintains consistency with the FinalAnswerTool + behavior and ensures reproducible results. + + Processing order: + 1. First check for any existing final_answer messages (from previously finalized polls) + 2. Then check active polls in chronological order (oldest created first) + 3. Return the answer from the first successful poll encountered + + Returns: + str: The final answer from the first successful poll, or None if no consensus + """ + logging.info(json.dumps({"event": "consensus_check_started", "run_id": self.run_id})) + + try: + # First, look for any existing finalized polls with final answers + # These are final answers from polls that already achieved voting threshold + all_messages = list(self.message_store._iter_messages()) + logging.info( + json.dumps({"event": "messages_retrieved", "run_id": self.run_id, "message_count": len(all_messages)}) + ) + + # Look for the first final_answer message (first poll that achieved threshold) + for msg in all_messages: + if isinstance(msg, dict): + content = msg.get("content", {}) + if isinstance(content, dict) and content.get("type") == "final_answer": + answer = content.get("answer", "") + poll_id = content.get("poll_id", "unknown") + logging.info( + json.dumps( + { + "event": "existing_final_answer_found", + "run_id": self.run_id, + "message_id": msg.get("id"), + "poll_id": poll_id, + "answer": str(answer)[:200], + "note": "first_finalized_poll_in_message_history", + } + ) + ) + print(f"βœ… Using answer from first finalized poll: {answer}") + return answer + + # Check for active polls that might need finalization + # IMPORTANT: We check all active polls to see which one FIRST achieves + # the voting threshold (N//2+1), regardless of creation order + active_polls = self.message_store.get_active_polls() + + print(f"πŸ” Found {len(active_polls)} active polls") + logging.info( + json.dumps( + { + "event": "active_polls_check", + "run_id": self.run_id, + "poll_count": len(active_polls), + "processing_strategy": "first_to_achieve_threshold", + } + ) + ) + + first_successful_answer = None + first_successful_poll_id = None + + for poll in active_polls: + if isinstance(poll, dict): + poll_id = poll.get("poll_id") + print(f"πŸ—³οΈ Checking if poll {poll_id} has achieved voting threshold") + logging.info( + json.dumps( + { + "event": "poll_threshold_check", + "run_id": self.run_id, + "poll_id": poll_id, + "poll_question": poll.get("question", ""), + } + ) + ) + + if poll_id: + # Check if this poll has achieved the voting threshold + result = self.message_store.finalize_poll_if_ready(poll_id) + print(f"πŸ“Š Poll {poll_id} finalization result: {result}") + logging.info( + json.dumps( + { + "event": "poll_finalization_attempted", + "run_id": self.run_id, + "poll_id": poll_id, + "result": str(result)[:200] if result else None, + } + ) + ) + + if result and not result.get("deleted"): + answer_content = result.get("content", {}) + if isinstance(answer_content, dict): + answer = answer_content.get("answer", "") + if first_successful_answer is None: + # This is the FIRST poll to achieve the voting threshold + first_successful_answer = answer + first_successful_poll_id = poll_id + print(f"βœ… First poll to achieve consensus: {poll_id} -> {answer}") + logging.info( + json.dumps( + { + "event": "first_poll_achieved_threshold", + "run_id": self.run_id, + "poll_id": poll_id, + "answer": str(answer)[:200], + "strategy": "first_to_reach_vote_threshold", + } + ) + ) + else: + # Another poll also achieved threshold, but we keep the first one + print( + f"ℹ️ Poll {poll_id} also achieved consensus, but {first_successful_poll_id} was first" + ) + logging.info( + json.dumps( + { + "event": "subsequent_poll_achieved_threshold", + "run_id": self.run_id, + "poll_id": poll_id, + "first_successful_poll": first_successful_poll_id, + "answer_ignored": str(answer)[:200], + } + ) + ) + elif result and result.get("deleted"): + print(f"πŸ—‘οΈ Poll {poll_id} was deleted due to insufficient support") + logging.info( + json.dumps( + { + "event": "poll_deleted", + "run_id": self.run_id, + "poll_id": poll_id, + "reason": "insufficient_support", + } + ) + ) + + # Return the answer from the first poll that achieved the voting threshold + if first_successful_answer is not None: + logging.info( + json.dumps( + { + "event": "returning_first_threshold_answer", + "run_id": self.run_id, + "winning_poll_id": first_successful_poll_id, + "answer": str(first_successful_answer)[:200], + } + ) + ) + return first_successful_answer + + except Exception as e: + print(f"⚠️ Error checking consensus: {e}") + logging.error( + json.dumps( + { + "event": "consensus_check_error", + "run_id": self.run_id, + "error": str(e), + "error_type": type(e).__name__, + } + ) + ) + + logging.info( + json.dumps({"event": "consensus_check_completed", "run_id": self.run_id, "result": "no_consensus"}) + ) + return None + + def _create_result_summary(self, status: str, duration: float) -> Dict[str, Any]: + """Create a comprehensive result summary.""" + logging.info( + json.dumps( + { + "event": "run_completed", + "run_id": self.run_id, + "status": status, + "duration_seconds": round(duration, 2), + } + ) + ) + + return { + "status": status, + "answer": self.final_answer, + "consensus_reached": self.consensus_reached, + "total_duration": round(duration, 2), + "agent_results": self.results, + "run_id": self.run_id, + "agent_count": len(self.agents), + "successful_agents": len([r for r in self.results if r.get("status") == "success"]), + "failed_agents": len([r for r in self.results if r.get("status") == "error"]), + } + + def _create_fallback_result(self, duration: float) -> Dict[str, Any]: + """Create result when no consensus is reached.""" + # Fallback to last valid result + valid_results = [r for r in self.results if r.get("status") == "success"] + + if valid_results: + answer = valid_results[-1]["result"] # Use the last valid result + print(f"\nπŸ“ Final result (fallback): {answer}") + logging.info( + json.dumps( + { + "event": "fallback_result_used", + "run_id": self.run_id, + "result": str(answer)[:200], + "valid_result_count": len(valid_results), + } + ) + ) + + self.final_answer = str(answer) + return self._create_result_summary("success_fallback", duration) + else: + print("\n❌ No valid results obtained") + logging.error( + json.dumps( + { + "event": "no_valid_results", + "run_id": self.run_id, + "total_results": len(self.results), + "error_results": len([r for r in self.results if r.get("status") == "error"]), + } + ) + ) + + self.final_answer = None + return self._create_result_summary("failure", duration) + + def get_agent_by_name(self, name: str) -> Optional[DecentralizedAgent]: + """Get an agent by name.""" + for agent in self.agents: + if agent.config.name == name: + return agent + return None + + def get_results(self) -> Dict[str, Any]: + """Get the current execution results.""" + return { + "consensus_reached": self.consensus_reached, + "final_answer": self.final_answer, + "results": self.results, + "run_id": self.run_id, + } + + @property + def agent_names(self) -> List[str]: + """Get list of agent names.""" + return [agent.config.name for agent in self.agents] + + @property + def agent_count(self) -> int: + """Get number of agents in the team.""" + return len(self.agents) diff --git a/examples/decentralized_smolagents_benchmark/scripts/consensus_protocol.py b/examples/decentralized_smolagents_benchmark/scripts/consensus_protocol.py new file mode 100644 index 000000000..80da1e26d --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/consensus_protocol.py @@ -0,0 +1,152 @@ +"""Decentralized consensus protocol based on emergent behavior.""" + +import re +from typing import Dict, List, Optional, Tuple + + +class ConsensusProtocol: + def __init__(self): + self.local_state = {} + self.confidence_threshold = 0.7 + self.adaptation_rate = 0.2 + + def analyze_conversation(self, messages: List[Dict]) -> Tuple[bool, Optional[str], float]: + """Analyze conversation to detect emerging consensus.""" + # Extract all proposals and their votes + proposals = {} # proposal_id -> {content, votes, confidence} + + for msg in messages: + if msg.get("type") == "final_answer_proposal": + pid = msg["id"] + proposals[pid] = { + "content": msg["content"], + "votes": {"yes": 0, "no": 0}, + "confidence": 0, + "timestamp": msg["timestamp"], + } + elif msg.get("type") == "vote": + content = msg.get("content", {}) + pid = content.get("proposal_id") + if pid in proposals: + vote = content.get("vote") + confidence = content.get("confidence", 0) + if vote in ("yes", "no"): + proposals[pid]["votes"][vote] += 1 + proposals[pid]["confidence"] = max(proposals[pid]["confidence"], confidence) + + if not proposals: + return False, None, 0 + + # Analyze emerging patterns + patterns = self._detect_patterns([p["content"] for p in proposals.values()]) + + # Look for consensus candidates + consensus_candidates = [] + for pid, prop in proposals.items(): + votes = prop["votes"] + total_votes = votes["yes"] + votes["no"] + if total_votes == 0: + continue + + agreement = votes["yes"] / total_votes + if agreement >= 0.5: # At least 50% agreement + score = self._calculate_consensus_score( + agreement, prop["confidence"], self._matches_pattern(prop["content"], patterns), prop["timestamp"] + ) + consensus_candidates.append((score, prop["content"])) + + if not consensus_candidates: + return False, None, 0 + + # Return the strongest consensus candidate + consensus_candidates.sort(reverse=True) + best_score, content = consensus_candidates[0] + + # Need a minimum consensus strength + if best_score > self.confidence_threshold: + return True, content, best_score + + return False, None, best_score + + def _detect_patterns(self, proposals: List[str]) -> List[Dict]: + """Detect emerging patterns in proposals.""" + patterns = [] + + # Number format pattern + number_formats = [] + for p in proposals: + numbers = re.findall(r"\d+\.?\d*", p) + if numbers: + number_formats.extend(numbers) + + if number_formats: + # Check if numbers tend to be integers or decimals + decimals = len([n for n in number_formats if "." in n]) + if decimals > len(number_formats) / 2: + patterns.append({"type": "number", "format": "decimal"}) + else: + patterns.append({"type": "number", "format": "integer"}) + + # Unit format pattern + unit_formats = [] + for p in proposals: + # Try to extract unit part + parts = p.strip().split(" ") + if len(parts) > 1: + unit_formats.append(" ".join(parts[1:])) + + if unit_formats: + # Find most common unit format + from collections import Counter + + unit_counts = Counter(unit_formats) + common_unit = unit_counts.most_common(1)[0][0] + patterns.append({"type": "unit", "format": common_unit}) + + return patterns + + def _matches_pattern(self, content: str, patterns: List[Dict]) -> float: + """Calculate how well content matches detected patterns.""" + if not patterns: + return 0.5 # Neutral when no patterns + + matches = 0 + total = len(patterns) + + for pattern in patterns: + if pattern["type"] == "number": + numbers = re.findall(r"\d+\.?\d*", content) + if numbers: + num = numbers[0] + if pattern["format"] == "decimal" and "." in num: + matches += 1 + elif pattern["format"] == "integer" and "." not in num: + matches += 1 + + elif pattern["type"] == "unit": + parts = content.strip().split(" ") + if len(parts) > 1: + unit = " ".join(parts[1:]) + if unit == pattern["format"]: + matches += 1 + + return matches / total if total > 0 else 0.5 + + def _calculate_consensus_score( + self, agreement: float, confidence: float, pattern_match: float, timestamp: str + ) -> float: + """Calculate overall consensus score combining multiple factors.""" + # Weight recent proposals more heavily + recency = 1.0 # Could factor in timestamp if needed + + # Combine factors with weights + weights = {"agreement": 0.4, "confidence": 0.3, "pattern_match": 0.2, "recency": 0.1} + + score = ( + agreement * weights["agreement"] + + confidence * weights["confidence"] + + pattern_match * weights["pattern_match"] + + recency * weights["recency"] + ) + + return score diff --git a/examples/decentralized_smolagents_benchmark/scripts/convert_all_runs_to_html.py b/examples/decentralized_smolagents_benchmark/scripts/convert_all_runs_to_html.py new file mode 100644 index 000000000..ef36a990c --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/convert_all_runs_to_html.py @@ -0,0 +1,61 @@ +"""Batch convert all runs' messages.jsonl to HTML index files. + +Example: + python -m examples.decentralized_smolagents_benchmark.scripts.convert_all_runs_to_html \ + --runs-dir /home/ecca/GitFiles/dec_smolagents/examples/decentralized_smolagents_benchmark/runs \ + --force +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from .html_renderer import MessagesHtmlRenderer + + +def main() -> None: + parser = argparse.ArgumentParser(description="Convert all run messages.jsonl to HTML") + parser.add_argument( + "--runs-dir", + required=True, + help="Path to runs directory containing subfolders with messages.jsonl", + ) + parser.add_argument( + "--title-prefix", + default="Run", + help="Prefix for HTML title; run folder name is appended", + ) + parser.add_argument( + "--force", + action="store_true", + help="Overwrite existing index.html files if present", + ) + args = parser.parse_args() + + runs_dir = Path(args.runs_dir) + if not runs_dir.exists() or not runs_dir.is_dir(): + raise SystemExit(f"Runs dir not found or not a directory: {runs_dir}") + + renderer = MessagesHtmlRenderer() + count = 0 + for messages_file in runs_dir.glob("*/messages.jsonl"): + run_folder = messages_file.parent + out_file = run_folder / "index.html" + if out_file.exists() and not args.force: + print(f"Skipping {run_folder.name}: index.html exists (use --force to overwrite)") + continue + renderer.title = f"{args.title_prefix} {run_folder.name}" + html = renderer.render_file(messages_file) + out_file.write_text(html, encoding="utf-8") + print(f"Wrote {out_file}") + count += 1 + + if count == 0: + print("No messages.jsonl files found.") + else: + print(f"Converted {count} run(s).") + + +if __name__ == "__main__": + main() diff --git a/examples/decentralized_smolagents_benchmark/scripts/convert_messages_to_html.py b/examples/decentralized_smolagents_benchmark/scripts/convert_messages_to_html.py new file mode 100644 index 000000000..7219d2c46 --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/convert_messages_to_html.py @@ -0,0 +1,35 @@ +"""CLI to convert messages.jsonl runs into a readable HTML file. + +Example: + python -m examples.decentralized_smolagents_benchmark.scripts.convert_messages_to_html \ + --input examples/decentralized_smolagents_benchmark/runs/4f0079d6/messages.jsonl \ + --output examples/decentralized_smolagents_benchmark/runs/4f0079d6/index.html \ + --title "Run 4f0079d6" +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from .html_renderer import MessagesHtmlRenderer + + +def main() -> None: + parser = argparse.ArgumentParser(description="Convert messages.jsonl to HTML") + parser.add_argument("--input", required=True, help="Path to messages.jsonl") + parser.add_argument("--output", required=True, help="Path to write HTML file") + parser.add_argument("--title", default="Run Messages", help="HTML title") + args = parser.parse_args() + + renderer = MessagesHtmlRenderer(title=args.title) + html = renderer.render_file(args.input) + + out_path = Path(args.output) + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(html, encoding="utf-8") + print(f"Wrote HTML to {out_path}") + + +if __name__ == "__main__": + main() diff --git a/examples/decentralized_smolagents_benchmark/scripts/cookies.py b/examples/decentralized_smolagents_benchmark/scripts/cookies.py new file mode 100644 index 000000000..8e4233356 --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/cookies.py @@ -0,0 +1,715 @@ +from requests.cookies import RequestsCookieJar + + +COOKIES_LIST = [ + { + "domain": ".youtube.com", + "expirationDate": 1718884961, + "hostOnly": False, + "httpOnly": False, + "name": "ST-xuwub9", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "session_logininfo=AFmmF2swRAIgf4gadACOuWOcipI1anW-dakEjtidNLkufnOC8uml7EECIDh2YisqWELDBJPTGUysCucJ3I0wjXxYjVHro1LHrdW0%3AQUQ3MjNmd2Jiajl3OWZYRnpFNnZlWWV5ZGJWZ0hpcmp4LVVPU280bk4zOS03Z0ozZG9fOFhWZ0dXaVo3NG1wTEg1b3hGaG10TFBlaFBnTlJfbER5bEp0aFhoNS1OLVhYNFRZT2F6ajgzOFpDbGhlUjZpMWRETlFFRjFfTTRiM0RnNTROSkdmMTFMVjFic1VuZ2trbGp4aktDa0JJUC1BWDh3", + }, + { + "domain": ".youtube.com", + "expirationDate": 1753004444.745411, + "hostOnly": False, + "httpOnly": True, + "name": "__Secure-YEC", + "path": "/", + "sameSite": "lax", + "secure": True, + "session": False, + "storeId": None, + "value": "CgtRVnI5LW1zRHlQVSjbtNCzBjIhCgJGUhIbEhcSFRMLFBUWFwwYGRobHB0eHw4PIBAREiAk", + }, + { + "domain": ".youtube.com", + "expirationDate": 1753434620.050824, + "hostOnly": False, + "httpOnly": True, + "name": "__Secure-3PSID", + "path": "/", + "sameSite": "no_restriction", + "secure": True, + "session": False, + "storeId": None, + "value": "g.a000kwibeLUu8Ea9Y-vLun7u3kU5VNJVuMAZl_jdfJaNm50JyDBB4ezJ_bdWu46a7YwObVn44wACgYKAakSARQSFQHGX2MicJcTzecTKH6bHzqU6TMbTxoVAUF8yKqQYK-MoI6Ql3vI2oYTB3E-0076", + }, + { + "domain": ".youtube.com", + "expirationDate": 1750420959.974642, + "hostOnly": False, + "httpOnly": False, + "name": "SIDCC", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "AKEyXzWQZauHKOo8t87zoEcjaVNIYUX54ohoWXT-tX4aAhEuZzIIptxZAcNkHuG2oDXYL6t-lw", + }, + { + "domain": ".youtube.com", + "expirationDate": 1753434620.050652, + "hostOnly": False, + "httpOnly": False, + "name": "SID", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "g.a000kwibeLUu8Ea9Y-vLun7u3kU5VNJVuMAZl_jdfJaNm50JyDBB6VHrZcC3gBAsFPbCQ0gF5AACgYKAYkSARQSFQHGX2Mi9kt0gHg5CxCYSkLQGHWaeBoVAUF8yKre_V6r3jZVak6JV4o2Q0FL0076", + }, + { + "domain": ".youtube.com", + "expirationDate": 1750420958.397534, + "hostOnly": False, + "httpOnly": True, + "name": "__Secure-1PSIDTS", + "path": "/", + "sameSite": None, + "secure": True, + "session": False, + "storeId": None, + "value": "sidts-CjIB3EgAEkYL2L-GfrEzW5Dfy62S9oefGNLgst78S_986htCnGcfkxECch_9oz-qytSsZBAA", + }, + { + "domain": ".youtube.com", + "expirationDate": 1753433494.44729, + "hostOnly": False, + "httpOnly": False, + "name": "_ga_M0180HEFCY", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "GS1.1.1718871908.1.0.1718873494.0.0.0", + }, + { + "domain": ".youtube.com", + "expirationDate": 1753434620.050933, + "hostOnly": False, + "httpOnly": False, + "name": "SAPISID", + "path": "/", + "sameSite": None, + "secure": True, + "session": False, + "storeId": None, + "value": "mfeuiC-HraNJ-A03/ASXvCPNJSw7yTFgd6", + }, + { + "domain": ".youtube.com", + "expirationDate": 1750420959.974764, + "hostOnly": False, + "httpOnly": True, + "name": "__Secure-1PSIDCC", + "path": "/", + "sameSite": None, + "secure": True, + "session": False, + "storeId": None, + "value": "AKEyXzWHDSoXGCZpZhPxRrnC7B1s8zGIUjeMVyvgtQfsm1fs92lXPtFEI_td9LBUyqVUe0xK", + }, + { + "domain": ".youtube.com", + "expirationDate": 1753434620.050881, + "hostOnly": False, + "httpOnly": True, + "name": "SSID", + "path": "/", + "sameSite": None, + "secure": True, + "session": False, + "storeId": None, + "value": "AmlwXHnQvOQ10LVd-", + }, + { + "domain": ".youtube.com", + "expirationDate": 1753434620.050959, + "hostOnly": False, + "httpOnly": False, + "name": "__Secure-1PAPISID", + "path": "/", + "sameSite": None, + "secure": True, + "session": False, + "storeId": None, + "value": "mfeuiC-HraNJ-A03/ASXvCPNJSw7yTFgd6", + }, + { + "domain": ".youtube.com", + "expirationDate": 1753434620.050795, + "hostOnly": False, + "httpOnly": True, + "name": "__Secure-1PSID", + "path": "/", + "sameSite": None, + "secure": True, + "session": False, + "storeId": None, + "value": "g.a000kwibeLUu8Ea9Y-vLun7u3kU5VNJVuMAZl_jdfJaNm50JyDBBrlk7lRpKQGywAHEon7WGQAACgYKAQsSARQSFQHGX2MirAmnSRdZl6GPG6KLd4hOihoVAUF8yKoV17Tcj1a_OenIOkf2wBjO0076", + }, + { + "domain": ".youtube.com", + "expirationDate": 1753434620.050993, + "hostOnly": False, + "httpOnly": False, + "name": "__Secure-3PAPISID", + "path": "/", + "sameSite": "no_restriction", + "secure": True, + "session": False, + "storeId": None, + "value": "mfeuiC-HraNJ-A03/ASXvCPNJSw7yTFgd6", + }, + { + "domain": ".youtube.com", + "expirationDate": 1750420959.974815, + "hostOnly": False, + "httpOnly": True, + "name": "__Secure-3PSIDCC", + "path": "/", + "sameSite": "no_restriction", + "secure": True, + "session": False, + "storeId": None, + "value": "AKEyXzXM5UjKUEXwSHVmRAIo6hGHA4G63adj3EE1VdNriD0f38jZQbsUKiD4LQbA3BValmTFDg", + }, + { + "domain": ".youtube.com", + "expirationDate": 1750420958.397647, + "hostOnly": False, + "httpOnly": True, + "name": "__Secure-3PSIDTS", + "path": "/", + "sameSite": "no_restriction", + "secure": True, + "session": False, + "storeId": None, + "value": "sidts-CjIB3EgAEkYL2L-GfrEzW5Dfy62S9oefGNLgst78S_986htCnGcfkxECch_9oz-qytSsZBAA", + }, + { + "domain": ".youtube.com", + "expirationDate": 1753434620.050908, + "hostOnly": False, + "httpOnly": False, + "name": "APISID", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "IlQWLPjdNqziwCrV/ANG7Z4x5FF-IBxbZk", + }, + { + "domain": ".youtube.com", + "expirationDate": 1753434620.050855, + "hostOnly": False, + "httpOnly": True, + "name": "HSID", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "AasA7hmRuTFv7vjoq", + }, + { + "domain": ".youtube.com", + "expirationDate": 1753435873.577793, + "hostOnly": False, + "httpOnly": True, + "name": "LOGIN_INFO", + "path": "/", + "sameSite": "no_restriction", + "secure": True, + "session": False, + "storeId": None, + "value": "AFmmF2swRAIgf4gadACOuWOcipI1anW-dakEjtidNLkufnOC8uml7EECIDh2YisqWELDBJPTGUysCucJ3I0wjXxYjVHro1LHrdW0:QUQ3MjNmd2Jiajl3OWZYRnpFNnZlWWV5ZGJWZ0hpcmp4LVVPU280bk4zOS03Z0ozZG9fOFhWZ0dXaVo3NG1wTEg1b3hGaG10TFBlaFBnTlJfbER5bEp0aFhoNS1OLVhYNFRZT2F6ajgzOFpDbGhlUjZpMWRETlFFRjFfTTRiM0RnNTROSkdmMTFMVjFic1VuZ2trbGp4aktDa0JJUC1BWDh3", + }, + { + "domain": ".youtube.com", + "expirationDate": 1753444956.555608, + "hostOnly": False, + "httpOnly": False, + "name": "PREF", + "path": "/", + "sameSite": None, + "secure": True, + "session": False, + "storeId": None, + "value": "f4=4000000&f6=40000000&tz=Europe.Paris&f5=30000&f7=100", + }, +] + +COOKIES_LIST += [ + { + "domain": ".www.researchgate.net", + "hostOnly": False, + "httpOnly": True, + "name": "isInstIp", + "path": "/", + "sameSite": None, + "secure": True, + "session": True, + "storeId": None, + "value": "False", + }, + { + "domain": ".researchgate.net", + "expirationDate": 1734423981, + "hostOnly": False, + "httpOnly": False, + "name": "__eoi", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "ID=c26f752377373146:T=1718871981:RT=1718884914:S=AA-AfjZw-T_OOX2kW2LLaFzXImgc", + }, + { + "domain": ".www.researchgate.net", + "expirationDate": 1753444909.646103, + "hostOnly": False, + "httpOnly": True, + "name": "ptc", + "path": "/", + "sameSite": None, + "secure": True, + "session": False, + "storeId": None, + "value": "RG1.8947708639250500550.1718872043", + }, + { + "domain": ".researchgate.net", + "expirationDate": 1750507578, + "hostOnly": False, + "httpOnly": False, + "name": "euconsent-v2-didomi", + "path": "/", + "sameSite": "lax", + "secure": True, + "session": False, + "storeId": None, + "value": "CQAgmoAQAgmoAAHABBENA5EsAP_gAEPgAAYgJ2pB5G5UTWlBIG53YMskIAUFhFBoQEAgAACAAwIBSBIAIIwEAGAAIAgAICACAAIAIBIAIABAGAAAAAAAYIAAIAAIAAAQIAAKIAAAAAAAAgBQAAgIAgggEAAAgEBEABAAgAAAEIIAQNgACgAAACCAAAAAAAABAAAAAAAAQAAAAAAAYCQAAAJIAAAAACAIABAIAAAAAAAAAAAAAAAABBAAIJ2wPIAFAAXABQAFQALgAcAA8ACAAEgALwAZAA0ACIAEcAJgAUgAqgBcADEAGgAPQAfgBEACOAE4AMMAZYA0QBsgDkAHOAO4AfsBBwEIAItARwBHQC6gHUAO2Ae0A_4CHQEXgJ2AUOAo8BT4CpQFqALYAXmAwQBkgDLAGXANjAhCBG8CbAE3gJ1gTtAA.f_wACHwAAAAA", + }, + { + "domain": ".researchgate.net", + "expirationDate": 1718885236, + "hostOnly": False, + "httpOnly": False, + "name": "_gat", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "1", + }, + { + "domain": "www.researchgate.net", + "expirationDate": 1721477183, + "hostOnly": True, + "httpOnly": False, + "name": "_pbjs_userid_consent_data", + "path": "/", + "sameSite": "lax", + "secure": False, + "session": False, + "storeId": None, + "value": "3524755945110770", + }, + { + "domain": ".researchgate.net", + "expirationDate": 1752567981, + "hostOnly": False, + "httpOnly": False, + "name": "__gads", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "ID=eca2adb88969c830:T=1718871981:RT=1718884914:S=ALNI_MY2qZchynrhWX6hWMlaI87Pcj9riQ", + }, + { + "domain": ".researchgate.net", + "expirationDate": 1718886709.646173, + "hostOnly": False, + "httpOnly": True, + "name": "__cf_bm", + "path": "/", + "sameSite": "no_restriction", + "secure": True, + "session": False, + "storeId": None, + "value": "IkQ_J4ciBzKQduRvjqsfSmQu8UygDWbHeROO5JVccfo-1718884909-1.0.1.1-qvNGEdbfI0HfhFP6kwe7R7mkTqODNhFuKhs72lLly6K2BOPMG3kbahpQFGvPK0U8FUfkznkq65gngd1sWj7sDA", + }, + { + "domain": ".researchgate.net", + "expirationDate": 1752567981, + "hostOnly": False, + "httpOnly": False, + "name": "__gpi", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "UID=00000e4e9aa2e6f2:T=1718871981:RT=1718884914:S=ALNI_MYFNrgzkKn7K6Bd2y8hC6GJCvDiSg", + }, + { + "domain": ".researchgate.net", + "hostOnly": False, + "httpOnly": True, + "name": "_cfuvid", + "path": "/", + "sameSite": "no_restriction", + "secure": True, + "session": True, + "storeId": None, + "value": "_GPmGZkBymiH3UiqTqzakEpi98br3nfFUWC2_u_wqkc-1718884909785-0.0.1.1-604800000", + }, + { + "domain": ".researchgate.net", + "expirationDate": 1753445177.271667, + "hostOnly": False, + "httpOnly": False, + "name": "_ga", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "GA1.1.1525244793.1718885177", + }, + { + "domain": ".researchgate.net", + "expirationDate": 1753445177.271482, + "hostOnly": False, + "httpOnly": False, + "name": "_ga_4P31SJ70EJ", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "GS1.1.1718885177.1.0.1718885177.0.0.0", + }, + { + "domain": ".researchgate.net", + "expirationDate": 1718971576, + "hostOnly": False, + "httpOnly": False, + "name": "_gid", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "GA1.2.854907463.1718885177", + }, + { + "domain": ".www.researchgate.net", + "expirationDate": 1750407982.506505, + "hostOnly": False, + "httpOnly": True, + "name": "did", + "path": "/", + "sameSite": None, + "secure": True, + "session": False, + "storeId": None, + "value": "1dWLO3C6am8l667Q4VUlBo0O1LI49Qi2Vw21SJEXHavBDYT56DI9007W5rYGVFVH", + }, + { + "domain": ".researchgate.net", + "expirationDate": 1750507578, + "hostOnly": False, + "httpOnly": False, + "name": "didomi_token", + "path": "/", + "sameSite": "lax", + "secure": True, + "session": False, + "storeId": None, + "value": "eyJ1c2VyX2lkIjoiMTkwMzU4YTUtNWU2My02Y2UzLWJlNzAtZGFjNzVmYjdiY2ExIiwiY3JlYXRlZCI6IjIwMjQtMDYtMjBUMTI6MDY6MTYuODA2WiIsInVwZGF0ZWQiOiIyMDI0LTA2LTIwVDEyOjA2OjE4Ljc4MVoiLCJ2ZW5kb3JzIjp7ImVuYWJsZWQiOlsidHdpdHRlciIsImdvb2dsZSIsImM6bGlua2VkaW4tbWFya2V0aW5nLXNvbHV0aW9ucyIsImM6b3duZXJpcSIsImM6b21uaXR1cmUtYWRvYmUtYW5hbHl0aWNzIiwiYzp0ZWNobm9yYXRpLW1lZGlhIiwiYzppbnRlcmNvbSIsImM6aW50ZW50LWlxIiwiYzppcHJvbSIsImM6bGlua2VkaW4iLCJjOmFtYXpvbmFkdi16Y1hGTEI2WCIsImM6bWVkaWFuZXQtY1V3YUtFNnoiLCJjOmluZGV4ZXhjaC1OWkNRTTY4UCIsImM6emVvdGFwZ21iLWQ3YndtdGp3IiwiYzp0cmlwbGVsaWYtZGRKSDM0clkiLCJjOnJ0YmhvdXNlLWI4Y2RIOHRNIiwiYzptZHByaW1pcy1lYU4yOVdjUCIsImM6bG9vcG1lbGktVGRhWXRCUHEiLCJjOm1hZ25pdGVpbi05d1RZTHFSRCIsImM6Ymlkc3dpdGNoLWQ2N0V3N1c5IiwiYzpvcmFjbGVhZHYtcUhlREptQUwiLCJjOmdvb2dsZWFuYS00VFhuSmlnUiIsImM6bG90YW1lc29sLURIaTdMUmpNIiwiYzpuZXh0bWlsbGUtR0pyZlg4VWMiLCJjOm5yaWNodGVjLXFVVlEyUlFxIiwiYzpicml0ZXBvb2wtQldWeVdHeVUiLCJjOnRhcGFkaW5jLXFxY2tVN1BXIiwiYzppZDV0ZWNobi16Tk1KNGR3ZiIsImM6bWljcm9zb2Z0IiwiYzpwZXJtdXRpdmUtSjdpaHJlTWsiLCJjOm9wZXJhc29mdC1CY1hjRFZKTSIsImM6cG9zdGhvZy1Cakp4RmRGOSJdfSwicHVycG9zZXMiOnsiZW5hYmxlZCI6WyJnZW9sb2NhdGlvbl9kYXRhIiwiZGV2aWNlX2NoYXJhY3RlcmlzdGljcyJdfSwidmVuZG9yc19saSI6eyJlbmFibGVkIjpbImdvb2dsZSIsImM6b3BlcmFzb2Z0LUJjWGNEVkpNIl19LCJ2ZXJzaW9uIjoyLCJhYyI6IkRIU0FvQUZrQWNnQTVnSHFnUUhBeGdCNndEMTRJR0FRTkFqMEJJd0NTY0VyQUtCd1YtZ3MxQmgwREc0R09nQUEuREhTQW9BRmtBY2dBNWdIcWdRSEF4Z0I2d0QxNElHQVFOQWowQkl3Q1NjRXJBS0J3Vi1nczFCaDBERzRHT2dBQSJ9", + }, + { + "domain": ".www.researchgate.net", + "hostOnly": False, + "httpOnly": True, + "name": "hasPdpNext", + "path": "/", + "sameSite": None, + "secure": True, + "session": True, + "storeId": None, + "value": "False", + }, + { + "domain": ".researchgate.net", + "expirationDate": 1750421183, + "hostOnly": False, + "httpOnly": False, + "name": "ph_phc_ma1XTQyee96N1GML6qUTgLQRiDifnRcE9STiHTZ0CfZ_posthog", + "path": "/", + "sameSite": "lax", + "secure": True, + "session": False, + "storeId": None, + "value": "%7B%22distinct_id%22%3A%220190358a-56a1-7313-83b0-d13dddeac787%22%2C%22%24sesid%22%3A%5B1718885183223%2C%220190358a-56a1-7313-83b0-d13b2b87778d%22%2C1718885176993%5D%2C%22%24session_is_sampled%22%3Atrue%7D", + }, + { + "domain": ".www.researchgate.net", + "hostOnly": False, + "httpOnly": True, + "name": "sid", + "path": "/", + "sameSite": None, + "secure": True, + "session": True, + "storeId": None, + "value": "qmH5Lc4f0CUJ3zeaxORcV0S8I8V1MuCFZtcIQqPYtv1XPejrbSLAQRbT50PL40TqeKQ1XsQDWt9gtYVzuL80bRmPjw6jn3cQ0ikNqW40maHcQ3JL2Vfa8ZZf0j7p35eJ", + }, +] + +COOKIES_LIST += [ + { + "domain": "github.com", + "hostOnly": True, + "httpOnly": True, + "name": "_gh_sess", + "path": "/", + "sameSite": "lax", + "secure": True, + "session": True, + "storeId": None, + "value": "P%2Fmof1avuqwHaUQUIJR%2FZYn7jqbT7lgGuTGjp1BGAFIG5UpNDusEE3b8dRjz0eATE5xPdPjLYFqMs%2FI9AOalKX4YuYfSEEnxCMawU01099b4o9Xzzcv%2BmecrmO0Q8q%2Bdq1h8SIv6nvPP7HzlFesl8ysafb9b%2F0q6dTArKdSOurasza8UgLSYD08ofA50Pcm0IG7CTzF8ZCizrGgGTMi%2F%2B7L3E17jav5PM1Sf2vQKg15Gbg1QIOppJJHzlufgQoZigqFv%2BWznaws0Tt7Y2lSFCw%3D%3D--CJRhqMXJnwOaJgk4--DhUErlL4GdROikEjKD4O9g%3D%3D", + }, + { + "domain": ".github.com", + "expirationDate": 1750408875.763785, + "hostOnly": False, + "httpOnly": False, + "name": "_octo", + "path": "/", + "sameSite": "lax", + "secure": True, + "session": False, + "storeId": None, + "value": "GH1.1.728652011.1718872875", + }, + { + "domain": ".github.com", + "expirationDate": 1750408875.763926, + "hostOnly": False, + "httpOnly": True, + "name": "logged_in", + "path": "/", + "sameSite": "lax", + "secure": True, + "session": False, + "storeId": None, + "value": "no", + }, + { + "domain": ".github.com", + "hostOnly": False, + "httpOnly": False, + "name": "preferred_color_mode", + "path": "/", + "sameSite": "lax", + "secure": True, + "session": True, + "storeId": None, + "value": "dark", + }, + { + "domain": ".github.com", + "hostOnly": False, + "httpOnly": False, + "name": "tz", + "path": "/", + "sameSite": "lax", + "secure": True, + "session": True, + "storeId": None, + "value": "Europe%2FParis", + }, +] + +COOKIES_LIST += [ + { + "domain": ".web.archive.org", + "expirationDate": 1718886430, + "hostOnly": False, + "httpOnly": False, + "name": "_gat", + "path": "/web/20201123221659/http://orcid.org/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "1", + }, + { + "domain": ".web.archive.org", + "expirationDate": 1718972770, + "hostOnly": False, + "httpOnly": False, + "name": "_gid", + "path": "/web/20201123221659/http://orcid.org/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "GA1.2.402246368.1606169825", + }, + { + "domain": ".web.archive.org", + "expirationDate": 1753446370.315621, + "hostOnly": False, + "httpOnly": False, + "name": "_ga", + "path": "/web/20201123221659/http://orcid.org/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "GA1.2.1301409987.1606169825", + }, + { + "domain": ".web.archive.org", + "expirationDate": 1750422367, + "hostOnly": False, + "httpOnly": False, + "name": "_hjid", + "path": "/web/20201123221659/http://orcid.org/", + "sameSite": "lax", + "secure": False, + "session": False, + "storeId": None, + "value": "07f80263-a631-4bf4-8ffd-8fc8912085e2", + }, + { + "domain": ".web.archive.org", + "expirationDate": 1718888167, + "hostOnly": False, + "httpOnly": False, + "name": "_hjFirstSeen", + "path": "/web/20201123221659/http://orcid.org/", + "sameSite": "lax", + "secure": False, + "session": False, + "storeId": None, + "value": "1", + }, +] +COOKIES_LIST += [ + { + "domain": "orcid.org", + "hostOnly": True, + "httpOnly": False, + "name": "AWSELBCORS", + "path": "/", + "sameSite": "no_restriction", + "secure": True, + "session": True, + "storeId": None, + "value": "CBD1D7FF1216388FA48838CBCA4774FD22800B8FB548A40EF92BB0994D5B77A8410307CDEAA69C52236663F2BF89B252C17BC0FCDF790FD59771BDDF6EA8CA4CFD29D8733F", + }, + { + "domain": ".orcid.org", + "expirationDate": 1753452454.637671, + "hostOnly": False, + "httpOnly": False, + "name": "_ga_9R61FWK9H5", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "GS1.1.1718892454.1.0.1718892454.0.0.0", + }, + { + "domain": ".orcid.org", + "expirationDate": 1753452454.63421, + "hostOnly": False, + "httpOnly": False, + "name": "_ga", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "GA1.1.2021310691.1718892455", + }, + { + "domain": "orcid.org", + "hostOnly": True, + "httpOnly": False, + "name": "AWSELB", + "path": "/", + "sameSite": None, + "secure": False, + "session": True, + "storeId": None, + "value": "CBD1D7FF1216388FA48838CBCA4774FD22800B8FB548A40EF92BB0994D5B77A8410307CDEAA69C52236663F2BF89B252C17BC0FCDF790FD59771BDDF6EA8CA4CFD29D8733F", + }, + { + "domain": ".orcid.org", + "expirationDate": 1750428454, + "hostOnly": False, + "httpOnly": False, + "name": "OptanonAlertBoxClosed", + "path": "/", + "sameSite": "lax", + "secure": False, + "session": False, + "storeId": None, + "value": "2024-06-20T14:07:34.583Z", + }, + { + "domain": ".orcid.org", + "expirationDate": 1750428454, + "hostOnly": False, + "httpOnly": False, + "name": "OptanonConsent", + "path": "/", + "sameSite": "lax", + "secure": False, + "session": False, + "storeId": None, + "value": "isGpcEnabled=0&datestamp=Thu+Jun+20+2024+16%3A07%3A34+GMT%2B0200+(heure+d%E2%80%99%C3%A9t%C3%A9+d%E2%80%99Europe+centrale)&version=202310.2.0&browserGpcFlag=0&isIABGlobal=False&hosts=&landingPath=NotLandingPage&groups=C0001%3A1%2CC0003%3A1%2CC0002%3A1%2CC0004%3A1", + }, + { + "domain": "orcid.org", + "hostOnly": True, + "httpOnly": False, + "name": "XSRF-TOKEN", + "path": "/", + "sameSite": None, + "secure": True, + "session": True, + "storeId": None, + "value": "6957be7a-bcb4-4d59-a522-ea9b6b210ed9", + }, +] + +# Create a RequestsCookieJar instance +COOKIES = RequestsCookieJar() + +# Add cookies to the jar +for cookie in COOKIES_LIST: + COOKIES.set(cookie["name"], cookie["value"], domain=cookie["domain"], path=cookie["path"]) diff --git a/examples/decentralized_smolagents_benchmark/scripts/decentralized_tools.py b/examples/decentralized_smolagents_benchmark/scripts/decentralized_tools.py new file mode 100644 index 000000000..062b75b84 --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/decentralized_tools.py @@ -0,0 +1,873 @@ +""" +Decentralized communication and polling tools for multi-agent collaboration. + +This module provides tools for agents to: +1. Send private messages to specific agents +2. Send messages to channels/threads for group discussion +3. Read messages and notifications +4. Create and vote on polls for decision making +5. Propose final answers through consensus mechanism +""" + +import re +from typing import Any, Dict, List, Optional + +from smolagents.tools import Tool + +from .message_store import MessageStore + + +# ============================================================================= +# MESSAGING TOOLS +# ============================================================================= + + +class SendMessageToAgent(Tool): + """Tool for sending private messages directly to specific agents.""" + + name = "send_message_to_agent" + description = """Send a private message directly to a specific agent for one-on-one communication. + Use this for sharing sensitive information, detailed technical discussions, or coordination that + doesn't need to involve the whole team. The target agent will receive a notification.""" + inputs = { + "target_agent": { + "type": "string", + "description": "Name of the recipient agent (CodeAgent, WebSearchAgent, DeepResearchAgent, DocumentReaderAgent)", + }, + "message": {"type": "string", "description": "The message content to send to the target agent"}, + } + output_type = "string" + + def __init__(self, message_store: MessageStore, agent_name: str): + super().__init__() + self.message_store = message_store + self.agent_name = agent_name + + def forward(self, target_agent: str, message: str) -> str: + """Send a private message to the specified agent.""" + try: + result = self.message_store.append_message( + sender=self.agent_name, + content=message, + recipients=[target_agent], + thread_id=None, # Private messages don't use threads + msg_type="private_message", + ) + return f"βœ‰οΈ Private message sent to {target_agent}: {message[:50]}, result: {result}..." + except Exception as e: + return f"❌ Failed to send message to {target_agent}: {str(e)}" + + +class CreateChannel(Tool): + """Tool for creating a new channel for group discussions.""" + + name = "create_channel" + description = """Create a new channel for team discussions on specific topics. + Channels help organize conversations by topic or participant group. + Use this to establish dedicated spaces for focused discussions. + A unique channel ID will be automatically generated and returned.""" + inputs = { + "channel_subject": { + "type": "string", + "description": "Short topic or theme for the new channel (e.g., 'research', 'analysis', 'implementation')", + }, + "channel_description": { + "type": "string", + "description": "Brief description of the channel's purpose and topic", + }, + "initial_members": { + "type": "string", + "description": "Optional comma-separated list of agent names to initially notify about this channel", + "nullable": True, + }, + } + output_type = "object" + + def __init__(self, message_store: MessageStore, agent_name: str): + super().__init__() + self.message_store = message_store + self.agent_name = agent_name + + def forward( + self, channel_subject: str, channel_description: str, initial_members: Optional[str] = None + ) -> Dict[str, Any]: + """Create a new channel for team discussions with auto-generated unique ID.""" + try: + import re + import uuid + + # Generate a unique channel ID based on subject and timestamp + # Clean the subject to make it URL/ID friendly + clean_subject = re.sub(r"[^a-zA-Z0-9\-_]", "-", channel_subject.lower().strip()) + clean_subject = re.sub(r"-+", "-", clean_subject).strip("-") + + # Generate unique ID with subject and short UUID + short_uuid = str(uuid.uuid4())[:8] + channel_id = f"{clean_subject}-{short_uuid}" + + # Parse initial members + member_list = None + if initial_members: + member_list = [m.strip() for m in initial_members.split(",") if m.strip()] + + # Create channel by posting a channel creation message + channel_message = { + "type": "channel_created", + "channel_id": channel_id, + "subject": channel_subject, + "description": channel_description, + "creator": self.agent_name, + "initial_members": member_list or [], + } + + self.message_store.append_message( + sender=self.agent_name, + content=channel_message, + recipients=member_list or ["@all"], + thread_id=channel_id, + msg_type="channel_created", + ) + + member_info = f" with members: {', '.join(member_list)}" if member_list else "" + + return { + "channel_id": channel_id, + "subject": channel_subject, + "description": channel_description, + "creator": self.agent_name, + "initial_members": member_list or [], + "message": f"πŸ“’ Channel created: '{channel_id}' - {channel_description}{member_info}", + } + + except Exception as e: + return {"error": f"❌ Failed to create channel: {str(e)}", "channel_id": None} + + +class SendMessageToChannel(Tool): + """Tool for sending messages to channels/threads for group discussion.""" + + name = "send_message_to_channel" + description = """Send a message to a channel/thread where multiple agents can participate. + You can specify existing channel IDs or create new topic-based channels: + + - Use existing channel IDs: from create_channel or list_channels + - Create topic-based channels: 'research', 'analysis', 'implementation', 'hypothesis-testing' + - Create agent-group channels by listing agents: 'CodeAgent,WebSearchAgent' + - Use 'main' for general discussion + + Channels will be auto-created if they don't exist. Use @AgentName to mention specific agents.""" + inputs = { + "thread_id": { + "type": "string", + "description": """Channel/thread ID or specification: + - Existing channel ID (e.g., 'research-a1b2c3d4') + - Topic-based: 'research', 'analysis', 'implementation', 'hypothesis-testing' + - Agent-based: 'CodeAgent,WebSearchAgent' (comma-separated agent names) + - General: 'main' for general discussion + Channels will be auto-created if they don't exist.""", + }, + "message": { + "type": "string", + "description": "The message content to send. Use @AgentName to mention specific agents.", + }, + "recipients": { + "type": "string", + "description": "Optional comma-separated list of specific agent names to notify. If not provided, uses channel members or mentioned agents.", + "nullable": True, + }, + } + output_type = "object" + + def __init__(self, message_store: MessageStore, agent_name: str): + super().__init__() + self.message_store = message_store + self.agent_name = agent_name + + def forward(self, thread_id: str, message: str, recipients: Optional[str] = None) -> Dict[str, Any]: + """Send a message to the specified channel/thread with auto-creation.""" + try: + # Ensure thread_id is a string to prevent type errors + if not isinstance(thread_id, str): + thread_id = str(thread_id) + + # Process the thread_id to determine channel type and auto-create if needed + processed_channel_id, auto_recipients = self._process_channel_id(thread_id) + + # Process mentions in the message + mentioned_agents = self._extract_mentions(message) + + # Determine final recipient list + recipient_list = None + if recipients: + recipient_list = [r.strip() for r in recipients.split(",")] + elif mentioned_agents: + recipient_list = mentioned_agents + elif auto_recipients: + recipient_list = auto_recipients + # If no specific recipients, use @all for public channels + + result = self.message_store.append_message( + sender=self.agent_name, + content=message, + recipients=recipient_list, + thread_id=processed_channel_id, + msg_type="channel_message", + ) + + # Create info message about recipients/mentions + recipient_info = "" + if mentioned_agents: + recipient_info += f" (mentioning: {', '.join(mentioned_agents)})" + if recipient_list and recipient_list != mentioned_agents: + recipient_info += f" (recipients: {', '.join(recipient_list)})" + + return { + "channel_id": processed_channel_id, + "message_sent": message, + "recipients": recipient_list, + "mentions": mentioned_agents, + "message": f"πŸ“’ Message sent to #{processed_channel_id}{recipient_info}: {message[:50]}...", + "message_id": result.get("id"), + } + + except Exception as e: + return {"error": f"❌ Failed to send message to #{thread_id}: {str(e)}", "channel_id": None} + + def _process_channel_id(self, thread_id: str) -> tuple[str, Optional[List[str]]]: + """Process thread_id to determine channel and auto-create if needed.""" + + # Ensure thread_id is a string to avoid 'int is not iterable' errors + if not isinstance(thread_id, str): + thread_id = str(thread_id) + + # If thread_id contains agent names (has comma or ends with 'Agent'), it's an agent-based channel + if "," in thread_id or thread_id.endswith("Agent"): + # Extract agent names + if "," in thread_id: + agent_names = [name.strip() for name in thread_id.split(",") if name.strip()] + else: + agent_names = [thread_id.strip()] + + # Create a channel ID from agent names + channel_id = f"group-{'-'.join(sorted(agent_names)).lower()}" + + # Check if this agent-based channel exists + if not self._channel_exists(channel_id): + self._auto_create_channel( + channel_id=channel_id, + description=f"Private discussion group for: {', '.join(agent_names)}", + members=agent_names, + ) + + return channel_id, agent_names + + # Topic-based or standard channels + else: + # Check if the channel exists + if not self._channel_exists(thread_id): + # Auto-create topic-based channel + description = self._generate_topic_description(thread_id) + self._auto_create_channel( + channel_id=thread_id, + description=description, + members=None, # Public channel + ) + + return thread_id, None + + def _channel_exists(self, channel_id: str) -> bool: + """Check if a channel already exists by looking for channel creation messages.""" + try: + # Look for existing messages in this thread + existing_messages = self.message_store.get_thread_messages(channel_id) + return len(existing_messages) > 0 + except Exception: + return False + + def _auto_create_channel(self, channel_id: str, description: str, members: Optional[List[str]]) -> None: + """Auto-create a channel with given parameters.""" + try: + channel_message = { + "type": "channel_created", + "channel_id": channel_id, + "description": description, + "creator": self.agent_name, + "auto_created": True, + "initial_members": members or [], + } + + self.message_store.append_message( + sender="system", + content=channel_message, + recipients=members or ["@all"], + thread_id=channel_id, + msg_type="channel_created", + ) + + print(f"πŸ†• Auto-created channel: #{channel_id} - {description}") + + except Exception as e: + print(f"⚠️ Failed to auto-create channel #{channel_id}: {e}") + + def _generate_topic_description(self, topic: str) -> str: + """Generate a description for topic-based channels.""" + # Ensure topic is a string to prevent type errors + if not isinstance(topic, str): + topic = str(topic) + + topic_descriptions = { + "research": "Web research and information gathering", + "analysis": "Deep analysis and data examination", + "implementation": "Code development and implementation", + "hypothesis": "Hypothesis development and testing", + "documentation": "Document analysis and review", + "planning": "Project planning and coordination", + "testing": "Testing and validation discussions", + "review": "Code and content review sessions", + "brainstorm": "Brainstorming and ideation", + "debug": "Debugging and troubleshooting", + } + + # Try to match common topics + for key, desc in topic_descriptions.items(): + if key in topic.lower(): + return desc + + # Default description + return f"Discussion channel for {topic.replace('-', ' ').replace('_', ' ')}" + + def _extract_mentions(self, text: str) -> List[str]: + """Extract @mentions from message text.""" + return re.findall(r"@([A-Za-z0-9_-]+)", text or "") + + +# ============================================================================= +# MESSAGE READING TOOLS +# ============================================================================= + + +class ReadMessagesTool(Tool): + """Tool for reading messages addressed to the current agent.""" + + name = "read_messages" + description = """Read all new messages addressed to this agent including: + - Private messages sent directly to you + - Channel/thread messages where you were mentioned or that match your interests + - Poll notifications where your vote is needed + Returns a list of message objects with sender, content, thread, and type information. + Messages are marked as read after retrieval.""" + inputs = { + "since_timestamp": { + "type": "string", + "description": "Optional timestamp to get only messages after this time (ISO format). If not provided, gets recent unread messages.", + "nullable": True, + }, + "thread_id": { + "type": "string", + "description": "Optional channel/thread ID to filter messages from specific discussions", + "nullable": True, + }, + "mark_as_read": { + "type": "boolean", + "description": "Whether to mark messages as read after retrieving them (default: True)", + "nullable": True, + }, + } + output_type = "array" + + def __init__(self, message_store: MessageStore, agent_name: str): + super().__init__() + self.message_store = message_store + self.agent_name = agent_name + + def forward( + self, since_timestamp: Optional[str] = None, thread_id: Optional[str] = None, mark_as_read: bool = True + ) -> List[Dict[str, Any]]: + """Read all messages for this agent with enhanced filtering.""" + try: + messages = self.message_store.get_messages( + agent_id=self.agent_name, + last_seen_ts=since_timestamp, + thread_id=thread_id, + include_mentions=True, + include_private=True, + ) + + if not messages: + return [] + + # Format messages for display + formatted_messages = [] + for msg in messages: + formatted_msg = { + "id": msg.get("id"), + "sender": msg.get("sender"), + "type": msg.get("type", "message"), + "content": msg.get("content"), + "thread_id": msg.get("thread_id"), + "timestamp": msg.get("timestamp"), + "recipients": msg.get("recipients"), + "is_mention": f"@{self.agent_name}" in str(msg.get("content", "")), + "is_private": self.agent_name in msg.get("recipients", []), + } + formatted_messages.append(formatted_msg) + + # Sort by timestamp (oldest first) + formatted_messages.sort(key=lambda m: m.get("timestamp", "")) + + return formatted_messages + + except Exception as e: + return [{"error": f"Failed to read messages: {str(e)}"}] + + +class ReadNotificationsTool(Tool): + """Tool for checking notifications including mentions, direct messages, and polls.""" + + name = "read_notifications" + description = """Check all notifications for this agent. Returns categorized notifications: + - mentions: Messages where you were mentioned with @YourName + - direct_messages: Private messages sent directly to you + - polls_needing_votes: Active polls where you haven't voted yet + - thread_updates: New activity in threads you're following + Use this to stay updated on important communications and required actions.""" + inputs = { + "since_timestamp": { + "type": "string", + "description": "Optional timestamp to get notifications since a specific time (ISO format)", + "nullable": True, + } + } + output_type = "object" + + def __init__(self, message_store: MessageStore, agent_name: str): + super().__init__() + self.message_store = message_store + self.agent_name = agent_name + + def forward(self, since_timestamp: Optional[str] = None) -> Dict[str, Any]: + """Get categorized notifications for this agent.""" + try: + notifications = self.message_store.get_notifications(agent_id=self.agent_name, since_ts=since_timestamp) + + # Add polls needing votes + active_polls = self.message_store.get_active_polls() + polls_needing_votes = [] + + for poll in active_polls: + poll_id = poll.get("poll_id") + if poll_id: + vote_info = self.message_store.count_votes(poll_id) + voters = vote_info.get("votes_by_voter", {}) + if self.agent_name not in voters: + polls_needing_votes.append( + { + "poll_id": poll_id, + "question": poll.get("question"), + "proposal": poll.get("proposal"), + "proposer": poll.get("proposer"), + "thread_id": poll.get("thread_id", "main"), + } + ) + + notifications["polls_needing_votes"] = polls_needing_votes + + # Add channel information to thread updates + if "thread_updates" in notifications: + channels_info = self.message_store.get_channels_info(agent_id=self.agent_name) + for thread_id in notifications["thread_updates"]: + if thread_id in channels_info: + notifications["thread_updates"][thread_id] = { + "messages": notifications["thread_updates"][thread_id], + "channel_info": { + "subject": channels_info[thread_id].get("subject", thread_id), + "description": channels_info[thread_id].get("description", ""), + "members": channels_info[thread_id].get("members", []), + }, + } + + return notifications + + except Exception as e: + return {"error": f"Failed to get notifications: {str(e)}"} + + +class ListChannelsTool(Tool): + """Tool for listing all available channels and their details.""" + + name = "list_channels" + description = """List all available discussion channels with their details including: + - Channel IDs and subjects + - Descriptions and creators + - Member lists and activity information + - Message counts and last activity timestamps + Use this to see what discussion channels are available for communication.""" + inputs = { + "include_inactive": { + "type": "boolean", + "description": "Include channels with no recent activity (default: False)", + "nullable": True, + }, + "since_timestamp": { + "type": "string", + "description": "Optional timestamp to filter channels by recent activity (ISO format)", + "nullable": True, + }, + } + output_type = "array" + + def __init__(self, message_store: MessageStore, agent_name: str): + super().__init__() + self.message_store = message_store + self.agent_name = agent_name + + def forward(self, include_inactive: bool = False, since_timestamp: Optional[str] = None) -> List[Dict[str, Any]]: + """List all channels visible to this agent with filtering options.""" + try: + channels_info = self.message_store.get_channels_info(agent_id=self.agent_name) + + channels_list = [] + for channel_id, info in channels_info.items(): + # Filter by activity if timestamp provided + if since_timestamp and info.get("last_activity", "") <= since_timestamp: + if not include_inactive: + continue + + # Skip channels with very low activity if not including inactive + if not include_inactive and info.get("message_count", 0) < 2: + continue + + channel_data = { + "channel_id": channel_id, + "subject": info.get("subject", channel_id), + "description": info.get("description", ""), + "creator": info.get("creator", ""), + "created_at": info.get("created_at", ""), + "members": info.get("members", []), + "member_count": len(info.get("members", [])), + "message_count": info.get("message_count", 0), + "last_activity": info.get("last_activity", ""), + "is_created_channel": info.get("is_created_channel", False), + "is_member": self.agent_name in info.get("members", []), + } + channels_list.append(channel_data) + + # Sort by last activity (most recent first) + channels_list.sort(key=lambda x: x.get("last_activity", ""), reverse=True) + + return channels_list + + except Exception as e: + return [{"error": f"Failed to list channels: {str(e)}"}] + + +class SearchMessagesTool(Tool): + """Tool for searching through message history.""" + + name = "search_messages" + description = """Search through the message history to find relevant information. + Use this to find previous discussions, research findings, or decisions made by the team. + You can search by keywords, filter by thread, or limit results by time period.""" + inputs = { + "query": { + "type": "string", + "description": "Search query - keywords or phrases to search for in message content", + }, + "thread_id": { + "type": "string", + "description": "Optional thread ID to search within (e.g., 'main', 'research', 'implementation')", + "nullable": True, + }, + "limit": { + "type": "integer", + "description": "Maximum number of results to return (default: 20)", + "nullable": True, + }, + } + output_type = "array" + + def __init__(self, message_store: MessageStore, agent_name: str): + super().__init__() + self.message_store = message_store + self.agent_name = agent_name + + def forward(self, query: str, thread_id: Optional[str] = None, limit: int = 20) -> List[Dict[str, Any]]: + """Search messages visible to this agent.""" + try: + messages = self.message_store.search_messages( + query=query, agent_id=self.agent_name, thread_id=thread_id, limit=limit + ) + return messages + except Exception as e: + return [{"error": f"Failed to search messages: {str(e)}"}] + + +# ============================================================================= +# POLLING TOOLS +# ============================================================================= + + +class CreateGeneralPollTool(Tool): + """Tool for creating general polls for team decision making.""" + + name = "create_general_poll" + description = """Create a general poll to gather team consensus on intermediate decisions, approaches, or strategies. + Use this for collaborative decision-making when you need team input on research directions, implementation approaches, + or other non-final decisions.""" + inputs = { + "question": {"type": "string", "description": "The question or decision to vote on"}, + "proposal": {"type": "string", "description": "Your proposed answer or approach"}, + "thread_id": {"type": "string", "description": "Thread ID for the poll (default: 'main')", "nullable": True}, + } + output_type = "string" + + def __init__(self, message_store: MessageStore, agent_name: str): + super().__init__() + self.message_store = message_store + self.agent_name = agent_name + + def forward(self, question: str, proposal: str, thread_id: str = "main") -> str: + """Create a general poll for team decision-making.""" + try: + # Check if there are any active polls + # active_polls = self.message_store.get_active_polls() + # if active_polls: + # return f"🚫 Cannot create poll: There is already an active poll in progress (ID: {active_polls[0].get('poll_id')})" + + result = self.message_store.create_poll( + question=question, proposal=proposal, proposer=self.agent_name, thread_id=thread_id + ) + + poll_id = result.get("content", {}).get("poll_id", "unknown") + return f"πŸ—³οΈ General poll created (ID: {poll_id}): {question[:50]}..." + + except Exception as e: + return f"❌ Failed to create poll: {str(e)}" + + +class CreateFinalAnswerPollTool(Tool): + """Tool for creating polls specifically for proposing final answers.""" + + name = "create_final_answer_poll" + description = """Create a poll to propose a final answer to the user's original question. + Use this when you have a confident, complete answer that should be presented to the user. + The proposal will be voted on by all agents, and if it reaches majority consensus (N//2 + 1 votes), + it will be returned as the final answer to the user. + Do not put here elements like, "I will do...", just the answer. + + CRITICAL FORMAT INSTRUCTIONS: + Always carefully follow the format required by the question. This could be for instance: + - For math problems: final_answer should be ONLY the number, expression, or mathematical result (e.g., "7", "3.14", "$50") + - For factual questions: final_answer should be ONLY the specific fact requested (e.g., "1925", "John Smith", "Paris") + - For yes/no questions: final_answer should be ONLY "Yes" or "No" + - Do NOT include explanations, reasoning, or phrases like "The answer is..." in final_answer + - Put all explanations in supporting_evidence, not in final_answer + """ + inputs = { + "final_answer": { + "type": "string", + "description": "ONLY the core answer - no explanations or reasoning. For math: just the number/result. For facts: just the specific requested information. For yes/no: just 'Yes' or 'No'.", + }, + "supporting_evidence": { + "type": "string", + "description": "Supporting evidence, reasoning, calculations, or sources for your answer. Include all explanations HERE, not in final_answer.", + "nullable": True, + }, + } + output_type = "string" + + def __init__(self, message_store: MessageStore, agent_name: str): + super().__init__() + self.message_store = message_store + self.agent_name = agent_name + + def forward(self, final_answer: str, supporting_evidence: str = "") -> str: + """Create a poll for a final answer proposal.""" + try: + # Check if there are any active polls + # active_polls = self.message_store.get_active_polls() + # if active_polls: + # return f"🚫 Cannot create final answer poll: There is already an active poll in progress (ID: {active_polls[0].get('poll_id')})" + + # Create question and proposal for final answer + question = "Should this be our final answer to the user?" + full_proposal = ( + f"{final_answer}\n\n**Supporting Evidence:**\n{supporting_evidence}" + if supporting_evidence + else final_answer + ) + + # Store the clean final answer separately in the poll for proper extraction + result = self.message_store.create_poll( + question=question, + proposal=full_proposal, + proposer=self.agent_name, + thread_id="main", + final_answer=final_answer, # Store clean answer separately + ) + + poll_id = result.get("content", {}).get("poll_id", "unknown") + return f"πŸ—³οΈ Final answer poll created (ID: {poll_id}): {final_answer[:50]}..." + + except Exception as e: + return f"❌ Failed to create final answer poll: {str(e)}" + + +class VoteOnPollTool(Tool): + """Tool for voting on active polls with detailed evaluation.""" + + name = "vote_on_poll" + description = """Vote on a specific active poll. Provide the poll ID, your vote (YES/NO), confidence level, + and detailed rationale based on your expertise. Your vote helps reach team consensus on important decisions.""" + inputs = { + "poll_id": {"type": "string", "description": "ID of the poll you want to vote on", "nullable": True}, + "vote": {"type": "string", "description": "Your vote: 'YES' to approve or 'NO' to reject the proposal"}, + "confidence": { + "type": "number", + "description": "Confidence level in your vote (0.0 to 1.0, where 1.0 = completely confident)", + }, + "rationale": { + "type": "string", + "description": "Detailed explanation for your vote, including your reasoning and any suggestions for improvement", + }, + } + output_type = "string" + + def __init__(self, message_store: MessageStore, agent_name: str): + super().__init__() + self.message_store = message_store + self.agent_name = agent_name + + def forward(self, vote: str, confidence: float, rationale: str, poll_id: Optional[str] = None) -> str: + """Vote on an active poll.""" + try: + # Validate vote + vote = vote.upper() + if vote not in ["YES", "NO"]: + return "❌ Invalid vote. Must be 'YES' or 'NO'." + + # Get active polls + active_polls = self.message_store.get_active_polls() + if not active_polls: + return "❌ No active polls to vote on." + + # Find the specific poll to vote on + target_poll = None + if poll_id: + # Vote on specific poll ID + for poll in active_polls: + if poll.get("poll_id") == poll_id: + target_poll = poll + break + if not target_poll: + return f"❌ Poll with ID {poll_id} not found or is not active." + else: + # If no poll_id specified and only one active poll, use it + if len(active_polls) == 1: + target_poll = active_polls[0] + else: + poll_list = "\n".join( + [ + f"- {p.get('poll_id', 'unknown')}: {p.get('question', 'Unknown question')[:60]}..." + for p in active_polls + ] + ) + return f"❌ Multiple active polls found. Please specify poll_id parameter:\n{poll_list}" + + poll_id = target_poll.get("poll_id") + if not poll_id: + return "❌ Invalid poll ID." + + # Check if already voted + vote_info = self.message_store.count_votes(poll_id) + voters = vote_info.get("votes_by_voter", {}) + if self.agent_name in voters: + return ( + f"❌ You have already voted on poll {poll_id}. Current vote: {voters[self.agent_name].get('vote')}" + ) + + # Record vote + self.message_store.record_vote( + poll_id=poll_id, + voter=self.agent_name, + vote=vote, + confidence=confidence, + rationale=rationale, + thread_id=target_poll.get("thread_id", "main"), + ) + + return f"βœ… Vote recorded on poll {poll_id}: {vote} (confidence: {confidence:.1f}) - {rationale[:50]}..." + + except Exception as e: + return f"❌ Failed to record vote: {str(e)}" + + +class ViewActivePollsTool(Tool): + """Tool for viewing currently active polls and their details.""" + + name = "view_active_polls" + description = """View details of currently active polls including the question, proposal, current vote counts, + and which agents have voted. Use this to see what decisions are pending and check voting progress.""" + inputs = {} + output_type = "array" + + def __init__(self, message_store: MessageStore, agent_name: str): + super().__init__() + self.message_store = message_store + self.agent_name = agent_name + + def forward(self) -> List[Dict[str, Any]]: + """View active polls with voting status.""" + try: + active_polls = self.message_store.get_active_polls() + + if not active_polls: + return [{"message": "No active polls"}] + + poll_details = [] + for poll in active_polls: + poll_id = poll.get("poll_id") + if poll_id: + vote_info = self.message_store.count_votes(poll_id) + poll_details.append( + { + "poll_id": poll_id, + "question": poll.get("question"), + "proposal": poll.get("proposal"), + "proposer": poll.get("proposer"), + "thread_id": poll.get("thread_id"), + "vote_counts": vote_info.get("tally", {}), + "voters": list(vote_info.get("votes_by_voter", {}).keys()), + "has_voted": self.agent_name in vote_info.get("votes_by_voter", {}), + } + ) + + return poll_details + + except Exception as e: + return [{"error": f"Failed to view polls: {str(e)}"}] + + +# ============================================================================= +# HELPER FUNCTIONS +# ============================================================================= + + +def create_decentralized_tools(message_store: MessageStore, agent_name: str) -> List[Tool]: + """Create all decentralized communication and polling tools for an agent.""" + return [ + # Communication tools + SendMessageToAgent(message_store, agent_name), + SendMessageToChannel(message_store, agent_name), + CreateChannel(message_store, agent_name), + # Reading tools + ReadMessagesTool(message_store, agent_name), + ReadNotificationsTool(message_store, agent_name), + SearchMessagesTool(message_store, agent_name), + ListChannelsTool(message_store, agent_name), + # Polling tools + CreateGeneralPollTool(message_store, agent_name), + CreateFinalAnswerPollTool(message_store, agent_name), + VoteOnPollTool(message_store, agent_name), + ViewActivePollsTool(message_store, agent_name), + ] diff --git a/examples/decentralized_smolagents_benchmark/scripts/gaia_scorer.py b/examples/decentralized_smolagents_benchmark/scripts/gaia_scorer.py new file mode 100644 index 000000000..532e0c380 --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/gaia_scorer.py @@ -0,0 +1,124 @@ +import re +import string +import warnings + + +def normalize_number_str(number_str: str) -> float: + # we replace these common units and commas to allow + # conversion to float + for char in ["$", "%", ","]: + number_str = number_str.replace(char, "") + try: + return float(number_str) + except ValueError: + print(f"String {number_str} cannot be normalized to number str.") + return float("inf") + + +def split_string( + s: str, + char_list: list[str] = [",", ";"], +) -> list[str]: + pattern = f"[{''.join(char_list)}]" + return re.split(pattern, s) + + +def is_float(element: any) -> bool: + try: + float(element) + return True + except ValueError: + return False + + +def question_scorer( + model_answer: str, + ground_truth: str, +) -> bool: + # if gt is a number + if is_float(ground_truth): + normalized_answer = normalize_number_str(str(model_answer)) + return normalized_answer == float(ground_truth) + + # if gt is a list + elif any(char in ground_truth for char in [",", ";"]): + # question with the fish: normalization removes punct + + gt_elems = split_string(ground_truth) + ma_elems = split_string(model_answer) + + # check length is the same + if len(gt_elems) != len(ma_elems): + warnings.warn("Answer lists have different lengths, returning False.", UserWarning) + return False + + # compare each element as float or str + comparisons = [] + for ma_elem, gt_elem in zip(ma_elems, gt_elems): + if is_float(gt_elem): + normalized_ma_elem = normalize_number_str(ma_elem) + comparisons.append(normalized_ma_elem == float(gt_elem)) + else: + # we do not remove punct since comparisons can include punct + comparisons.append( + normalize_str(ma_elem, remove_punct=False) == normalize_str(gt_elem, remove_punct=False) + ) + return all(comparisons) + + # if gt is a str + else: + return normalize_str(model_answer) == normalize_str(ground_truth) + + +def check_prediction_contains_answer_letters_in_order(prediction, true_answer): + prediction = prediction.lower() + true_answer = true_answer.lower() + if len(prediction) > len(true_answer) * 3: + return False + i = 0 + for letter in true_answer: + if letter in prediction[i:]: + i += prediction[i:].index(letter) + else: + return False + return True + + +def check_close_call(prediction, true_answer, is_correct): + if is_correct: + return True + else: + if is_float(true_answer): + return is_correct + else: + if ( + check_prediction_contains_answer_letters_in_order(str(prediction), str(true_answer)) + and len(str(true_answer)) * 0.5 <= len(str(prediction)) <= len(str(true_answer)) * 2 + ): + print(f"Close call: {prediction} vs {true_answer}") + return True + else: + return False + + +def normalize_str(input_str, remove_punct=True) -> str: + """ + Normalize a string by: + - Removing all white spaces + - Optionally removing punctuation (if remove_punct is True) + - Converting to lowercase + Parameters: + - input_str: str, the string to normalize + - remove_punct: bool, whether to remove punctuation (default: True) + Returns: + - str, the normalized string + """ + # Remove all white spaces. Required e.g for seagull vs. sea gull + no_spaces = re.sub(r"\s", "", input_str) + + # Remove punctuation, if specified. + if remove_punct: + translator = str.maketrans("", "", string.punctuation) + return no_spaces.lower().translate(translator) + else: + return no_spaces.lower() diff --git a/examples/decentralized_smolagents_benchmark/scripts/html_renderer.py b/examples/decentralized_smolagents_benchmark/scripts/html_renderer.py new file mode 100644 index 000000000..54138500b --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/html_renderer.py @@ -0,0 +1,168 @@ +"""Render decentralized_smolagents messages.jsonl into a readable HTML page. + +Usage (library): + from scripts.html_renderer import MessagesHtmlRenderer + html = MessagesHtmlRenderer().render_file("path/to/messages.jsonl") + +This module intentionally avoids external deps. +""" + +from __future__ import annotations + +import html +import json +from pathlib import Path +from typing import Any, Dict, Iterable, List, Optional + + +class MessagesHtmlRenderer: + """Render JSONL chat-like logs produced by MessageStore into HTML. + + The renderer is defensive against schema variations: + - Handles dict or string `content` fields + - Handles missing optional fields + - Escapes HTML in user-provided text + """ + + def __init__(self, title: str = "Run Messages", theme: str = "light") -> None: + self.title = title + self.theme = theme + + def render_file(self, jsonl_path: str | Path) -> str: + messages = list(self._iter_messages(Path(jsonl_path))) + return self.render(messages) + + def render(self, messages: List[Dict[str, Any]]) -> str: + head = self._build_head() + body = self._build_body(messages) + return f'{head}{body}' + + # ------------------------------- internals ------------------------------- + def _iter_messages(self, path: Path) -> Iterable[Dict[str, Any]]: + if not path.exists(): + return + with path.open("r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + obj = json.loads(line) + if isinstance(obj, dict): + yield obj + except Exception: + # Skip malformed lines + continue + + def _build_head(self) -> str: + # Minimal CSS, no external fonts for offline use + css = ( + "body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;" + "margin:0;padding:0;background:#0b0c10;color:#e6edf3;}" + ".container{max-width:1100px;margin:0 auto;padding:24px;}" + ".header{position:sticky;top:0;background:#0b0c10;border-bottom:1px solid #1f2328;padding:12px 24px;z-index:1;}" + ".title{font-size:20px;font-weight:600;}" + ".msg{display:grid;grid-template-columns:220px 1fr;gap:12px;padding:14px 10px;border-bottom:1px solid #1f2328;}" + ".meta{color:#9ea7b3;font-size:12px;line-height:1.35;}" + ".sender{font-weight:600;color:#c9d1d9;}" + ".type{display:inline-block;border:1px solid #30363d;border-radius:999px;padding:2px 8px;font-size:11px;color:#c9d1d9;margin-left:6px;}" + ".thread{color:#a5d6ff;margin-left:8px;}" + ".content{white-space:pre-wrap;word-break:break-word;font-size:14px;line-height:1.5;}" + ".pill{display:inline-block;margin-right:6px;margin-top:4px;border:1px solid #30363d;border-radius:999px;padding:2px 8px;font-size:11px;color:#9ea7b3;}" + ".section{margin-top:18px;}" + ".code{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;display:block;overflow:auto;}" + ) + return ( + "" + f'' + f"{html.escape(self.title)}" + f"" + "" + ) + + def _build_body(self, messages: List[Dict[str, Any]]) -> str: + header = f'
{html.escape(self.title)}
' + items = [self._render_message(m) for m in messages] + return f'{header}
{"".join(items)}
' + + def _render_message(self, m: Dict[str, Any]) -> str: + ts = html.escape(str(m.get("timestamp", ""))) + sender = html.escape(str(m.get("sender", ""))) + msg_type = html.escape(str(m.get("type", "message") or "message")) + thread = html.escape(str(m.get("thread_id", "main") or "main")) + recipients = m.get("recipients", []) + recipients = recipients if isinstance(recipients, list) else [recipients] + recipients_html = "".join(f'{html.escape(str(r))}' for r in recipients if r) + + content_html = self._render_content(m.get("content")) + + meta = ( + f'
{sender}' + f'{msg_type}' + f'# {thread}
{ts}
{recipients_html}
' + ) + + return f'
{meta}
{content_html}
' + + def _render_content(self, content: Any) -> str: + # String content + if isinstance(content, str): + return html.escape(content) + + # Dict content with known subtypes + if isinstance(content, dict): + t = content.get("type") + if t == "poll": + return self._render_poll(content) + if t == "vote": + return self._kv_block(content, keys=["poll_id", "voter", "vote", "confidence", "rationale"]) + if t == "final_answer": + return self._kv_block(content, keys=["answer", "poll_id", "tally", "source_proposer"]) + # Generic dict fallback + return self._kv_block(content) + + # Anything else + try: + return self._kv_block(json.loads(str(content))) + except Exception: + return f'{html.escape(str(content))}' + + def _kv_block(self, obj: Dict[str, Any], keys: Optional[List[str]] = None) -> str: + if not isinstance(obj, dict): + return f'{html.escape(str(obj))}' + items: List[str] = [] + if keys is None: + keys = list(obj.keys()) + for k in keys: + if k in obj: + v = obj[k] + pretty = self._pretty(v) + items.append(f"
{html.escape(str(k))}: {pretty}
") + # Include any remaining keys not listed explicitly + for k, v in obj.items(): + if k in keys: + continue + items.append(f"
{html.escape(str(k))}: {self._pretty(v)}
") + return '
' + "".join(items) + "
" + + def _pretty(self, value: Any) -> str: + if isinstance(value, (str, int, float)) or value is None: + return html.escape(str(value)) + try: + dump = json.dumps(value, ensure_ascii=False, indent=2) + return f'{html.escape(dump)}' + except Exception: + return f'{html.escape(str(value))}' + + def _render_poll(self, poll: Dict[str, Any]) -> str: + # Render poll with a friendly layout emphasizing question and proposal + question = html.escape(str(poll.get("question", "Poll"))) + proposal = poll.get("proposal") + proposal_html = self._pretty(proposal) + header = f"
Question: {question}
" + body = f"
Proposal: {proposal_html}
" + meta = self._kv_block( + poll, + keys=["poll_id", "options", "threshold", "status", "proposer", "final_answer"], + ) + return '
' + header + body + meta + "
" diff --git a/examples/decentralized_smolagents_benchmark/scripts/mdconvert.py b/examples/decentralized_smolagents_benchmark/scripts/mdconvert.py new file mode 100644 index 000000000..5d5273336 --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/mdconvert.py @@ -0,0 +1,1027 @@ +# This is copied from Magentic-one's great repo: https://github.com/microsoft/autogen/blob/v0.4.4/python/packages/autogen-magentic-one/src/autogen_magentic_one/markdown_browser/mdconvert.py +# Thanks to Microsoft researchers for open-sourcing this! +# type: ignore +import base64 +import copy +import html +import json +import mimetypes +import os +import re +import shutil +import subprocess +import sys +import tempfile +import traceback +import zipfile +from typing import Any +from urllib.parse import parse_qs, quote, unquote, urlparse, urlunparse + +import mammoth +import markdownify +import pandas as pd +import pdfminer +import pdfminer.high_level +import pptx + +# File-format detection +import puremagic +import pydub +import requests +import speech_recognition as sr +from bs4 import BeautifulSoup +from youtube_transcript_api import YouTubeTranscriptApi +from youtube_transcript_api.formatters import SRTFormatter + + +class _CustomMarkdownify(markdownify.MarkdownConverter): + """ + A custom version of markdownify's MarkdownConverter. Changes include: + + - Altering the default heading style to use '#', '##', etc. + - Removing javascript hyperlinks. + - Truncating images with large data:uri sources. + - Ensuring URIs are properly escaped, and do not conflict with Markdown syntax + """ + + def __init__(self, **options: Any): + options["heading_style"] = options.get("heading_style", markdownify.ATX) + # Explicitly cast options to the expected type if necessary + super().__init__(**options) + + def convert_hn(self, n: int, el: Any, text: str, convert_as_inline: bool) -> str: + """Same as usual, but be sure to start with a new line""" + if not convert_as_inline: + if not re.search(r"^\n", text): + return "\n" + super().convert_hn(n, el, text, convert_as_inline) # type: ignore + + return super().convert_hn(n, el, text, convert_as_inline) # type: ignore + + def convert_a(self, el: Any, text: str, convert_as_inline: bool): + """Same as usual converter, but removes Javascript links and escapes URIs.""" + prefix, suffix, text = markdownify.chomp(text) # type: ignore + if not text: + return "" + href = el.get("href") + title = el.get("title") + + # Escape URIs and skip non-http or file schemes + if href: + try: + parsed_url = urlparse(href) # type: ignore + if parsed_url.scheme and parsed_url.scheme.lower() not in ["http", "https", "file"]: # type: ignore + return "%s%s%s" % (prefix, text, suffix) + href = urlunparse(parsed_url._replace(path=quote(unquote(parsed_url.path)))) # type: ignore + except ValueError: # It's not clear if this ever gets thrown + return "%s%s%s" % (prefix, text, suffix) + + # For the replacement see #29: text nodes underscores are escaped + if ( + self.options["autolinks"] + and text.replace(r"\_", "_") == href + and not title + and not self.options["default_title"] + ): + # Shortcut syntax + return "<%s>" % href + if self.options["default_title"] and not title: + title = href + title_part = ' "%s"' % title.replace('"', r"\"") if title else "" + return "%s[%s](%s%s)%s" % (prefix, text, href, title_part, suffix) if href else text + + def convert_img(self, el: Any, text: str, convert_as_inline: bool) -> str: + """Same as usual converter, but removes data URIs""" + + alt = el.attrs.get("alt", None) or "" + src = el.attrs.get("src", None) or "" + title = el.attrs.get("title", None) or "" + title_part = ' "%s"' % title.replace('"', r"\"") if title else "" + if convert_as_inline and el.parent.name not in self.options["keep_inline_images_in"]: + return alt + + # Remove dataURIs + if src.startswith("data:"): + src = src.split(",")[0] + "..." + + return "![%s](%s%s)" % (alt, src, title_part) + + def convert_soup(self, soup: Any) -> str: + return super().convert_soup(soup) # type: ignore + + +class DocumentConverterResult: + """The result of converting a document to text.""" + + def __init__(self, title: str | None = None, text_content: str = ""): + self.title: str | None = title + self.text_content: str = text_content + + +class DocumentConverter: + """Abstract superclass of all DocumentConverters.""" + + def convert(self, local_path: str, **kwargs: Any) -> None | DocumentConverterResult: + raise NotImplementedError() + + +class PlainTextConverter(DocumentConverter): + """Anything with content type text/plain""" + + def convert(self, local_path: str, **kwargs: Any) -> None | DocumentConverterResult: + # Guess the content type from any file extension that might be around + content_type, _ = mimetypes.guess_type("__placeholder" + kwargs.get("file_extension", "")) + + # Only accept text files + if content_type is None: + return None + # elif "text/" not in content_type.lower(): + # return None + + text_content = "" + with open(local_path, "rt", encoding="utf-8") as fh: + text_content = fh.read() + return DocumentConverterResult( + title=None, + text_content=text_content, + ) + + +class HtmlConverter(DocumentConverter): + """Anything with content type text/html""" + + def convert(self, local_path: str, **kwargs: Any) -> None | DocumentConverterResult: + # Bail if not html + extension = kwargs.get("file_extension", "") + if extension.lower() not in [".html", ".htm"]: + return None + + result = None + with open(local_path, "rt", encoding="utf-8") as fh: + result = self._convert(fh.read()) + + return result + + def _convert(self, html_content: str) -> None | DocumentConverterResult: + """Helper function that converts and HTML string.""" + + # Parse the string + soup = BeautifulSoup(html_content, "html.parser") + + # Remove javascript and style blocks + for script in soup(["script", "style"]): + script.extract() + + # Print only the main content + body_elm = soup.find("body") + webpage_text = "" + if body_elm: + webpage_text = _CustomMarkdownify().convert_soup(body_elm) + else: + webpage_text = _CustomMarkdownify().convert_soup(soup) + + assert isinstance(webpage_text, str) + + return DocumentConverterResult( + title=None if soup.title is None else soup.title.string, text_content=webpage_text + ) + + +class WikipediaConverter(DocumentConverter): + """Handle Wikipedia pages separately, focusing only on the main document content.""" + + def convert(self, local_path: str, **kwargs: Any) -> None | DocumentConverterResult: + # Bail if not Wikipedia + extension = kwargs.get("file_extension", "") + if extension.lower() not in [".html", ".htm"]: + return None + url = kwargs.get("url", "") + if not re.search(r"^https?:\/\/[a-zA-Z]{2,3}\.wikipedia.org\/", url): + return None + + # Parse the file + soup = None + with open(local_path, "rt", encoding="utf-8") as fh: + soup = BeautifulSoup(fh.read(), "html.parser") + + # Remove javascript and style blocks + for script in soup(["script", "style"]): + script.extract() + + # Print only the main content + body_elm = soup.find("div", {"id": "mw-content-text"}) + title_elm = soup.find("span", {"class": "mw-page-title-main"}) + + webpage_text = "" + main_title = None if soup.title is None else soup.title.string + + if body_elm: + # What's the title + if title_elm and len(title_elm) > 0: + main_title = title_elm.string # type: ignore + assert isinstance(main_title, str) + + # Convert the page + webpage_text = f"# {main_title}\n\n" + _CustomMarkdownify().convert_soup(body_elm) + else: + webpage_text = _CustomMarkdownify().convert_soup(soup) + + return DocumentConverterResult( + title=main_title, + text_content=webpage_text, + ) + + +class YouTubeConverter(DocumentConverter): + """Handle YouTube specially, focusing on the video title, description, and transcript.""" + + def convert(self, local_path: str, **kwargs: Any) -> None | DocumentConverterResult: + # Bail if not YouTube + extension = kwargs.get("file_extension", "") + if extension.lower() not in [".html", ".htm"]: + return None + url = kwargs.get("url", "") + if not url.startswith("https://www.youtube.com/watch?"): + return None + + # Parse the file + soup = None + with open(local_path, "rt", encoding="utf-8") as fh: + soup = BeautifulSoup(fh.read(), "html.parser") + + # Read the meta tags + assert soup.title is not None and soup.title.string is not None + metadata: dict[str, str] = {"title": soup.title.string} + for meta in soup(["meta"]): + for a in meta.attrs: + if a in ["itemprop", "property", "name"]: + metadata[meta[a]] = meta.get("content", "") + break + + # We can also try to read the full description. This is more prone to breaking, since it reaches into the page implementation + try: + for script in soup(["script"]): + content = script.text + if "ytInitialData" in content: + lines = re.split(r"\r?\n", content) + obj_start = lines[0].find("{") + obj_end = lines[0].rfind("}") + if obj_start >= 0 and obj_end >= 0: + data = json.loads(lines[0][obj_start : obj_end + 1]) + attrdesc = self._findKey(data, "attributedDescriptionBodyText") # type: ignore + if attrdesc: + metadata["description"] = str(attrdesc["content"]) + break + except Exception: + pass + + # Start preparing the page + webpage_text = "# YouTube\n" + + title = self._get(metadata, ["title", "og:title", "name"]) # type: ignore + assert isinstance(title, str) + + if title: + webpage_text += f"\n## {title}\n" + + stats = "" + views = self._get(metadata, ["interactionCount"]) # type: ignore + if views: + stats += f"- **Views:** {views}\n" + + keywords = self._get(metadata, ["keywords"]) # type: ignore + if keywords: + stats += f"- **Keywords:** {keywords}\n" + + runtime = self._get(metadata, ["duration"]) # type: ignore + if runtime: + stats += f"- **Runtime:** {runtime}\n" + + if len(stats) > 0: + webpage_text += f"\n### Video Metadata\n{stats}\n" + + description = self._get(metadata, ["description", "og:description"]) # type: ignore + if description: + webpage_text += f"\n### Description\n{description}\n" + + transcript_text = "" + parsed_url = urlparse(url) # type: ignore + params = parse_qs(parsed_url.query) # type: ignore + if "v" in params: + assert isinstance(params["v"][0], str) + video_id = str(params["v"][0]) + try: + # Must be a single transcript. + transcript = YouTubeTranscriptApi.get_transcript(video_id) # type: ignore + # transcript_text = " ".join([part["text"] for part in transcript]) # type: ignore + # Alternative formatting: + transcript_text = SRTFormatter().format_transcript(transcript) + except Exception: + pass + if transcript_text: + webpage_text += f"\n### Transcript\n{transcript_text}\n" + + title = title if title else soup.title.string + assert isinstance(title, str) + + return DocumentConverterResult( + title=title, + text_content=webpage_text, + ) + + def _get(self, metadata: dict[str, str], keys: list[str], default: str | None = None) -> str | None: + for k in keys: + if k in metadata: + return metadata[k] + return default + + def _findKey(self, json: Any, key: str) -> str | None: # TODO: Fix json type + if isinstance(json, list): + for elm in json: + ret = self._findKey(elm, key) + if ret is not None: + return ret + elif isinstance(json, dict): + for k in json: + if k == key: + return json[k] + else: + ret = self._findKey(json[k], key) + if ret is not None: + return ret + return None + + +class PdfConverter(DocumentConverter): + """ + Converts PDFs to Markdown. Most style information is ignored, so the results are essentially plain-text. + """ + + def convert(self, local_path, **kwargs) -> None | DocumentConverterResult: + # Bail if not a PDF + extension = kwargs.get("file_extension", "") + if extension.lower() != ".pdf": + return None + + return DocumentConverterResult( + title=None, + text_content=pdfminer.high_level.extract_text(local_path), + ) + + +class DocxConverter(HtmlConverter): + """ + Converts DOCX files to Markdown. Style information (e.g.m headings) and tables are preserved where possible. + """ + + def convert(self, local_path, **kwargs) -> None | DocumentConverterResult: + # Bail if not a DOCX + extension = kwargs.get("file_extension", "") + if extension.lower() != ".docx": + return None + + result = None + with open(local_path, "rb") as docx_file: + result = mammoth.convert_to_html(docx_file) + html_content = result.value + result = self._convert(html_content) + + return result + + +class XlsxConverter(HtmlConverter): + """ + Converts XLSX files to Markdown, with each sheet presented as a separate Markdown table. + """ + + def convert(self, local_path, **kwargs) -> None | DocumentConverterResult: + # Bail if not a XLSX + extension = kwargs.get("file_extension", "") + if extension.lower() not in [".xlsx", ".xls"]: + return None + + sheets = pd.read_excel(local_path, sheet_name=None) + md_content = "" + for s in sheets: + md_content += f"## {s}\n" + html_content = sheets[s].to_html(index=False) + md_content += self._convert(html_content).text_content.strip() + "\n\n" + + return DocumentConverterResult( + title=None, + text_content=md_content.strip(), + ) + + +class PptxConverter(HtmlConverter): + """ + Converts PPTX files to Markdown. Supports heading, tables and images with alt text. + """ + + def convert(self, local_path, **kwargs) -> None | DocumentConverterResult: + # Bail if not a PPTX + extension = kwargs.get("file_extension", "") + if extension.lower() != ".pptx": + return None + + md_content = "" + + presentation = pptx.Presentation(local_path) + slide_num = 0 + for slide in presentation.slides: + slide_num += 1 + + md_content += f"\n\n\n" + + title = slide.shapes.title + for shape in slide.shapes: + # Pictures + if self._is_picture(shape): + # https://github.com/scanny/python-pptx/pull/512#issuecomment-1713100069 + alt_text = "" + try: + alt_text = shape._element._nvXxPr.cNvPr.attrib.get("descr", "") + except Exception: + pass + + # A placeholder name + filename = re.sub(r"\W", "", shape.name) + ".jpg" + md_content += "\n![" + (alt_text if alt_text else shape.name) + "](" + filename + ")\n" + + # Tables + if self._is_table(shape): + html_table = "" + first_row = True + for row in shape.table.rows: + html_table += "" + for cell in row.cells: + if first_row: + html_table += "" + else: + html_table += "" + html_table += "" + first_row = False + html_table += "
" + html.escape(cell.text) + "" + html.escape(cell.text) + "
" + md_content += "\n" + self._convert(html_table).text_content.strip() + "\n" + + # Text areas + elif shape.has_text_frame: + if shape == title: + md_content += "# " + shape.text.lstrip() + "\n" + else: + md_content += shape.text + "\n" + + md_content = md_content.strip() + + if slide.has_notes_slide: + md_content += "\n\n### Notes:\n" + notes_frame = slide.notes_slide.notes_text_frame + if notes_frame is not None: + md_content += notes_frame.text + md_content = md_content.strip() + + return DocumentConverterResult( + title=None, + text_content=md_content.strip(), + ) + + def _is_picture(self, shape): + if shape.shape_type == pptx.enum.shapes.MSO_SHAPE_TYPE.PICTURE: + return True + if shape.shape_type == pptx.enum.shapes.MSO_SHAPE_TYPE.PLACEHOLDER: + if hasattr(shape, "image"): + return True + return False + + def _is_table(self, shape): + if shape.shape_type == pptx.enum.shapes.MSO_SHAPE_TYPE.TABLE: + return True + return False + + +class MediaConverter(DocumentConverter): + """ + Abstract class for multi-modal media (e.g., images and audio) + """ + + def _get_metadata(self, local_path): + exiftool = shutil.which("exiftool") + if not exiftool: + return None + else: + try: + result = subprocess.run([exiftool, "-json", local_path], capture_output=True, text=True).stdout + return json.loads(result)[0] + except Exception: + return None + + +class WavConverter(MediaConverter): + """ + Converts WAV files to markdown via extraction of metadata (if `exiftool` is installed), and speech transcription (if `speech_recognition` is installed). + """ + + def convert(self, local_path, **kwargs) -> None | DocumentConverterResult: + # Bail if not a XLSX + extension = kwargs.get("file_extension", "") + if extension.lower() != ".wav": + return None + + md_content = "" + + # Add metadata + metadata = self._get_metadata(local_path) + if metadata: + for f in [ + "Title", + "Artist", + "Author", + "Band", + "Album", + "Genre", + "Track", + "DateTimeOriginal", + "CreateDate", + "Duration", + ]: + if f in metadata: + md_content += f"{f}: {metadata[f]}\n" + + # Transcribe + try: + transcript = self._transcribe_audio(local_path) + md_content += "\n\n### Audio Transcript:\n" + ("[No speech detected]" if transcript == "" else transcript) + except Exception: + md_content += "\n\n### Audio Transcript:\nError. Could not transcribe this audio." + + return DocumentConverterResult( + title=None, + text_content=md_content.strip(), + ) + + def _transcribe_audio(self, local_path) -> str: + recognizer = sr.Recognizer() + with sr.AudioFile(local_path) as source: + audio = recognizer.record(source) + return recognizer.recognize_google(audio).strip() + + +class Mp3Converter(WavConverter): + """ + Converts MP3 and M4A files to markdown via extraction of metadata (if `exiftool` is installed), and speech transcription (if `speech_recognition` AND `pydub` are installed). + """ + + def convert(self, local_path, **kwargs) -> None | DocumentConverterResult: + # Bail if not a MP3 + extension = kwargs.get("file_extension", "") + if extension.lower() not in [".mp3", ".m4a"]: + return None + + md_content = "" + + # Add metadata + metadata = self._get_metadata(local_path) + if metadata: + for f in [ + "Title", + "Artist", + "Author", + "Band", + "Album", + "Genre", + "Track", + "DateTimeOriginal", + "CreateDate", + "Duration", + ]: + if f in metadata: + md_content += f"{f}: {metadata[f]}\n" + + # Transcribe + handle, temp_path = tempfile.mkstemp(suffix=".wav") + os.close(handle) + try: + if extension.lower() == ".mp3": + sound = pydub.AudioSegment.from_mp3(local_path) + else: + sound = pydub.AudioSegment.from_file(local_path, format="m4a") + sound.export(temp_path, format="wav") + + _args = dict() + _args.update(kwargs) + _args["file_extension"] = ".wav" + + try: + transcript = super()._transcribe_audio(temp_path).strip() + md_content += "\n\n### Audio Transcript:\n" + ( + "[No speech detected]" if transcript == "" else transcript + ) + except Exception: + md_content += "\n\n### Audio Transcript:\nError. Could not transcribe this audio." + + finally: + os.unlink(temp_path) + + # Return the result + return DocumentConverterResult( + title=None, + text_content=md_content.strip(), + ) + + +class ZipConverter(DocumentConverter): + """ + Extracts ZIP files to a permanent local directory and returns a listing of extracted files. + """ + + def __init__(self, extract_dir: str = "downloads"): + """ + Initialize with path to extraction directory. + + Args: + extract_dir: The directory where files will be extracted. Defaults to "downloads" + """ + self.extract_dir = extract_dir + # Create the extraction directory if it doesn't exist + os.makedirs(self.extract_dir, exist_ok=True) + + def convert(self, local_path: str, **kwargs: Any) -> None | DocumentConverterResult: + # Bail if not a ZIP file + extension = kwargs.get("file_extension", "") + if extension.lower() != ".zip": + return None + + # Verify it's actually a ZIP file + if not zipfile.is_zipfile(local_path): + return None + + # Extract all files and build list + extracted_files = [] + with zipfile.ZipFile(local_path, "r") as zip_ref: + # Extract all files + zip_ref.extractall(self.extract_dir) + # Get list of all files + for file_path in zip_ref.namelist(): + # Skip directories + if not file_path.endswith("/"): + extracted_files.append(self.extract_dir + "/" + file_path) + + # Sort files for consistent output + extracted_files.sort() + + # Build the markdown content + md_content = "Downloaded the following files:\n" + for file in extracted_files: + md_content += f"* {file}\n" + + return DocumentConverterResult(title="Extracted Files", text_content=md_content.strip()) + + +class ImageConverter(MediaConverter): + """ + Converts images to markdown via extraction of metadata (if `exiftool` is installed), OCR (if `easyocr` is installed), and description via a multimodal LLM (if an mlm_client is configured). + """ + + def convert(self, local_path, **kwargs) -> None | DocumentConverterResult: + # Bail if not a XLSX + extension = kwargs.get("file_extension", "") + if extension.lower() not in [".jpg", ".jpeg", ".png"]: + return None + + md_content = "" + + # Add metadata + metadata = self._get_metadata(local_path) + if metadata: + for f in [ + "ImageSize", + "Title", + "Caption", + "Description", + "Keywords", + "Artist", + "Author", + "DateTimeOriginal", + "CreateDate", + "GPSPosition", + ]: + if f in metadata: + md_content += f"{f}: {metadata[f]}\n" + + # Try describing the image with GPTV + mlm_client = kwargs.get("mlm_client") + mlm_model = kwargs.get("mlm_model") + if mlm_client is not None and mlm_model is not None: + md_content += ( + "\n# Description:\n" + + self._get_mlm_description( + local_path, extension, mlm_client, mlm_model, prompt=kwargs.get("mlm_prompt") + ).strip() + + "\n" + ) + + return DocumentConverterResult( + title=None, + text_content=md_content, + ) + + def _get_mlm_description(self, local_path, extension, client, model, prompt=None): + if prompt is None or prompt.strip() == "": + prompt = "Write a detailed caption for this image." + + sys.stderr.write(f"MLM Prompt:\n{prompt}\n") + + data_uri = "" + with open(local_path, "rb") as image_file: + content_type, encoding = mimetypes.guess_type("_dummy" + extension) + if content_type is None: + content_type = "image/jpeg" + image_base64 = base64.b64encode(image_file.read()).decode("utf-8") + data_uri = f"data:{content_type};base64,{image_base64}" + + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": prompt}, + { + "type": "image_url", + "image_url": { + "url": data_uri, + }, + }, + ], + } + ] + + response = client.chat.completions.create(model=model, messages=messages) + return response.choices[0].message.content + + +class FileConversionException(Exception): + pass + + +class UnsupportedFormatException(Exception): + pass + + +class MarkdownConverter: + """(In preview) An extremely simple text-based document reader, suitable for LLM use. + This reader will convert common file-types or webpages to Markdown.""" + + def __init__( + self, + requests_session: requests.Session | None = None, + mlm_client: Any | None = None, + mlm_model: Any | None = None, + ): + if requests_session is None: + self._requests_session = requests.Session() + else: + self._requests_session = requests_session + + self._mlm_client = mlm_client + self._mlm_model = mlm_model + + self._page_converters: list[DocumentConverter] = [] + + # Register converters for successful browsing operations + # Later registrations are tried first / take higher priority than earlier registrations + # To this end, the most specific converters should appear below the most generic converters + self.register_page_converter(PlainTextConverter()) + self.register_page_converter(HtmlConverter()) + self.register_page_converter(WikipediaConverter()) + self.register_page_converter(YouTubeConverter()) + self.register_page_converter(DocxConverter()) + self.register_page_converter(XlsxConverter()) + self.register_page_converter(PptxConverter()) + self.register_page_converter(WavConverter()) + self.register_page_converter(Mp3Converter()) + self.register_page_converter(ImageConverter()) + self.register_page_converter(ZipConverter()) + self.register_page_converter(PdfConverter()) + + def convert( + self, source: str | requests.Response, **kwargs: Any + ) -> DocumentConverterResult: # TODO: deal with kwargs + """ + Args: + - source: can be a string representing a path or url, or a requests.response object + - extension: specifies the file extension to use when interpreting the file. If None, infer from source (path, uri, content-type, etc.) + """ + + # Local path or url + if isinstance(source, str): + if source.startswith("http://") or source.startswith("https://") or source.startswith("file://"): + return self.convert_url(source, **kwargs) + else: + return self.convert_local(source, **kwargs) + # Request response + elif isinstance(source, requests.Response): + return self.convert_response(source, **kwargs) + + def convert_local(self, path: str, **kwargs: Any) -> DocumentConverterResult: # TODO: deal with kwargs + # Normalize and resolve the path to handle relative paths properly + normalized_path = os.path.abspath(os.path.expanduser(path)) + + # Check if the file exists before proceeding + if not os.path.exists(normalized_path) or not os.path.isfile(normalized_path): + # Return an error result instead of crashing + return DocumentConverterResult( + title="File Not Found", + text_content=f"Error: File not found at path '{path}' (resolved to '{normalized_path}')", + ) + + # Prepare a list of extensions to try (in order of priority) + ext = kwargs.get("file_extension") + extensions = [ext] if ext is not None else [] + + # Get extension alternatives from the path and puremagic + base, ext = os.path.splitext(normalized_path) + self._append_ext(extensions, ext) + self._append_ext(extensions, self._guess_ext_magic(normalized_path)) + + # Convert using the normalized path + return self._convert(normalized_path, extensions, **kwargs) + + # TODO what should stream's type be? + def convert_stream(self, stream: Any, **kwargs: Any) -> DocumentConverterResult: # TODO: deal with kwargs + # Prepare a list of extensions to try (in order of priority) + ext = kwargs.get("file_extension") + extensions = [ext] if ext is not None else [] + + # Save the file locally to a temporary file. It will be deleted before this method exits + handle, temp_path = tempfile.mkstemp() + fh = os.fdopen(handle, "wb") + result = None + try: + # Write to the temporary file + content = stream.read() + if isinstance(content, str): + fh.write(content.encode("utf-8")) + else: + fh.write(content) + fh.close() + + # Use puremagic to check for more extension options + self._append_ext(extensions, self._guess_ext_magic(temp_path)) + + # Convert + result = self._convert(temp_path, extensions, **kwargs) + # Clean up + finally: + try: + fh.close() + except Exception: + pass + os.unlink(temp_path) + + return result + + def convert_url(self, url: str, **kwargs: Any) -> DocumentConverterResult: # TODO: fix kwargs type + # Send a HTTP request to the URL + user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0" + response = self._requests_session.get(url, stream=True, headers={"User-Agent": user_agent}) + response.raise_for_status() + return self.convert_response(response, **kwargs) + + def convert_response( + self, response: requests.Response, **kwargs: Any + ) -> DocumentConverterResult: # TODO fix kwargs type + # Prepare a list of extensions to try (in order of priority) + ext = kwargs.get("file_extension") + extensions = [ext] if ext is not None else [] + + # Guess from the mimetype + content_type = response.headers.get("content-type", "").split(";")[0] + self._append_ext(extensions, mimetypes.guess_extension(content_type)) + + # Read the content disposition if there is one + content_disposition = response.headers.get("content-disposition", "") + m = re.search(r"filename=([^;]+)", content_disposition) + if m: + base, ext = os.path.splitext(m.group(1).strip("\"'")) + self._append_ext(extensions, ext) + + # Read from the extension from the path + base, ext = os.path.splitext(urlparse(response.url).path) + self._append_ext(extensions, ext) + + # Save the file locally to a temporary file. It will be deleted before this method exits + handle, temp_path = tempfile.mkstemp() + fh = os.fdopen(handle, "wb") + result = None + try: + # Download the file + for chunk in response.iter_content(chunk_size=512): + fh.write(chunk) + fh.close() + + # Use puremagic to check for more extension options + self._append_ext(extensions, self._guess_ext_magic(temp_path)) + + # Convert + result = self._convert(temp_path, extensions, url=response.url) + except Exception as e: + print(f"Error in converting: {e}") + + # Clean up + finally: + try: + fh.close() + except Exception: + pass + os.unlink(temp_path) + + return result + + def _convert(self, local_path: str, extensions: list[str | None], **kwargs) -> DocumentConverterResult: + error_trace = "" + for ext in extensions + [None]: # Try last with no extension + for converter in self._page_converters: + _kwargs = copy.deepcopy(kwargs) + + # Overwrite file_extension appropriately + if ext is None: + if "file_extension" in _kwargs: + del _kwargs["file_extension"] + else: + _kwargs.update({"file_extension": ext}) + + # Copy any additional global options + if "mlm_client" not in _kwargs and self._mlm_client is not None: + _kwargs["mlm_client"] = self._mlm_client + + if "mlm_model" not in _kwargs and self._mlm_model is not None: + _kwargs["mlm_model"] = self._mlm_model + + # If we hit an error log it and keep trying + try: + res = converter.convert(local_path, **_kwargs) + except Exception: + error_trace = ("\n\n" + traceback.format_exc()).strip() + + if res is not None: + # Normalize the content + res.text_content = "\n".join([line.rstrip() for line in re.split(r"\r?\n", res.text_content)]) + res.text_content = re.sub(r"\n{3,}", "\n\n", res.text_content) + + # Todo + return res + + # If we got this far without success, report any exceptions + if len(error_trace) > 0: + raise FileConversionException( + f"Could not convert '{local_path}' to Markdown. File type was recognized as {extensions}. While converting the file, the following error was encountered:\n\n{error_trace}" + ) + + # Nothing can handle it! + raise UnsupportedFormatException( + f"Could not convert '{local_path}' to Markdown. The formats {extensions} are not supported." + ) + + def _append_ext(self, extensions, ext): + """Append a unique non-None, non-empty extension to a list of extensions.""" + if ext is None: + return + ext = ext.strip() + if ext == "": + return + # if ext not in extensions: + if True: + extensions.append(ext) + + def _guess_ext_magic(self, path): + """Use puremagic (a Python implementation of libmagic) to guess a file's extension based on the first few bytes.""" + # Use puremagic to guess + if not path or not os.path.exists(path) or not os.path.isfile(path): + # Log the issue but don't raise an error - just return None to continue processing + print(f"Warning: Invalid or non-existent file path in _guess_ext_magic: {path}") + return None + + try: + guesses = puremagic.magic_file(path) + if len(guesses) > 0: + ext = guesses[0].extension.strip() + if len(ext) > 0: + return ext + except puremagic.PureMagicError as e: + # If puremagic fails, we just ignore it and return None + print(f"Error in puremagic: {e}") + except puremagic.PureMagicNotFoundError: + # If puremagic is not installed, we just ignore it and return None + print("puremagic is not installed. Skipping magic file type detection.") + except ValueError: + # If the path is not a file, we just ignore it and return None + pass + except FileNotFoundError: + pass + except IsADirectoryError: + pass + except PermissionError: + pass + return None + + def register_page_converter(self, converter: DocumentConverter) -> None: + """Register a page text converter.""" + self._page_converters.insert(0, converter) diff --git a/examples/decentralized_smolagents_benchmark/scripts/message_store.py b/examples/decentralized_smolagents_benchmark/scripts/message_store.py new file mode 100644 index 000000000..2e7f7ac13 --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/message_store.py @@ -0,0 +1,953 @@ +"""Message store implementation for decentralized agents.""" + +import json +import logging +import threading +import uuid +from collections import defaultdict +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, Iterable, List, Optional + + +def now_ts() -> str: + return datetime.utcnow().replace(tzinfo=timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") + + +def majority_plus_one(n_agents: int) -> int: + return (n_agents // 2) + 1 + + +class MessageStore: + def __init__(self, run_id: str, agent_names: Optional[List[str]] = None): + self.run_id = run_id + script_dir = Path(__file__).parent.parent + self.run_dir = script_dir / "runs" / run_id + self.messages_file = self.run_dir / "messages.jsonl" + self.run_dir.mkdir(parents=True, exist_ok=True) + self._lock = threading.Lock() + self._configured_agents = list(agent_names or []) + + logging.info( + json.dumps( + { + "event": "message_store_initialized", + "run_id": run_id, + "agent_count": len(self._configured_agents), + "messages_file": str(self.messages_file), + } + ) + ) + + def get_messages( + self, + agent_id: str, + last_seen: Optional[str] = None, + last_seen_ts: Optional[str] = None, + thread_id: Optional[str] = None, + include_mentions: bool = True, + include_private: bool = True, + ) -> List[Dict]: + """Get messages visible to the given agent with enhanced filtering.""" + if not self.messages_file.exists(): + return [] + + messages = [] + with self._lock: + for msg in self._iter_messages(): + # Additional safety check + if not isinstance(msg, dict): + continue + + # Skip messages before last_seen timestamp + if last_seen_ts and msg.get("timestamp", "") <= last_seen_ts: + continue + + # Skip if message is before last_seen ID (legacy support) + if last_seen and msg.get("id", "") <= last_seen: + continue + + # Filter by thread if specified + if thread_id and msg.get("thread_id") != thread_id: + continue + + # Check message visibility with enhanced error logging + recipients = msg.get("recipients", []) + + # Enhanced type safety check with detailed logging - BULLETPROOF VERSION + if not isinstance(recipients, (list, tuple)): + logging.warning( + json.dumps( + { + "event": "type_safety_fix_applied", + "location": "get_messages", + "message_id": msg.get("id", "unknown"), + "recipients_type": type(recipients).__name__, + "recipients_value": str(recipients), + "sender": msg.get("sender", "unknown"), + "timestamp": msg.get("timestamp", "unknown"), + } + ) + ) + if recipients is None: + recipients = [] + elif isinstance(recipients, (int, float, bool)): + # Convert problematic types to empty list for safety + recipients = [] + else: + recipients = [str(recipients)] # Convert single value to list + + # Ensure recipients is a list of strings + recipients = [str(r) for r in recipients if r is not None] + + sender = msg.get("sender", "") + content_str = str(msg.get("content", "")) + + # Message visibility logic: + # 1. Public messages (empty recipients or @all) + # 2. Direct messages to this agent + # 3. Messages mentioning this agent (@agent_id) + # 4. Messages from this agent (own messages) + visible = False + + if not recipients or "@all" in recipients: + visible = True # Public message + elif agent_id in recipients: + visible = include_private # Direct message + elif include_mentions and f"@{agent_id}" in content_str: + visible = True # Mentioned in message + elif sender == agent_id: + visible = True # Own message + + if visible: + messages.append(msg) + + return sorted(messages, key=lambda m: m.get("timestamp", "")) + + def get_thread_messages(self, thread_id: str, agent_id: Optional[str] = None) -> List[Dict[str, Any]]: + """Get all messages from a specific thread, filtered by agent visibility if specified.""" + return self.get_messages( + agent_id=agent_id or "system", # Default to system if no agent specified + thread_id=thread_id, + ) + + def search_messages( + self, + query: str, + agent_id: Optional[str] = None, + thread_id: Optional[str] = None, + limit: int = 50, + after_ts: Optional[str] = None, + before_ts: Optional[str] = None, + ) -> List[Dict[str, Any]]: + """Enhanced search with agent visibility and better filtering.""" + q = query.lower().strip() + if not q: + return [] + + out: List[Dict[str, Any]] = [] + with self._lock: + for msg in self._iter_messages(): + ts = msg.get("timestamp", "") + + # Time filtering + if after_ts and ts <= after_ts: + continue + if before_ts and ts >= before_ts: + continue + + # Thread filtering + if thread_id and msg.get("thread_id") != thread_id: + continue + + # Agent visibility check + if agent_id: + recipients = msg.get("recipients", []) + + # Enhanced type safety check with detailed logging - BULLETPROOF VERSION + if not isinstance(recipients, (list, tuple)): + logging.warning( + json.dumps( + { + "event": "type_safety_fix_applied", + "location": "search_messages", + "message_id": msg.get("id", "unknown"), + "recipients_type": type(recipients).__name__, + "recipients_value": str(recipients), + "sender": msg.get("sender", "unknown"), + "query": query, + "timestamp": msg.get("timestamp", "unknown"), + } + ) + ) + if recipients is None: + recipients = [] + elif isinstance(recipients, (int, float, bool)): + # Convert problematic types to empty list for safety + recipients = [] + else: + recipients = [str(recipients)] # Convert single value to list + + # Ensure recipients is a list of strings + recipients = [str(r) for r in recipients if r is not None] + + sender = msg.get("sender", "") + content_str = str(msg.get("content", "")) + + # Additional safety check for the actual operations + try: + visible = ( + not recipients + or "@all" in recipients # Public + or agent_id in recipients # Direct message + or f"@{agent_id}" in content_str # Mentioned + or sender == agent_id # Own message + ) + except TypeError as e: + # Log detailed error information if the type check above missed something + logging.error( + json.dumps( + { + "event": "type_error_caught", + "location": "search_messages_visibility_check", + "error": str(e), + "message_id": msg.get("id", "unknown"), + "recipients": recipients, + "recipients_type": type(recipients).__name__, + "agent_id": agent_id, + "sender": sender, + "query": query, + "run_id": self.run_id, + } + ) + ) + # Default to not visible if we can't determine visibility + continue + if not visible: + continue + + # Content search + blob = json.dumps(msg, ensure_ascii=False).lower() + if q in blob: + out.append(msg) + if len(out) >= limit: + break + + out.sort(key=lambda m: m.get("timestamp", "")) + return out + + # ------------------------------ notifications & agent tools ----------- + def get_notifications(self, agent_id: str, since_ts: Optional[str] = None) -> Dict[str, List[Dict[str, Any]]]: + """Get notifications for an agent including mentions, direct messages, and thread activity.""" + notifications = {"mentions": [], "direct_messages": [], "thread_updates": {}, "polls_needing_votes": []} + + with self._lock: + for msg in self._iter_messages(): + if since_ts and msg.get("timestamp", "") <= since_ts: + continue + + msg_type = msg.get("type", "message") + sender = msg.get("sender", "") + recipients = msg.get("recipients", []) + + # Enhanced type safety check with detailed logging - BULLETPROOF VERSION + if not isinstance(recipients, (list, tuple)): + logging.warning( + json.dumps( + { + "event": "type_safety_fix_applied", + "location": "get_notifications", + "message_id": msg.get("id", "unknown"), + "recipients_type": type(recipients).__name__, + "recipients_value": str(recipients), + "sender": sender, + "agent_id": agent_id, + "timestamp": msg.get("timestamp", "unknown"), + } + ) + ) + if recipients is None: + recipients = [] + elif isinstance(recipients, (int, float, bool)): + # Convert problematic types to empty list for safety + recipients = [] + else: + recipients = [str(recipients)] # Convert single value to list + + # Ensure recipients is a list of strings + recipients = [str(r) for r in recipients if r is not None] + + content_str = str(msg.get("content", "")) + thread_id = msg.get("thread_id", "main") + + # Skip own messages + if sender == agent_id: + continue + + # Additional safety check for the actual operations + try: + # Direct messages + if agent_id in recipients: + notifications["direct_messages"].append(msg) + + # Mentions + if f"@{agent_id}" in content_str: + notifications["mentions"].append(msg) + + # Thread updates (public messages in threads agent is following) + if not recipients or "@all" in recipients: + if thread_id not in notifications["thread_updates"]: + notifications["thread_updates"][thread_id] = [] + notifications["thread_updates"][thread_id].append(msg) + + except TypeError as e: + # Log detailed error information if the type check above missed something + logging.error( + json.dumps( + { + "event": "type_error_caught", + "location": "get_notifications_processing", + "error": str(e), + "message_id": msg.get("id", "unknown"), + "recipients": recipients, + "recipients_type": type(recipients).__name__, + "agent_id": agent_id, + "sender": sender, + "run_id": self.run_id, + } + ) + ) + continue # Skip this message if we can't process it safely + + # Polls needing votes + if msg_type == "poll": + poll_content = msg.get("content", {}) + if poll_content.get("status") == "open": + # Check if agent hasn't voted yet + poll_id = poll_content.get("poll_id") + if poll_id: + vote_info = self.count_votes(poll_id) + if agent_id not in vote_info.get("votes_by_voter", {}): + notifications["polls_needing_votes"].append(msg) + + # Sort all notification lists by timestamp + for key, value in notifications.items(): + if isinstance(value, list): + notifications[key] = sorted(value, key=lambda m: m.get("timestamp", "")) + elif isinstance(value, dict): + for thread, msgs in value.items(): + notifications[key][thread] = sorted(msgs, key=lambda m: m.get("timestamp", "")) + + return notifications + + def get_active_threads(self, agent_id: Optional[str] = None, since_ts: Optional[str] = None) -> List[str]: + """Get list of active thread IDs, optionally filtered by agent visibility.""" + threads = set() + + with self._lock: + for msg in self._iter_messages(): + if since_ts and msg.get("timestamp", "") <= since_ts: + continue + + if agent_id: + # Apply agent visibility filtering + recipients = msg.get("recipients", []) + + # Enhanced type safety check with detailed logging - BULLETPROOF VERSION + if not isinstance(recipients, (list, tuple)): + logging.warning( + json.dumps( + { + "event": "type_safety_fix_applied", + "location": "get_active_threads", + "message_id": msg.get("id", "unknown"), + "recipients_type": type(recipients).__name__, + "recipients_value": str(recipients), + "sender": msg.get("sender", "unknown"), + "agent_id": agent_id, + "timestamp": msg.get("timestamp", "unknown"), + } + ) + ) + if recipients is None: + recipients = [] + elif isinstance(recipients, (int, float, bool)): + # Convert problematic types to empty list for safety + recipients = [] + else: + recipients = [str(recipients)] # Convert single value to list + + # Ensure recipients is a list of strings + recipients = [str(r) for r in recipients if r is not None] + + sender = msg.get("sender", "") + content_str = str(msg.get("content", "")) + + # Additional safety check for the actual operations + try: + visible = ( + not recipients + or "@all" in recipients # Public + or agent_id in recipients # Direct message + or f"@{agent_id}" in content_str # Mentioned + or sender == agent_id # Own message + ) + except TypeError as e: + # Log detailed error information if the type check above missed something + logging.error( + json.dumps( + { + "event": "type_error_caught", + "location": "get_active_threads_visibility_check", + "error": str(e), + "message_id": msg.get("id", "unknown"), + "recipients": recipients, + "recipients_type": type(recipients).__name__, + "agent_id": agent_id, + "sender": sender, + "run_id": self.run_id, + } + ) + ) + # Skip this message if we can't process it safely + continue + if not visible: + continue + + thread_id = msg.get("thread_id", "main") + threads.add(thread_id) + + return sorted(list(threads)) + + def get_channels_info(self, agent_id: Optional[str] = None) -> Dict[str, Any]: + """Get detailed information about all channels visible to the agent.""" + channels = {} + + with self._lock: + for msg in self._iter_messages(): + msg_type = msg.get("type", "message") + thread_id = msg.get("thread_id", "main") + + # Skip if agent filtering is enabled and message not visible to agent + if agent_id: + recipients = msg.get("recipients", []) + + # Enhanced type safety check - BULLETPROOF VERSION + if not isinstance(recipients, (list, tuple)): + if recipients is None: + recipients = [] + elif isinstance(recipients, (int, float, bool)): + # Convert problematic types to empty list for safety + recipients = [] + else: + recipients = [str(recipients)] + + # Ensure recipients is a list of strings + recipients = [str(r) for r in recipients if r is not None] + + sender = msg.get("sender", "") + content_str = str(msg.get("content", "")) + + try: + visible = ( + not recipients + or "@all" in recipients # Public + or agent_id in recipients # Direct message + or f"@{agent_id}" in content_str # Mentioned + or sender == agent_id # Own message + ) + except TypeError: + continue # Skip problematic messages + + if not visible: + continue + + # Initialize channel info if not exists + if thread_id not in channels: + channels[thread_id] = { + "channel_id": thread_id, + "subject": None, + "description": None, + "creator": None, + "created_at": None, + "members": set(), + "last_activity": None, + "message_count": 0, + "is_created_channel": False, + } + + # Update channel info + channels[thread_id]["message_count"] += 1 + channels[thread_id]["last_activity"] = msg.get("timestamp", "") + + # Extract channel creation info + if msg_type == "channel_created": + content = msg.get("content", {}) + if isinstance(content, dict): + channels[thread_id].update( + { + "subject": content.get("subject") or content.get("channel_id", thread_id), + "description": content.get("description", ""), + "creator": content.get("creator") or msg.get("sender", ""), + "created_at": msg.get("timestamp", ""), + "is_created_channel": True, + } + ) + initial_members = content.get("initial_members", []) + if initial_members: + channels[thread_id]["members"].update(initial_members) + + # Add message sender to members + sender = msg.get("sender", "") + if sender and sender not in ["system", "Coordinator"]: + channels[thread_id]["members"].add(sender) + + # Convert sets to lists for JSON serialization + for channel in channels.values(): + channel["members"] = sorted(list(channel["members"])) + + return channels + + def _append_line(self, obj: Dict[str, Any]) -> None: + line = json.dumps(obj, ensure_ascii=False) + with self.messages_file.open("a", encoding="utf-8") as f: + f.write(line + "\n") + + def _iter_messages(self) -> Iterable[Dict[str, Any]]: + if not self.messages_file.exists(): + return + with self.messages_file.open("r", encoding="utf-8") as f: + for line_num, line in enumerate(f, 1): + line = line.strip() + if not line: + continue + try: + parsed = json.loads(line) + # Ensure we only yield dictionary objects + if isinstance(parsed, dict): + yield parsed + except json.JSONDecodeError as e: + # Log JSON parsing errors but continue + print(f"Warning: Skipping malformed JSON on line {line_num}: {e}") + continue + except Exception as e: + # Handle other parsing errors + print(f"Warning: Unexpected error parsing line {line_num}: {e}") + continue + + # ------------------------------ core API --------------------------------- + def append_message( + self, + *, + sender: str, + content: Any, + recipients: Optional[List[str]] = None, + thread_id: Optional[str] = None, + msg_type: Optional[str] = None, + reply_to: Optional[str] = None, + ) -> Dict[str, Any]: + """Append a message to the log and return the full record.""" + with self._lock: + # Ensure recipients is always properly defined and type-safe + if recipients is None: + # Default behavior: most messages should be visible to all agents + if msg_type in ["task", "poll", "final_answer", "channel_message"]: + recipients = ["@all"] + elif msg_type == "private_message": + # This should have been specified, but default to empty for safety + recipients = [] + else: + # Default to public for unknown message types + recipients = ["@all"] + elif not isinstance(recipients, (list, tuple)): + # Convert non-list types to proper format + if isinstance(recipients, (int, float, bool)): + # These types are problematic - convert to empty list for safety + logging.warning( + json.dumps( + { + "event": "problematic_recipients_type_fixed", + "location": "append_message", + "recipients_type": type(recipients).__name__, + "recipients_value": str(recipients), + "sender": sender, + "msg_type": msg_type, + } + ) + ) + recipients = [] + else: + # Convert to list + recipients = [str(recipients)] + + # Ensure all recipients are strings + recipients = [str(r) for r in recipients if r is not None] + + msg_id = str(uuid.uuid4()) + record = { + "id": msg_id, + "timestamp": now_ts(), + "sender": sender, + "type": msg_type or (content.get("type") if isinstance(content, dict) else None), + "content": content, + "recipients": recipients, # Now guaranteed to be a list + "thread_id": thread_id, + "reply_to": reply_to, + } + + # Log message creation with proper recipients + logging.info( + json.dumps( + { + "event": "message_posted", + "message_id": msg_id, + "sender": sender, + "type": msg_type, + "thread_id": thread_id, + "recipients": recipients, + "content_preview": str(content)[:100], + } + ) + ) + + self._append_line(record) + return record + + # ------------------------------ agent set -------------------------------- + def get_all_agents(self) -> List[str]: + if self._configured_agents: + return list(self._configured_agents) + seen = set() + for m in self._iter_messages() or []: + s = m.get("sender") + if s: + seen.add(s) + return sorted(seen) + + # ------------------------------ POLLS ------------------------------------ + def get_active_polls(self) -> List[Dict[str, Any]]: + """Get all currently active (open) polls. + + IMPORTANT: Polls are NOT sorted by timestamp. The first poll to achieve + voting consensus (N//2+1 votes) should provide the final answer, regardless + of creation order. + + Returns: + List[Dict]: Active polls in message iteration order + """ + import json + + active_polls = [] + for msg in self._iter_messages() or []: + if not isinstance(msg, dict): + continue + + content = msg.get("content", {}) + + if isinstance(content, dict) and content.get("type") == "poll": + status = content.get("status") + if status == "open": # Only open polls are active + active_polls.append(content) + elif isinstance(content, str): + # Try to parse string content as JSON + try: + parsed_content = json.loads(content) + if isinstance(parsed_content, dict) and parsed_content.get("type") == "poll": + status = parsed_content.get("status") + if status == "open": + active_polls.append(parsed_content) + except json.JSONDecodeError: + pass # Skip invalid JSON + + return active_polls + + def create_poll( + self, + *, + question: str, + proposal: Any, + proposer: str, + options: Optional[List[str]] = None, + threshold: Optional[int] = None, + thread_id: Optional[str] = None, + final_answer: Optional[str] = None, # Store clean final answer separately + ) -> Dict[str, Any]: + # Check if there are any active polls - only allow one poll at a time + # active_polls = self.get_active_polls() + # if active_polls: + # print("🚫 Cannot create poll: There is already an active poll in progress") + # print(f" Active poll ID: {active_polls[0].get('poll_id')} by {active_polls[0].get('proposer')}") + # logging.info(json.dumps({ + # "event": "poll_creation_blocked", + # "reason": "active_poll_exists", + # "blocked_proposer": proposer, + # "active_poll_id": active_polls[0].get('poll_id'), + # "active_poll_proposer": active_polls[0].get('proposer') + # })) + # Return the existing active poll instead of creating a new one + # return {"error": "Poll already active", "active_poll": active_polls[0]} + + poll_id = str(uuid.uuid4()) + n_agents = len(self.get_all_agents()) or len(self._configured_agents) or 4 # Default to 4 agents + thr = threshold if threshold is not None else majority_plus_one(n_agents) + + # Log poll creation with explicit threshold + print(f"πŸ—³οΈ Creating poll: {thr} out of {n_agents} agents must vote YES for consensus") + logging.info( + json.dumps( + { + "event": "poll_created", + "poll_id": poll_id, + "proposer": proposer, + "question": question, + "proposal_preview": str(proposal)[:200], + "threshold": thr, + "total_agents": n_agents, + "options": options or ["YES", "NO"], + "thread_id": thread_id, + "configured_agents": self._configured_agents, # Debug info + "detected_agents": self.get_all_agents(), # Debug info + } + ) + ) + + payload = { + "type": "poll", + "poll_id": poll_id, + "question": question, + "proposal": proposal, + "options": options or ["YES", "NO"], + "threshold": thr, + "status": "open", + "proposer": proposer, + } + + # Store clean final answer if provided (for final answer polls) + if final_answer is not None: + payload["final_answer"] = final_answer + + return self.append_message( + sender=proposer, + content=payload, + thread_id=thread_id, + msg_type="poll", + ) + + def record_vote( + self, + *, + poll_id: str, + voter: str, + vote: str, + confidence: float = 0.5, + rationale: str = "", + thread_id: Optional[str] = None, + ) -> Dict[str, Any]: + v = vote.upper() + if v not in {"YES", "NO"}: + raise ValueError("vote must be 'YES' or 'NO'") + + logging.info( + json.dumps( + { + "event": "vote_recorded", + "poll_id": poll_id, + "voter": voter, + "vote": v, + "confidence": confidence, + "rationale": rationale[:200] if rationale else "", + } + ) + ) + + return self.append_message( + sender=voter, + content={ + "type": "vote", + "poll_id": poll_id, + "voter": voter, + "vote": v, + "confidence": float(confidence), + "rationale": rationale, + }, + thread_id=thread_id, + msg_type="vote", + reply_to=poll_id, + ) + + def count_votes(self, poll_id: str) -> Dict[str, Any]: + """Tally votes for a poll; latest vote per voter wins.""" + import json + + poll = None + closed = False + deleted = False + votes_by_voter: Dict[str, Dict[str, Any]] = {} + for msg in self._iter_messages() or []: + if not isinstance(msg, dict): + continue # Skip non-dict messages + + c = msg.get("content", {}) + + # Handle case where content might be a string (JSON) + if isinstance(c, str): + try: + c = json.loads(c) + except json.JSONDecodeError: + continue # Skip messages with unparsable content + + if not isinstance(c, dict): + continue # Skip if content is not a dict after parsing + + t = c.get("type") + if t == "poll" and c.get("poll_id") == poll_id: + poll = c + status = c.get("status") + closed = status == "closed" + deleted = status == "deleted" + elif t == "vote" and c.get("poll_id") == poll_id and not deleted: + # Don't count votes for deleted polls + voter = c.get("voter") + if voter: + votes_by_voter[voter] = c # last wins + elif t == "final_answer" and c.get("poll_id") == poll_id: + closed = True + + tally = defaultdict(int) + for v in votes_by_voter.values(): + vv = v.get("vote") + if vv in ("YES", "NO"): + tally[vv] += 1 + + n_agents = len(self.get_all_agents()) or 1 + # Ensure poll is a dict before accessing its attributes + if isinstance(poll, dict): + threshold = poll.get("threshold", majority_plus_one(n_agents)) + else: + threshold = majority_plus_one(n_agents) + + return { + "poll": poll, + "closed": closed, + "deleted": deleted, + "tally": {"YES": tally["YES"], "NO": tally["NO"], "eligible": n_agents, "threshold": threshold}, + "votes_by_voter": votes_by_voter, + } + + def finalize_poll_if_ready(self, poll_id: str) -> Optional[Dict[str, Any]]: + info = self.count_votes(poll_id) + poll = info.get("poll") + if not poll or info.get("closed"): + logging.info( + json.dumps( + { + "event": "poll_finalization_skipped", + "poll_id": poll_id, + "reason": "poll_not_found_or_closed", + "has_poll": bool(poll), + "is_closed": info.get("closed", False), + } + ) + ) + return None + + # Ensure poll is a dict before accessing its attributes + if not isinstance(poll, dict): + logging.error( + json.dumps( + { + "event": "poll_finalization_error", + "poll_id": poll_id, + "error": "poll_is_not_dict", + "poll_type": type(poll).__name__, + } + ) + ) + return None + + tally = info["tally"] + logging.info( + json.dumps( + { + "event": "poll_finalization_check", + "poll_id": poll_id, + "yes_votes": tally["YES"], + "no_votes": tally["NO"], + "threshold": tally["threshold"], + "eligible_voters": tally["eligible"], + } + ) + ) + + # Check if poll should be deleted due to too many NO votes + if tally["NO"] >= 2: # Delete poll if 2+ NO votes + print(f"πŸ—‘οΈ Deleting poll {poll_id} due to {tally['NO']} NO votes (threshold: 2)") + logging.info( + json.dumps( + { + "event": "poll_deleted", + "poll_id": poll_id, + "no_votes": tally["NO"], + "reason": "too_many_no_votes", + } + ) + ) + # Mark poll as closed/deleted + self.append_message( + sender="Coordinator", + content={**poll, "status": "deleted", "reason": f"Too many NO votes ({tally['NO']})"}, + msg_type="poll", + reply_to=poll_id, + ) + return {"deleted": True, "poll_id": poll_id, "reason": f"Too many NO votes ({tally['NO']})"} + + # Check if poll passed with enough YES votes + if tally["YES"] >= tally["threshold"]: + print(f"βœ… Poll {poll_id} passed with {tally['YES']} YES votes (threshold: {tally['threshold']})") + logging.info( + json.dumps( + { + "event": "poll_passed", + "poll_id": poll_id, + "yes_votes": tally["YES"], + "threshold": tally["threshold"], + "proposer": poll.get("proposer"), + "answer_preview": str(poll.get("proposal", ""))[:200], + } + ) + ) + # mark closed (shadow append) + self.append_message( + sender=poll.get("proposer", "system"), + content={**poll, "status": "closed"}, + msg_type="poll", + reply_to=poll_id, + ) + # emit final answer + clean_answer = poll.get("final_answer") or poll.get("proposal", "No proposal found") + return self.append_message( + sender="Coordinator", + content={ + "type": "final_answer", + "poll_id": poll_id, + "answer": clean_answer, + "tally": tally, + "source_proposer": poll.get("proposer"), + }, + msg_type="final_answer", + reply_to=poll_id, + ) + + logging.info( + json.dumps( + { + "event": "poll_not_ready", + "poll_id": poll_id, + "yes_votes": tally["YES"], + "no_votes": tally["NO"], + "threshold": tally["threshold"], + "reason": "insufficient_votes", + } + ) + ) + return None diff --git a/examples/decentralized_smolagents_benchmark/scripts/reformulator.py b/examples/decentralized_smolagents_benchmark/scripts/reformulator.py new file mode 100644 index 000000000..a56c2c1bf --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/reformulator.py @@ -0,0 +1,86 @@ +# Shamelessly stolen from Microsoft Autogen team: thanks to them for this great resource! +# https://github.com/microsoft/autogen/blob/gaia_multiagent_v01_march_1st/autogen/browser_utils.py +import copy + +from smolagents.models import MessageRole, Model + + +def prepare_response(original_task: str, inner_messages, reformulation_model: Model) -> str: + messages = [ + { + "role": MessageRole.SYSTEM, + "content": [ + { + "type": "text", + "text": f"""Earlier you were asked the following: + +{original_task} + +Your team then worked diligently to address that request. Read below a transcript of that conversation:""", + } + ], + } + ] + + # The first message just repeats the question, so remove it + # if len(inner_messages) > 1: + # del inner_messages[0] + + # copy them to this context + try: + for message in inner_messages: + if not message.content: + continue + message = copy.deepcopy(message) + message.role = MessageRole.USER + messages.append(message) + except Exception: + messages += [{"role": MessageRole.ASSISTANT, "content": str(inner_messages)}] + + # ask for the final answer + messages.append( + { + "role": MessageRole.USER, + "content": [ + { + "type": "text", + "text": f""" +Read the above conversation and output a FINAL ANSWER to the question. The question is repeated here for convenience: + +{original_task} + +To output the final answer, use the following template: FINAL ANSWER: [YOUR FINAL ANSWER] +Your FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. +ADDITIONALLY, your FINAL ANSWER MUST adhere to any formatting instructions specified in the original question (e.g., alphabetization, sequencing, units, rounding, decimal places, etc.) +If you are asked for a number, express it numerically (i.e., with digits rather than words), don't use commas, and DO NOT INCLUDE UNITS such as $ or USD or percent signs unless specified otherwise. +If you are asked for a string, don't use articles or abbreviations (e.g. for cities), unless specified otherwise. Don't output any final sentence punctuation such as '.', '!', or '?'. +If you are asked for a comma separated list, apply the above rules depending on whether the elements are numbers or strings. +If you are unable to determine the final answer, output 'FINAL ANSWER: Unable to determine' +""", + } + ], + } + ) + + response = reformulation_model(messages).content + + final_answer = response.split("FINAL ANSWER: ")[-1].strip() + print("> Reformulated answer: ", final_answer) + + # if "unable to determine" in final_answer.lower(): + # messages.append({"role": MessageRole.ASSISTANT, "content": response }) + # messages.append({"role": MessageRole.USER, "content": [{"type": "text", "text": """ + # I understand that a definitive answer could not be determined. Please make a well-informed EDUCATED GUESS based on the conversation. + + # To output the educated guess, use the following template: EDUCATED GUESS: [YOUR EDUCATED GUESS] + # Your EDUCATED GUESS should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. DO NOT OUTPUT 'I don't know', 'Unable to determine', etc. + # ADDITIONALLY, your EDUCATED GUESS MUST adhere to any formatting instructions specified in the original question (e.g., alphabetization, sequencing, units, rounding, decimal places, etc.) + # If you are asked for a number, express it numerically (i.e., with digits rather than words), don't use commas, and don't include units such as $ or percent signs unless specified otherwise. + # If you are asked for a string, don't use articles or abbreviations (e.g. cit for cities), unless specified otherwise. Don't output any final sentence punctuation such as '.', '!', or '?'. + # If you are asked for a comma separated list, apply the above rules depending on whether the elements are numbers or strings. + # """.strip()}]}) + + # response = model(messages).content + # print("\n>>>Making an educated guess.\n", response) + # final_answer = response.split("EDUCATED GUESS: ")[-1].strip() + return final_answer diff --git a/examples/decentralized_smolagents_benchmark/scripts/run_agents.py b/examples/decentralized_smolagents_benchmark/scripts/run_agents.py new file mode 100644 index 000000000..fa2df2ad2 --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/run_agents.py @@ -0,0 +1,96 @@ +import json +import os +import shutil +import textwrap +from pathlib import Path + +# import tqdm.asyncio +from smolagents.utils import AgentError + + +def serialize_agent_error(obj): + """Serialize AgentError objects for JSON output.""" + if isinstance(obj, AgentError): + return {"error_type": obj.__class__.__name__, "message": obj.message} + else: + return str(obj) + + +def get_image_description(file_name: str, question: str, visual_inspection_tool) -> str: + """ + Get description of an image file for context. + + DEBUGGING FIX: Changed parameter from file_path to image_path + - ISSUE: TypeError due to mismatched parameter name + - SOLUTION: Use image_path parameter as expected by visualizer tool + """ + prompt = f"""Write a caption of 5 sentences for this image. Pay special attention to any details that might be useful for someone answering the following question: +{question}. But do not try to answer the question directly! +Do not add any information that is not present in the image.""" + return visual_inspection_tool(image_path=file_name, question=prompt) + + +def get_document_description(file_path: str, question: str, document_inspection_tool) -> str: + prompt = f"""Write a caption of 5 sentences for this document. Pay special attention to any details that might be useful for someone answering the following question: +{question}. But do not try to answer the question directly! +Do not add any information that is not present in the document.""" + return document_inspection_tool.forward_initial_exam_mode(file_path=file_path, question=prompt) + + +def get_single_file_description(file_path: str, question: str, visual_inspection_tool, document_inspection_tool): + file_extension = file_path.split(".")[-1] + if file_extension in ["png", "jpg", "jpeg"]: + file_description = f" - Attached image: {file_path}" + file_description += ( + f"\n -> Image description: {get_image_description(file_path, question, visual_inspection_tool)}" + ) + return file_description + elif file_extension in ["pdf", "xls", "xlsx", "docx", "doc", "xml"]: + image_path = file_path.split(".")[0] + ".png" + if os.path.exists(image_path): + description = get_image_description(image_path, question, visual_inspection_tool) + file_path = image_path + else: + description = get_document_description(file_path, question, document_inspection_tool) + file_description = f" - Attached document: {file_path}" + file_description += f"\n -> File description: {description}" + return file_description + elif file_extension in ["mp3", "m4a", "wav"]: + return f" - Attached audio: {file_path}" + else: + return f" - Attached file: {file_path}" + + +def get_zip_description(file_path: str, question: str, visual_inspection_tool, document_inspection_tool): + folder_path = file_path.replace(".zip", "") + os.makedirs(folder_path, exist_ok=True) + shutil.unpack_archive(file_path, folder_path) + + prompt_use_files = "" + for root, dirs, files in os.walk(folder_path): + for file in files: + file_path = os.path.join(root, file) + prompt_use_files += "\n" + textwrap.indent( + get_single_file_description(file_path, question, visual_inspection_tool, document_inspection_tool), + prefix=" ", + ) + return prompt_use_files + + +def get_tasks_to_run(data, total: int, base_filename: Path, tasks_ids: list[int]): + f = base_filename.parent / f"{base_filename.stem}_answers.jsonl" + done = set() + if f.exists(): + with open(f, encoding="utf-8") as fh: + done = {json.loads(line)["task_id"] for line in fh if line.strip()} + + tasks = [] + for i in range(total): + task_id = int(data[i]["task_id"]) + if task_id not in done: + if tasks_ids is not None: + if task_id in tasks_ids: + tasks.append(data[i]) + else: + tasks.append(data[i]) + return tasks diff --git a/examples/decentralized_smolagents_benchmark/scripts/text_inspector_tool.py b/examples/decentralized_smolagents_benchmark/scripts/text_inspector_tool.py new file mode 100644 index 000000000..cbe00209b --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/text_inspector_tool.py @@ -0,0 +1,140 @@ +from smolagents import Tool +from smolagents.models import Model + + +class TextInspectorTool(Tool): + name = "inspect_file_as_text" + description = """ +You cannot load files yourself: instead call this tool to read a file as markdown text and ask questions about it. +This tool handles the following file extensions: [".html", ".htm", ".xlsx", ".pptx", ".wav", ".mp3", ".m4a", ".flac", ".pdf", ".docx"], and all other types of text files. IT DOES NOT HANDLE IMAGES. + +IMPORTANT: If you have a URL to a file (like a .docx, .xlsx, .pptx, .wav, .mp3, .m4a, .png file), you MUST first use the 'download_file' tool to download it locally, then use this tool with the downloaded file path. For .pdf, .txt, .htm files, use 'visit_page' tool instead.""" + + inputs = { + "file_path": { + "description": "The path to the file you want to read as text. Must be a '.something' file, like '.pdf'. If it is an image, use the visualizer tool instead! DO NOT use this tool for an HTML webpage: use the web_search tool instead!", + "type": "string", + }, + "question": { + "description": "[Optional]: Your question, as a natural language sentence. Provide as much context as possible. Do not pass this parameter if you just want to directly return the content of the file.", + "type": "string", + "nullable": True, + }, + } + output_type = "string" + + def __init__(self, model: Model = None, text_limit: int = 100000): + super().__init__() + self.model = model + self.text_limit = text_limit + from .mdconvert import MarkdownConverter + + self.md_converter = MarkdownConverter() + + def forward_initial_exam_mode(self, file_path, question): + from smolagents.models import MessageRole + + result = self.md_converter.convert(file_path) + + if file_path[-4:] in [".png", ".jpg"]: + raise Exception("Cannot use inspect_file_as_text tool with images: use visualizer instead!") + + if ".zip" in file_path: + return result.text_content + + if not question: + return result.text_content + + if len(result.text_content) < 4000: + return "Document content: " + result.text_content + + messages = [ + { + "role": MessageRole.SYSTEM, + "content": [ + { + "type": "text", + "text": "Here is a file:\n### " + + str(result.title) + + "\n\n" + + result.text_content[: self.text_limit], + } + ], + }, + { + "role": MessageRole.USER, + "content": [ + { + "type": "text", + "text": "Now please write a short, 5 sentence caption for this document, that could help someone asking this question: " + + question + + "\n\nDon't answer the question yourself! Just provide useful notes on the document", + } + ], + }, + ] + return self.model(messages).content + + def forward(self, file_path, question: str | None = None) -> str: + from smolagents.models import MessageRole + + result = self.md_converter.convert(file_path) + + if file_path[-4:] in [".png", ".jpg"]: + raise Exception("Cannot use inspect_file_as_text tool with images: use visualizer instead!") + + if ".zip" in file_path: + return result.text_content + + if not question: + return result.text_content + + messages = [ + { + "role": MessageRole.SYSTEM, + "content": [ + { + "type": "text", + "text": "You will have to write a short caption for this file, then answer this question:" + + question, + } + ], + }, + { + "role": MessageRole.USER, + "content": [ + { + "type": "text", + "text": "Here is the complete file:\n### " + + str(result.title) + + "\n\n" + + result.text_content[: self.text_limit], + } + ], + }, + { + "role": MessageRole.USER, + "content": [ + { + "type": "text", + "text": "Now answer the question below. Use these three headings: '1. Short answer', '2. Extremely detailed answer', '3. Additional Context on the document and question asked'." + + question, + } + ], + }, + ] + return self.model(messages).content + + +class FileReaderTool(Tool): + """Tool for reading file contents.""" + + name = "file_reader" + inputs = {"file_path": {"type": "string", "description": "Path to the file to read"}} + output_type = "string" + description = "Read raw text contents of a file. IMPORTANT: If you have a URL to a file, first use 'download_file' tool to download it, then use this tool with the downloaded file path." + + def forward(self, file_path: str) -> str: + """Read contents of a file.""" + with open(file_path) as f: + return f.read() diff --git a/examples/decentralized_smolagents_benchmark/scripts/text_web_browser.py b/examples/decentralized_smolagents_benchmark/scripts/text_web_browser.py new file mode 100644 index 000000000..efd03df62 --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/text_web_browser.py @@ -0,0 +1,622 @@ +# Shamelessly stolen from Microsoft Autogen team: thanks to them for this great resource! +# https://github.com/microsoft/autogen/blob/gaia_multiagent_v01_march_1st/autogen/browser_utils.py +import mimetypes +import os +import pathlib +import re +import time +import uuid +from typing import Any +from urllib.parse import unquote, urljoin, urlparse + +import pathvalidate +import requests +from serpapi import GoogleSearch + +from smolagents import Tool + +from .cookies import COOKIES +from .mdconvert import FileConversionException, MarkdownConverter, UnsupportedFormatException + + +class SimpleTextBrowser: + """(In preview) An extremely simple text-based web browser comparable to Lynx. Suitable for Agentic use.""" + + def __init__( + self, + start_page: str | None = None, + viewport_size: int | None = 1024 * 8, + downloads_folder: str | None | None = None, + serpapi_key: str | None | None = None, + request_kwargs: dict[str, Any] | None | None = None, + ): + self.start_page: str = start_page if start_page else "about:blank" + self.viewport_size = viewport_size # Applies only to the standard uri types + self.downloads_folder = downloads_folder + self.history: list[tuple[str, float]] = list() + self.page_title: str | None = None + self.viewport_current_page = 0 + self.viewport_pages: list[tuple[int, int]] = list() + self.set_address(self.start_page) + self.serpapi_key = serpapi_key + self.request_kwargs = request_kwargs + self.request_kwargs["cookies"] = COOKIES + self._mdconvert = MarkdownConverter() + self._page_content: str = "" + + self._find_on_page_query: str | None = None + self._find_on_page_last_result: int | None = None # Location of the last result + + @property + def address(self) -> str: + """Return the address of the current page.""" + return self.history[-1][0] + + def set_address(self, uri_or_path: str, filter_year: int | None = None) -> None: + # TODO: Handle anchors + self.history.append((uri_or_path, time.time())) + + # Handle special URIs + if uri_or_path == "about:blank": + self._set_page_content("") + elif uri_or_path.startswith("google:"): + self._serpapi_search(uri_or_path[len("google:") :].strip(), filter_year=filter_year) + else: + if ( + not uri_or_path.startswith("http:") + and not uri_or_path.startswith("https:") + and not uri_or_path.startswith("file:") + ): + if len(self.history) > 1: + prior_address = self.history[-2][0] + uri_or_path = urljoin(prior_address, uri_or_path) + # Update the address with the fully-qualified path + self.history[-1] = (uri_or_path, self.history[-1][1]) + self._fetch_page(uri_or_path) + + self.viewport_current_page = 0 + self.find_on_page_query = None + self.find_on_page_viewport = None + + @property + def viewport(self) -> str: + """Return the content of the current viewport.""" + bounds = self.viewport_pages[self.viewport_current_page] + return self.page_content[bounds[0] : bounds[1]] + + @property + def page_content(self) -> str: + """Return the full contents of the current page.""" + return self._page_content + + def _set_page_content(self, content: str) -> None: + """Sets the text content of the current page.""" + self._page_content = content + self._split_pages() + if self.viewport_current_page >= len(self.viewport_pages): + self.viewport_current_page = len(self.viewport_pages) - 1 + + def page_down(self) -> None: + self.viewport_current_page = min(self.viewport_current_page + 1, len(self.viewport_pages) - 1) + + def page_up(self) -> None: + self.viewport_current_page = max(self.viewport_current_page - 1, 0) + + def find_on_page(self, query: str) -> str | None: + """Searches for the query from the current viewport forward, looping back to the start if necessary.""" + + # Did we get here via a previous find_on_page search with the same query? + # If so, map to find_next + if query == self._find_on_page_query and self.viewport_current_page == self._find_on_page_last_result: + return self.find_next() + + # Ok it's a new search start from the current viewport + self._find_on_page_query = query + viewport_match = self._find_next_viewport(query, self.viewport_current_page) + if viewport_match is None: + self._find_on_page_last_result = None + return None + else: + self.viewport_current_page = viewport_match + self._find_on_page_last_result = viewport_match + return self.viewport + + def find_next(self) -> str | None: + """Scroll to the next viewport that matches the query""" + + if self._find_on_page_query is None: + return None + + starting_viewport = self._find_on_page_last_result + if starting_viewport is None: + starting_viewport = 0 + else: + starting_viewport += 1 + if starting_viewport >= len(self.viewport_pages): + starting_viewport = 0 + + viewport_match = self._find_next_viewport(self._find_on_page_query, starting_viewport) + if viewport_match is None: + self._find_on_page_last_result = None + return None + else: + self.viewport_current_page = viewport_match + self._find_on_page_last_result = viewport_match + return self.viewport + + def _find_next_viewport(self, query: str, starting_viewport: int) -> int | None: + """Search for matches between the starting viewport looping when reaching the end.""" + + if query is None: + return None + + # Normalize the query, and convert to a regular expression + nquery = re.sub(r"\*", "__STAR__", query) + nquery = " " + (" ".join(re.split(r"\W+", nquery))).strip() + " " + nquery = nquery.replace(" __STAR__ ", "__STAR__ ") # Merge isolated stars with prior word + nquery = nquery.replace("__STAR__", ".*").lower() + + if nquery.strip() == "": + return None + + idxs = list() + idxs.extend(range(starting_viewport, len(self.viewport_pages))) + idxs.extend(range(0, starting_viewport)) + + for i in idxs: + bounds = self.viewport_pages[i] + content = self.page_content[bounds[0] : bounds[1]] + + # TODO: Remove markdown links and images + ncontent = " " + (" ".join(re.split(r"\W+", content))).strip().lower() + " " + if re.search(nquery, ncontent): + return i + + return None + + def visit_page(self, path_or_uri: str, filter_year: int | None = None) -> str: + """Update the address, visit the page, and return the content of the viewport.""" + self.set_address(path_or_uri, filter_year=filter_year) + return self.viewport + + def _split_pages(self) -> None: + # Do not split search results + if self.address.startswith("google:"): + self.viewport_pages = [(0, len(self._page_content))] + return + + # Handle empty pages + if len(self._page_content) == 0: + self.viewport_pages = [(0, 0)] + return + + # Break the viewport into pages + self.viewport_pages = [] + start_idx = 0 + while start_idx < len(self._page_content): + end_idx = min(start_idx + self.viewport_size, len(self._page_content)) # type: ignore[operator] + # Adjust to end on a space + while end_idx < len(self._page_content) and self._page_content[end_idx - 1] not in [" ", "\t", "\r", "\n"]: + end_idx += 1 + self.viewport_pages.append((start_idx, end_idx)) + start_idx = end_idx + + def _serpapi_search(self, query: str, filter_year: int | None = None) -> None: + if self.serpapi_key is None: + raise ValueError("Missing SerpAPI key.") + + params = { + "engine": "google", + "q": query, + "api_key": self.serpapi_key, + } + if filter_year is not None: + params["tbs"] = f"cdr:1,cd_min:01/01/{filter_year},cd_max:12/31/{filter_year}" + + search = GoogleSearch(params) + results = search.get_dict() + self.page_title = f"{query} - Search" + if "organic_results" not in results.keys(): + raise Exception(f"No results found for query: '{query}'. Use a less specific query.") + if len(results["organic_results"]) == 0: + year_filter_message = f" with filter year={filter_year}" if filter_year is not None else "" + self._set_page_content( + f"No results found for '{query}'{year_filter_message}. Try with a more general query, or remove the year filter." + ) + return + + def _prev_visit(url): + for i in range(len(self.history) - 1, -1, -1): + if self.history[i][0] == url: + return f"You previously visited this page {round(time.time() - self.history[i][1])} seconds ago.\n" + return "" + + web_snippets: list[str] = list() + idx = 0 + if "organic_results" in results: + for page in results["organic_results"]: + idx += 1 + date_published = "" + if "date" in page: + date_published = "\nDate published: " + page["date"] + + source = "" + if "source" in page: + source = "\nSource: " + page["source"] + + snippet = "" + if "snippet" in page: + snippet = "\n" + page["snippet"] + + redacted_version = f"{idx}. [{page['title']}]({page['link']}){date_published}{source}\n{_prev_visit(page['link'])}{snippet}" + + redacted_version = redacted_version.replace("Your browser can't play this video.", "") + web_snippets.append(redacted_version) + + content = ( + f"A Google search for '{query}' found {len(web_snippets)} results:\n\n## Web Results\n" + + "\n\n".join(web_snippets) + ) + + self._set_page_content(content) + + def _fetch_page(self, url: str) -> None: + download_path = "" + try: + if url.startswith("file://"): + download_path = os.path.normcase(os.path.normpath(unquote(url[7:]))) + res = self._mdconvert.convert_local(download_path) + self.page_title = res.title + self._set_page_content(res.text_content) + else: + # Prepare the request parameters + request_kwargs = self.request_kwargs.copy() if self.request_kwargs is not None else {} + request_kwargs["stream"] = True + + # Send a HTTP request to the URL + response = requests.get(url, **request_kwargs) + response.raise_for_status() + + # If the HTTP request was successful + content_type = response.headers.get("content-type", "") + + # Text or HTML + if "text/" in content_type.lower(): + res = self._mdconvert.convert_response(response) + self.page_title = res.title + self._set_page_content(res.text_content) + # A download + else: + # Try producing a safe filename + fname = None + download_path = None + try: + fname = pathvalidate.sanitize_filename(os.path.basename(urlparse(url).path)).strip() + download_path = os.path.abspath(os.path.join(self.downloads_folder, fname)) + + suffix = 0 + while os.path.exists(download_path) and suffix < 1000: + suffix += 1 + base, ext = os.path.splitext(fname) + new_fname = f"{base}__{suffix}{ext}" + download_path = os.path.abspath(os.path.join(self.downloads_folder, new_fname)) + + except NameError: + pass + + # No suitable name, so make one + if fname is None: + extension = mimetypes.guess_extension(content_type) + if extension is None: + extension = ".download" + fname = str(uuid.uuid4()) + extension + download_path = os.path.abspath(os.path.join(self.downloads_folder, fname)) + + # Open a file for writing + with open(download_path, "wb") as fh: + for chunk in response.iter_content(chunk_size=512): + fh.write(chunk) + + # Render it + local_uri = pathlib.Path(download_path).as_uri() + self.set_address(local_uri) + + except UnsupportedFormatException as e: + print(e) + self.page_title = ("Download complete.",) + self._set_page_content(f"# Download complete\n\nSaved file to '{download_path}'") + except FileConversionException as e: + print(e) + self.page_title = ("Download complete.",) + self._set_page_content(f"# Download complete\n\nSaved file to '{download_path}'") + except FileNotFoundError: + self.page_title = "Error 404" + self._set_page_content(f"## Error 404\n\nFile not found: {download_path}") + except requests.exceptions.RequestException as request_exception: + try: + self.page_title = f"Error {response.status_code}" + + # If the error was rendered in HTML we might as well render it + content_type = response.headers.get("content-type", "") + if content_type is not None and "text/html" in content_type.lower(): + res = self._mdconvert.convert(response) + self.page_title = f"Error {response.status_code}" + self._set_page_content(f"## Error {response.status_code}\n\n{res.text_content}") + else: + text = "" + for chunk in response.iter_content(chunk_size=512, decode_unicode=True): + text += chunk + self.page_title = f"Error {response.status_code}" + self._set_page_content(f"## Error {response.status_code}\n\n{text}") + except NameError: + self.page_title = "Error" + self._set_page_content(f"## Error\n\n{str(request_exception)}") + + def _state(self) -> tuple[str, str]: + header = f"Address: {self.address}\n" + if self.page_title is not None: + header += f"Title: {self.page_title}\n" + + current_page = self.viewport_current_page + total_pages = len(self.viewport_pages) + + address = self.address + for i in range(len(self.history) - 2, -1, -1): # Start from the second last + if self.history[i][0] == address: + header += f"You previously visited this page {round(time.time() - self.history[i][1])} seconds ago.\n" + break + + header += f"Viewport position: Showing page {current_page + 1} of {total_pages}.\n" + return (header, self.viewport) + + +class SearchInformationTool(Tool): + name = "web_search" + description = "Perform a web search query (think a google search) and returns the search results." + inputs = {"query": {"type": "string", "description": "The web search query to perform."}} + inputs["filter_year"] = { + "type": "string", + "description": "[Optional parameter]: filter the search results to only include pages from a specific year. For example, '2020' will only include pages from 2020. Make sure to use this parameter if you're trying to search for articles from a specific date!", + "nullable": True, + } + output_type = "string" + + def __init__(self, browser): + super().__init__() + self.browser = browser + + def forward(self, query: str, filter_year: int | None = None) -> str: + self.browser.visit_page(f"google: {query}", filter_year=filter_year) + header, content = self.browser._state() + return header.strip() + "\n=======================\n" + content + + +class VisitTool(Tool): + name = "visit_page" + description = "Visit a webpage at a given URL and return its text. Given a url to a YouTube video, this returns the transcript." + inputs = {"url": {"type": "string", "description": "The relative or absolute url of the webpage to visit."}} + output_type = "string" + + def __init__(self, browser=None): + super().__init__() + self.browser = browser + + def forward(self, url: str) -> str: + self.browser.visit_page(url) + header, content = self.browser._state() + return header.strip() + "\n=======================\n" + content + + +class DownloadTool(Tool): + name = "download_file" + description = """ +Download a file at a given URL to inspect its contents. Use this tool for files with extensions: [".xlsx", ".pptx", ".wav", ".mp3", ".m4a", ".png", ".docx"] + +WORKFLOW: +1. Use this tool to download the file locally +2. Then use 'inspect_file_as_text' tool with the returned file path to read and analyze the content +3. For images (.png), use the visualizer tool after downloading + +DO NOT use this tool for .pdf or .txt or .htm files: for these types use visit_page instead.""" + inputs = {"url": {"type": "string", "description": "The relative or absolute url of the file to be downloaded."}} + output_type = "string" + + def __init__(self, browser): + super().__init__() + self.browser = browser + + def forward(self, url: str) -> str: + import os + import uuid + from urllib.parse import urlparse + + import pathvalidate + import requests + + try: + if "arxiv" in url: + url = url.replace("abs", "pdf") + + response = requests.get(url, stream=True, timeout=30) + response.raise_for_status() + + content_type = response.headers.get("content-type", "") + + # Try to get filename from URL or Content-Disposition header + fname = None + if "Content-Disposition" in response.headers: + import re + + cd = response.headers["Content-Disposition"] + filename_match = re.search(r'filename="?([^"]+)"?', cd) + if filename_match: + fname = pathvalidate.sanitize_filename(filename_match.group(1)).strip() + + if not fname: + # Extract from URL path + parsed_url = urlparse(url) + path_fname = os.path.basename(parsed_url.path) + if path_fname: + fname = pathvalidate.sanitize_filename(path_fname).strip() + + if not fname: + # Generate filename from content type + extension = mimetypes.guess_extension(content_type) + if not extension: + extension = ".download" + fname = f"file_{str(uuid.uuid4())[:8]}{extension}" + + # Ensure downloads directory exists + downloads_dir = self.browser.downloads_folder or "./downloads" + os.makedirs(downloads_dir, exist_ok=True) + + # Create unique path to avoid overwriting + download_path = os.path.join(downloads_dir, fname) + suffix = 0 + while os.path.exists(download_path) and suffix < 1000: + suffix += 1 + base, ext = os.path.splitext(fname) + new_fname = f"{base}__{suffix}{ext}" + download_path = os.path.join(downloads_dir, new_fname) + + # Download the file + with open(download_path, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + + # Get file extension to check restrictions + _, extension = os.path.splitext(download_path) + extension = extension.lower() + + return f"File was downloaded and saved under path {download_path}." + + except requests.RequestException as e: + return f"Error downloading file: Network error - {str(e)}" + except Exception as e: + return f"Error downloading file: {str(e)}" + + +class ArchiveSearchTool(Tool): + name = "find_archived_url" + description = "Given a url, searches the Wayback Machine and returns the archived version of the url that's closest in time to the desired date." + inputs = { + "url": {"type": "string", "description": "The url you need the archive for."}, + "date": { + "type": "string", + "description": "The date that you want to find the archive for. Give this date in the format 'YYYYMMDD', for instance '27 June 2008' is written as '20080627'.", + }, + } + output_type = "string" + + def __init__(self, browser=None): + super().__init__() + self.browser = browser + + def forward(self, url, date) -> str: + import requests + + no_timestamp_url = f"https://archive.org/wayback/available?url={url}" + archive_url = no_timestamp_url + f"×tamp={date}" + response = requests.get(archive_url).json() + response_notimestamp = requests.get(no_timestamp_url).json() + if "archived_snapshots" in response and "closest" in response["archived_snapshots"]: + closest = response["archived_snapshots"]["closest"] + print("Archive found!", closest) + + elif "archived_snapshots" in response_notimestamp and "closest" in response_notimestamp["archived_snapshots"]: + closest = response_notimestamp["archived_snapshots"]["closest"] + print("Archive found!", closest) + else: + raise Exception(f"Your {url=} was not archived on Wayback Machine, try a different url.") + target_url = closest["url"] + self.browser.visit_page(target_url) + header, content = self.browser._state() + return ( + f"Web archive for url {url}, snapshot taken at date {closest['timestamp'][:8]}:\n" + + header.strip() + + "\n=======================\n" + + content + ) + + +class PageUpTool(Tool): + name = "page_up" + description = "Scroll the viewport UP one page-length in the current webpage and return the new viewport content." + inputs = {} + output_type = "string" + + def __init__(self, browser=None): + super().__init__() + self.browser = browser + + def forward(self) -> str: + self.browser.page_up() + header, content = self.browser._state() + return header.strip() + "\n=======================\n" + content + + +class PageDownTool(Tool): + name = "page_down" + description = ( + "Scroll the viewport DOWN one page-length in the current webpage and return the new viewport content." + ) + inputs = {} + output_type = "string" + + def __init__(self, browser=None): + super().__init__() + self.browser = browser + + def forward(self) -> str: + self.browser.page_down() + header, content = self.browser._state() + return header.strip() + "\n=======================\n" + content + + +class FinderTool(Tool): + name = "find_on_page_ctrl_f" + description = "Scroll the viewport to the first occurrence of the search string. This is equivalent to Ctrl+F." + inputs = { + "search_string": { + "type": "string", + "description": "The string to search for on the page. This search string supports wildcards like '*'", + } + } + output_type = "string" + + def __init__(self, browser=None): + super().__init__() + self.browser = browser + + def forward(self, search_string: str) -> str: + find_result = self.browser.find_on_page(search_string) + header, content = self.browser._state() + + if find_result is None: + return ( + header.strip() + + f"\n=======================\nThe search string '{search_string}' was not found on this page." + ) + else: + return header.strip() + "\n=======================\n" + content + + +class FindNextTool(Tool): + name = "find_next" + description = "Scroll the viewport to next occurrence of the search string. This is equivalent to finding the next match in a Ctrl+F search." + inputs = {} + output_type = "string" + + def __init__(self, browser=None): + super().__init__() + self.browser = browser + + def forward(self) -> str: + find_result = self.browser.find_next() + header, content = self.browser._state() + + if find_result is None: + return header.strip() + "\n=======================\nThe search string was not found on this page." + else: + return header.strip() + "\n=======================\n" + content diff --git a/examples/decentralized_smolagents_benchmark/scripts/visual_qa.py b/examples/decentralized_smolagents_benchmark/scripts/visual_qa.py new file mode 100644 index 000000000..01d60b30a --- /dev/null +++ b/examples/decentralized_smolagents_benchmark/scripts/visual_qa.py @@ -0,0 +1,189 @@ +import base64 +import json +import mimetypes +import os +import uuid +from io import BytesIO + +import PIL.Image +import requests +from dotenv import load_dotenv +from huggingface_hub import InferenceClient + +from smolagents import Tool, tool + + +load_dotenv(override=True) + + +def process_images_and_text(image_path, query, client): + from transformers import AutoProcessor + + messages = [ + { + "role": "user", + "content": [ + {"type": "image"}, + {"type": "text", "text": query}, + ], + }, + ] + idefics_processor = AutoProcessor.from_pretrained("HuggingFaceM4/idefics2-8b-chatty") + prompt_with_template = idefics_processor.apply_chat_template(messages, add_generation_prompt=True) + + # load images from local directory + + # encode images to strings which can be sent to the endpoint + def encode_local_image(image_path): + # load image + image = PIL.Image.open(image_path).convert("RGB") + + # Convert the image to a base64 string + buffer = BytesIO() + image.save(buffer, format="JPEG") # Use the appropriate format (e.g., JPEG, PNG) + base64_image = base64.b64encode(buffer.getvalue()).decode("utf-8") + + # add string formatting required by the endpoint + image_string = f"data:image/jpeg;base64,{base64_image}" + + return image_string + + image_string = encode_local_image(image_path) + prompt_with_images = prompt_with_template.replace("", "![]({}) ").format(image_string) + + payload = { + "inputs": prompt_with_images, + "parameters": { + "return_full_text": False, + "max_new_tokens": 200, + }, + } + + return json.loads(client.post(json=payload).decode())[0] + + +# Function to encode the image +def encode_image(image_path): + if image_path.startswith("http"): + user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0" + request_kwargs = { + "headers": {"User-Agent": user_agent}, + "stream": True, + } + + # Send a HTTP request to the URL + response = requests.get(image_path, **request_kwargs) + response.raise_for_status() + content_type = response.headers.get("content-type", "") + + extension = mimetypes.guess_extension(content_type) + if extension is None: + extension = ".download" + + fname = str(uuid.uuid4()) + extension + download_path = os.path.abspath(os.path.join("downloads", fname)) + + with open(download_path, "wb") as fh: + for chunk in response.iter_content(chunk_size=512): + fh.write(chunk) + + image_path = download_path + + with open(image_path, "rb") as image_file: + return base64.b64encode(image_file.read()).decode("utf-8") + + +def resize_image(image_path): + img = PIL.Image.open(image_path) + width, height = img.size + img = img.resize((int(width / 2), int(height / 2))) + new_image_path = f"resized_{image_path}" + img.save(new_image_path) + return new_image_path + + +class VisualQATool(Tool): + name = "visualizer" + description = "A tool that can answer questions about attached images." + inputs = { + "image_path": { + "description": "The path to the image on which to answer the question", + "type": "string", + }, + "question": {"description": "the question to answer", "type": "string", "nullable": True}, + } + output_type = "string" + + client = InferenceClient("HuggingFaceM4/idefics2-8b-chatty") + + def forward(self, image_path: str, question: str | None = None) -> str: + output = "" + add_note = False + if not question: + add_note = True + question = "Please write a detailed caption for this image." + try: + output = process_images_and_text(image_path, question, self.client) + except Exception as e: + print(e) + if "Payload Too Large" in str(e): + new_image_path = resize_image(image_path) + output = process_images_and_text(new_image_path, question, self.client) + + if add_note: + output = ( + f"You did not provide a particular question, so here is a detailed caption for the image: {output}" + ) + + return output + + +@tool +def visualizer(image_path: str, question: str | None = None) -> str: + """A tool that can answer questions about attached images. + + Args: + image_path: The path to the image on which to answer the question. This should be a local path to downloaded image. + question: The question to answer. + """ + import mimetypes + import os + + import requests + + from .visual_qa import encode_image + + add_note = False + if not question: + add_note = True + question = "Please write a detailed caption for this image." + if not isinstance(image_path, str): + raise Exception("You should provide at least `image_path` string argument to this tool!") + + mime_type, _ = mimetypes.guess_type(image_path) + base64_image = encode_image(image_path) + + payload = { + "model": "gpt-4o", + "messages": [ + { + "role": "user", + "content": [ + {"type": "text", "text": question}, + {"type": "image_url", "image_url": {"url": f"data:{mime_type};base64,{base64_image}"}}, + ], + } + ], + "max_tokens": 1000, + } + headers = {"Content-Type": "application/json", "Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"} + response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload) + try: + output = response.json()["choices"][0]["message"]["content"] + except Exception: + raise Exception(f"Response format unexpected: {response.json()}") + + if add_note: + output = f"You did not provide a particular question, so here is a detailed caption for the image: {output}" + + return output diff --git a/examples/open_deep_research/analysis.ipynb b/examples/open_deep_research/analysis.ipynb index 53e7bd430..736d86105 100644 --- a/examples/open_deep_research/analysis.ipynb +++ b/examples/open_deep_research/analysis.ipynb @@ -6,14 +6,30 @@ "metadata": {}, "outputs": [], "source": [ - "!pip install plotly kaleido datasets nbformat -U -q" + "!pip install plotly kaleido datasets nbformat -U -q\n", + "#!export HF_TOKEN=" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "97da58e888e1464b9bfb1a79abe11f02", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(HTML(value='
\u001b[39m\u001b[32m10\u001b[39m result_df = \u001b[43mpd\u001b[49m\u001b[43m.\u001b[49m\u001b[43mconcat\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresults\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 11\u001b[39m result_df[\u001b[33m\"\u001b[39m\u001b[33mprediction\u001b[39m\u001b[33m\"\u001b[39m] = result_df[\u001b[33m\"\u001b[39m\u001b[33mprediction\u001b[39m\u001b[33m\"\u001b[39m].fillna(\u001b[33m\"\u001b[39m\u001b[33mNo prediction\u001b[39m\u001b[33m\"\u001b[39m)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/GitFiles/smolagents/.venv/lib/python3.12/site-packages/pandas/core/reshape/concat.py:382\u001b[39m, in \u001b[36mconcat\u001b[39m\u001b[34m(objs, axis, join, ignore_index, keys, levels, names, verify_integrity, sort, copy)\u001b[39m\n\u001b[32m 379\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m copy \u001b[38;5;129;01mand\u001b[39;00m using_copy_on_write():\n\u001b[32m 380\u001b[39m copy = \u001b[38;5;28;01mFalse\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m382\u001b[39m op = \u001b[43m_Concatenator\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 383\u001b[39m \u001b[43m \u001b[49m\u001b[43mobjs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 384\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 385\u001b[39m \u001b[43m \u001b[49m\u001b[43mignore_index\u001b[49m\u001b[43m=\u001b[49m\u001b[43mignore_index\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 386\u001b[39m \u001b[43m \u001b[49m\u001b[43mjoin\u001b[49m\u001b[43m=\u001b[49m\u001b[43mjoin\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 387\u001b[39m \u001b[43m \u001b[49m\u001b[43mkeys\u001b[49m\u001b[43m=\u001b[49m\u001b[43mkeys\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 388\u001b[39m \u001b[43m \u001b[49m\u001b[43mlevels\u001b[49m\u001b[43m=\u001b[49m\u001b[43mlevels\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 389\u001b[39m \u001b[43m \u001b[49m\u001b[43mnames\u001b[49m\u001b[43m=\u001b[49m\u001b[43mnames\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 390\u001b[39m \u001b[43m \u001b[49m\u001b[43mverify_integrity\u001b[49m\u001b[43m=\u001b[49m\u001b[43mverify_integrity\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 391\u001b[39m \u001b[43m \u001b[49m\u001b[43mcopy\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcopy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 392\u001b[39m \u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[43m=\u001b[49m\u001b[43msort\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 393\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 395\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m op.get_result()\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/GitFiles/smolagents/.venv/lib/python3.12/site-packages/pandas/core/reshape/concat.py:445\u001b[39m, in \u001b[36m_Concatenator.__init__\u001b[39m\u001b[34m(self, objs, axis, join, keys, levels, names, ignore_index, verify_integrity, copy, sort)\u001b[39m\n\u001b[32m 442\u001b[39m \u001b[38;5;28mself\u001b[39m.verify_integrity = verify_integrity\n\u001b[32m 443\u001b[39m \u001b[38;5;28mself\u001b[39m.copy = copy\n\u001b[32m--> \u001b[39m\u001b[32m445\u001b[39m objs, keys = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_clean_keys_and_objs\u001b[49m\u001b[43m(\u001b[49m\u001b[43mobjs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mkeys\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 447\u001b[39m \u001b[38;5;66;03m# figure out what our result ndim is going to be\u001b[39;00m\n\u001b[32m 448\u001b[39m ndims = \u001b[38;5;28mself\u001b[39m._get_ndims(objs)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/GitFiles/smolagents/.venv/lib/python3.12/site-packages/pandas/core/reshape/concat.py:507\u001b[39m, in \u001b[36m_Concatenator._clean_keys_and_objs\u001b[39m\u001b[34m(self, objs, keys)\u001b[39m\n\u001b[32m 504\u001b[39m objs_list = \u001b[38;5;28mlist\u001b[39m(objs)\n\u001b[32m 506\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(objs_list) == \u001b[32m0\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m507\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mNo objects to concatenate\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 509\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m keys \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 510\u001b[39m objs_list = \u001b[38;5;28mlist\u001b[39m(com.not_none(*objs_list))\n", + "\u001b[31mValueError\u001b[39m: No objects to concatenate" + ] + } + ], "source": [ "import glob\n", "\n", @@ -435,7 +491,7 @@ ], "metadata": { "kernelspec": { - "display_name": "agents", + "display_name": "smolagents", "language": "python", "name": "python3" }, @@ -449,7 +505,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.0" + "version": "3.12.11" } }, "nbformat": 4, diff --git a/examples/open_deep_research/requirements.txt b/examples/open_deep_research/requirements.txt index fe6c98ef2..29c868a34 100644 --- a/examples/open_deep_research/requirements.txt +++ b/examples/open_deep_research/requirements.txt @@ -12,7 +12,6 @@ openai>=1.52.2 openpyxl pandas>=2.2.3 pathvalidate>=3.2.1 -pdfminer>=20191125 pdfminer.six>=20240706 Pillow>=11.0.0 puremagic>=1.28 diff --git a/examples/open_deep_research/run_gaia.py b/examples/open_deep_research/run_gaia.py index 9c7bacd4e..dadb8dc43 100644 --- a/examples/open_deep_research/run_gaia.py +++ b/examples/open_deep_research/run_gaia.py @@ -1,4 +1,10 @@ -# EXAMPLE COMMAND: from folder examples/open_deep_research, run: python run_gaia.py --concurrency 32 --run-name generate-traces-03-apr-noplanning --model-id gpt-4o +# GAIA Benchmark Runner for Open Deep Research +# ============================================= +# +# +# EXAMPLE COMMAND: from folder examples/open_deep_research, run: +# python run_gaia.py --concurrency 32 --run-name generate-traces-03-apr-noplanning --model-id gpt-4o + import argparse import json import os @@ -8,10 +14,16 @@ from pathlib import Path from typing import Any +from langfuse import get_client + + +BASE_DIR = Path(__file__).resolve().parent + import datasets import pandas as pd from dotenv import load_dotenv from huggingface_hub import login, snapshot_download +from scripts.gaia_scorer import check_close_call, question_scorer from scripts.reformulator import prepare_response from scripts.run_agents import ( get_single_file_description, @@ -40,6 +52,25 @@ load_dotenv(override=True) + +langfuse = get_client() + +# Verify connection +if langfuse.auth_check(): + print("Langfuse client is authenticated and ready!") +else: + print("Authentication failed. Please check your credentials and host.") + +from openinference.instrumentation.smolagents import SmolagentsInstrumentor + + +with langfuse.start_as_current_span(name="another-operation"): + # Add to the current trace + langfuse.update_current_trace(session_id="zero_shot_1", user_id="cvt8") + + +SmolagentsInstrumentor().instrument() + login(os.getenv("HF_TOKEN")) append_answer_lock = threading.Lock() @@ -93,6 +124,7 @@ def create_agent_team(model: Model): FindNextTool(browser), ArchiveSearchTool(browser), TextInspectorTool(model, text_limit), + visualizer, # Correcting error: "prediction": "Critical Error: Cannot use inspect_file_as_text tool with images: use visualizer instead!", ] text_webbrowser_agent = ToolCallingAgent( @@ -127,12 +159,13 @@ def create_agent_team(model: Model): def load_gaia_dataset(use_raw_dataset: bool, set_to_run: str) -> datasets.Dataset: - if not os.path.exists("data/gaia"): + data_dir = BASE_DIR / "data" / "gaia" + if not data_dir.exists(): if use_raw_dataset: snapshot_download( repo_id="gaia-benchmark/GAIA", repo_type="dataset", - local_dir="data/gaia", + local_dir=str(data_dir), ignore_patterns=[".gitattributes", "README.md"], ) else: @@ -140,20 +173,20 @@ def load_gaia_dataset(use_raw_dataset: bool, set_to_run: str) -> datasets.Datase snapshot_download( repo_id="smolagents/GAIA-annotated", repo_type="dataset", - local_dir="data/gaia", + local_dir=str(data_dir), ignore_patterns=[".gitattributes", "README.md"], ) def preprocess_file_paths(row): if len(row["file_name"]) > 0: - row["file_name"] = f"data/gaia/{set_to_run}/" + row["file_name"] + row["file_name"] = str(data_dir / "2023" / set_to_run / row["file_name"]) return row eval_ds = datasets.load_dataset( - "data/gaia/GAIA.py", - name="2023_all", + path=str(data_dir), + name="default", split=set_to_run, - # data_files={"validation": "validation/metadata.jsonl", "test": "test/metadata.jsonl"}, + data_files={"validation": "2023/validation/metadata.jsonl", "test": "2023/test/metadata.jsonl"}, ) eval_ds = eval_ds.rename_columns({"Question": "question", "Final answer": "true_answer", "Level": "task"}) @@ -161,91 +194,132 @@ def preprocess_file_paths(row): return eval_ds -def append_answer(entry: dict, jsonl_file: str) -> None: +def append_answer(entry: dict, jsonl_file: Path) -> None: jsonl_path = Path(jsonl_file) jsonl_path.parent.mkdir(parents=True, exist_ok=True) - with append_answer_lock, open(jsonl_file, "a", encoding="utf-8") as fp: - fp.write(json.dumps(entry) + "\n") - assert jsonl_path.exists(), "File not found!" - print("Answer exported to file:", jsonl_path.resolve()) + + def convert_to_serializable(obj): + """Convert objects to JSON serializable format""" + if hasattr(obj, "dict"): + return obj.dict() + elif hasattr(obj, "__dict__"): + return obj.__dict__ + else: + return str(obj) + + try: + with append_answer_lock, jsonl_path.open("a", encoding="utf-8") as fp: + fp.write(json.dumps(entry, default=convert_to_serializable, ensure_ascii=False) + "\n") + fp.flush() # Ensure the buffer is flushed immediately + os.fsync(fp.fileno()) # Force the file system to write the data to disk + except Exception as e: + print(f"Error writing to answers file {jsonl_path}: {e}") def answer_single_question( - example: dict, model_id: str, answers_file: str, visual_inspection_tool: TextInspectorTool + example: dict, model_id: str, answers_file: str, errors_file: str, visual_inspection_tool ) -> None: - model_params: dict[str, Any] = { - "model_id": model_id, - "custom_role_conversions": custom_role_conversions, - } - if model_id == "o1": - model_params["reasoning_effort"] = "high" - model_params["max_completion_tokens"] = 8192 - else: - model_params["max_tokens"] = 4096 - model = LiteLLMModel(**model_params) - # model = InferenceClientModel(model_id="Qwen/Qwen3-32B", provider="novita", max_tokens=4096) - document_inspection_tool = TextInspectorTool(model, 100000) + print(f"Processing question: {example['question']}") - agent = create_agent_team(model) + # Initialize variables to avoid unbound errors + start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + end_time = None + intermediate_steps = [] + parsing_error = False + iteration_limit_exceeded = False + raised_exception = False + exception = None + output = None + augmented_question = example["question"] # Default value + + try: + model_params: dict[str, Any] = { + "model_id": model_id, + "custom_role_conversions": custom_role_conversions, + } + if model_id == "o1": + model_params["reasoning_effort"] = "high" + model_params["max_completion_tokens"] = 8192 + else: + model_params["max_tokens"] = 4096 + model = LiteLLMModel(**model_params) + document_inspection_tool = TextInspectorTool(model, 100000) - augmented_question = """You have one question to answer. It is paramount that you provide a correct answer. + agent = create_agent_team(model) + + augmented_question = """You have one question to answer. It is paramount that you provide a correct answer. Give it all you can: I know for a fact that you have access to all the relevant tools to solve it and find the correct answer (the answer does exist). Failure or 'I cannot answer' or 'None found' will not be tolerated, success will be rewarded. Run verification steps if that's needed, you must make sure you find the correct answer! Here is the task: """ + example["question"] - if example["file_name"]: - if ".zip" in example["file_name"]: - prompt_use_files = "\n\nTo solve the task above, you will have to use these attached files:\n" - prompt_use_files += get_zip_description( - example["file_name"], example["question"], visual_inspection_tool, document_inspection_tool - ) - else: - prompt_use_files = "\n\nTo solve the task above, you will have to use this attached file:\n" - prompt_use_files += get_single_file_description( - example["file_name"], example["question"], visual_inspection_tool, document_inspection_tool - ) - augmented_question += prompt_use_files - - start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - try: - # Run agent πŸš€ - final_result = agent.run(augmented_question) - - agent_memory = agent.write_memory_to_messages() - - final_result = prepare_response(augmented_question, agent_memory, reformulation_model=model) - - output = str(final_result) - for memory_step in agent.memory.steps: - memory_step.model_input_messages = None - intermediate_steps = agent_memory - - # Check for parsing errors which indicate the LLM failed to follow the required format - parsing_error = True if any(["AgentParsingError" in step for step in intermediate_steps]) else False - - # check if iteration limit exceeded - iteration_limit_exceeded = True if "Agent stopped due to iteration limit or time limit." in output else False - raised_exception = False + if example.get("file_name"): + if ".zip" in example["file_name"]: + prompt_use_files = "\n\nTo solve the task above, you will have to use these attached files:\n" + prompt_use_files += get_zip_description( + example["file_name"], example["question"], visual_inspection_tool, document_inspection_tool + ) + else: + prompt_use_files = "\n\nTo solve the task above, you will have to use this attached file:\n" + prompt_use_files += get_single_file_description( + example["file_name"], example["question"], visual_inspection_tool, document_inspection_tool + ) + augmented_question += prompt_use_files + + try: + final_result = agent.run(augmented_question) + agent_memory = agent.write_memory_to_messages() + final_result = prepare_response(augmented_question, agent_memory, reformulation_model=model) + output = str(final_result) + # Fix: Use .dict() method for ChatMessage objects instead of dict() constructor + intermediate_steps = [msg.dict() if hasattr(msg, "dict") else str(msg) for msg in agent_memory] + parsing_error = any("AgentParsingError" in str(step) for step in intermediate_steps) + iteration_limit_exceeded = "Agent stopped due to iteration limit or time limit." in output + except Exception as e: + print(f"Error processing question '{example['question']}': {e}") + output = f"Error: {str(e)}" + exception = e + raised_exception = True + + end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Safely get token counts with fallback + try: + token_counts_manager = agent.monitor.get_total_token_counts() + token_counts_web = list(agent.managed_agents.values())[0].monitor.get_total_token_counts() + total_token_counts = { + "input": getattr(token_counts_manager, "input", 0) + getattr(token_counts_web, "input", 0), + "output": getattr(token_counts_manager, "output", 0) + getattr(token_counts_web, "output", 0), + } + except Exception as e: + print(f"Error getting token counts: {e}") + total_token_counts = {"input": 0, "output": 0} except Exception as e: - print("Error on ", augmented_question, e) - output = None - intermediate_steps = [] - parsing_error = False - iteration_limit_exceeded = False + print(f"Critical error in answer_single_question for '{example['question']}': {e}") + end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + output = f"Critical Error: {str(e)}" exception = e raised_exception = True - end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - token_counts_manager = agent.monitor.get_total_token_counts() - token_counts_web = list(agent.managed_agents.values())[0].monitor.get_total_token_counts() - total_token_counts = { - "input": token_counts_manager["input"] + token_counts_web["input"], - "output": token_counts_manager["output"] + token_counts_web["output"], - } + total_token_counts = {"input": 0, "output": 0} + intermediate_steps = [] + + # Always compute scores and append answer, regardless of errors + try: + is_correct = question_scorer(str(output), str(example["true_answer"])) + is_near_correct = check_close_call(str(output), str(example["true_answer"]), is_correct) + except Exception as e: + print(f"Error computing scores: {e}") + is_correct = False + is_near_correct = False + + print(f"Question: {example['question'][:50]}{'...' if len(example['question']) > 50 else ''}") + print(f"Prediction: {str(output)[:100]}{'...' if len(str(output)) > 100 else ''}") + print(f"Correct: {is_correct}") + annotated_example = { - "agent_name": model.model_id, + "agent_name": model_id, "question": example["question"], "augmented_question": augmented_question, "prediction": output, @@ -256,14 +330,22 @@ def answer_single_question( "task": example["task"], "task_id": example["task_id"], "true_answer": example["true_answer"], + "is_correct": is_correct, + "is_near_correct": is_near_correct, "start_time": start_time, "end_time": end_time, "token_counts": total_token_counts, } - append_answer(annotated_example, answers_file) + + # Always append to main answers file + append_answer(annotated_example, Path(answers_file)) + + # If there was an error, also append to errors file + if raised_exception or parsing_error or iteration_limit_exceeded: + append_answer(annotated_example, Path(errors_file)) -def get_examples_to_answer(answers_file: str, eval_ds: datasets.Dataset) -> list[dict]: +def get_examples_to_answer(answers_file: Path, eval_ds: datasets.Dataset) -> list[dict]: print(f"Loading answers from {answers_file}...") try: done_questions = pd.read_json(answers_file, lines=True)["question"].tolist() @@ -272,31 +354,193 @@ def get_examples_to_answer(answers_file: str, eval_ds: datasets.Dataset) -> list print("Error when loading records: ", e) print("No usable records! ▢️ Starting new.") done_questions = [] - return [line for line in eval_ds.to_list() if line["question"] not in done_questions and line["file_name"]] + return [line for line in eval_ds.to_list() if line["question"] not in done_questions] + + +def compute_score(answers_file: Path) -> None: + if not answers_file.exists(): + print(f"Error: The file {answers_file} does not exist.") + return + + if answers_file.stat().st_size == 0: + print(f"Error: The file {answers_file} is empty.") + return + + try: + df = pd.read_json(answers_file, lines=True) + except ValueError as e: + print(f"Error reading JSON from {answers_file}: {e}") + return + + if "is_correct" not in df.columns: + df["is_correct"] = df.apply(lambda x: question_scorer(str(x["prediction"]), str(x["true_answer"])), axis=1) + + # Calculate comprehensive scores + total_questions = len(df) + correct_answers = df["is_correct"].sum() + accuracy = df["is_correct"].mean() + + # Calculate additional metrics + error_count = df["agent_error"].notna().sum() + parsing_error_count = df["parsing_error"].sum() + iteration_limit_count = df["iteration_limit_exceeded"].sum() + + # Group by task level for detailed analysis + task_scores = None + if "task" in df.columns: + task_scores = ( + df.groupby("task") + .agg( + { + "is_correct": ["count", "sum", "mean"], + "agent_error": lambda x: x.notna().sum(), + "parsing_error": "sum", + "iteration_limit_exceeded": "sum", + } + ) + .round(3) + ) + task_scores.columns = ["total", "correct", "accuracy", "errors", "parsing_errors", "iteration_limits"] + + # Save detailed score analysis + score_file = answers_file.parent / "detailed_scores.txt" + with score_file.open("w", encoding="utf-8") as f: + f.write("GAIA Benchmark Detailed Results\n") + f.write("=" * 50 + "\n\n") + f.write("Overall Performance:\n") + f.write(f" Total Questions: {total_questions}\n") + f.write(f" Correct Answers: {correct_answers}\n") + f.write(f" Accuracy: {accuracy * 100:.2f}%\n\n") + + f.write("Error Analysis:\n") + f.write(f" Agent Errors: {error_count} ({error_count / total_questions * 100:.1f}%)\n") + f.write(f" Parsing Errors: {parsing_error_count} ({parsing_error_count / total_questions * 100:.1f}%)\n") + f.write( + f" Iteration Limits: {iteration_limit_count} ({iteration_limit_count / total_questions * 100:.1f}%)\n\n" + ) + + if "task" in df.columns: + f.write("Performance by Task Level:\n") + if task_scores is not None: + f.write(str(task_scores) + "\n\n") + + f.write("Individual Results:\n") + f.write("-" * 50 + "\n") + for idx, row in df.iterrows(): + status = "βœ… CORRECT" if row["is_correct"] else "❌ INCORRECT" + f.write(f"{status} | Task {row.get('task', 'N/A')} | {row['question'][:80]}...\n") + f.write(f" Predicted: {str(row['prediction'])[:100]}...\n") + f.write(f" Expected: {str(row['true_answer'])[:100]}...\n") + if row.get("agent_error"): + f.write(f" Error: {str(row['agent_error'])[:100]}...\n") + f.write("\n") + + # Save JSON summary for programmatic access + summary_file = answers_file.parent / "score_summary.json" + summary_data = { + "total_questions": int(total_questions), + "correct_answers": int(correct_answers), + "accuracy": float(accuracy), + "error_rate": float(error_count / total_questions), + "parsing_error_rate": float(parsing_error_count / total_questions), + "iteration_limit_rate": float(iteration_limit_count / total_questions), + "timestamp": datetime.now().isoformat(), + "answers_file": str(answers_file.name), + } + + if "task" in df.columns and task_scores is not None: + summary_data["task_performance"] = task_scores.to_dict() + + with summary_file.open("w", encoding="utf-8") as f: + json.dump(summary_data, f, indent=2, ensure_ascii=False) + + print("\n" + "=" * 60) + print("πŸ“Š GAIA BENCHMARK RESULTS") + print("=" * 60) + print("πŸ“ˆ Overall Performance:") + print(f" Total Questions: {total_questions}") + print(f" Correct Answers: {correct_answers}") + print(f" Accuracy: {accuracy * 100:.2f}%") + print() + print("⚠️ Error Analysis:") + print(f" Agent Errors: {error_count} ({error_count / total_questions * 100:.1f}%)") + print(f" Parsing Errors: {parsing_error_count} ({parsing_error_count / total_questions * 100:.1f}%)") + print(f" Iteration Limits: {iteration_limit_count} ({iteration_limit_count / total_questions * 100:.1f}%)") + + if "task" in df.columns: + print() + print("πŸ“‹ Performance by Task Level:") + for task_level in sorted(df["task"].unique()): + task_data = df[df["task"] == task_level] + task_acc = task_data["is_correct"].mean() + task_count = len(task_data) + task_correct = task_data["is_correct"].sum() + print(f" Level {task_level}: {task_acc * 100:.1f}% ({task_correct}/{task_count})") + + print() + print(f"πŸ’Ύ Detailed results saved to: {score_file}") + print(f"πŸ’Ύ Summary data saved to: {summary_file}") + print("=" * 60) def main(): args = parse_args() print(f"Starting run with arguments: {args}") + def create_output_folders(set_to_run): + """Create output folders if they don't exist.""" + output_folder = Path(f"output/{set_to_run}") + output_folder.mkdir(parents=True, exist_ok=True) + print(f"Ensured output folder exists at: {output_folder}") + + create_output_folders(args.set_to_run) + eval_ds = load_gaia_dataset(args.use_raw_dataset, args.set_to_run) print("Loaded evaluation dataset:") - print(pd.DataFrame(eval_ds)["task"].value_counts()) + eval_df = pd.DataFrame(list(eval_ds)) + print(eval_df["task"].value_counts()) - answers_file = f"output/{args.set_to_run}/{args.run_name}.jsonl" + answers_file = BASE_DIR / "output" / args.set_to_run / f"{args.run_name}.jsonl" + errors_file = BASE_DIR / "output" / args.set_to_run / f"{args.run_name}_errors.jsonl" tasks_to_run = get_examples_to_answer(answers_file, eval_ds) + print(f"Tasks to run: {len(tasks_to_run)}") + if len(tasks_to_run) == 0: + print("No new tasks to process. All questions may have been completed already.") + print("To rerun all tasks, delete the existing output file.") + return + with ThreadPoolExecutor(max_workers=args.concurrency) as exe: futures = [ - exe.submit(answer_single_question, example, args.model_id, answers_file, visualizer) + exe.submit( + answer_single_question, + example, + args.model_id, + answers_file, + errors_file, + visualizer, # Fix: Use visualizer for images instead of TextInspectorTool + ) for example in tasks_to_run ] for f in tqdm(as_completed(futures), total=len(tasks_to_run), desc="Processing tasks"): - f.result() + try: + f.result() + except Exception as e: + print(f"Error in task: {e}") + continue + + if not answers_file.exists(): + print(f"Error: The answers file {answers_file} was not created. Check the append_answer function.") + else: + print(f"βœ… Main answers file created successfully: {answers_file}") + + if errors_file.exists(): + print(f"⚠️ Errors file created: {errors_file}") + else: + print("βœ… No errors file created (no errors encountered)") - # for example in tasks_to_run: - # answer_single_question(example, args.model_id, answers_file, visualizer) print("All tasks processed.") + compute_score(answers_file) if __name__ == "__main__": diff --git a/examples/open_deep_research/scripts/mdconvert.py b/examples/open_deep_research/scripts/mdconvert.py index 939cd121a..6a4b40f62 100644 --- a/examples/open_deep_research/scripts/mdconvert.py +++ b/examples/open_deep_research/scripts/mdconvert.py @@ -983,12 +983,23 @@ def _append_ext(self, extensions, ext): def _guess_ext_magic(self, path): """Use puremagic (a Python implementation of libmagic) to guess a file's extension based on the first few bytes.""" # Use puremagic to guess + if not os.path.isfile(path): + raise ValueError(f"Invalid file path passed to _guess_ext_magic: {path}") try: guesses = puremagic.magic_file(path) if len(guesses) > 0: ext = guesses[0].extension.strip() if len(ext) > 0: return ext + except puremagic.PureMagicError as e: + # If puremagic fails, we just ignore it and return None + print(f"Error in puremagic: {e}") + except puremagic.PureMagicNotFoundError: + # If puremagic is not installed, we just ignore it and return None + print("puremagic is not installed. Skipping magic file type detection.") + except ValueError: + # If the path is not a file, we just ignore it and return None + pass except FileNotFoundError: pass except IsADirectoryError: diff --git a/examples/open_deep_research/scripts/run_agents.py b/examples/open_deep_research/scripts/run_agents.py index e2e020cb3..fa2df2ad2 100644 --- a/examples/open_deep_research/scripts/run_agents.py +++ b/examples/open_deep_research/scripts/run_agents.py @@ -9,6 +9,7 @@ def serialize_agent_error(obj): + """Serialize AgentError objects for JSON output.""" if isinstance(obj, AgentError): return {"error_type": obj.__class__.__name__, "message": obj.message} else: @@ -16,6 +17,13 @@ def serialize_agent_error(obj): def get_image_description(file_name: str, question: str, visual_inspection_tool) -> str: + """ + Get description of an image file for context. + + DEBUGGING FIX: Changed parameter from file_path to image_path + - ISSUE: TypeError due to mismatched parameter name + - SOLUTION: Use image_path parameter as expected by visualizer tool + """ prompt = f"""Write a caption of 5 sentences for this image. Pay special attention to any details that might be useful for someone answering the following question: {question}. But do not try to answer the question directly! Do not add any information that is not present in the image.""" diff --git a/examples/open_deep_research/visual_vs_text_browser.ipynb b/examples/open_deep_research/visual_vs_text_browser.ipynb index 1acc5c704..5eae43434 100644 --- a/examples/open_deep_research/visual_vs_text_browser.ipynb +++ b/examples/open_deep_research/visual_vs_text_browser.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -20,9 +20,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Using the latest cached version of the dataset since gaia-benchmark/GAIA couldn't be found on the Hugging Face Hub\n", + "Found the latest cached dataset configuration '2023_all' at /home/ecca/.cache/huggingface/datasets/gaia-benchmark___gaia/2023_all/0.0.1/ec492fe4320ee795b1aed6bb46229c5f693226b0f1316347501c24b4baeee005 (last modified on Mon Jul 21 11:03:18 2025).\n" + ] + } + ], "source": [ "import datasets\n", "\n", @@ -34,7 +43,22 @@ "cell_type": "code", "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0132ebaa6aed4c348770d58aae5ef8a1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Filter: 0%| | 0/165 [00:00 \u001b[39m\u001b[32m1\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mscripts\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mrun_agents\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m answer_questions\n\u001b[32m 2\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mscripts\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mtext_inspector_tool\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m TextInspectorTool\n\u001b[32m 3\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mscripts\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mtext_web_browser\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m (\n\u001b[32m 4\u001b[39m ArchiveSearchTool,\n\u001b[32m 5\u001b[39m FinderTool,\n\u001b[32m (...)\u001b[39m\u001b[32m 11\u001b[39m VisitTool,\n\u001b[32m 12\u001b[39m )\n", + "\u001b[31mImportError\u001b[39m: cannot import name 'answer_questions' from 'scripts.run_agents' (/home/ecca/GitFiles/smolagents/examples/open_deep_research/scripts/run_agents.py)" + ] + } + ], "source": [ "from scripts.run_agents import answer_questions\n", "from scripts.text_inspector_tool import TextInspectorTool\n", @@ -337,7 +388,7 @@ ], "metadata": { "kernelspec": { - "display_name": "gaia", + "display_name": "smolagents", "language": "python", "name": "python3" }, @@ -351,7 +402,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.0" + "version": "3.12.11" } }, "nbformat": 4, diff --git a/examples/smolagents_benchmark/model_performance_comparison.png b/examples/smolagents_benchmark/model_performance_comparison.png new file mode 100644 index 000000000..30cb23506 Binary files /dev/null and b/examples/smolagents_benchmark/model_performance_comparison.png differ diff --git a/examples/smolagents_benchmark/run.py b/examples/smolagents_benchmark/run.py index aa8e48570..f7716fc0a 100644 --- a/examples/smolagents_benchmark/run.py +++ b/examples/smolagents_benchmark/run.py @@ -1,15 +1,25 @@ +# Smolagents Benchmark Runner +# =========================== +# +# Example usage: +# python run.py --model-type LiteLLMModel --model-id gpt-4o --provider openai --agent-action-type tool-calling +# python run.py --model-type LiteLLMModel --model-id gpt-4o --provider openai --agent-action-type code + import argparse import datetime import json import os +import re import threading import time from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path +from typing import Generator import datasets import pandas as pd from dotenv import load_dotenv +from langfuse import get_client from tqdm import tqdm from smolagents import ( @@ -22,9 +32,30 @@ ToolCallingAgent, VisitWebpageTool, ) +from smolagents.memory import ActionStep, FinalAnswerStep, PlanningStep +from smolagents.models import ChatMessageStreamDelta load_dotenv() + +langfuse = get_client() + +# Verify connection +if langfuse.auth_check(): + print("Langfuse client is authenticated and ready!") +else: + print("Authentication failed. Please check your credentials and host.") + +from openinference.instrumentation.smolagents import SmolagentsInstrumentor + + +with langfuse.start_as_current_span(name="another-operation"): + # Add to the current trace + langfuse.update_current_trace(session_id="zero_shot_1", user_id="cvt8") + + +SmolagentsInstrumentor().instrument() + os.makedirs("output", exist_ok=True) APPEND_ANSWER_LOCK = threading.Lock() @@ -129,7 +160,6 @@ def answer_single_question(example, model, answers_file, action_type): agent = CodeAgent( tools=[GoogleSearchTool(provider="serper"), VisitWebpageTool()], model=model, - additional_authorized_imports=["numpy", "sympy"], max_steps=10, ) elif action_type == "tool-calling": @@ -154,17 +184,45 @@ def answer_single_question(example, model, answers_file, action_type): try: if action_type == "vanilla": answer = agent([{"role": "user", "content": augmented_question}]).content - token_counts = agent.monitor.get_total_token_counts() + # For vanilla agents, the agent is just the model and doesn't have a monitor + token_counts = {"input": 0, "output": 0} intermediate_steps = answer else: # Run agent πŸš€ - answer = str(agent.run(augmented_question)) + result = agent.run(augmented_question) + try: + if isinstance(result, Generator): + steps = list(result) + final_step = steps[-1] if steps else None + if isinstance(final_step, FinalAnswerStep): + answer = final_step.output + elif isinstance(final_step, ActionStep): + answer = final_step.action_output + elif isinstance(final_step, PlanningStep): + answer = final_step.plan + elif isinstance(final_step, ChatMessageStreamDelta): + answer = final_step.content + else: + answer = str(final_step) + else: + answer = result if isinstance(result, str) else str(result) + except Exception as e: + answer = f"Error extracting answer: {str(e)}" + token_counts = agent.monitor.get_total_token_counts() + intermediate_steps = [message.dict() for message in agent.write_memory_to_messages()] end_time = time.time() + duration = end_time - start_time + answer_preview = str(answer)[:100] + ("..." if len(str(answer)) > 100 else "") if answer else "No answer" + print(f"βœ… Question processed in {duration:.2f}s - Answer: {answer_preview}") except Exception as e: - print("Error on ", augmented_question, e) + end_time = time.time() + duration = end_time - start_time + question_preview = str(augmented_question)[:50] + ("..." if len(str(augmented_question)) > 50 else "") + print(f"❌ Error after {duration:.2f}s on question: {question_preview}") + print(f" Error details: {str(e)}") intermediate_steps = [] token_counts = {"input": 0, "output": 0} answer = str(e) @@ -185,13 +243,161 @@ def answer_single_question(example, model, answers_file, action_type): append_answer(annotated_example, answers_file) +# ============================== +# SCORING SYSTEM (ADDED FEATURE) +# ============================== +# This section was added to provide comprehensive benchmark evaluation +# with multiple scoring metrics and detailed performance analysis. + + +def normalize_answer(answer): + """ + Normalize answer for comparison. + + Removes extra whitespace, converts to lowercase, and strips punctuation + to enable more flexible answer matching. + """ + if answer is None: + return "" + answer = str(answer).strip().lower() + # Remove extra whitespace + answer = re.sub(r"\s+", " ", answer) + # Remove common punctuation at the end + answer = re.sub(r"[.!?;,]+$", "", answer) + return answer + + +def calculate_exact_match_score(predicted_answer, true_answer): + """ + Calculate exact match score (1.0 for perfect match, 0.0 otherwise). + + This is the strictest scoring metric. + """ + return 1.0 if normalize_answer(predicted_answer) == normalize_answer(true_answer) else 0.0 + + +def calculate_contains_score(predicted_answer, true_answer): + """Calculate score based on whether the predicted answer contains the true answer.""" + normalized_pred = normalize_answer(predicted_answer) + normalized_true = normalize_answer(true_answer) + + if not normalized_true: + return 0.0 + + return 1.0 if normalized_true in normalized_pred else 0.0 + + +def calculate_benchmark_scores(jsonl_file_path): + """Calculate scores for a benchmark result file.""" + if not os.path.exists(jsonl_file_path): + return {"error": "File not found"} + + total_questions = 0 + exact_matches = 0 + contains_matches = 0 + + with open(jsonl_file_path, "r", encoding="utf-8") as f: + for line in f: + try: + data = json.loads(line.strip()) + if not data: + continue + + predicted = data.get("answer", "") + true_answer = data.get("true_answer", "") + + total_questions += 1 + exact_matches += calculate_exact_match_score(predicted, true_answer) + contains_matches += calculate_contains_score(predicted, true_answer) + + except json.JSONDecodeError: + continue + + if total_questions == 0: + return {"error": "No valid questions found"} + + return { + "total_questions": total_questions, + "exact_match_score": exact_matches / total_questions, + "contains_score": contains_matches / total_questions, + "exact_matches": exact_matches, + "contains_matches": contains_matches, + } + + +def save_benchmark_scores(output_dir, model_id, action_type, date, eval_ds): + """Calculate and save scores for all benchmarks.""" + scores_file = f"{output_dir}/benchmark_scores_{model_id.replace('/', '__')}__{action_type}__{date}.json" + + all_scores = { + "model_id": model_id, + "action_type": action_type, + "date": date, + "timestamp": datetime.datetime.now().isoformat(), + "benchmarks": {}, + } + + total_questions_all = 0 + total_exact_matches_all = 0 + total_contains_matches_all = 0 + + print("\nπŸ“Š Calculating benchmark scores...") + + for task in eval_ds: + jsonl_file = f"{output_dir}/{model_id.replace('/', '__')}__{action_type}__{task}__{date}.jsonl" + scores = calculate_benchmark_scores(jsonl_file) + + if "error" not in scores: + all_scores["benchmarks"][task] = scores + total_questions_all += scores["total_questions"] + total_exact_matches_all += scores["exact_matches"] + total_contains_matches_all += scores["contains_matches"] + + print(f" πŸ“ˆ {task.upper()}:") + print(f" Questions: {scores['total_questions']}") + print( + f" Exact Match: {scores['exact_match_score']:.1%} ({scores['exact_matches']}/{scores['total_questions']})" + ) + print( + f" Contains: {scores['contains_score']:.1%} ({scores['contains_matches']}/{scores['total_questions']})" + ) + else: + print(f" ❌ {task.upper()}: {scores['error']}") + + # Overall scores + if total_questions_all > 0: + all_scores["overall"] = { + "total_questions": total_questions_all, + "exact_match_score": total_exact_matches_all / total_questions_all, + "contains_score": total_contains_matches_all / total_questions_all, + "exact_matches": total_exact_matches_all, + "contains_matches": total_contains_matches_all, + } + + print("\n🎯 OVERALL SCORES:") + print(f" Questions: {total_questions_all}") + print( + f" Exact Match: {all_scores['overall']['exact_match_score']:.1%} ({total_exact_matches_all}/{total_questions_all})" + ) + print( + f" Contains: {all_scores['overall']['contains_score']:.1%} ({total_contains_matches_all}/{total_questions_all})" + ) + + # Save scores to file + with open(scores_file, "w", encoding="utf-8") as f: + json.dump(all_scores, f, indent=2, ensure_ascii=False) + + print(f"\nπŸ’Ύ Scores saved to: {scores_file}") + return all_scores + + def answer_questions( eval_ds, model, date, action_type: str = "code", output_dir: str = "output", - answers_dataset: str = None, + answers_dataset: str | None = None, push_answers_to_hub: bool = False, parallel_workers: int = 32, ): @@ -200,7 +406,8 @@ def answer_questions( for task in eval_ds: file_name = f"{output_dir}/{model_id.replace('/', '__')}__{action_type}__{task}__{date}.jsonl" - print(f"Starting processing and writing output to '{file_name}'") + print(f"\nπŸš€ Starting benchmark: {task}") + print(f"πŸ“„ Writing output to: {file_name}") answered_questions = [] if os.path.exists(file_name): with open(file_name, "r") as f: @@ -208,7 +415,19 @@ def answer_questions( answered_questions.append(json.loads(line)["original_question"]) examples_todo = [example for example in eval_ds[task] if example["question"] not in answered_questions] - print(f"Launching {parallel_workers} parallel workers.") + total_questions = len(eval_ds[task]) + remaining_questions = len(examples_todo) + completed_questions = total_questions - remaining_questions + + # IMPROVED LOGGING: Added detailed progress tracking with emojis for better readability + print( + f"πŸ“Š Progress: {completed_questions}/{total_questions} questions completed ({remaining_questions} remaining)" + ) + if remaining_questions == 0: + print(f"βœ… All questions for {task} already completed!") + continue + + print(f"πŸ‘₯ Launching {parallel_workers} parallel workers...") with ThreadPoolExecutor(max_workers=parallel_workers) as exe: futures = [ @@ -217,7 +436,7 @@ def answer_questions( for f in tqdm(as_completed(futures), total=len(examples_todo), desc="Processing tasks"): f.result() - print("All tasks processed.") + print(f"βœ… All tasks for {task} processed.") if push_answers_to_hub and answers_dataset: print("Pushing answers to hub...") @@ -232,6 +451,9 @@ def answer_questions( commit_message=f"Upload {config}", ) + # Calculate and save benchmark scores + save_benchmark_scores(output_dir, model_id, action_type, date, eval_ds) + if __name__ == "__main__": args = parse_arguments() diff --git a/examples/smolagents_benchmark/score.ipynb b/examples/smolagents_benchmark/score.ipynb index 7d8709498..659bc0c49 100644 --- a/examples/smolagents_benchmark/score.ipynb +++ b/examples/smolagents_benchmark/score.ipynb @@ -2,24 +2,26 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ - "!pip install -e .. datasets sympy numpy matplotlib seaborn -q # Install dev version of smolagents + some packages" + "#!pip install -e .. datasets sympy numpy matplotlib seaborn -q # Install dev version of smolagents + some packages\n", + "#!export HF_TOKEN=" ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Benchmark date\n", "# - set a concrete date:\n", - "DATE = \"2024-12-26\"\n", + "DATE = \"2025-07-17\"\n", "# - or use default: today\n", "# DATE = None\n", + "# DATE = DATE or datetime.date.today().isoformat()\n", "\n", "# Evaluation dataset\n", "# - the dataset is gated, so you must first visit its page to request access: https://huggingface.co/datasets/smolagents-benchmark/benchmark-v1\n", @@ -33,7 +35,7 @@ "# Results dataset\n", "RESULTS_DATASET = \"smolagents/results\"\n", "# Whether to push the results dataset to the Hub\n", - "PUSH_RESULTS_DATASET_TO_HUB = True" + "PUSH_RESULTS_DATASET_TO_HUB = False" ] }, { @@ -218,7 +220,10 @@ " if push_to_hub_dataset:\n", " ds = datasets.Dataset.from_pandas(df)\n", " config = date\n", - " ds.push_to_hub(push_to_hub_dataset, config_name=config, commit_message=f\"Upload {config} results\")\n", + " ds.push_to_hub(\n", + " push_to_hub_dataset, config_name=config, commit_message=f\"Upload {config} results\", create_pr=True\n", + " )\n", + " # ds.push_to_hub(push_to_hub_dataset, config_name=config, commit_message=f\"Upload {config} results\")\n", " return df" ] }, @@ -231,9 +236,1035 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/ecca/GitFiles/dec_smolagents/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of answers_subsets 129\n", + "Example of answers_subset Qwen__QwQ-32B__code__gaia\n", + "smolagents/answers Qwen__QwQ-32B__code__gaia\n", + "smolagents/answers Qwen__QwQ-32B__code__math\n", + "smolagents/answers Qwen__QwQ-32B__code__simpleqa\n", + "smolagents/answers Qwen__Qwen2.5-72B-Instruct__code__gaia\n", + "smolagents/answers Qwen__Qwen2.5-72B-Instruct__code__math\n", + "smolagents/answers Qwen__Qwen2.5-72B-Instruct__code__simpleqa\n", + "smolagents/answers Qwen__Qwen2.5-72B-Instruct__vanilla__gaia\n", + "smolagents/answers Qwen__Qwen2.5-72B-Instruct__vanilla__math\n", + "smolagents/answers Qwen__Qwen2.5-72B-Instruct__vanilla__simpleqa\n", + "smolagents/answers Qwen__Qwen2.5-7B-Instruct__vanilla__gaia\n", + "smolagents/answers Qwen__Qwen2.5-7B-Instruct__vanilla__math\n", + "smolagents/answers Qwen__Qwen2.5-7B-Instruct__vanilla__simpleqa\n", + "smolagents/answers Qwen__Qwen2.5-Coder-32B-Instruct__code__gaia\n", + "smolagents/answers Qwen__Qwen2.5-Coder-32B-Instruct__code__math\n", + "smolagents/answers Qwen__Qwen2.5-Coder-32B-Instruct__code__simpleqa\n", + "smolagents/answers Qwen__Qwen2.5-Coder-32B-Instruct__tool-calling__gaia\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processing tasks: 1%| | 1/129 [00:09<20:22, 9.55s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers Qwen__Qwen2.5-Coder-32B-Instruct__tool-calling__math\n", + "smolagents/answers Qwen__Qwen2.5-Coder-32B-Instruct__tool-calling__simpleqa\n", + "smolagents/answers Qwen__Qwen2.5-Coder-32B-Instruct__vanilla__gaia\n", + "smolagents/answers Qwen__Qwen2.5-Coder-32B-Instruct__vanilla__math\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processing tasks: 4%|▍ | 5/129 [00:09<03:03, 1.48s/it]/tmp/ipykernel_121064/3316273531.py:86: UserWarning: Answer lists have different lengths, returning False.\n", + " warnings.warn(\"Answer lists have different lengths, returning False.\", UserWarning)\n", + "/tmp/ipykernel_121064/3316273531.py:86: UserWarning: Answer lists have different lengths, returning False.\n", + " warnings.warn(\"Answer lists have different lengths, returning False.\", UserWarning)\n", + "Processing tasks: 6%|β–Œ | 8/129 [00:09<01:35, 1.26it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers Qwen__Qwen2.5-Coder-32B-Instruct__vanilla__simpleqa\n", + "smolagents/answers Qwen__Qwen3-235B-A22B__code__gaia\n", + "smolagents/answers Qwen__Qwen3-235B-A22B__code__math\n", + "smolagents/answers Qwen__Qwen3-235B-A22B__code__simpleqa\n", + "smolagents/answers Qwen__Qwen3-32B__code__gaia\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_121064/3316273531.py:86: UserWarning: Answer lists have different lengths, returning False.\n", + " warnings.warn(\"Answer lists have different lengths, returning False.\", UserWarning)\n", + "Processing tasks: 8%|β–Š | 10/129 [00:10<01:09, 1.70it/s]/tmp/ipykernel_121064/3316273531.py:86: UserWarning: Answer lists have different lengths, returning False.\n", + " warnings.warn(\"Answer lists have different lengths, returning False.\", UserWarning)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers Qwen__Qwen3-32B__code__math\n", + "smolagents/answers Qwen__Qwen3-32B__code__simpleqa\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processing tasks: 9%|β–‰ | 12/129 [00:11<01:02, 1.88it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers anthropic__claude-3-5-sonnet-latest__code__gaia\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processing tasks: 10%|β–ˆ | 13/129 [00:14<02:09, 1.11s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers anthropic__claude-3-5-sonnet-latest__code__math\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processing tasks: 11%|β–ˆ | 14/129 [00:15<02:05, 1.09s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers anthropic__claude-3-5-sonnet-latest__code__simpleqa\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processing tasks: 12%|β–ˆβ– | 15/129 [00:16<01:54, 1.01s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers anthropic__claude-3-5-sonnet-latest__tool-calling__gaia\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processing tasks: 12%|β–ˆβ– | 16/129 [00:16<01:35, 1.18it/s]/tmp/ipykernel_121064/3316273531.py:86: UserWarning: Answer lists have different lengths, returning False.\n", + " warnings.warn(\"Answer lists have different lengths, returning False.\", UserWarning)\n", + "Processing tasks: 13%|β–ˆβ–Ž | 17/129 [00:17<01:15, 1.48it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers anthropic__claude-3-5-sonnet-latest__tool-calling__math\n", + "smolagents/answers anthropic__claude-3-5-sonnet-latest__tool-calling__simpleqa\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_121064/3316273531.py:86: UserWarning: Answer lists have different lengths, returning False.\n", + " warnings.warn(\"Answer lists have different lengths, returning False.\", UserWarning)\n", + "Processing tasks: 14%|β–ˆβ– | 18/129 [00:17<01:06, 1.68it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers anthropic__claude-3-5-sonnet-latest__vanilla__gaia\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processing tasks: 15%|β–ˆβ– | 19/129 [00:17<00:56, 1.93it/s]/tmp/ipykernel_121064/3316273531.py:86: UserWarning: Answer lists have different lengths, returning False.\n", + " warnings.warn(\"Answer lists have different lengths, returning False.\", UserWarning)\n", + "Processing tasks: 16%|β–ˆβ–‹ | 21/129 [00:17<00:33, 3.18it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers anthropic__claude-3-5-sonnet-latest__vanilla__math\n", + "smolagents/answers anthropic__claude-3-5-sonnet-latest__vanilla__simpleqa\n", + "smolagents/answers anthropic__claude-3-7-sonnet-20250219__code__gaia\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_121064/3316273531.py:86: UserWarning: Answer lists have different lengths, returning False.\n", + " warnings.warn(\"Answer lists have different lengths, returning False.\", UserWarning)\n", + "Processing tasks: 17%|β–ˆβ–‹ | 22/129 [00:18<00:33, 3.22it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers anthropic__claude-3-7-sonnet-20250219__code__math\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_121064/3316273531.py:86: UserWarning: Answer lists have different lengths, returning False.\n", + " warnings.warn(\"Answer lists have different lengths, returning False.\", UserWarning)\n", + "Processing tasks: 19%|β–ˆβ–Š | 24/129 [00:18<00:31, 3.37it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers anthropic__claude-3-7-sonnet-20250219__code__simpleqa\n", + "smolagents/answers anthropic__claude-3-7-sonnet__code__gaia\n", + "smolagents/answers anthropic__claude-3-7-sonnet__code__math\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processing tasks: 20%|β–ˆβ–ˆ | 26/129 [00:19<00:30, 3.35it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers anthropic__claude-3-7-sonnet__code__simpleqa\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_121064/3316273531.py:86: UserWarning: Answer lists have different lengths, returning False.\n", + " warnings.warn(\"Answer lists have different lengths, returning False.\", UserWarning)\n", + "Processing tasks: 21%|β–ˆβ–ˆ | 27/129 [00:22<01:31, 1.11it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers anthropic__claude-opus-4-20250514__code__gaia\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processing tasks: 22%|β–ˆβ–ˆβ– | 28/129 [00:23<01:30, 1.12it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answerssmolagents/answers anthropic__claude-opus-4-20250514__code__simpleqa\n", + " anthropic__claude-opus-4-20250514__code__math\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processing tasks: 23%|β–ˆβ–ˆβ–Ž | 30/129 [00:23<01:03, 1.55it/s]/tmp/ipykernel_121064/3316273531.py:86: UserWarning: Answer lists have different lengths, returning False.\n", + " warnings.warn(\"Answer lists have different lengths, returning False.\", UserWarning)\n", + "Processing tasks: 24%|β–ˆβ–ˆβ– | 31/129 [00:23<00:48, 2.00it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers anthropic__claude-sonnet-4-20250514__code__gaia\n", + "smolagents/answers anthropic__claude-sonnet-4-20250514__code__math\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_121064/3316273531.py:86: UserWarning: Answer lists have different lengths, returning False.\n", + " warnings.warn(\"Answer lists have different lengths, returning False.\", UserWarning)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers anthropic__claude-sonnet-4-20250514__code__simpleqa\n", + "smolagents/answers deepseek-ai__DeepSeek-R1-Distill-Qwen-32B__code__gaia\n", + "smolagents/answers deepseek-ai__DeepSeek-R1-Distill-Qwen-32B__code__math\n", + "smolagents/answers deepseek-ai__DeepSeek-R1-Distill-Qwen-32B__code__simpleqa\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processing tasks: 27%|β–ˆβ–ˆβ–‹ | 35/129 [00:27<01:10, 1.34it/s]/tmp/ipykernel_121064/3316273531.py:86: UserWarning: Answer lists have different lengths, returning False.\n", + " warnings.warn(\"Answer lists have different lengths, returning False.\", UserWarning)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smolagents/answers deepseek-ai__DeepSeek-R1-Distill-Qwen-32B__tool-calling__gaia\n", + "smolagents/answers deepseek-ai__DeepSeek-R1-Distill-Qwen-32B__tool-calling__math\n", + "smolagents/answers deepseek-ai__DeepSeek-R1-Distill-Qwen-32B__tool-calling__simpleqa\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "Generating test split: 0%| | 0/50 [00:00\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
model_idagent_action_typesourceacc
0Qwen/Qwen2.5-Coder-32B-InstructcodeMATH84.0
1Qwen/Qwen2.5-7B-InstructvanillaMATH38.0
2Qwen/Qwen2.5-Coder-32B-InstructcodeSimpleQA74.0
3Qwen/QwQ-32BcodeSimpleQA74.0
4Qwen/Qwen2.5-72B-InstructvanillaSimpleQA10.0
\n", + "" + ], + "text/plain": [ + " model_id agent_action_type source acc\n", + "0 Qwen/Qwen2.5-Coder-32B-Instruct code MATH 84.0\n", + "1 Qwen/Qwen2.5-7B-Instruct vanilla MATH 38.0\n", + "2 Qwen/Qwen2.5-Coder-32B-Instruct code SimpleQA 74.0\n", + "3 Qwen/QwQ-32B code SimpleQA 74.0\n", + "4 Qwen/Qwen2.5-72B-Instruct vanilla SimpleQA 10.0" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "import datasets\n", "import pandas as pd\n", @@ -253,7 +1284,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -274,18 +1305,800 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agent_action_typemodel_idsourcecodetool-callingtool_callingvanilla
0Qwen/QwQ-32BGAIA37.50NaNNaNNaN
1Qwen/QwQ-32BMATH94.00NaNNaNNaN
2Qwen/QwQ-32BSimpleQA74.00NaNNaNNaN
3Qwen/Qwen2.5-72B-InstructGAIA28.12NaNNaN6.25
4Qwen/Qwen2.5-72B-InstructMATH76.00NaNNaN30.00
5Qwen/Qwen2.5-72B-InstructSimpleQA88.00NaNNaN10.00
6Qwen/Qwen2.5-7B-InstructGAIANaNNaNNaN3.12
7Qwen/Qwen2.5-7B-InstructMATHNaNNaNNaN38.00
8Qwen/Qwen2.5-7B-InstructSimpleQANaNNaNNaN4.00
9Qwen/Qwen2.5-Coder-32B-InstructGAIA21.8818.75NaN3.12
10Qwen/Qwen2.5-Coder-32B-InstructMATH84.0044.00NaN62.00
11Qwen/Qwen2.5-Coder-32B-InstructSimpleQA74.0058.00NaN8.00
12Qwen/Qwen3-235B-A22BGAIA15.62NaNNaNNaN
13Qwen/Qwen3-235B-A22BMATH58.00NaNNaNNaN
14Qwen/Qwen3-235B-A22BSimpleQA76.00NaNNaNNaN
15Qwen/Qwen3-32BGAIA25.00NaNNaNNaN
16Qwen/Qwen3-32BMATH90.00NaNNaNNaN
17Qwen/Qwen3-32BSimpleQA78.00NaNNaNNaN
18anthropic/claude-3-5-sonnet-latestGAIA56.25NaN0.003.12
19anthropic/claude-3-5-sonnet-latestMATH86.00NaN54.0050.00
20anthropic/claude-3-5-sonnet-latestSimpleQA82.00NaN0.0034.00
21anthropic/claude-3-7-sonnet-20250219GAIA50.00NaNNaNNaN
22anthropic/claude-3-7-sonnet-20250219MATH96.00NaNNaNNaN
23anthropic/claude-3-7-sonnet-20250219SimpleQA86.00NaNNaNNaN
24anthropic/claude-opus-4-20250514GAIA59.38NaNNaNNaN
25anthropic/claude-opus-4-20250514MATH98.00NaNNaNNaN
26anthropic/claude-opus-4-20250514SimpleQA94.00NaNNaNNaN
27anthropic/claude-sonnet-4-20250514GAIA56.25NaNNaNNaN
28anthropic/claude-sonnet-4-20250514MATH100.00NaNNaNNaN
29anthropic/claude-sonnet-4-20250514SimpleQA82.00NaNNaNNaN
30deepseek-ai/DeepSeek-R1GAIA50.00NaNNaNNaN
31deepseek-ai/DeepSeek-R1MATH94.00NaNNaNNaN
32deepseek-ai/DeepSeek-R1SimpleQA74.00NaNNaNNaN
33deepseek-ai/DeepSeek-R1-Distill-Qwen-32BGAIA31.2512.50NaNNaN
34deepseek-ai/DeepSeek-R1-Distill-Qwen-32BMATH92.0044.00NaNNaN
35deepseek-ai/DeepSeek-R1-Distill-Qwen-32BSimpleQA40.0048.00NaNNaN
36gpt-4.5-previewGAIA56.25NaNNaN15.62
37gpt-4.5-previewMATH92.00NaNNaN32.00
38gpt-4.5-previewSimpleQA88.00NaNNaN58.00
39gpt-4oGAIA34.38NaN15.623.12
40gpt-4oMATH78.00NaN58.0040.00
41gpt-4oSimpleQA80.00NaN86.006.00
42meta-llama/Llama-3.1-70B-InstructGAIA15.6218.75NaNNaN
43meta-llama/Llama-3.1-70B-InstructMATH70.0016.00NaNNaN
44meta-llama/Llama-3.1-70B-InstructSimpleQA64.0018.00NaNNaN
45meta-llama/Llama-3.1-8B-InstructGAIA0.006.25NaN0.00
46meta-llama/Llama-3.1-8B-InstructMATH14.0012.00NaN18.00
47meta-llama/Llama-3.1-8B-InstructSimpleQA2.0012.00NaN6.00
48meta-llama/Llama-3.2-3B-InstructGAIA3.12NaNNaN0.00
49meta-llama/Llama-3.2-3B-InstructMATH40.00NaNNaN12.00
50meta-llama/Llama-3.2-3B-InstructSimpleQA20.00NaNNaN0.00
51meta-llama/Llama-3.3-70B-InstructGAIA31.25NaNNaN3.12
52meta-llama/Llama-3.3-70B-InstructMATH72.00NaNNaN40.00
53meta-llama/Llama-3.3-70B-InstructSimpleQA78.00NaNNaN12.00
54meta-llama/Llama-4-Maverick-17B-128E-InstructGAIA46.88NaNNaNNaN
55meta-llama/Llama-4-Maverick-17B-128E-InstructMATH88.00NaNNaNNaN
56meta-llama/Llama-4-Maverick-17B-128E-InstructSimpleQA88.00NaNNaNNaN
57meta-llama/Llama-4-Scout-17B-16E-InstructGAIA25.00NaNNaNNaN
58meta-llama/Llama-4-Scout-17B-16E-InstructMATH84.00NaNNaNNaN
59meta-llama/Llama-4-Scout-17B-16E-InstructSimpleQA90.00NaNNaNNaN
60o1GAIA50.00NaNNaN18.75
61o1MATH96.00NaNNaN80.00
62o1SimpleQA84.00NaNNaN34.00
63o3-miniGAIA46.88NaNNaNNaN
64o3-miniMATH98.00NaNNaNNaN
65o3-miniSimpleQA80.00NaNNaNNaN
\n", + "
" + ], + "text/plain": [ + "agent_action_type model_id source \\\n", + "0 Qwen/QwQ-32B GAIA \n", + "1 Qwen/QwQ-32B MATH \n", + "2 Qwen/QwQ-32B SimpleQA \n", + "3 Qwen/Qwen2.5-72B-Instruct GAIA \n", + "4 Qwen/Qwen2.5-72B-Instruct MATH \n", + "5 Qwen/Qwen2.5-72B-Instruct SimpleQA \n", + "6 Qwen/Qwen2.5-7B-Instruct GAIA \n", + "7 Qwen/Qwen2.5-7B-Instruct MATH \n", + "8 Qwen/Qwen2.5-7B-Instruct SimpleQA \n", + "9 Qwen/Qwen2.5-Coder-32B-Instruct GAIA \n", + "10 Qwen/Qwen2.5-Coder-32B-Instruct MATH \n", + "11 Qwen/Qwen2.5-Coder-32B-Instruct SimpleQA \n", + "12 Qwen/Qwen3-235B-A22B GAIA \n", + "13 Qwen/Qwen3-235B-A22B MATH \n", + "14 Qwen/Qwen3-235B-A22B SimpleQA \n", + "15 Qwen/Qwen3-32B GAIA \n", + "16 Qwen/Qwen3-32B MATH \n", + "17 Qwen/Qwen3-32B SimpleQA \n", + "18 anthropic/claude-3-5-sonnet-latest GAIA \n", + "19 anthropic/claude-3-5-sonnet-latest MATH \n", + "20 anthropic/claude-3-5-sonnet-latest SimpleQA \n", + "21 anthropic/claude-3-7-sonnet-20250219 GAIA \n", + "22 anthropic/claude-3-7-sonnet-20250219 MATH \n", + "23 anthropic/claude-3-7-sonnet-20250219 SimpleQA \n", + "24 anthropic/claude-opus-4-20250514 GAIA \n", + "25 anthropic/claude-opus-4-20250514 MATH \n", + "26 anthropic/claude-opus-4-20250514 SimpleQA \n", + "27 anthropic/claude-sonnet-4-20250514 GAIA \n", + "28 anthropic/claude-sonnet-4-20250514 MATH \n", + "29 anthropic/claude-sonnet-4-20250514 SimpleQA \n", + "30 deepseek-ai/DeepSeek-R1 GAIA \n", + "31 deepseek-ai/DeepSeek-R1 MATH \n", + "32 deepseek-ai/DeepSeek-R1 SimpleQA \n", + "33 deepseek-ai/DeepSeek-R1-Distill-Qwen-32B GAIA \n", + "34 deepseek-ai/DeepSeek-R1-Distill-Qwen-32B MATH \n", + "35 deepseek-ai/DeepSeek-R1-Distill-Qwen-32B SimpleQA \n", + "36 gpt-4.5-preview GAIA \n", + "37 gpt-4.5-preview MATH \n", + "38 gpt-4.5-preview SimpleQA \n", + "39 gpt-4o GAIA \n", + "40 gpt-4o MATH \n", + "41 gpt-4o SimpleQA \n", + "42 meta-llama/Llama-3.1-70B-Instruct GAIA \n", + "43 meta-llama/Llama-3.1-70B-Instruct MATH \n", + "44 meta-llama/Llama-3.1-70B-Instruct SimpleQA \n", + "45 meta-llama/Llama-3.1-8B-Instruct GAIA \n", + "46 meta-llama/Llama-3.1-8B-Instruct MATH \n", + "47 meta-llama/Llama-3.1-8B-Instruct SimpleQA \n", + "48 meta-llama/Llama-3.2-3B-Instruct GAIA \n", + "49 meta-llama/Llama-3.2-3B-Instruct MATH \n", + "50 meta-llama/Llama-3.2-3B-Instruct SimpleQA \n", + "51 meta-llama/Llama-3.3-70B-Instruct GAIA \n", + "52 meta-llama/Llama-3.3-70B-Instruct MATH \n", + "53 meta-llama/Llama-3.3-70B-Instruct SimpleQA \n", + "54 meta-llama/Llama-4-Maverick-17B-128E-Instruct GAIA \n", + "55 meta-llama/Llama-4-Maverick-17B-128E-Instruct MATH \n", + "56 meta-llama/Llama-4-Maverick-17B-128E-Instruct SimpleQA \n", + "57 meta-llama/Llama-4-Scout-17B-16E-Instruct GAIA \n", + "58 meta-llama/Llama-4-Scout-17B-16E-Instruct MATH \n", + "59 meta-llama/Llama-4-Scout-17B-16E-Instruct SimpleQA \n", + "60 o1 GAIA \n", + "61 o1 MATH \n", + "62 o1 SimpleQA \n", + "63 o3-mini GAIA \n", + "64 o3-mini MATH \n", + "65 o3-mini SimpleQA \n", + "\n", + "agent_action_type code tool-calling tool_calling vanilla \n", + "0 37.50 NaN NaN NaN \n", + "1 94.00 NaN NaN NaN \n", + "2 74.00 NaN NaN NaN \n", + "3 28.12 NaN NaN 6.25 \n", + "4 76.00 NaN NaN 30.00 \n", + "5 88.00 NaN NaN 10.00 \n", + "6 NaN NaN NaN 3.12 \n", + "7 NaN NaN NaN 38.00 \n", + "8 NaN NaN NaN 4.00 \n", + "9 21.88 18.75 NaN 3.12 \n", + "10 84.00 44.00 NaN 62.00 \n", + "11 74.00 58.00 NaN 8.00 \n", + "12 15.62 NaN NaN NaN \n", + "13 58.00 NaN NaN NaN \n", + "14 76.00 NaN NaN NaN \n", + "15 25.00 NaN NaN NaN \n", + "16 90.00 NaN NaN NaN \n", + "17 78.00 NaN NaN NaN \n", + "18 56.25 NaN 0.00 3.12 \n", + "19 86.00 NaN 54.00 50.00 \n", + "20 82.00 NaN 0.00 34.00 \n", + "21 50.00 NaN NaN NaN \n", + "22 96.00 NaN NaN NaN \n", + "23 86.00 NaN NaN NaN \n", + "24 59.38 NaN NaN NaN \n", + "25 98.00 NaN NaN NaN \n", + "26 94.00 NaN NaN NaN \n", + "27 56.25 NaN NaN NaN \n", + "28 100.00 NaN NaN NaN \n", + "29 82.00 NaN NaN NaN \n", + "30 50.00 NaN NaN NaN \n", + "31 94.00 NaN NaN NaN \n", + "32 74.00 NaN NaN NaN \n", + "33 31.25 12.50 NaN NaN \n", + "34 92.00 44.00 NaN NaN \n", + "35 40.00 48.00 NaN NaN \n", + "36 56.25 NaN NaN 15.62 \n", + "37 92.00 NaN NaN 32.00 \n", + "38 88.00 NaN NaN 58.00 \n", + "39 34.38 NaN 15.62 3.12 \n", + "40 78.00 NaN 58.00 40.00 \n", + "41 80.00 NaN 86.00 6.00 \n", + "42 15.62 18.75 NaN NaN \n", + "43 70.00 16.00 NaN NaN \n", + "44 64.00 18.00 NaN NaN \n", + "45 0.00 6.25 NaN 0.00 \n", + "46 14.00 12.00 NaN 18.00 \n", + "47 2.00 12.00 NaN 6.00 \n", + "48 3.12 NaN NaN 0.00 \n", + "49 40.00 NaN NaN 12.00 \n", + "50 20.00 NaN NaN 0.00 \n", + "51 31.25 NaN NaN 3.12 \n", + "52 72.00 NaN NaN 40.00 \n", + "53 78.00 NaN NaN 12.00 \n", + "54 46.88 NaN NaN NaN \n", + "55 88.00 NaN NaN NaN \n", + "56 88.00 NaN NaN NaN \n", + "57 25.00 NaN NaN NaN \n", + "58 84.00 NaN NaN NaN \n", + "59 90.00 NaN NaN NaN \n", + "60 50.00 NaN NaN 18.75 \n", + "61 96.00 NaN NaN 80.00 \n", + "62 84.00 NaN NaN 34.00 \n", + "63 46.88 NaN NaN NaN \n", + "64 98.00 NaN NaN NaN \n", + "65 80.00 NaN NaN NaN " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ + "pd.set_option(\"display.max_rows\", None)\n", "display(pivot_df)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAADuQAAAXRCAYAAADcrhoyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3XmYlnW9P/A3DMuwo4LigoAoaEfAcQERUwM9KG7gbqm5ZJpbVuaSnVNulaXH41ZRZh60AGUVyQW0cMkNxXDDfUUBUZGdYZnfH/yYHIGHQR/iMV6v65rrmvnc3/u+39+H6fIqfPetU1VVVRUAAAAAAAAAAAAAAAAAAGCV6q7vAAAAAAAAAAAAAAAAAAAAUMoUcgEAAAAAAAAAAAAAAAAAoACFXAAAAAAAAAAAAAAAAAAAKEAhFwAAAAAAAAAAAAAAAAAAClDIBQAAAAAAAAAAAAAAAACAAhRyAQAAAAAAAAAAAAAAAACgAIVcAAAAAAAAAAAAAAAAAAAoQCEXAAAAAAAAAAAAAAAAAAAKUMgFAAAAAAAAAAAAAAAAAIACFHIBAAAAAADWoQsvvDCdO3fORRddtNo1Z511Vjp37pzrr7++KO/8PM/q3LlzrrrqqtVeHzFiRDp37lzja8cdd8x//ud/5tJLL82HH374RWMnSaqqqnLhhRemoqIiBx98cFGe+e9swYIFGThwYPr375+KiopUVFTkwAMPzP/8z//ko48+Wt/xiub444/PUUcdtb5jAAAAAAAAALABq7e+AwAAAAAAAPy7a9y4ce6777785Cc/SXl5eY1rs2fPzoQJE9KoUaP1lG7t/OlPf0q7du2SJAsXLsyUKVPyy1/+Mg8++GDGjBnzhffx/PPPZ+TIkTnjjDNyzDHHFCPyv62PP/44J554YmbNmpWzzjoru+22W6qqqvLUU0/l+uuvz5gxYzJo0KC0bdt2fUf9wopVVgcAAAAAAACAz8sJuQAAAAAAAOvYDjvskLKystx///0rXbv33nuz9dZbZ+ONN14PydbeRhttlNatW6d169Zp27Zt9ttvv1x++eV55513ct99933u51ZWViZZXjJNkt133z2bbbbZ537e4sWLP/e9XxaXXHJJpk+fnqFDh+bII49M+/bt06FDhxxxxBEZOnRo5syZkxtuuGF9x/xCVvxetGzZMi1btly/YQAAAAAAAADYoCnkAgAAAAAArGNlZWXZZ599Mnr06JWu3XnnnenTp89K88rKylx99dXp3bt3dtxxx/Tq1SsXXXRRPvrooxrrfvOb32TPPfdM165dc8wxx+T5559f6VkffvhhLrroovTs2TM77rhjDjzwwAwbNqxo++vcuXOS5P3336+ePf/88znllFNSUVGRbt265bjjjsvTTz9dff3xxx9P586dc++99+aggw7Knnvumeuvvz7f+ta3kiQnnHBCevfuXevP4sILL8yhhx6aIUOGpHv37vnlL3+ZxYsXp3Pnzrntttty6aWXZpdddsmuu+6an//856msrMzll1+e7t27p3v37vnxj39co8T71ltv5eyzz84ee+yRLl26ZL/99stvfvObLFu2rHrNMccckzPOOCPjxo3LAQcckC5duuSAAw5YqXj99NNP5xvf+Ea6deuWnj175vzzz8/MmTOrr8+fPz+XX3559tprr+y4447Zb7/98rvf/S5VVVWr/cynTp2ae+65J6ecckratGmz0vU2bdpk1KhR+fnPf149mz59en7wgx9k9913z4477ph99903N9xwQ5YuXVq95qtf/Wquuuqq3HDDDenZs2cqKipy3nnnZdGiRbnhhhvSq1ev7Lzzzjn77LMzZ86cJKn+nG+66ab86le/Ss+ePdO1a9eccMIJefPNN6ufvXTp0lx33XXp27dvunbtml69euWcc87Ju+++W71mxIgR6dy5cx588MH06dMnRx11VJLk+OOPr/4+ScaNG5fDDz88O++8c3beeeccc8wx+fvf/17jMxgxYkQOPvjgdOnSJbvssktOOeWUGv/5uP3229O5c+e88sorOe2007LTTjulV69eueSSS7JkyZLVfvYAAAAAAAAAbJgUcgEAAAAAAP4FDjzwwDzyyCP58MMPq2fvv/9+nnzyyfTr12+l9T/+8Y/zpz/9KWeddVbGjh2bK664Io8++mhOPfXU6qLm8OHD87//+7856qijMnr06HznO9/JpZdeWuM5lZWVOfHEE/PYY4/lF7/4RcaMGZODDz44F198cUaNGlWUvb399ttJUl0Mfeutt3LcccdlyZIlGTRoUG6//fa0atUqJ510Ul5//fUa9w4cODDnnntuRo4cmZNPPjlXX311kuT666+vLg3X5rNIklmzZmX8+PG57bbb8p3vfCf169dPktx2221p165dRowYkaOOOiq33HJLTjrppGyxxRa54447ctppp+WOO+7I2LFjkyRVVVX59re/nalTp2bgwIG55557cs455+TGG2/Mn/70p+r31a9fP6+++mpuv/32XH311Rk1alRatWqV888/P3Pnzk2SvPbaaznxxBPToUOHDB8+PL/5zW/ywgsv5PTTT69+ztlnn50xY8bkwgsvzNixY3PKKafk+uuvz4033rjaz/zJJ59MVVVV9t5779Wu2WqrrVK37vK/Dly0aFFOOOGEPPfcc7n66qtz11135eSTT87AgQPzq1/9qsaexo0bl0WLFmXw4MH57ne/mzFjxuSkk07KvHnzctttt+WSSy7Jfffdl0GDBiVJ6tWrV/0516tXL0OHDs3AgQPz9ttv56yzzqr+Mxo4cGAGDhyY7373u7nnnnvym9/8Ju+++27OOeeclbL//ve/z89+9rMMHDhwpWtvvPFGzj333PTt2zejR4/OHXfckS5duuTb3/52dSl82LBhueiii9K7d++MGjUqN998cyorK3PCCSdk+vTpNXL/5Cc/yYABA6o/kz//+c8ZM2bMaj9XAAAAAAAAADZMCrkAAAAAAAD/Ar169UqLFi1qFP3uuuuubLfddtl+++1rrJ0+fXruvPPOnHzyyTnssMPSrl277LPPPvnhD3+Y5557Lk899VSS5YXczp0755xzzkmHDh2y995755RTTqnxrPvvvz8vv/xyLrvssuy9997p0KFDTj/99PTu3XuVZce1UVVVlVdffTWXXXZZNttss+y7775JkltuuSVJcu2116ZLly7p3LlzrrzyyjRt2jT/93//V+MZPXr0yL777pstt9wyTZo0SfPmzZMkLVq0yMYbb1zrzyJJpk2blvPPPz+dOnXKxhtvXD3fcsst881vfjPt2rWrPoG3QYMGOfnkk9OuXbucfPLJady4cV544YXqewYNGpQ//OEP6dKlS7bccsscfPDB2WGHHfLQQw/VyD9t2rRceeWV+cpXvpKOHTvmuOOOy9y5c6uLx4MGDUqLFi1y6aWXZtttt81OO+2Un/70p9lmm23y4YcfZvLkyXn44Yfz/e9/P/369Uu7du1yzDHH5Oijj84tt9ySysrKVX72M2bMSJJsscUWtfqzGjduXN58881cfvnl6dWrV9q3b5+vf/3rGTBgQIYMGbLSe37wgx+kffv2+eY3v5nGjRvnww8/zPnnn58OHTrk4IMPzrbbblv9edWpUydJstFGG+V73/tett566/Ts2TPf/e5388orr+TFF19Mkhx33HG577770q9fv2yxxRbp2rVrjjjiiDz//PMrnfzct2/f9OjRI5ttttlKe3nxxRezZMmSHHbYYWnbtm06duyYiy66KLfeemv178/vf//79OjRI9/73vfSsWPHdOvWLVdddVUWLFiw0unQ/fr1y/7775+tttoqJ598cpo0aZLJkyfX6nMFAAAAAAAAYMOhkAsAAAAAAPAvUK9evfTr1y933nln9WzFabWf9dxzz6Wqqirdu3evMd9pp52SpLrg+Morr6RLly411uy88841fv7HP/6ROnXqrPSsnj175vXXX8+sWbPWah+HH354KioqUlFRkS5duuTQQw/NJptskj/96U9p2rRp9Ts7d+6cli1bVt/XsGHDVFRU5Omnn67xvB133LHg+2r7WSTLS7adOnVa6RmdO3eu/n6jjTZKkuywww7Vszp16qRly5aZM2dO9c9Tp07NBRdckD333LN6v88999xKn9fWW29do/zbokWLJMknn3ySJHn22Wezww47VJ9UmyS77rprfvnLX2aTTTbJP/7xjyTLi8mf1rNnz8yZMyevvPLKKj+XFae7Llu2bJXXP+vZZ59NWVlZdtlllxrzioqKLFiwIG+88Ub17NMF8RWfzfbbb19dvE2Wf46zZ8+u8azP/u595StfSbL8lOAV/vjHP6Zv377ZddddU1FRkZ///OdJko8//rjGvYV+L3bZZZdssskmOeGEE3LLLbdkypQpqVu3bioqKtKkSZPMnTs3b7755kq/M5tttlnatGmTKVOm1Jh369atxn5btGhR/ecHAAAAAAAAACvUW98BAAAAAAAANhSHHHJIbr311rz66qtZtmxZXn755Rx00EErrZs7d26Sf5Y7V1jx84rrc+fOrT4RdIXP/jxnzpxUVVWtVPhcsmRJkmTmzJk1irNrcsMNN6Rt27ZJkrKysrRp06a6HPrpd06dOjUVFRU15pWVlSvla9asWcH31fazSFbe+wqNGjWq/n5FqfTTsxXzqqqqJMtPvT311FPTsWPHXHnlldl8881TVlaW8847b6VnN27ceKXnJKl+1pw5c7LVVlutdn8rSsADBgyoMV9RtJ05c+Yq71txcuzbb7+9xlJzsvxzatasWY1icPLPz+zTn+OqPptVzT5rRSF7hRWfzcKFC5MkF198cR5++OFccMEFqaioSHl5ee67775cddVVKz2r0O/FZpttljvuuCM333xzbrnllvz85z/PlltumTPPPDOHH374an9nVsw+vddP5/z03lb8+QEAAAAAAADACgq5AAAAAAAA/yJdu3ZNhw4dctddd2Xp0qXZZZddssUWW6y0bkUZ8bOnsa44RXRFibJRo0bVZccVPntP8+bN07Bhw4waNWqVmTbffPO12sPmm2+edu3aFVzTvHnztGnTJpdffvlK1z5bCF2T2n4WxfTQQw9l7ty5+cUvfpFtttmmej5//vw0adJkrZ7VrFmzlU5//bQVpdFbbrlllcXo1q1br/K+nj17pqysLOPGjVttIffhhx9O8+bN07Vr1zRr1iyzZ8/O0qVLU1ZWVr1mxedajM/xsyfmzps3L0nSpEmTVFZW5v77788pp5ySY445pnrNqoq9tbHlllvmv/7rv/Jf//VfefXVV3PbbbflRz/6Udq2bVt9Mu+qTn+eNWtWdaEcAAAAAAAAANbG2v0bDwAAAAAAAHwhhxxySB588MH89a9/zcEHH7zKNV26dEndunXzxBNP1JhPnDix+nqSdOzYMS+++GKNNU8++WSNn3faaacsWrQoCxYsSLt27aq/ysvL07x58zRo0KBYW6vxzjfeeKO6vLviq6qqKptuuulaPau2n0UxrThBdeONN66eTZ48Oa+++upan5zapUuXvPDCC1mwYEH17Jlnnsmxxx6bd955J926dUuy/CTcT39WzZs3T6NGjVY6vXWFjTfeOAcffHAGDRqU1157baXr7733Xi644IL87ne/S5J069Yty5Ytq/7cVpg4cWKaNm2a9u3br9W+VmXSpEk1fn7hhReSJNttt13mz5+fpUuX1vhMlyxZkjFjxqz1e1588cU8+uij1T9vu+22+elPf5qWLVvm+eefT9OmTbPtttuu9DvzzjvvZNq0aevkdwYAAAAAAACAf38KuQAAAAAAAP9ChxxySKZMmZI333wz+++//yrXtG7dOocddlhuvvnmjBo1Km+99VbGjx+fq6++Oj169EjXrl2TJIceemgmTZqU3/72t3nzzTczYcKE/PGPf0y9evWqn/W1r30tnTp1yg9/+MM8+uijmTp1aiZMmJDjjjsuP/3pT9fJHk844YTMmzcv5513Xp5//vm88847uf3229O/f/8MGzZsrZ5V28+imHbaaackycCBA/POO+/kvvvuy6WXXpo+ffrknXfeyeuvv56lS5fW6lnHH398Kisrc+GFF+all17K5MmTc+mll6aysjJbbbVVdtxxx+y555657LLLMn78+Lz77rt54okn8q1vfStnnnlmwQLwj370o2yzzTb5+te/nkGDBuX111/Pm2++mTvuuCPHHHNMNttss1xyySVJkj59+qRjx4757//+7zz66KN54403csstt2TMmDE56aSTUr9+/S/8uc2YMSO//OUv89prr+XRRx/Nddddlx133DHbbbddWrZsmQ4dOmTEiBF56aWX8txzz+Wss87KzjvvnGR5kXxFEXpNnnnmmZxxxhkZPnx43nnnnbz11lv54x//mDlz5mSXXXZJkpx66ql54okncs011+S1117LU089lfPOOy8bbbRRDj/88C+8VwAAAAAAAAA2PPXWvAQAAAAAAIBi2WqrrVJRUZHmzZunZcuWq13305/+NJtsskmuvfbazJgxIxtttFH222+//OAHP6he8/Wvfz0ffPBBBg0alBtvvDE77LBDfvrTn+akk07KkiVLkiQNGjTILbfckquuuirf//73M2fOnGy66abp27dvvvvd766TPbZr1y633nprrrnmmhx33HFZtmxZ2rVrlwsvvDDHHHPMWj+vNp9FMVVUVOSHP/xh/u///i+DBw9Ot27dcuWVV+bjjz/OM888kxNPPDFjx46t1bO23Xbb/PGPf8zVV1+dI488Mk2aNMlXv/rVnH/++alTp06S5Prrr88111yTSy+9NB999FE22mij7LPPPvnBD35QvWZVWrRokT//+c/505/+lFGjRuW6667LsmXLsvXWW+eEE07Icccdl/Ly8iT//D248sorc+6552bevHnZcsstc9555+Wb3/zmF//Qkhx55JFZtGhRvvGNb2T+/PnZddddc9lll1Vfv+qqq/KTn/wkRx55ZNq0aZNTTz01/fv3z8svv5xf/vKXadiwYa3ec+yxx2bhwoW5+eabc9lll6WsrCzbbrttrrvuuuqCdv/+/ZMkf/jDH/KHP/wh5eXl6d69e37+85/XOKUXAAAAAAAAAGqrTlWh/1ttAAAAAAAAgDXo3LlzzjrrrJx99tnrOwoAAAAAAAAArBN113cAAAAAAAAA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LJxVlNcdfvjhGTp0aCZMmJCZM2emVatWqaqqyqhRo5IkAwYMWOMzqqqqMnTo0CTJjBkz8pWvfKXg+gULFmT06NE57rjjvnB+is8JuQAAAAAAAAAAAAAAAABA6apalrw4unAZ981HkrcfS8pbFuWV3bp1y3bbbZclS5ZUl3Afe+yxvPfee2nTpk322GOPNT7j4Ycfzttvv71W711R4KX0KOQCAAAAAAAAAAAAAAAAAKVr7oxk/oeFy7hvPJhsvXtS3qJorz3ssMOSJHfeeWeSVBdz+/fvn7p111zPHDJkSJJkl112yeTJkwt+rVj78ssv56mnniraHigehVwAAAAAAAAAAAAAAAAAoHQtW5x8pX/hMm6HvZK2uxX1tYceemjq16+fl156KW+++WbGjx+fJBkwYMAa7502bVr++te/Vq9v2LBhwa+Kiop07NgxiVNyS5VCLgAAAAAAAAAAAAAAAABQuppsmjRrs/L802Xc9r2K/tpNNtkke++9d5Lkf//3fzN37txUVFSkffv2a7z39ttvz9KlS9OwYcPsv//+tXrfoYcemiS555578sknn3zu3Kwb672Q+9BDD2WPPfbI9773vZWujR07Nn379k2XLl1y0EEH5ZFHHqm+tmzZslxzzTXp1atXunXrlhNPPDHvvPPOvzI6AAAAAAAAAAAAAAAAALCu1Wu48mwdl3FXOPzww5Mk9957b42fC1myZEnuuOOOJMm+++6bZs2a1epdhxxySOrWrZtFixZl5MiRnzMx68p6LeT+/ve/z+WXX5527dqtdO25557LBRdckO9+97t58skn881vfjNnnnlmpk2bliQZNGhQhg8fnj/84Q955JFH0rZt25x55pmpqqr6V28DAAAAAAAAAAAAAAAAAFiX5s1M5kxb/vXCmGTKX5LNdkw26fjP+byZRX/tXnvtldatW2fZsmVp1KhRDjjggDXec//992fGjBlJ/nnqbW1svvnm6d69e5Jk6NChny8w60y99fnyhg0bZtiwYbniiiuyaNGiGteGDx+evfbaK/369UuSHHnkkbnjjjsyevTonHbaabnjjjvyrW99K9tvv32S5IILLsjuu++eZ555JhUVFf/yvQAAAAAAAAAAAAAAAAAAn3HahC92f/1GSVm95MUxy39e+EmycFZS3jKZ/tzyr0+rU2f5PUVSr169HHroobnpppuy7777pmnTpmu8Z/DgwUmS1q1bZ88991yr9/Xv3z+PPfZYXn/99Tz++OPp0aPH58pN8dWpKoEjZS+88MIsWrQo11xzTfXs6KOPzl577ZUzzzyzevbTn/40H374Ya666qp069Ytt912W3bdddfq6/369cuxxx6b448/vsbz+/TpkyQZP358jXmdOnVWeaLuupyvj3eu63kpZSnWvJSyFGteSlmKNS+lLMWal1KWYs1LKUux5qWUpVjzUspSrHkpZSnWvJSyFGteSlmKNS+lLMWal1KWYs1LKUux5qWUpVjzUspSrHkpZSnWvJSyFGteSlmKNS+lLMWal1KWYs1LKUux5qWUpVjzUspSrHkpZSnWvJSyFGteSlmKNS+lLMWal1KWYs1LKUux5qWUpVjzUspSrHkpZSnWvJSyFGteSlmKNS+lLMWal1KWYs1LKUux5qWUpVjzUspSrHkpZSnWvJSyFGteSlmKNS+lLMWal1KWYs1LKUux5qWUpVjzUspSrHkpZSnWvJSyFGteSlmKNS+lLMWal1KWYs1LKUux5qWUpVjzUspSrHkpZSnWvJSyFGteSlmKNS+lLMWal1KWYs1LKUux5sV+dp06dVa6BmR5CXfxgtqtrd8oKW+xbvOwQVqvJ+QW8vHHH6dly5Y1Zi1atMgrr7ySWbNmpaqqKi1atFjp+kcffbTK51VVVeWTTz6p/rlBgwZp3LhxFixYkMrKyup5eXl5ysvLM2/evCxZsqR63rhx4zRo0CBz587N0qVLq+dNmjRJ/fr1M3v27Br/MGzWrFnq1q1b450rMi5btixz5sypntWpUyctWrTIkiVLMm/evOp5WVlZmjVrlsWLF2f+/PnV83r16qVp06ZZtGhRFi5caE/2ZE/2ZE/2ZE/2ZE/2ZE/2ZE/2ZE/2ZE/2ZE/2ZE/2lCRp+ueDUqdOnZTVrZuqqqosW7asxvoV89nH3Pml2VPy7/fnZE/2ZE/2ZE/2ZE/2ZE/2ZE/2ZE/2ZE/2ZE/2ZE/2ZE/2ZE/2ZE/2ZE/2tLo9lZeXB1iF8hZKtqx3JXtCbt++fXPCCSfkG9/4RvXsmmuuyVNPPZWrr746e+21V+66665st9121dePPfbY9OzZM+ecc06N5zsh157Wdl5KWYo1L6UsxZqXUpZizUspS7HmpZSlWPNSylKseSllKda8lLIUa15KWYo1L6UsxZqXUpZizUspS7HmpZSlWPNSylKseSllKda8lLIUa15KWYo1L6UsxZqXUpZizUspS7HmpZSlWPNSylKseSllKda8lLIUa15KWYo1r/Xa3+2zfJ5k5dWfmn/7b/+y7Kubl9LnW6x5KWUp1ryUshRrXkpZijUvpSzFmpdSlmLNSylLseallKVY81LKUqx5KWUp1ryUshRrXkpZijUvpSzFmpdSlmLNSylLseallKVY81LKUqx5KWUp1ryUshRrXkpZijUvpSzFmpdSlmLNSylLseallKVY81LKUqx5KWUp1ryUshRrXkpZijUvpSzFmpdSlmLNSylLseallKVY81LKUqx5KWUp1ryUshRrXuxn16njhFyAUlWyJ+RutNFG+fjjj2vMPv7442y88cbZaKONUrdu3cyaNWul65tssslqn7mqfyCt7h9S63K+Pt65ruellKVY81LKUqx5KWUp1ryUshRrXkpZijUvpSzFmpdSlmLNSylLseallKVY81LKUqx5KWUp1ryUshRrXkpZijUvpSzFmpdSlmLNSylLseallKVY81LKUqx5KWUp1ryUshRrXkpZijUvpSzFmpdSlmLNSylLseallKVY81LKUqz5Wj9jldP/Py+Rz6yUPt9izUspS7HmpZSlWPNSylKseSllKda8lLIUa15KWYo1L6UsxZqXUpZizUspS7HmpZSlWPNSylKseSllKda8lLIUa15KWYo1L6UsxZqXUpZizUspS7HmpZSlWPNSylKseSllKda8lLIUa15KWYo1L6UsxZqXUpZizUspS7HmpZSlWPNSylKseSllKda8lLIUa15KWYo1L6UsxZqXUpZizUspS7HmpZSlWPN1/U4ASkPd9R1gdbp06ZLnn3++xuzZZ59N165d06BBg3Tq1KnG9VmzZuXtt99Oly5d/tVRAQAAAAAAAAAAAAAAAAD4N1ayhdwjjzwyjzzySP7yl79k4cKFufXWW/P222+nf//+SZJjjz02N910U6ZMmZI5c+bk8ssvz4477piuXbuu3+AAAAAAAAAAAAAAAAAAAPxbqbc+X77iNNslS5YkScaPH59k+Um4nTp1ylVXXZWrr746F1xwQTp27JiBAwemVatWSZJjjjkmH3zwQU4++eTMmzcvPXr0yHXXXbd+NgIAAAAAAAAAAAAAAAAAwL+tOlVVVVXrO8S61qdPnyTJ/fffv56TAAAAAADAOjRw79qtO23Cus0BAAAAAAAAAAD/Zuqu7wAAAAAAAAAAAAAAAAAAAFDKFHIBAAAAAAAAAAAAAAAAAKAAhVwAAAAAAAAAAAAAAAAAAChAIRcAAAAAAAAAAAAAAAAAAApQyAUAAAAAAAAAAAAAAAAAgAIUcgEAAAAAAAAAAAAAAAAAvoALL7wwnTt3Tu/evdd3lFrr3bt3OnfunAsvvHB9R/lSqLe+AwAAAAAAAAAAAAAAAAAArG9z587N6NGj8+CDD+aVV17Jxx9/nEWLFqVhw4Zp3bp1tttuu+yzzz456KCD0qhRoxr3br755tl+++2z6aabrqf0pWvixIm57777MnHixEyfPj2ffPJJ6tWrl1atWmX77bfP3nvvnQMPPDCNGzdeq+dWVVWlb9++eeutt5Ik5557br7zne+siy0kSepUVVVVrbOnl4g+ffokSe6///71nAQAAAAAANahgXvXbt1pE9ZtDgAAAAAAAACAL5kHH3ww559/fj7++OMkScOGDdOmTZvUr18/H374YWbNmpUVdcytttoqv/3tb7Pddtutz8hfWO/evTN16tQMGDAgv/jFL4r+/Lfeeis/+tGPMnHixOpZq1at0qJFi8yfPz8zZ87M4sWLkyQtW7bMD3/4wxxxxBG1fv4jjzySk08+OW3atMm0adOy5ZZbZvz48albt27R95I4IRcAAAAAAAAAAAAAAAAA2IC99NJLOeOMM7J48eJ06tQpF1xwQXbffffUq/fPCuaMGTMybNiw3HDDDXn33Xdz6qmnZuzYsWnSpMl6TF66nn766Zx++un55JNP0rJly5x66qk56KCD0qZNm+o18+bNy8MPP5wbbrghL7/8ci6++OJMmTIlP/7xj2v1jsGDBydJTjnllNx2221566238tBDD2XvvWv5f2q/ltZNzRcAAAAAAAAAAAAAAAAA4Evg5ptvzuLFi9OsWbMMGjQoe+65Z40ybpJsuummOeOMM3LJJZckSd5///3qQig1TZ8+PWeddVY++eSTdOzYMaNGjcq3vvWtGmXcJGnSpEn69u2bESNG5KCDDkqS3HrrrfnTn/5Uq3f89a9/Td26ddO3b9/sv//+SZIhQ4YUf0P/n0IuAAAAAAAAAAAAAAAAALDBeuGFF5Ik3bp1y0YbbVRw7WGHHZaTTz45//3f/51evXpVzy+88MJ07tw5vXv3rrH+tttuS+fOnbPffvslSZ566qmceuqp2X333dOtW7f07ds3119/fSorK5MkCxYsyHXXXZf9998/Xbt2za677ppTTjklzzzzzEpZ/vrXv6Zz587p3Llz5s+fn7fffjs/+tGP8rWvfS1dunRJjx49ctJJJ+WBBx74XJ/LBx98kKuuuiqHHHJIdtlll3Tp0iX77LNPzj333Dz66KOrve9//ud/8uGHH6ZRo0b59a9/nc0337zge+rXr59f/OIX+Y//+I8kydVXX52PP/644D233357lixZkp49e2azzTbLoYcemiSZMGFCpk+fvpY7rR2FXAAAAAAAAAAAAAAAAABgg/f++++vcU1ZWVkuuOCCfOMb38gOO+ywxvUNGzas/n7ChAn55je/mUmTJqVFixZZunRp3nzzzdxwww256KKLsmDBghx33HH59a9/nWXLlqVZs2aZM2dOHn744Rx//PF56aWXVvvsF154IUceeWRGjhyZZcuWZYsttsjs2bPz97//Pd/5znfy61//ei0+ieTvf/979t9///z+97/PSy+9lJYtW6Z169aZNm1a7r777px44om55JJLUlVVVeO+6dOn56677kqSHHXUUWnfvn2t3le/fv2cd955SZJ58+Zl2LBhq127ZMmS3H777UmSww8/PEnSsWPHVFRUZOnSpdXXik0hFwAAAAAAAAAAAAAAAADYYHXp0iVJ8tprr+WXv/xl9Wm1xVC37vIa5/z58/PjH/84Z511Vh577LHce++9mThxYvr27ZskGTt2bL7//e9nyZIlueeee3LfffflkUceyaBBg9K4ceNUVlbm5ptvXuWzk+Tiiy/ODjvskHHjxmXChAm5995789e//jUVFRVJkuuvvz4vvvhirTK//fbbOfPMMzN37tzsscceGT9+fO6///488MADmThxYk4++eQkyZ///OfcdtttNe79+9//niVLliRJ9am1tbXHHntks802S5KCp/o+8MADmTFjRlq2bFl98nDyz3LusGHDsnTp0rV6d20o5AIAAAAAAAAAAAAAAAAAJWtO5Zx8MP+DWn3NqZyz1s8/9dRT07hx4yTJH/7wh/znf/5nrrzyyjz00EOZO3fuF8pep06dJMnMmTPTpUuXnH766alXr16SpLy8PD/60Y+SJFVVVXnwwQdzww031DhVtkePHunXr1+S5Kmnnlrls5Pkk08+yY033pitttqqetamTZtce+21qV+/fpYtW5YhQ4bUKvO1116b+fPnp3379vnNb36Ttm3bVl9r2rRpLrjgghx11FFJkl//+tdZvHhx9fV//OMfSZJGjRpl++23r9X7Pm2nnXZKkkyZMmWl03dXWLGPgw8+OA0aNKie9+vXL40bN860adPyt7/9ba3fvSb1iv5EAAAAAAAAAAAAAAAAAIAkR991dK3XDj1o6EqzOZVzMnjK4CxZtmSl+ezK2WneoHmaNWhWPa9Xt16O3f7YGrM16dChQ2666ab88Ic/zNSpU/P+++/n5ptvzs0335y6detmu+22yy677JK99torPXv2THl5ea2f/WnHH3/8SrM2bdqkefPmmT17drp3716j/LpCp06dkiQffPDBap89YMCANGnSZKX5Zpttll133TWPPvpoHnvssTVmXLRoUe67774kyVFHHbXavX7jG9/I7bffno8++ihPPPFEevXqlST58MMPkySbbLJJysrK1vi+VeVNlp8oPGvWrGy00UY1rr/11lv5+9//niQ54ogjalxr0qRJ+vXrl2HDhmXIkCHp06fPWr+/ECfkAgAAAAAAAAAAAAAAAAAlaeGShVmybEn6bN0nR3Y6Mkd2OjKdNuqUunXq5mttv5aTdzy5et5n6z5ZsmxJFi5ZuNbv2WWXXXLPPffkF7/4Rfbcc8/qk1eXLVuWl156KX/+859z+umn56tf/Wp+9atfZd68eWv9jm222WaV8xVF2hXF29VdX7hw9fuqqKhY7bXtttsuSfLOO+/UOM12VaZMmZLKysoa961K586d06hRoyTJ888/Xz2fM2f5CcUrrq2tFScVJ6nO8WlDhgxJVVVV/uM//mOVJ/CuKOk+/PDDmTp16ufKsDpOyAUAAAAAAAAAAAAAAAAAStrG5RundePWmThtYl7++OV8re3XsmubXVdaV7l05RJnbTVo0CADBgzIgAEDsmjRokyePDlPP/10Jk2alEmTJmXWrFmZPXt2brrppjzwwAO55ZZbqk90rY3WrVuvcl637vKzV1d1wu2nrxey6aabrvZaq1atkiRLly7NnDlzsvHGG6927fTp06u/P/XUU9f43qTmyb0NGzZMksydO7dW937Wp4vOn/08Fi1alBEjRiRZ+XTcFSoqKrLtttvm1Vdfze23357vfe97nyvHqijkAgAAAAAAAAAAAAAAAAAlb+K0iXli2hPp3qb7Ksu4H8z/IDMXzCzKuxo2bJjddtstu+22W5LlZdbHH3881113XSZNmpTXX389P/vZz3LttdfW+plrKtbWqVPnc+ddXZk3SfVpv8mqT539tAULFlR/3759+5SXl6/x3c2bN6/+fkVBeebMmVm0aFF1Qbe23nvvvSTLT8pt2rRpjWt33313Zs2alSS55JJLcskllxR81vDhw3P22WenXr3iVGkVcgEAAAAAAAAAAAAAAACAkvaPD/6Rlz9+ebVl3Onzpmfc2+NSv279dfL+srKy7LHHHtltt91y/PHHZ9KkSbnvvvsye/bsGoXU9WXJkiWrvbZo0aLq79dUsP10sffKK6/MTjvttFY5vvKVryRJFi9enGeeeSY9evSo9b1VVVWZPHlykqRr164rXR88ePBaZfnggw9y//33p2/fvmt13+qs+ZxiAAAAAAAAAAAAAAAAAID1ZE7lnEyaMalgGXfM62PSsmHLbNJok8/1jkKF1k+rX79+DjzwwCTJsmXL8sEHH3yu9xXbjBkzVntt5szlpwbXr18/zZo1K/icNm3aVH///vvvr3WOr371q9Un/Q4bNmyt7n3ooYeqP8/evXvXuDZlypQ888wzSZKbbropkydPLvi19957J0mGDBmy1ntYHSfkAgAAAACUioF7137taRPWXQ4AAAAAAACAUuLvUgE2eLMrZ+drbb9WsIy7cfnG6dGmR0a/Nnqtnn3zzTdn+PDhmTZtWu6///60bNlyjfcsXLiw+vvarP9X+HQJ9bOmTJmSJNlmm21SVlZW8DmdOnVKw4YNs2jRojzxxBM54IADVru2srIyDRo0qDHbcsst06dPn4wfPz5jx47N17/+9VRUVKwx/+LFi3PNNdckSZo1a5bDDjusxvU///nPSZLNN988vXr1St26hc+r7d+/fyZMmJBHH300b7/9drbeeus1ZlgTJ+QCAAAAAAAAAAAAAAAAACWreYPm6da620rzT5dxD9rmoDQoa7CKuwvbZJNN8uqrr2bu3Ln51a9+laqqqoLrFyxYkJEjRyZJtt9++2yyyec7kbfYRo4cmcrKypXmU6dOzaRJk5IsP712TRo0aJC+ffsmSUaPHp3p06evct3EiRPTo0ePnH766Zk7d26NaxdffHGaNGmSpUuX5pxzzsmrr75a8J2LFy/OhRdemBdeeCFJcsEFF9Q4yXfu3LkZM2ZMkuSQQw5ZYxk3Sfr06ZNmzZqlqqoqQ4cOXeP62lDIBQAAAAAAAAAAAAAAAABKVrMGzVaaFaOMmyQHH3xwevbsmSQZNmxYTj/99DzzzDNZtmxZjXWVlZV54IEHcvTRR+e1115LWVlZLrroos/1znWhsrIy55xzTo0C7dSpU3PuuedmyZIladCgQY455phaPeuss85K48aNM2/evHzrW9/K888/X31t6dKluffee3PmmWdm/vz5adCgQZo2bVrj/i222CJXXHFF6tevnxkzZuTII4/M9ddfn6lTp9ZYN3/+/Nx777054ogjctdddyVJTjzxxBx55JE11o0ePTrz589Psvzk29po2LBh9t9//yTJiBEjVllWXlv1vvATAAAAAAAAAAAAAAAAAADWoY8WflT9/QfzP8i4t8elZcOW6dGmRz5Z9MlKa2qrbt26ufHGG3PppZdm1KhR+dvf/pa//e1vKS8vT5s2bdKgQYMsWLAg77//fpYsWZIk2XTTTXPFFVdk9913L87miuC//uu/cvHFF2efffbJVlttlbp16+btt9+uLhZffPHFadu2ba2e1a5du1x//fU555xz8vLLL+ewww7LZpttlsaNG2f69OnV5dju3bvn0ksvXeUzDjjggLRq1Srf/e538+GHH+aGG27IDTfckFatWqVly5aZP39+PvjggyxevDhJ0rhx45x//vk59thjV3rWkCFDkiRdu3bNNttsU+vPpH///rnjjjvy0UcfZdy4cTnwwANrfe+qKOQCAAAAAAAAAAAAAAAAAOvE0IOGfqH7y+uVp17dern/7fuTJJVLKzNzwczUr1s/ZXXKMvq10TXWl9UpS3m98rV6R5MmTXLllVfmlFNOyV133ZWnnnoqb731VnUJt7y8PFtvvXU6d+6cvffeOwcccEDKy9fuHevaf/zHf2T48OH5/e9/n0ceeSQzZ85M8+bN06VLl5xyyinVpwDX1p577pl77703gwYNyoMPPph33303H3/8cVq3bp2dd945hx56aA444IDUr19/tc/YbbfdMm7cuIwcOTIPPPBAXnrppcyaNSszZ85MkjRv3jw9evTIV7/61QwYMCAtWrRY6RkTJ07Myy+/nKT2p+OusMsuu2SrrbbKu+++myFDhnzhQm6dqqqqqi/0hC+BPn36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t7xFP8+jjz6q7N27V/nGN76hJJNJ5YknnlC2bt2qTE1NKYqiKHfeeafy4IMPKuFwWJEkSVlYWFBuueUW5ZOf/KQyPz+vDA0NKQcOHFD+5m/+Rj//zp07lX/8x39Uksmk8txzzylbtmxRXnjhBf18X/nKVxRFUZQnnnhCOXDggBIOh5W5uTnlne98p/KhD33oCv5brF18fqgUfH6oVHyGKJ9UKqXcdtttyl/91V8p8XhcOXnypHLrrbcqd955Z8Ex7HPPPafcdNNNSn9/v5JMJpXvfve7yvbt2/Ux9J133qnccccdys9//nMlFospf/RHf6TcddddiizLyre+9S3lwQcfVKamphRJkpRvfOMbyr59+5REIlGW62rPzGOPPab89//+35VEIpH373/58mWlt7dX+dKXvqRIkqS88sorSm9vr/Jf//VfiqKYn3/KxueHz08+CwsLyo033qj8xV/8hRKPx5Uf//jHys0336zceeedylNPPaX09PQoH/3oR5WFhQXlzJkzys6dO5WnnnpKURT1M+aDH/xgwWt84hOfUB577DHlhRdeUO68885Fv+7RRx9V3v72tysPPPCAsmvXLuVtb3ubcvr06UW/vpj727t3r/Lss88qkiQpiqIoH/7wh5Xf+73fU2ZmZpRoNKp8+MMfVh555BFFURTlz//8z5X3vve9+vkvX76s9PT0KC+88ILywgsvKNdee60Sj8cVRVGUt771rcpHP/pRZX5+XpmcnFQefvhh5fHHH1cURSl67oHsW8rn9ec//7ly3XXXKd/73veUhYUF5cc//rHS09Oj/7emK+vaa69V3vGOdygTExPKuXPnlG3btikPPvig8vOf/1yJRCLKfffdp3z84x9XfvnLXyq9vb3KT37yEyWVSinHjx9Xent7lVOnTimKYn4uHnvsMeUd73iHEolElMuXLyvvfOc7lfe///2L3ofxs+Opp55SbrrpJkVRCn/mPPXUU8rOnTuVT3/608rCwoLymc98Runt7VUeffRRZW5uTnnuueeUa6+9VnnllVcc3RdZ4/PD56dUfIb4DJWCzw+fn1Lw+eHzUwo+P3x+los3vvGNyqOPPqpEIhFlfHxceetb32paVzDOCdLKU8wzkEwmleeff17Zs2eP8oMf/GDRc915553KAw88oJw7d06ZnJxUbr75ZuXuu+9WfvKTnyjxeFx5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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "import matplotlib.pyplot as plt\n", "from matplotlib.legend_handler import HandlerTuple # Added import\n", @@ -297,12 +2110,12 @@ "\n", "# Create figure and axis\n", "plt.style.use(\"seaborn-v0_8-white\")\n", - "fig, ax = plt.subplots(figsize=(15, 6))\n", + "fig, ax = plt.subplots(figsize=(38, 15))\n", "\n", "# Set the width of each bar group and positions of the bars\n", "width = 0.15 # width of each bar\n", "spacing = 0.02 # space between bars within a group\n", - "group_spacing = 0.2 # space between model groups\n", + "group_spacing = 1 # space between model groups\n", "\n", "# Calculate positions for the bars\n", "num_sources = len(sources)\n", @@ -367,19 +2180,88 @@ " loc=\"upper left\",\n", ")\n", "\n", + "for text in custom_legend.get_texts():\n", + " current_size = text.get_fontsize()\n", + " text.set_fontsize(current_size * 2)\n", + "\n", "ax.yaxis.grid(True, linestyle=\"--\", alpha=0.3)\n", "ax.set_ylim(bottom=0)\n", "plt.tight_layout()\n", "ax.spines[\"top\"].set_visible(False)\n", "ax.spines[\"right\"].set_visible(False)\n", "\n", + "plt.savefig(\"model_performance_comparison.png\", bbox_inches=\"tight\", dpi=300)\n", "plt.show()" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Todos\n", + "\n", + "- Run one model on the benchmark, as a sanity check.\n", + "- Run les modΓ¨les de base sur Gaia\n", + "- Check agents id dans le systΓ¨me prompt.\n", + "\n", + "- Run les diffΓ©rents benchmarks et regarder les traces. Voir si jamais erreurs d'implΓ©mentation.\n", + "\n", + "\n", + "### Done\n", + "\n", + "- Check timeout tools, the 200 first Fibonacchi number (timeout in the interpreter) ; #OK : Timeout at the trajectory level (kill at more than 5 minutes) #OK - Implemented in the run/py function + in the local_python_executor.py file.\n", + "- Make the decentralization in the tools.py rather that in agents.py\n", + "- Check if they return well the final answer (is the final_asnwer check is still well implemented or broken ? If yes, reimplement it. For instance, ask to make a tag or a majority vote or global concensus).\n", + "- Check if they have one tool to show the traces. If not, create one - We decided to use langfuse as a visualisation tool of the trace (json/html format)\n", + "\n", + "\n", + "\n", + "### Next steps\n", + "- debug the tests noted in installation.md\n", + "- Actualize make tests\n", + "- Create a new score.ipynb\n", + "- Integrate trl\n", + "- RΓ©flΓ©chir sur les mΓ©thodes de final answer : avoir en tΓͺte que l'on veut avoir plusieurs faΓ§ons de le faire.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Meeting of Michal W (July 11th 2025)\n", + "\n", + "This dynamic batching for vllm should be integrated in TRL. So far, not much infos.\n", + "\n", + "Dynamic batching = run several agents and try to batch their code. The issue is when they send it to the API. One of the issue could be that when we send the old batch, one of the old ...\n", + "\n", + "Check the score.ipynb, but with the new model (maybe only a part of this to get time, just for a sanity check)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "### Web search tools used:\n", + "\n", + "The repository uses several implementations of a web_search tool:\n", + "\n", + " - DuckDuckGoSearchTool – relies on the duckduckgo_search library.\n", + "\n", + " - GoogleSearchTool – queries Google via SerpAPI or Serper using requests and API keys.\n", + "\n", + " - ApiWebSearchTool – performs searches using Brave’s API.\n", + "\n", + " - WebSearchTool – a general tool that can scrape DuckDuckGo or Bing results with plain HTTP requests.\n", + "\n", + "These tools provide markdown-formatted search results for agents in the project." + ] } ], "metadata": { "kernelspec": { - "display_name": "agents", + "display_name": "dec_smolagents", "language": "python", "name": "python3" }, @@ -393,7 +2275,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.0" + "version": "3.12.11" } }, "nbformat": 4, diff --git a/installation.md b/installation.md new file mode 100644 index 000000000..bb82e9f6c --- /dev/null +++ b/installation.md @@ -0,0 +1,42 @@ +# Installation of smolagent + +1. Install vllm + 1. Install uv (see https://docs.astral.sh/uv/getting-started/installation/) + 2. Run + ```bash + uv venv --python 3.12 --seed + source .venv/bin/activate + uv pip install vllm --torch-backend=auto + ``` + +2. Install other python packages: + +```bash +uv pip install flit +uv pip install . +flit install + +uv pip install -e "smolagents[dev] @ ." +``` + +3. Pass Hugging Face key as an environment variable: run `export HF_TOKEN=your_huggingface_api_key_here` +You can also set mistral-AI API keys. For commodity, put all the keys that you need in a .env file. For instance + +```bash +LANGFUSE_PUBLIC_KEY= +LANGFUSE_SECRET_KEY= +LANGFUSE_HOST="https://cloud.langfuse.com" +HF_TOKEN= +``` + +4. Compile the file: +```bash +make +make test +``` + +You can test easlily that the code works by using: +```bash +smolagent "Compute 5 + 3" --num-agents 2 --tools python_interpreter --imports langfuse +python examples/decentralized_smolagents_benchmark/run.py --model-id gpt-4o --agent-action-type vanilla #agent-action-type code or tool-calling require a SERPER API key +``` diff --git a/pyproject.toml b/pyproject.toml index 05577220b..a193109d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,11 @@ build-backend = "setuptools.build_meta" [project] name = "smolagents" + version = "1.23.0.dev0" + description = "πŸ€— smolagents: a barebones library for agents. Agents write python code to call tools or orchestrate other agents." + authors = [ { name="Aymeric Roucher", email="aymeric@hf.co" }, ] @@ -16,8 +19,8 @@ dependencies = [ "requests>=2.32.3", "rich>=13.9.4", "jinja2>=3.1.4", - "pillow>=10.0.1", - # Security fix for CVE-2023-4863: https://pillow.readthedocs.io/en/stable/releasenotes/10.0.1.html + "pillow>=10.0.1", # Security fix for CVE-2023-4863: https://pillow.readthedocs.io/en/stable/releasenotes/10.0.1.html + "multiprocess", "python-dotenv", ] @@ -63,6 +66,7 @@ openai = [ "openai>=1.58.1" ] telemetry = [ + "langfuse>=2.0.0", "arize-phoenix", "opentelemetry-sdk", "opentelemetry-exporter-otlp", diff --git a/tests/test_local_python_executor.py b/tests/test_local_python_executor.py index d9da9e78c..7f59a3916 100644 --- a/tests/test_local_python_executor.py +++ b/tests/test_local_python_executor.py @@ -48,8 +48,9 @@ def add_two(x): class TestEvaluatePythonCode: def assertDictEqualNoPrint(self, dict1, dict2): - assert {k: v for k, v in dict1.items() if k != "_print_outputs"} == { - k: v for k, v in dict2.items() if k != "_print_outputs" + exclude_keys = ["_print_outputs", "_start_time", "_timeout"] + assert {k: v for k, v in dict1.items() if k not in exclude_keys} == { + k: v for k, v in dict2.items() if k not in exclude_keys } def test_evaluate_assign(self):