ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
@@ -0,0 +1,226 @@
# IMO Super Agent with Guard Agent
This folder contains the Super Agent and Guard Agent dialogue system migrated from the GAIA project, specifically designed for solving IMO (International Mathematical Olympiad) problems.
## Quick Start
1. **Setup Environment**:
```bash
cd AWorld/examples/imo
./setup_env.sh
```
2. **Configure Environment Variables**:
```bash
cp .env_template .env
# Edit .env file with your API keys
```
3. **Run the Program**:
```bash
conda activate aworld_imo_env
python run.py --q imo4
```
## File Structure
```
imo/
├── run.py # Main execution file
├── guard_tool_caller.py # Guard tool caller
├── prompt.py # System prompts
├── utils.py # Utility functions
├── metadata.jsonl # IMO problem dataset
├── requirements.txt # Python dependencies
├── setup_env.sh # Environment setup script
├── README.md # Documentation
└── .env # Environment variables configuration file
```
## Environment Setup
### Method 1: Automatic Setup (Recommended)
```bash
# Navigate to the imo directory
cd AWorld/examples/imo
# Run the automatic setup script
./setup_env.sh
```
This script will automatically:
1. Create a new conda environment named `aworld_imo_env`
2. Install all necessary dependencies
3. Install the AWorld framework
4. Provide usage instructions
### Method 2: Manual Setup
If you prefer manual setup, follow these steps:
```bash
# 1. Create a new conda environment
conda create -n aworld_imo_env python=3.11 -y
# 2. Activate the environment
conda activate aworld_imo_env
# 3. Install dependencies
pip install -r requirements.txt
# 4. Install AWorld framework
cd ../../../
pip install -e .
cd AWorld/examples/imo
```
## Environment Configuration
Before running the program, you need to set up your environment variables:
1. **Copy the template file**:
```bash
cp .env_template .env
```
2. **Edit the `.env` file** with your actual API keys and configurations:
```bash
# LLM Configuration
LLM_MODEL_NAME="your_model_name" # e.g., "google/gemini-2.5-pro-preview"
LLM_API_KEY="your_api_key" # Your API key from OpenAI, OpenRouter, etc.
LLM_BASE_URL="your_base_url" # e.g., "https://openrouter.ai/api/v1"
LLM_TEMPERATURE=0.1
# Path Configurations (use relative paths)
IMO_DATASET_PATH="." # Current directory
AWORLD_WORKSPACE="Record" # Record directory
# IMO Server (same as LLM configuration for most cases)
IMO_LLM_API_KEY="your_imo_api_key" # Same as LLM_API_KEY
IMO_LLM_BASE_URL="your_imo_base_url" # Same as LLM_BASE_URL
IMO_LLM_MODEL_NAME="your_imo_model_name" # Same as LLM_MODEL_NAME
```
**Important Notes**:
- The `.env_template` file contains a template with empty values. You need to fill in your actual API keys and configurations in the `.env` file.
- For most users, the IMO Server configuration can be the same as the LLM configuration.
- You can obtain API keys from services like OpenAI, OpenRouter, or other LLM providers.
- The path configurations use relative paths (`.`) which means the current directory.
## Using the Environment
After setup, use the IMO project:
```bash
# 1. Activate the environment
conda activate aworld_imo_env
# 2. Navigate to the project directory
cd AWorld/examples/imo
# 3. Run the program
python run.py --q imo4
```
## Dataset Description
The IMO dataset is contained in the `metadata.jsonl` file, including the following IMO problems:
- imo1: Plane geometry problem
- imo2: Circle and triangle problem
- imo3: Function problem
- imo4: Sequence problem
- imo5: Game theory problem
- imo6: Grid covering problem
**Dataset Format**: Each line in `metadata.jsonl` is a JSON object with:
- `task_id`: Unique identifier for the problem (e.g., "imo1", "imo2")
- `Question`: The mathematical problem statement
**Adding New Problems**: You can add new problems by appending JSON lines to `metadata.jsonl`:
```json
{"task_id": "your_problem_id", "Question": "Your mathematical problem statement"}
```
## Running the Main Program
```bash
# Run a specific problem (recommended to start with test for testing)
python run.py --q test
# Run a range of problems
python run.py --start 0 --end 5
# Run all problems
python run.py --start 0 --end 6
```
## Main Features
1. **Super Agent**: Responsible for solving IMO mathematical problems
2. **Guard Agent**: Acts as an IMO grader to verify the correctness of solutions
3. **Dialogue Mechanism**: Two agents engage in multi-round conversations to refine solutions
4. **Solution Recording**: Records the complete conversation history and final solution
## Parameter Description
- `--q`: Specify problem ID (highest priority), e.g., `imo4`
- `--specific_task`: Run only a specific task_id, e.g., `imo4` (overrides --start and --end)
- `--start/--end`: Specify problem range (0-5 for all 6 IMO problems)
- `--skip`: Skip previously processed problems
## Output Files
- Log files: `~/.aworld/solution_*.log`
- Result files: `~/.aworld/results.json` (contains conversation history and solutions)
## Environment Information
- **Environment Name**: `aworld_imo_env`
- **Python Version**: 3.11
- **Main Dependencies**:
- AWorld framework core components
- OpenAI client
- Environment variable management tools
- Other necessary utility packages
## Advantages
1. **Environment Isolation**: Avoids dependency conflicts with existing `aworld_gaia_July` environment
2. **Lightweight**: Only installs packages necessary for the IMO project
3. **Reproducible**: Ensures environment consistency through `requirements.txt`
4. **Easy Management**: Independent environment for easy maintenance and cleanup
## Troubleshooting
If you encounter issues:
1. **conda command not found**: Ensure Miniconda or Anaconda is installed
2. **Dependency installation failed**: Try manual installation: `pip install -r requirements.txt`
3. **AWorld framework installation failed**: Execute manually: `cd ../../../ && pip install -e .`
## Cleanup
To remove this environment:
```bash
conda deactivate
conda env remove -n aworld_imo_env
```
## Important Notes
1. Ensure the AWorld framework is properly installed
2. Check that environment variables are correctly configured
3. IMO dataset is pre-configured in the imo folder
4. Recommended to start testing with test problem
5. The system focuses on solution quality and reasoning process rather than exact answer matching
## Acknowledgements
The IMO-related prompt code in this repository is adapted from the work of Lin Yang and Yichen Huang. We are grateful for their original implementation.
- Original Repository: [https://github.com/lyang36/IMO25](https://github.com/lyang36/IMO25)
@@ -0,0 +1,2 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
@@ -0,0 +1,165 @@
# MIT License
#
# Copyright (c) 2025 Lin Yang, Yichen Huang
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import logging
from typing import Dict, Any
from dotenv import load_dotenv
from aworld.agents.llm_agent import Agent
from aworld.config.conf import AgentConfig, TaskConfig
from aworld.core.task import Task
from aworld.runner import Runners
class GuardToolCaller:
"""simple reasoning diagnostic tool caller"""
def __init__(self):
# loading env
env_path = ".env"
load_dotenv(env_path, override=True, verbose=True)
# initialize guard llm
self.guard_llm = self._init_guard_llm()
def _init_guard_llm(self):
"""initialize reasoning diagnostic tool"""
try:
agent_config = AgentConfig(
llm_provider="openai",
llm_model_name=os.getenv("IMO_LLM_MODEL_NAME", "deepseek/deepseek-r1-0528:free"),
llm_api_key=os.getenv("IMO_LLM_API_KEY"),
llm_base_url=os.getenv("IMO_LLM_BASE_URL"),
llm_temperature=0.1,
)
guard_llm = Agent(
conf=agent_config,
name="guard_llm",
system_prompt=self._get_guard_system_prompt(),
)
logging.info("reasoning diagnostic tool LLM initialized successfully")
return guard_llm
except Exception as e:
logging.error(f"reasoning diagnostic tool LLM initialized failed: {e}")
return None
def _get_guard_system_prompt(self) -> str:
"""get reasoning diagnostic tool system prompt"""
return """You are an expert mathematician and a meticulous grader for an International Mathematical Olympiad (IMO) level exam. Your primary task is to rigorously verify the provided mathematical solution. A solution is to be judged correct only if every step is rigorously justified. A solution that arrives at a correct final answer through flawed reasoning, educated guesses, or with gaps in its arguments must be flagged as incorrect or incomplete.
### Instructions ###
**1. Core Instructions**
* Your sole task is to find and report all issues in the provided solution. You must act as a **verifier**, NOT a solver. **Do NOT attempt to correct the errors or fill the gaps you find.**
* You must perform a **step-by-step** check of the entire solution. This analysis will be presented in a **Detailed Verification Log**, where you justify your assessment of each step: for correct steps, a brief justification suffices; for steps with errors or gaps, you must provide a detailed explanation.
**2. How to Handle Issues in the Solution**
When you identify an issue in a step, you MUST first classify it into one of the following two categories and then follow the specified procedure.
* **a. Critical Error:**
This is any error that breaks the logical chain of the proof. This includes both **logical fallacies** (e.g., claiming that A > B, C > D implies A-C>B-D) and factual errors (e.g., a calculation error like 2+3=6).
* **Procedure:**
* Explain the specific error and state that it **invalidates the current line of reasoning**.
* Do NOT check any further steps that rely on this error.
* You MUST, however, scan the rest of the solution to identify and verify any fully independent parts. For example, if a proof is split into multiple cases, an error in one case does not prevent you from checking the other cases.
* **b. Justification Gap:**
This is for steps where the conclusion may be correct, but the provided argument is incomplete, hand-wavy, or lacks sufficient rigor.
* **Procedure:**
* Explain the gap in the justification.
* State that you will **assume the steps conclusion is true** for the sake of argument.
* Then, proceed to verify all subsequent steps to check if the remainder of the argument is sound.
**3. Output Format**
Your response MUST be structured into two main sections: a **Summary** followed by the **Detailed Verification Log**.
* **a. Summary**
This section MUST be at the very beginning of your response. It must contain two components:
* **Final Verdict**: A single, clear sentence declaring the overall validity of the solution. For example: "The solution is correct," "The solution contains a Critical Error and is therefore invalid," or "The solutions approach is viable but contains several Justification Gaps."
* **List of Findings**: A bulleted list that summarizes **every** issue you discovered. For each finding, you must provide:
* **Location:** A direct quote of the key phrase or equation where the issue occurs.
* **Issue:** A brief description of the problem and its classification (**Critical Error** or **Justification Gap**).
* **b. Detailed Verification Log**
Following the summary, provide the full, step-by-step verification log as defined in the Core Instructions. When you refer to a specific part of the solution, **quote the relevant text** to make your reference clear before providing your detailed analysis of that part.
**Example of the Required Summary Format**
*This is a generic example to illustrate the required format. Your findings must be based on the actual solution provided below.*
**Final Verdict:** The solution is **invalid** because it contains a Critical Error.
**List of Findings:**
* **Location:** "By interchanging the limit and the integral, we get ..."
* **Issue:** Justification Gap - The solution interchanges a limit and an integral without providing justification, such as proving uniform convergence.
* **Location: **"From $A > B$ and $C > D$, it follows that $AC>BD$"
* **Issue:** Critical Error - This step is a logical fallacy. Subtracting inequalities in this manner is not a valid mathematical operation.
================================================================================================================================================================
### Problem ###
[Paste the TeX for the problem statement here]
================================================================================================================================================================
### Solution ###
[Paste the TeX for the solution to be verified here]
================================================================================================================================================================
### Verification Task Reminder ###
Your task is to act as an IMO grader. Now, generate the **summary** and the **step-by-step verification log** for the solution above. In your log, justify each correct step and explain in detail any errors or justification gaps you find, as specified in the instructions above."""
async def call_guard_tool(self, super_output: str, original_task: str) -> str:
"""call reasoning diagnostic tool"""
try:
if not self.guard_llm:
return "reasoning diagnostic tool is not availble"
# construct the input
input_text = f"""Here is the provided solution:
{super_output}
Here is the original task:
{original_task}
Please do your job as expert mathematician and a meticulous grader for an International Mathematical Olympiad (IMO) level exam."""
# create task
task = Task(
input=input_text,
agent=self.guard_llm,
conf=TaskConfig()
)
# run task
result = Runners.sync_run_task(task=task)
# extract the answer
if result and task.id in result:
return result[task.id].answer
else:
return "The reasoning diagnostic tool did not return valid results."
except Exception as e:
logging.error(f"Failed to invoke the reasoning diagnostic tool: {e}")
return f"Failed to invoke the reasoning diagnostic tool.: {str(e)}"
@@ -0,0 +1,7 @@
{"task_id": "imo1", "Question": "A line in the plane is called sunny if it is not parallel to any of the xaxis, the yaxis, or the line x + y = 0. Let n ≥ 3 be a given integer. Determine all nonnegative integers k such that there exist n distinct lines in the plane satisfying both of the following: 1. for all positive integers a and b with a + b ≤ n + 1, the point (a, b) lies on at least one of the lines; and 2. exactly k of the n lines are sunny."}
{"task_id": "imo2", "Question": "Let Ω and Γ be circles with centres M and N, respectively, such that the radius of Ω is less than the radius of Γ. Suppose Ω and Γ intersect at two distinct points A and B. Line MN intersects Ω at C and Γ at D, so that C, M, N, D lie on MN in that order. Let P be the circumcenter of triangle ACD. Line AP meets Ω again at E != A and meets Γ again at F != A. Let H be the orthocenter of triangle PMN. Prove that the line through H parallel to AP is tangent to the circumcircle of triangle BEF."}
{"task_id": "imo3", "Question": "A function f: N → N is said to be bonza if f(a) divides b^a f(b)^f(a) for all positive integers a and b. Determine the smallest real constant c such that f(n) ≤ cn for all bonza functions f and all positive integers n."}
{"task_id": "imo4", "Question": "An infinite sequence a_1, a_2, . . . consists of positive integers has each of which has at least three proper divisors. Suppose that for each n ≥ 1, a_n+1 is the sum of the three largest proper divisors of an. Determine all possible values of a_1."}
{"task_id": "imo5", "Question": "Alice and Bazza are playing the inekoalaty game, a twoplayer game whose rules depend on a positive real number λ which is known to both players. On the nth turn of the game (starting with n = 1) the following happens:• If n is odd, Alice chooses a nonnegative real number xn such that x_1 + x_2 + ... + x_n ≤ λn. • If n is even, Bazza chooses a nonnegative real number xn such that (x_1)^2 + (x_2)^2 + ...+ (x_n)^2 ≤ n. If a player cannot choose a suitable x_n, the game ends and the other player wins. If the game goes on forever, neither player wins. All chosen numbers are known to both players. Determine all values of λ for which Alice has a winning strategy and all those for which Bazza has a winning strategy."}
{"task_id": "imo6", "Question": "Consider a 2025 × 2025 grid of unit squares. Matilda wishes to place on the grid some rectangular tiles, possibly of different sizes, such that each side of every tile lies on a grid line and every unit square is covered by at most one tile. Determine the minimum number of tiles Matilda needs to place so that each row and each column of the grid has exactly one unit square that is not covered by any tile."}
{"task_id": "test", "Question": "1+1="}
@@ -0,0 +1,62 @@
# MIT License
#
# Copyright (c) 2025 Lin Yang, Yichen Huang
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
system_prompt = """
### Core Instructions ###
* ** Rigor is Paramount:** Your primary goal is to produce a complete and rigorously justified solution. Every step in your solution must be logically sound and clearly explained. A correct final answer derived from flawed or incomplete reasoning is considered a failure.
* ** Honesty About Completeness:** If you cannot find a complete solution, you must **not** guess or create a solution that appears correct but contains hidden flaws or justification gaps. Instead, you should present only significant partial results that you can rigorously prove. A partial result is considered significant if it represents a substantial advancement toward a full solution. Examples include:
* Proving a key lemma.
* Fully resolving one or more cases within a logically sound case - based proof.
* Establishing a critical property of the mathematical objects in the problem.
* For an optimization problem, proving an upper or lower bound without proving that this bound is achievable.
* ** Use TeX for All Mathematics:** All mathematical variables, expressions, and relations must be enclosed in TeX delimiters (e.g., Let $n$ be an integer.).
### Output Format ###
Your response MUST be structured into the following sections, in this exact order.
**1. Summary **
Provide a concise overview of your findings. This section must contain two parts:
* ** a. Verdict:** State clearly whether you have found a complete solution or a partial solution.
* ** For a complete solution :** State the final answer, e.g., "I have successfully solved the problem. The final answer is ..."
* ** For a partial solution :** State the main rigorous conclusion (s) you were able to prove, e.g., "I have not found a complete solution, but I have rigorously proven that ..."
* ** b. Method Sketch:** Present a high-level, conceptual outline of your solution. This sketch should allow an expert to understand the logical flow of your argument without reading the full detail. It should include:
* A narrative of your overall strategy.
* The full and precise mathematical statements of any key lemmas or major intermediate results.
* If applicable, describe any key constructions or case splits that form the backbone of your argument.
**2. Detailed Solution**
Present the full , step-by-step mathematical proof. Each step must be logically justified and clearly explained. The level of detail should be sufficient for an expert to verify the correctness of your reasoning without needing to fill in any gaps. This section must contain ONLY the complete, rigorous proof, free of any internal commentary, alternative approaches, or failed attempts.
### Self-Correction Instruction ###
Before finalizing your output, carefully review your "Method Sketch" and "Detailed Solution" to ensure they are clean, rigorous, and strictly adhere to all instructions provided above. Verify that every statement contributes directly to the final, coherent mathematical argument.
"""
@@ -0,0 +1,24 @@
# IMO Project Requirements
# Core AWorld Framework Dependencies
pydantic>=2.9.2
pyyaml~=6.0.2
openai~=1.66.3
mcp[cli]~=1.10.1
python-dotenv>=1.0.1
executing~=2.2.0
tiktoken~=0.9.0
fastapi
aiohttp~=3.9.5
# Additional dependencies for IMO project
requests
loguru
pathvalidate
packaging
tabulate
# For potential future extensions (optional)
# numpy
# pandas
# matplotlib
# sympy # for mathematical computations
@@ -0,0 +1,306 @@
import argparse
import json
import logging
import os
import re
import sys
import traceback
from pathlib import Path
from typing import Any, Dict, List
from dotenv import load_dotenv
from pathlib import Path
from aworld.agents.llm_agent import Agent
from aworld.config.conf import AgentConfig, TaskConfig
from aworld.core.task import Task
from aworld.runner import Runners
from aworld.core.task import Task
from prompt import system_prompt
from utils import (
add_file_path,
load_dataset_meta,
)
from guard_tool_caller import GuardToolCaller
# Create log directory if it doesn't exist
if not os.path.exists(os.getenv("AWORLD_WORKSPACE", "~")):
os.makedirs(os.getenv("AWORLD_WORKSPACE", "~"))
parser = argparse.ArgumentParser()
parser.add_argument(
"--q",
type=str,
help="Question Index, e.g., imo6. Highest priority: override other arguments if provided.",
)
parser.add_argument(
"--skip",
action="store_true",
help="Skip the question if it has been processed before.",
)
args = parser.parse_args()
def setup_logging():
logging_logger = logging.getLogger()
logging_logger.setLevel(logging.INFO)
log_file_name = f"/solution_{args.q}.log" if args.q else f"/solution.log"
file_handler = logging.FileHandler(
os.getenv("AWORLD_WORKSPACE", "~") + log_file_name,
mode="a",
encoding="utf-8",
)
file_handler.setLevel(logging.INFO)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
file_handler.setFormatter(formatter)
logging_logger.addHandler(file_handler)
class GuardRunner:
"""The guard tool runner"""
def __init__(self, super_agent: Agent, guard_tool_caller: GuardToolCaller, original_task: str):
self.super_agent = super_agent
self.guard_tool_caller = guard_tool_caller
self.original_task = original_task
self.conversation_history = []
self.max_iterations = 10 # max conversation rounds
async def run_conversation(self, question: str) -> str:
"""run the conversation"""
current_input = question
iteration = 0
while iteration < self.max_iterations:
logging.info(f"=== The {iteration + 1} round of the conversation ===")
# 1. Super-agent handles the current input
logging.info(f"Super-agent input: {current_input[:100]}...")
super_output = await self._call_super_agent(current_input)
logging.info(f"Super-agent output: {super_output[:200]}...")
# 2. Check if the output contains the final answer
if self._has_final_answer(super_output) and iteration > 1:
logging.info("find the final answer, the conversation should stop")
return super_output
if iteration == self.max_iterations - 1:
logging.info("reach the max conversation rounds, the conversation should stop and return the final answer")
return super_output
# 3. Call the guard tool
logging.info("guard tool is being called...")
guard_output = await self._call_guard_tool(super_output)
logging.info(f"guard tool output: {guard_output[:200]}...")
# 4. Record the current round of the conversation history (before preparing the next input)
self.conversation_history.append({
"iteration": iteration + 1,
"super_input": current_input,
"super_output": super_output,
"guard_output": guard_output
})
# 5. Prepare the next input
next_input = self._prepare_next_input(super_output, guard_output)
current_input = next_input
iteration += 1
# Check if the conversation should continue
if self._should_stop_conversation(super_output, guard_output):
logging.info("the conversation should stop")
break
logging.info(f"the converstation finished, there are totally {iteration} rounds")
return super_output
async def _call_super_agent(self, input_text: str) -> str:
"""call the super-agent"""
try:
# create the task
task = Task(
input=input_text,
agent=self.super_agent,
conf=TaskConfig()
)
# run the task
result = Runners.sync_run_task(task=task)
# extract the answer
if result and task.id in result:
return result[task.id].answer
else:
return "Super-agent fail to return the result"
except Exception as e:
logging.error(f"fail to call the super-agent: {e}")
return f"fail to call the super-agent: {str(e)}"
async def _call_guard_tool(self, super_output: str) -> str:
"""call the guard tool"""
try:
# call the guard tool
guard_result = await self.guard_tool_caller.call_guard_tool(super_output, self.original_task)
return guard_result
except Exception as e:
logging.error(f"fail to call the guard tool: {e}")
return f"fail to call the guard tool: {str(e)}"
def _has_final_answer(self, output: str) -> bool:
"""check if the output contains the final answer"""
answer_patterns = [
r"<answer>.*?</answer>",
r"Final answer[:]\s*",
r"final answer[:]\s*",
r"The final answer is[:]\s*",
r"the final answer is[:]\s*",
r"the final answer is\s*",
r"I have successfully solved the problem."
]
for pattern in answer_patterns:
if re.search(pattern, output, re.IGNORECASE):
return True
return False
def _prepare_next_input(self, super_output: str, guard_output: str) -> str:
"""Prepare for the next round with complete conversation history"""
# build the complete conversation history
conversation_text = ""
# add the original task
conversation_text += f"Original Task:\n{self.original_task}\n\n"
# add all the history conversation rounds (not including the current round)
for i, history in enumerate(self.conversation_history):
round_num = history["iteration"]
conversation_text += f"=== Round {round_num} ===\n"
conversation_text += f"Previous solution:\n{history['super_output']}\n\n"
conversation_text += f"IMO grader review:\n{history['guard_output']}\n\n"
# build the final input
combined_input = f"""
{conversation_text}
Please seriously consider all the above reviews from the IMO grader across all rounds, and provide a refined and improved solution that addresses all the issues identified.
"""
return combined_input.strip()
def _should_stop_conversation(self, super_output: str, guard_output: str) -> bool:
"""check if the conversation should stop"""
# check if the guard tool thinks the conversation should stop
stop_indicators = [
"The answer is completed",
"No need to further refine",
"The question has been correctly solved",
"No loopholes or oversights found"
]
for indicator in stop_indicators:
if indicator in guard_output:
return True
return False
if __name__ == "__main__":
env_path = ".env"
load_dotenv(env_path, override=True, verbose=True)
setup_logging()
imo_dataset_path = os.getenv("IMO_DATASET_PATH", "./imo_dataset")
full_dataset = load_dataset_meta(imo_dataset_path)
logging.info(f"Total questions: {len(full_dataset)}")
# create the super-agent (without MCP tools)
agent_config = AgentConfig(
llm_provider="openai",
llm_model_name=os.getenv("LLM_MODEL_NAME", "gpt-4o"),
llm_api_key=os.getenv("LLM_API_KEY"),
llm_base_url=os.getenv("LLM_BASE_URL"),
llm_temperature=0.1,
)
super_agent = Agent(
conf=agent_config,
name="gaia_super_agent",
system_prompt=system_prompt,
)
# create the guard tool caller
guard_tool_caller = GuardToolCaller()
# load results from the checkpoint file
if os.path.exists(os.getenv("AWORLD_WORKSPACE", "~") + "/results.json"):
with open(os.getenv("AWORLD_WORKSPACE", "~") + "/results.json", "r", encoding="utf-8") as results_f:
results: List[Dict[str, Any]] = json.load(results_f)
else:
results: List[Dict[str, Any]] = []
try:
# appoint the task+id
if args.q is not None:
dataset_slice = [dataset_record for dataset_record in full_dataset if dataset_record["task_id"] == args.q]
if not dataset_slice:
logging.error(f"Task ID '{args.q}' not found in dataset")
sys.exit()
else:
logging.error("Please specify a task_id using --q parameter")
sys.exit()
# main loop to execute questions
for i, dataset_i in enumerate(dataset_slice):
# run
try:
logging.info(f"Start to process: {dataset_i['task_id']}")
logging.info(f"Question: {dataset_i['Question']}")
question = add_file_path(dataset_i, file_path=imo_dataset_path)["Question"]
# use the guard tool runner
guard_runner = GuardRunner(
super_agent=super_agent,
guard_tool_caller=guard_tool_caller,
original_task=question
)
# run the conversation
import asyncio
result = asyncio.run(guard_runner.run_conversation(question))
# Create the new result record
new_result = {
"task_id": dataset_i["task_id"],
"question": question,
"response": result,
"conversation_history": guard_runner.conversation_history,
}
# Check if this task_id already exists in results
existing_index = next(
(i for i, result in enumerate(results) if result.get("task_id") == dataset_i["task_id"]),
None,
)
if existing_index is not None:
# Update existing record
results[existing_index] = new_result
logging.info(f"Updated existing record for task_id: {dataset_i['task_id']}")
else:
# Append new record
results.append(new_result)
logging.info(f"Added new record for task_id: {dataset_i['task_id']}")
except Exception:
logging.error(f"Error processing {i}: {traceback.format_exc()}")
continue
except KeyboardInterrupt:
pass
finally:
# Save results to file
with open(os.getenv("AWORLD_WORKSPACE", "~") + "/results.json", "w", encoding="utf-8") as f:
json.dump(results, f, indent=4, ensure_ascii=False)
+80
View File
@@ -0,0 +1,80 @@
#!/bin/bash
# IMO Project Environment Setup Script
# This script creates a new conda environment and installs all necessary dependencies
echo "🚀 Setting up IMO Project Environment..."
# Check if conda is available
if ! command -v conda &> /dev/null; then
echo "❌ Error: conda is not installed or not in PATH"
echo "Please install Miniconda or Anaconda first"
exit 1
fi
# Environment name
ENV_NAME="imo_env"
echo "📦 Creating conda environment: $ENV_NAME"
# Create new conda environment with Python 3.11
conda create -n $ENV_NAME python=3.11 -y
if [ $? -ne 0 ]; then
echo "❌ Failed to create conda environment"
exit 1
fi
echo "✅ Conda environment created successfully"
# Activate the environment
echo "🔄 Activating environment..."
source $(conda info --base)/etc/profile.d/conda.sh
conda activate $ENV_NAME
if [ $? -ne 0 ]; then
echo "❌ Failed to activate conda environment"
exit 1
fi
echo "✅ Environment activated"
# Install requirements
echo "📥 Installing dependencies from requirements.txt..."
pip install -r requirements.txt
if [ $? -ne 0 ]; then
echo "❌ Failed to install dependencies"
echo "You can try installing them manually:"
echo "conda activate $ENV_NAME"
echo "pip install -r requirements.txt"
exit 1
fi
echo "✅ Dependencies installed successfully"
# Install AWorld framework in development mode
echo "🔧 Installing AWorld framework..."
cd ../../../
pip install -e .
if [ $? -ne 0 ]; then
echo "⚠️ Warning: Failed to install AWorld framework in development mode"
echo "You may need to install it manually:"
echo "cd ../../../ && pip install -e ."
fi
echo "✅ AWorld framework installed"
# Go back to imo directory
cd AWorld/examples/imo
echo ""
echo "🎉 Environment setup completed successfully!"
echo ""
echo "To use this environment:"
echo "1. Activate the environment: conda activate $ENV_NAME"
echo "2. Navigate to the imo directory: cd AWorld/examples/imo"
echo "3. Run your script: python run.py --q imo6"
echo ""
echo "To deactivate the environment: conda deactivate"
@@ -0,0 +1,272 @@
import json
import logging
import os
import re
import string
from pathlib import Path
from typing import Any, Dict, List, Optional
from tabulate import tabulate
logger = logging.getLogger(__name__)
def normalize_str(input_str, remove_punct=True) -> str:
no_spaces = re.sub(r"\s", "", input_str)
if remove_punct:
translator = str.maketrans("", "", string.punctuation)
return no_spaces.lower().translate(translator)
else:
return no_spaces.lower()
def split_string(s: str, char_list: Optional[List[str]] = None) -> list[str]:
if char_list is None:
char_list = [",", ";"]
pattern = f"[{''.join(char_list)}]"
return re.split(pattern, s)
def normalize_number_str(number_str: str) -> float:
for char in ["$", "%", ","]:
number_str = number_str.replace(char, "")
try:
return float(number_str)
except ValueError:
logger.error(f"String {number_str} cannot be normalized to number str.")
return float("inf")
def question_scorer(model_answer: str, ground_truth: str) -> bool:
def is_float(element: Any) -> bool:
try:
float(element)
return True
except ValueError:
return False
try:
if is_float(ground_truth):
logger.info(f"Evaluating {model_answer} as a number.")
normalized_answer = normalize_number_str(model_answer)
return normalized_answer == float(ground_truth)
elif any(char in ground_truth for char in [",", ";"]):
logger.info(f"Evaluating {model_answer} as a comma separated list.")
gt_elems = split_string(ground_truth)
ma_elems = split_string(model_answer)
if len(gt_elems) != len(ma_elems):
logger.warning("Answer lists have different lengths, returning False.")
return False
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:
ma_elem = normalize_str(ma_elem, remove_punct=False)
gt_elem = normalize_str(gt_elem, remove_punct=False)
comparisons.append(ma_elem == gt_elem)
return all(comparisons)
else:
logger.info(f"Evaluating {model_answer} as a string.")
ma_elem = normalize_str(model_answer)
gt_elem = normalize_str(ground_truth)
return ma_elem == gt_elem
except Exception as e:
logger.error(f"Error during evaluation: {e}")
return False
def load_dataset_meta(path: str):
# For IMO dataset, metadata.jsonl is directly placed in the imo folder
data_dir = Path(path)
dataset = []
metadata_file = data_dir / "metadata.jsonl"
if not metadata_file.exists():
logger.error(f"Metadata file not found: {metadata_file}")
return []
with open(metadata_file, "r", encoding="utf-8") as metaf:
lines = metaf.readlines()
for line_num, line in enumerate(lines, 1):
try:
# Clean trailing commas at the end of lines
line = line.strip().rstrip(',')
if not line:
continue
data = json.loads(line)
if data["task_id"] == "0-0-0-0-0":
continue
# IMO dataset may not have file_name field
if "file_name" in data and data["file_name"]:
data["file_name"] = data_dir / data["file_name"]
dataset.append(data)
except json.JSONDecodeError as e:
logger.warning(f"JSON decode error at line {line_num}: {e}")
logger.warning(f"Problematic line: {line[:100]}...")
continue
except Exception as e:
logger.error(f"Error processing line {line_num}: {e}")
continue
return dataset
def load_dataset_meta_dict(path: str, split: str = "validation"):
data_dir = Path(path) / split
dataset = {}
with open(data_dir / "metadata.jsonl", "r", encoding="utf-8") as metaf:
lines = metaf.readlines()
for line in lines:
data = json.loads(line)
if data["task_id"] == "0-0-0-0-0":
continue
if data["file_name"]:
data["file_name"] = data_dir / data["file_name"]
dataset[data["task_id"]] = data
return dataset
def add_file_path(task: Dict[str, Any], file_path: str = "./imo_dataset"):
if "file_name" in task and task["file_name"]:
# For IMO dataset, file paths may need adjustment
base_path = Path(file_path)
file_path = base_path / task["file_name"]
if file_path.suffix in [".pdf", ".docx", ".doc", ".txt"]:
task["Question"] += f" Here are the necessary document files: {file_path}"
elif file_path.suffix in [".jpg", ".jpeg", ".png"]:
task["Question"] += f" Here are the necessary image files: {file_path}"
elif file_path.suffix in [".xlsx", "xls", ".csv"]:
task["Question"] += (
f" Here are the necessary table files: {file_path}, for processing excel file,"
" you can use the excel tool or write python code to process the file"
" step-by-step and get the information."
)
elif file_path.suffix in [".py"]:
task["Question"] += f" Here are the necessary python files: {file_path}"
else:
task["Question"] += f" Here are the necessary files: {file_path}"
return task
def report_results(entries):
# Initialize counters
total_entries = len(entries)
total_correct = 0
# Initialize level statistics
level_stats = {}
# Process each entry
for entry in entries:
level = entry.get("level")
is_correct = entry.get("is_correct", False)
# Initialize level stats if not already present
if level not in level_stats:
level_stats[level] = {"total": 0, "correct": 0, "accuracy": 0}
# Update counters
level_stats[level]["total"] += 1
if is_correct:
total_correct += 1
level_stats[level]["correct"] += 1
# Calculate accuracy for each level
for level, stats in level_stats.items():
if stats["total"] > 0:
stats["accuracy"] = (stats["correct"] / stats["total"]) * 100
# Print overall statistics with colorful logging
logger.info("Overall Statistics:")
overall_accuracy = (total_correct / total_entries) * 100
# Create overall statistics table
overall_table = [
["Total Entries", total_entries],
["Total Correct", total_correct],
["Overall Accuracy", f"{overall_accuracy:.2f}%"],
]
logger.info(tabulate(overall_table, tablefmt="grid"))
logger.info("")
# Create level statistics table
logger.info("Statistics by Level:")
level_table = []
headers = ["Level", "Total Entries", "Correct Answers", "Accuracy"]
for level in sorted(level_stats.keys()):
stats = level_stats[level]
level_table.append([level, stats["total"], stats["correct"], f"{stats['accuracy']:.2f}%"])
logger.info(tabulate(level_table, headers=headers, tablefmt="grid"))
def setup_logger(logger_name, output_folder_path, file_name="main.log"):
"""
Set up a logger with the given name that writes to the specified file.
Returns a configured logger instance.
"""
if not os.path.exists(output_folder_path):
os.makedirs(output_folder_path)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
log_file = os.path.join(output_folder_path, file_name)
# Check if the logger already has handlers to avoid duplicates
logger = logging.getLogger(logger_name)
# Remove existing handlers if any
if logger.hasHandlers():
for handler in logger.handlers[:]:
logger.removeHandler(handler)
# Add file handler
handler = logging.FileHandler(log_file, mode="a", encoding="utf-8")
handler.setLevel(logging.INFO)
handler.setFormatter(formatter)
logger.setLevel(logging.INFO)
logger.addHandler(handler)
return logger
def color_log(logger: logging.Logger, value: str, color: Optional[str], level: str | None = None):
# Default to 'info' level if none specified
if level is None:
level = "info"
# Format the message with color
if color is None:
message = f"{value}"
else:
message = f"{color}{value}"
# Log according to the specified level
level_lower = level.lower()
if level_lower == "debug":
logger.debug(message)
elif level_lower == "info":
logger.info(message)
elif level_lower == "warning" or level_lower == "warn":
logger.warning(message)
elif level_lower == "error":
logger.error(message)
elif level_lower == "critical":
logger.critical(message)
else:
# Default to info for unknown levels
logger.info(message)