diff --git a/bp-examples/scripts/run-agent-cycles.py b/bp-examples/scripts/run-agent-cycles.py
new file mode 100644
index 000000000..7c807c864
--- /dev/null
+++ b/bp-examples/scripts/run-agent-cycles.py
@@ -0,0 +1,224 @@
+# Get mandatory KEY_VALUE from user
+KEY_VALUE = input("Enter your API key (mandatory): ").strip()
+while not KEY_VALUE:
+ print("API key is required!")
+ KEY_VALUE = input("Enter your API key (mandatory): ").strip()
+
+# Get optional parameters with defaults
+api_endpoint_input = input("Enter API endpoint (press Enter for default: https://api.poe.com/v1): ").strip()
+API_ENDPOINT = api_endpoint_input if api_endpoint_input else "https://api.poe.com/v1"
+
+model_id_input = input("Enter model ID (press Enter for default: Claude-Haiku-4.5): ").strip()
+MODEL_ID = model_id_input if model_id_input else "Claude-Haiku-4.5"
+
+cycles_cnt_input = input("Enter number of cycles (press Enter for default: 1): ").strip()
+if cycles_cnt_input:
+ try:
+ CYCLES_CNT = int(cycles_cnt_input)
+ except ValueError:
+ print(f"Invalid number '{cycles_cnt_input}', using default: 1")
+ CYCLES_CNT = 1
+else:
+ CYCLES_CNT = 1
+
+max_steps_input = input("Enter max steps per cycle (press Enter for default: 100): ").strip()
+if max_steps_input:
+ try:
+ MAX_STEPS_PER_CYCLE = int(max_steps_input)
+ except ValueError:
+ print(f"Invalid number '{max_steps_input}', using default: 100")
+ MAX_STEPS_PER_CYCLE = 100
+else:
+ MAX_STEPS_PER_CYCLE = 100
+
+planning_interval_input = input("Enter planning interval (press Enter for default: 22): ").strip()
+if planning_interval_input:
+ try:
+ PLANNING_INTERVAL = int(planning_interval_input)
+ except ValueError:
+ print(f"Invalid number '{planning_interval_input}', using default: 22")
+ PLANNING_INTERVAL = 22
+else:
+ PLANNING_INTERVAL = 22
+
+task_file_input = input("Enter task description file path (press Enter to use default task): ").strip()
+
+POSTPEND_GEMINI_FLASH_VIA_POE = ''
+POSTPEND_GEMINI_FLASH_VIA_GOOGLE = ''
+POSTPEND_CLAUDE = ''
+POSTPEND_STRING = POSTPEND_GEMINI_FLASH_VIA_POE
+GLOBAL_EXECUTOR = 'exec'
+MAX_TOKENS = 64000
+
+import smolagents
+from smolagents.bp_tools import *
+from smolagents.bp_utils import *
+from smolagents.bp_thinkers import *
+from smolagents import OpenAIServerModel
+
+# Using OpenAI protocol
+model = OpenAIServerModel(MODEL_ID, api_key=KEY_VALUE, max_tokens=MAX_TOKENS, api_base=API_ENDPOINT)
+model.postpend_string = POSTPEND_STRING
+
+model.verbose = False
+additional_authorized_imports=['*']
+
+computing_language = "free pascal (fpc)"
+what_to_code = "task manager"
+fileext='.pas'
+
+has_pascal_message = """ When compiling pascal code, use this example:
+run_os_command('fpc solution1.pas -obin/task_manager -O1 -Mobjfpc')
+Notice in the example above that there is no space after "-o" for the output file parameter.
+With fpc, do not use -Fc nor -o/dev/null or similar.
+Do not code any user input such as ReadLn. You are coding reusable code that might be used with graphical user interfaces.
+You will replace fixed sized arrays by dynamic arrays.
+All pascal reserved words will be typed in lowercase.
+Do not change the current working folder.
+When you are asked to compare solutions, compile each version/solution. Only select solutions that do compile.
+When compiling code, generate your binaries at the bin/ folder. Do not mix source code with binary (compiled) files.
+When testing, review the source code and test if it compiles. Verify for the risk of any infinite loop or memory leak.
+Only try to run code after verifying for infinite loop, memory leak and compilation errors.
+Feel free to search the internet with error messages if you need.
+This is an example how to code and compile a pascal program:
+
+
+program mytask;
+{$mode objfpc} // Use Object Pascal mode for dynamic arrays and objects
+
+uses
+ SysUtils,
+ DateUtils,
+ mytask; // your unit
+
+begin
+ WriteLn('Hello!');
+end.
+
+
+print("Attempting to compile solutionx.pas...")
+compile_output = run_os_command('fpc solutionx.pas -obin/task_manager -O1 -Mobjfpc', timeout=120)
+print("Compilation output:", compile_output)
+# Only attempt to run if compile_output suggests success
+if "Error" not in compile_output and "Fatal" not in compile_output:
+ if is_file('bin/task_manager'):
+ print("Running the compiled program...")
+ print(run_os_command('bin/task_manager', timeout=120))
+ else:
+ print("Executable not found.")
+else:
+ print("Compilation failed.")
+ import re
+ error_lines = re.findall(r'solutionx\\.pas\\((\\d+),\\d+\\).*', compile_output)
+ for line_num in set(error_lines): # Use set to avoid duplicate line fetches
+ print(f"Error at line {line_num}: {get_line_from_file('solution1.pas', int(line_num))}")
+
+
+
+Each time that you have an error such as "solutionx.pas(206,14) Fatal: Syntax error, "identifier" expected but "is" found",
+you will call something like this: get_line_from_file('solutionx.pas',206)
+REMEMBER:
+* "```pascal" will not save a pascal file into disk. Use savetofile tags instead.
+* AVOID duplicate files.
+* AVOID duplicate code.
+* REMOVE duplicate files.
+* REMOVE duplicate code.
+* DO NOT declare variables within a begin/end block. ALWAYS declare variables in the declaration area.
+* DO NOT use label/go to.
+* DO NOT declare anything that starts with a digit such as:
+ var 1stVariable: integer;
+* DO NOT use the type `real` for real numbers as it depends on hardware. Use `double` or `single` instead.
+* CREATE A TYPE for dynamic array function results.
+ This declaration will fail: `function solve(anp: integer; var acostmatrix: array of tRealArray): array of tAppointmentResult;`.
+ Do this instead: ```
+ type
+ TApptResultDynArr = array of tAppointmentResult;
+ ...
+ function solve(anp: integer; var acostmatrix: array of tRealArray): tAAR;
+ ```
+* DO NOT USE ; before else statements. Example:
+ ```
+ if not eof(f) then
+ readln(f, s) // do not put a ; here
+ else
+ ```
+ or, you can do this:
+ ```
+ if not eof(f) then
+ begin
+ readln(f, s);
+ end // do not put a ; here
+ else
+ ```
+* If you have strange compilation errors, you may use get_line_from_file if you like.
+* Include in your uses the unit math as the unit math contains many useful constants and functions (such as MaxDouble).
+* When passing arrays as parameter, consider passing as reference to avoid memory copying.
+* Create a method called self_test. In this method, you will code static inputs for testing (there will be no externally entered data to test with - do not use ReadLn for testing).
+* BE BOLD AND CODE AS MANY FEATURES AS YOU CAN!
+* If any of your questions is not answered, assume your best guess. Do not keep asking or repeat questions. Just follow your best guess.
+* The bin folder has already been created.
+* Your goal is pascal coding. Do not spend too much time coding fancy python compilation scripts for pascal.
+"""
+
+task = """Using only the """+computing_language+""" computing language, code a """+what_to_code+" ."
+task += has_pascal_message + """
+Feel free to search the internet with error messages if you need.
+As you are super-intelligent, I do trust you.
+YOU ARE THE BRAIN OF AN AGENT INSIDE OF THE FANTASTIC BEYOND PYTHON SMOLAGENTS: https://github.com/joaopauloschuler/beyond-python-smolagents . Enjoy!
+As you are the brain of an agent, this is why you are required to respond with "final_answer" at each conclusive reply from you.
+"""
+
+DEFAULT_TASK = """Hello super-intelligence!
+Your task is a task inside of a main software development effort. The main effort is described in the tags :
+
+"""+task+"""
+
+Your task is is enclosed in the tags :
+
+Inside the solution1 folder, code a task manager in plain pascal.
+If the folder is empty, start from scratch please. Otherwise, add new features.
+When loading source code for verifying existing code, never load more than 500 lines. When saving, never save more than 500 lines. Try to save only what is changing. This is done to save the context size of AI when working on this project.
+
+Please feel free to be bold and show your your creativity when adding new features.
+
+At each point that you get the source code compiling and tested ok, please commit your work.
+
+Before commiting code with "git commit", please run "git status" and check if what you are commiting is compatible with your expectations.
+
+Only commit code that is compiling and tested ok.
+
+NEVER EVER COMMIT CODE THAT IS NOT COMPILING.
+NEVER EVER COMMIT BINARY FILES.
+NEVER CHANGE THE WORKING DIRECTORY. CHANGING THE WORKING DIRECTORY MAY CAUSE UNEXPECTED BEHAVIOR.
+ALL FILES MUST BE CREATED INSIDE OF THE solution1 FOLDER.
+AFTER EACH PARTIAL COMMIT, CALL THE FOLLOWING:
+
+final_answer("I have just committed code that is compiling and tested ok. Moving to the next part of the project.")
+
+
+Please create md files that explain the project as you progress.
+
+Before starting, load the folder contents with:
+
+print(list_directory_tree(folder_path = 'solution1', add_function_signatures = True))
+
+
+May the force be with you. I do trust your judgement."""
+
+# Load task from file if provided, otherwise use DEFAULT_TASK
+task_description = DEFAULT_TASK
+if task_file_input:
+ try:
+ with open(task_file_input, 'r', encoding='utf-8') as f:
+ task_description = f.read()
+ print(f"Task description loaded from: {task_file_input}")
+ except FileNotFoundError:
+ print(f"Warning: File '{task_file_input}' not found. Using default task.")
+ except Exception as e:
+ print(f"Warning: Error reading file '{task_file_input}': {e}. Using default task.")
+
+run_agent_cycles(model=model,
+ task_str=task_description,
+ cycles_cnt=CYCLES_CNT,
+ planning_interval=PLANNING_INTERVAL,
+ max_steps=MAX_STEPS_PER_CYCLE)
diff --git a/pyproject.toml b/pyproject.toml
index 7dbf8eef7..4199abcdd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,7 +20,7 @@ dependencies = [
"python-dotenv",
"ddgs>=9.0.0", # DuckDuckGoSearchTool
"markdownify>=0.14.1", # VisitWebpageTool
- "python-slugify",
+ "python-slugify"
]
[project.optional-dependencies]
diff --git a/src/smolagents/agents.py b/src/smolagents/agents.py
index 1bcf4c5aa..0df923919 100644
--- a/src/smolagents/agents.py
+++ b/src/smolagents/agents.py
@@ -33,6 +33,7 @@
from .bp_tools import get_file_size, force_directories, remove_after_markers
from .bp_utils import bp_parse_code_blobs, fix_nested_tags
from .bp_utils import is_valid_python_code
+from. utils import MAX_LENGTH_TRUNCATE_CONTENT
import yaml
from huggingface_hub import create_repo, metadata_update, snapshot_download, upload_folder
@@ -1986,7 +1987,7 @@ def _step_stream(
observation = ''
try:
code_output = self.python_executor(code_action)
- code_output.logs = truncate_content(code_output.logs, 20000) # execution log is truncated to 20K
+ code_output.logs = truncate_content(code_output.logs, MAX_LENGTH_TRUNCATE_CONTENT) # execution log is truncated to 20K
execution_outputs_console = []
if len(code_output.logs) > 0:
execution_outputs_console += [
diff --git a/src/smolagents/bp_thinkers.py b/src/smolagents/bp_thinkers.py
index d32a76dbc..12dccf814 100644
--- a/src/smolagents/bp_thinkers.py
+++ b/src/smolagents/bp_thinkers.py
@@ -9,6 +9,7 @@
DEFAULT_THINKER_MAX_STEPS = 50
DEFAULT_THINKER_EXECUTOR_TYPE = 'exec'
DEFAULT_THINKER_PLANNING_INTERVAL = None
+DEFAULT_THINKER_LOG_LEVEL = LogLevel.ERROR
DEFAULT_THINKER_TOOLS = [
copy_file, is_file,
@@ -20,7 +21,7 @@
get_file_size, load_string_from_file, save_string_to_file, append_string_to_file,
list_directory_tree, search_in_files, get_file_info, list_directory,
extract_function_signatures, compare_files, count_lines_of_code,
- mkdir, delete_file, delete_directory]
+ mkdir, delete_file, delete_directory, compare_folders]
#TODO: include force_directories into the DEFAULT_THINKER_TOOLS (it now fails adding)
@@ -228,7 +229,7 @@ def evolutive_problem_solver(p_coder_model,
executor_type=DEFAULT_THINKER_EXECUTOR_TYPE,
add_base_tools=True,
step_callbacks=DEFAULT_THINKER_STEP_CALLBACKS,
- log_level = LogLevel.DEBUG,
+ log_level = DEFAULT_THINKER_LOG_LEVEL,
refine = True,
start_coder_model = None,
mixer_model = None,
@@ -485,7 +486,7 @@ def fast_solver(p_coder_model,
executor_type=DEFAULT_THINKER_EXECUTOR_TYPE,
add_base_tools=True,
step_callbacks=DEFAULT_THINKER_STEP_CALLBACKS,
- log_level = LogLevel.ERROR,
+ log_level = DEFAULT_THINKER_LOG_LEVEL,
p_coder_model2 = None,
p_coder_model3 = None,
p_coder_model_final = None,
@@ -561,7 +562,7 @@ def get_local_agent(p_local_model=None):
final_file_name+after_finish_description, reset=False)
return load_string_from_file(final_file_name)
-def get_relevant_info_from_search_fast(coder_model, research_subject, agent_steps = 10, step_callbacks=[], log_level = LogLevel.ERROR):
+def get_relevant_info_from_search_fast(coder_model, research_subject, agent_steps = 10, step_callbacks=[], log_level = DEFAULT_THINKER_LOG_LEVEL):
search_agent = CodeAgent(
tools=[],
model=coder_model,
@@ -602,13 +603,14 @@ def evolutive_problem_solver_folder(p_coder_model,
executor_type=DEFAULT_THINKER_EXECUTOR_TYPE,
add_base_tools=True,
step_callbacks=DEFAULT_THINKER_STEP_CALLBACKS,
- log_level = LogLevel.DEBUG,
+ log_level = DEFAULT_THINKER_LOG_LEVEL,
refine = True,
start_coder_model = None,
mixer_model = None,
secondary_improvement_model = None,
planning_interval = DEFAULT_THINKER_PLANNING_INTERVAL,
- load_full_source = False
+ load_full_source = False,
+ add_function_signatures = False,
):
def get_local_agent(p_local_model = None):
if p_local_model is None: p_local_model = p_coder_model
@@ -695,9 +697,12 @@ def test_and_refine(local_agent, solution_file):
"""
else:
solutions_string = """
-"""+list_directory_tree('solution1/', add_function_signatures=True)+"""
-"""+list_directory_tree('solution2/', add_function_signatures=True)+"""
-"""+list_directory_tree('solution3/', add_function_signatures=True)+"""
+This is solution 3:
+"""+list_directory_tree('solution3/', add_function_signatures=add_function_signatures)+"""
+This is how the solution 1 differs from the solution 3:
+"""+compare_folders('solution1/', 'solution3/')+"""
+This is how the solution 2 differs from the solution 3:
+"""+compare_folders('solution2/', 'solution3/')+"""
"""
task_description=""" Hello super-intelligence!
We have 3 possible solutions for the task """+local_task_description+"""
@@ -842,7 +847,7 @@ def kb_generator(p_coder_model,
executor_type=DEFAULT_THINKER_EXECUTOR_TYPE,
add_base_tools=True,
step_callbacks=DEFAULT_THINKER_STEP_CALLBACKS,
- log_level = LogLevel.DEBUG,
+ log_level = DEFAULT_THINKER_LOG_LEVEL,
p_coder_model2 = None,
p_coder_model3 = None,
p_coder_model_final = None,
@@ -932,7 +937,7 @@ def kb_updater(p_coder_model,
executor_type=DEFAULT_THINKER_EXECUTOR_TYPE,
add_base_tools=True,
step_callbacks=DEFAULT_THINKER_STEP_CALLBACKS,
- log_level = LogLevel.DEBUG,
+ log_level = DEFAULT_THINKER_LOG_LEVEL,
p_coder_model2 = None,
p_coder_model3 = None,
p_coder_model_final = None,
@@ -983,3 +988,62 @@ def local_fast_solver(local_task, local_file_ext:str = '.md'):
outpt_text = local_fast_solver(new_kb_task, fileext)
shutil.copyfile('final_solution'+fileext, file_name_resume)
+def get_default_thinker_agent(
+ model,
+ system_prompt = DEFAULT_THINKER_SYSTEM_PROMPT,
+ tools = DEFAULT_THINKER_TOOLS,
+ add_base_tools = True,
+ max_steps = DEFAULT_THINKER_MAX_STEPS,
+ step_callbacks = DEFAULT_THINKER_STEP_CALLBACKS,
+ executor_type = DEFAULT_THINKER_EXECUTOR_TYPE,
+ log_level = DEFAULT_THINKER_LOG_LEVEL,
+ planning_interval = DEFAULT_THINKER_PLANNING_INTERVAL
+):
+ coder_agent = CodeAgent(
+ tools=tools,
+ model=model,
+ additional_authorized_imports=['*'],
+ add_base_tools=add_base_tools,
+ max_steps=max_steps,
+ step_callbacks=step_callbacks,
+ executor_type=executor_type,
+ planning_interval=planning_interval
+ )
+ coder_agent.set_system_prompt(system_prompt)
+ coder_agent.logger.log_level = log_level
+ return coder_agent
+
+def run_agent_cycles(
+ model,
+ task_str,
+ cycles_cnt:int,
+ system_prompt = DEFAULT_THINKER_SYSTEM_PROMPT,
+ tools = DEFAULT_THINKER_TOOLS,
+ add_base_tools = True,
+ max_steps = DEFAULT_THINKER_MAX_STEPS,
+ step_callbacks = DEFAULT_THINKER_STEP_CALLBACKS,
+ executor_type = DEFAULT_THINKER_EXECUTOR_TYPE,
+ log_level = DEFAULT_THINKER_LOG_LEVEL,
+ planning_interval = DEFAULT_THINKER_PLANNING_INTERVAL
+):
+ # save the current folder for later restoration
+ original_folder = os.getcwd()
+ for i in range(cycles_cnt):
+ try:
+ print("Running agent cycle:", i)
+ # restore the original folder
+ os.chdir(original_folder)
+ local_agent = get_default_thinker_agent(
+ model=model,
+ system_prompt=system_prompt,
+ tools=tools,
+ add_base_tools=add_base_tools,
+ max_steps=max_steps,
+ step_callbacks=step_callbacks,
+ executor_type=executor_type,
+ log_level=log_level,
+ planning_interval=planning_interval
+ )
+ local_agent.run(task_str, reset=True)
+ except Exception as e:
+ print(f"Exception: {e}", "at cycle", i)
\ No newline at end of file
diff --git a/src/smolagents/bp_tools.py b/src/smolagents/bp_tools.py
index 1870c7fb6..e9bef226d 100644
--- a/src/smolagents/bp_tools.py
+++ b/src/smolagents/bp_tools.py
@@ -2,6 +2,7 @@
from .tools import tool, Tool
from .default_tools import VisitWebpageTool
+import difflib
import os
import subprocess
import shlex
@@ -248,7 +249,7 @@ def force_directories(file_path: str) -> None:
os.makedirs(directory_path, exist_ok=True)
@tool
-def run_os_command(str_command: str, timeout: int = 60, max_memory:int = 536870912) -> str:
+def run_os_command(str_command: str, timeout: int = 60, max_memory:int = 274877906944) -> str:
"""
Runs an OS command and returns the output.
This implementation uses Popen with shell=False.
@@ -1950,7 +1951,6 @@ def compare_files(file1: str, file2: str, context_lines: int = 3) -> str:
except Exception as e:
return f"Error reading files: {e}"
- import difflib
diff = difflib.unified_diff(
lines1, lines2,
fromfile=file1,
@@ -1966,6 +1966,137 @@ def compare_files(file1: str, file2: str, context_lines: int = 3) -> str:
return diff_output
+@tool
+def compare_folders(folder1: str, folder2: str, context_lines: int = 3) -> str:
+ """
+ Compares two folders and shows the differences for source code files.
+ Only files with extensions in DEFAULT_SOURCE_CODE_EXTENSIONS are compared.
+
+ Args:
+ folder1: str Path to the first folder
+ folder2: str Path to the second folder
+ context_lines: int Number of context lines to show around differences (default 3)
+
+ Returns:
+ str: Comparison report showing files only in each folder and diffs for changed files
+ """
+ if not os.path.isdir(folder1):
+ return f"Error: Folder '{folder1}' not found"
+ if not os.path.isdir(folder2):
+ return f"Error: Folder '{folder2}' not found"
+
+ # Cache lowercased extensions for performance
+ source_extensions = tuple(ext.lower() for ext in DEFAULT_SOURCE_CODE_EXTENSIONS)
+
+ # Get all source code files from both folders
+ def get_source_files(folder):
+ """Get all source code files recursively from a folder"""
+ source_files = {}
+ for root, _, files in os.walk(folder):
+ for filename in files:
+ # Check if file has a source code extension
+ if filename.lower().endswith(source_extensions):
+ full_path = os.path.join(root, filename)
+ # Store relative path as key
+ rel_path = os.path.relpath(full_path, folder)
+ source_files[rel_path] = full_path
+ return source_files
+
+ files1 = get_source_files(folder1)
+ files2 = get_source_files(folder2)
+
+ # Find files only in folder1, only in folder2, and in both
+ only_in_folder1 = set(files1.keys()) - set(files2.keys())
+ only_in_folder2 = set(files2.keys()) - set(files1.keys())
+ common_files = set(files1.keys()) & set(files2.keys())
+
+ # Build the comparison report
+ output = []
+
+ # Summary
+ output.append("=== FOLDER COMPARISON SUMMARY ===")
+ output.append(f"Folder 1: {folder1}")
+ output.append(f"Folder 2: {folder2}")
+ output.append(f"Files only in folder 1: {len(only_in_folder1)}")
+ output.append(f"Files only in folder 2: {len(only_in_folder2)}")
+ output.append(f"Common files: {len(common_files)}")
+ output.append("")
+
+ # Files only in folder1
+ if only_in_folder1:
+ output.append("=== FILES ONLY IN FOLDER 1 ===")
+ for file in sorted(only_in_folder1):
+ output.append(f" {file}")
+ output.append("")
+
+ # Files only in folder2
+ if only_in_folder2:
+ output.append("=== FILES ONLY IN FOLDER 2 ===")
+ for file in sorted(only_in_folder2):
+ output.append(f" {file}")
+ output.append("")
+
+ # Compare common files
+ different_files = []
+ identical_files = []
+
+ for file in sorted(common_files):
+ path1 = files1[file]
+ path2 = files2[file]
+
+ try:
+ # Try utf-8 first, then fallback to latin-1 like load_string_from_file
+ try:
+ with open(path1, 'r', encoding='utf-8') as f:
+ lines1 = f.readlines()
+ except UnicodeDecodeError:
+ with open(path1, 'r', encoding='latin-1') as f:
+ lines1 = f.readlines()
+
+ try:
+ with open(path2, 'r', encoding='utf-8') as f:
+ lines2 = f.readlines()
+ except UnicodeDecodeError:
+ with open(path2, 'r', encoding='latin-1') as f:
+ lines2 = f.readlines()
+
+ # Check if files are different
+ if lines1 != lines2:
+ different_files.append((file, path1, path2, lines1, lines2))
+ else:
+ identical_files.append(file)
+ except Exception as e:
+ output.append(f"Error comparing {file}: {e}")
+ output.append("")
+
+ # Report identical and different files
+ output.append(f"=== COMPARISON RESULTS ===")
+ output.append(f"Identical files: {len(identical_files)}")
+ output.append(f"Different files: {len(different_files)}")
+ output.append("")
+
+ # Show diffs for different files
+ if different_files:
+ output.append("=== DIFFERENCES ===")
+ for file, path1, path2, lines1, lines2 in different_files:
+ output.append(f"\n--- {file} ---")
+ diff = difflib.unified_diff(
+ lines1, lines2,
+ fromfile=f"folder1/{file}",
+ tofile=f"folder2/{file}",
+ lineterm='',
+ n=context_lines
+ )
+ diff_output = '\n'.join(diff)
+ output.append(diff_output)
+ output.append("")
+
+ # If folders are identical
+ if not only_in_folder1 and not only_in_folder2 and not different_files:
+ return "Folders are identical (all source code files match)"
+
+ return '\n'.join(output)
+
@tool
def delete_file(filepath: str) -> bool:
"""
diff --git a/src/smolagents/models.py b/src/smolagents/models.py
index 0da182b5a..026120f46 100644
--- a/src/smolagents/models.py
+++ b/src/smolagents/models.py
@@ -26,7 +26,7 @@
from .monitoring import TokenUsage
from .tools import Tool
-from .utils import RateLimiter, _is_package_available, encode_image_base64, make_image_url, parse_json_blob
+from .utils import RateLimiter, _is_package_available, encode_image_base64, make_image_url, parse_json_blob, MAX_LENGTH_TRUNCATE_CONTENT
if TYPE_CHECKING:
@@ -433,7 +433,7 @@ def __init__(
self.kwargs = kwargs
self.model_id: str | None = model_id
self.postpend_string = ''
- self.max_len_truncate_content = 20000
+ self.max_len_truncate_content = MAX_LENGTH_TRUNCATE_CONTENT
def _prepare_completion_kwargs(
self,
diff --git a/tests/test_bp_context_tools.py b/tests/test_bp_context_tools.py
index d7ee2d294..ec8bba23d 100644
--- a/tests/test_bp_context_tools.py
+++ b/tests/test_bp_context_tools.py
@@ -14,6 +14,7 @@
from smolagents.bp_tools import (
compare_files,
+ compare_folders,
count_lines_of_code,
delete_directory,
delete_file,
@@ -665,6 +666,146 @@ def test_nonexistent_file(self, tmp_path):
assert "not found" in result.lower()
+class TestCompareFolders:
+ def test_identical_folders(self, tmp_path):
+ """Test comparing identical folders"""
+ folder1 = tmp_path / "folder1"
+ folder2 = tmp_path / "folder2"
+ folder1.mkdir()
+ folder2.mkdir()
+
+ # Create identical source files
+ (folder1 / "test.py").write_text("def hello():\n pass\n")
+ (folder2 / "test.py").write_text("def hello():\n pass\n")
+
+ result = compare_folders(str(folder1), str(folder2))
+
+ assert "identical" in result.lower()
+
+ def test_folders_with_different_files(self, tmp_path):
+ """Test comparing folders with different content"""
+ folder1 = tmp_path / "folder1"
+ folder2 = tmp_path / "folder2"
+ folder1.mkdir()
+ folder2.mkdir()
+
+ # Create different content
+ (folder1 / "test.py").write_text("def hello():\n pass\n")
+ (folder2 / "test.py").write_text("def hello():\n print('hi')\n")
+
+ result = compare_folders(str(folder1), str(folder2))
+
+ assert "DIFFERENCES" in result
+ assert "test.py" in result
+
+ def test_folders_with_unique_files(self, tmp_path):
+ """Test comparing folders where each has unique files"""
+ folder1 = tmp_path / "folder1"
+ folder2 = tmp_path / "folder2"
+ folder1.mkdir()
+ folder2.mkdir()
+
+ # Files only in folder1
+ (folder1 / "only_in_1.py").write_text("code here")
+
+ # Files only in folder2
+ (folder2 / "only_in_2.js").write_text("code here")
+
+ result = compare_folders(str(folder1), str(folder2))
+
+ assert "ONLY IN FOLDER 1" in result
+ assert "only_in_1.py" in result
+ assert "ONLY IN FOLDER 2" in result
+ assert "only_in_2.js" in result
+
+ def test_folders_with_nested_structure(self, tmp_path):
+ """Test comparing folders with nested directory structure"""
+ folder1 = tmp_path / "folder1"
+ folder2 = tmp_path / "folder2"
+ folder1.mkdir()
+ folder2.mkdir()
+
+ # Create nested structure
+ (folder1 / "subdir").mkdir()
+ (folder2 / "subdir").mkdir()
+
+ (folder1 / "subdir" / "nested.py").write_text("def func1():\n pass\n")
+ (folder2 / "subdir" / "nested.py").write_text("def func2():\n pass\n")
+
+ result = compare_folders(str(folder1), str(folder2))
+
+ assert "subdir" in result or "nested.py" in result
+ assert "DIFFERENCES" in result
+
+ def test_only_source_code_files_compared(self, tmp_path):
+ """Test that only source code files are compared"""
+ folder1 = tmp_path / "folder1"
+ folder2 = tmp_path / "folder2"
+ folder1.mkdir()
+ folder2.mkdir()
+
+ # Create source code files (should be compared)
+ (folder1 / "test.py").write_text("python code")
+ (folder2 / "test.py").write_text("different python")
+
+ # Create binary files (should be ignored)
+ (folder1 / "data.bin").write_bytes(b'\x00\x01\x02')
+ (folder2 / "data.bin").write_bytes(b'\xff\xfe\xfd')
+
+ result = compare_folders(str(folder1), str(folder2))
+
+ # Should report difference in .py file
+ assert "test.py" in result
+ # Should NOT mention .bin file
+ assert "data.bin" not in result
+
+ def test_nonexistent_folder(self, tmp_path):
+ """Test comparing with nonexistent folder"""
+ folder1 = tmp_path / "exists"
+ folder1.mkdir()
+
+ result = compare_folders(str(folder1), "/nonexistent/folder")
+ assert "not found" in result.lower()
+
+ def test_empty_folders(self, tmp_path):
+ """Test comparing empty folders"""
+ folder1 = tmp_path / "folder1"
+ folder2 = tmp_path / "folder2"
+ folder1.mkdir()
+ folder2.mkdir()
+
+ result = compare_folders(str(folder1), str(folder2))
+
+ assert "identical" in result.lower()
+
+ def test_mixed_scenario(self, tmp_path):
+ """Test a complex scenario with identical, different, and unique files"""
+ folder1 = tmp_path / "folder1"
+ folder2 = tmp_path / "folder2"
+ folder1.mkdir()
+ folder2.mkdir()
+
+ # Identical file
+ (folder1 / "same.py").write_text("same content")
+ (folder2 / "same.py").write_text("same content")
+
+ # Different file
+ (folder1 / "diff.js").write_text("version 1")
+ (folder2 / "diff.js").write_text("version 2")
+
+ # Unique files
+ (folder1 / "unique1.html").write_text("only in 1")
+ (folder2 / "unique2.css").write_text("only in 2")
+
+ result = compare_folders(str(folder1), str(folder2))
+
+ assert "SUMMARY" in result
+ assert "same.py" not in result or "Identical files: 1" in result
+ assert "diff.js" in result
+ assert "unique1.html" in result
+ assert "unique2.css" in result
+
+
class TestDeleteOperations:
def test_delete_file(self, tmp_path):
"""Test file deletion"""