feat: implement Fast Apply edit_file tool

- Add EditFileTool with Fast Apply approach using full-file rewrites
- Implement LLM-powered file editing with context awareness
- Add comprehensive unit and integration tests
- Include backup functionality for safety
- Support multiple file types with proper encoding handling

Resolves #3238

Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
Devin AI
2025-07-30 07:43:46 +00:00
parent cb522cf500
commit f2fa1755ae
5 changed files with 382 additions and 2 deletions

View File

@@ -0,0 +1,182 @@
import pytest
import tempfile
import os
from pathlib import Path
from unittest.mock import Mock, patch
from crewai.tools.agent_tools.edit_file_tool import EditFileTool, EditFileToolInput
class TestEditFileTool:
def setup_method(self):
"""Set up test fixtures."""
self.tool = EditFileTool()
def test_tool_initialization(self):
"""Test that the tool initializes correctly."""
assert self.tool.name == "edit_file"
assert "Fast Apply" in self.tool.description
assert self.tool.args_schema == EditFileToolInput
def test_input_schema_validation(self):
"""Test input schema validation."""
valid_input = EditFileToolInput(
file_path="/path/to/file.py",
edit_instructions="Add a new function",
context="This is for testing"
)
assert valid_input.file_path == "/path/to/file.py"
assert valid_input.edit_instructions == "Add a new function"
assert valid_input.context == "This is for testing"
with pytest.raises(ValueError):
EditFileToolInput(file_path="/path/to/file.py")
def test_file_not_exists(self):
"""Test handling of non-existent files."""
result = self.tool._run(
file_path="/non/existent/file.py",
edit_instructions="Add a function"
)
assert "Error: File /non/existent/file.py does not exist" in result
def test_path_is_directory(self):
"""Test handling when path points to a directory."""
with tempfile.TemporaryDirectory() as temp_dir:
result = self.tool._run(
file_path=temp_dir,
edit_instructions="Add a function"
)
assert f"Error: {temp_dir} is not a file" in result
def test_binary_file_handling(self):
"""Test handling of binary files."""
with tempfile.NamedTemporaryFile(delete=False, suffix='.bin') as temp_file:
temp_file.write(b'\x00\x01\x02\x03')
temp_file.flush()
try:
result = self.tool._run(
file_path=temp_file.name,
edit_instructions="Edit this file"
)
assert "Cannot read" in result and "unsupported encoding" in result
finally:
os.unlink(temp_file.name)
@patch('crewai.tools.agent_tools.edit_file_tool.LLM')
def test_successful_file_edit(self, mock_llm_class):
"""Test successful file editing."""
mock_llm = Mock()
mock_llm.call.return_value = "def new_function():\n return 'edited'"
mock_llm_class.return_value = mock_llm
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py') as temp_file:
temp_file.write("def old_function():\n return 'original'")
temp_file.flush()
try:
tool = EditFileTool()
result = tool._run(
file_path=temp_file.name,
edit_instructions="Replace old_function with new_function"
)
assert "Successfully edited" in result
assert "Backup saved" in result
with open(temp_file.name, 'r') as f:
content = f.read()
assert "new_function" in content
assert "old_function" not in content
backup_path = f"{temp_file.name}.backup"
assert os.path.exists(backup_path)
with open(backup_path, 'r') as f:
backup_content = f.read()
assert "old_function" in backup_content
os.unlink(backup_path)
finally:
os.unlink(temp_file.name)
def test_build_fast_apply_prompt(self):
"""Test Fast Apply prompt building."""
prompt = self.tool._build_fast_apply_prompt(
current_content="print('hello')",
edit_instructions="Change hello to world",
file_path="/test/file.py",
context="Testing context"
)
assert "Fast Apply file editing" in prompt
assert "print('hello')" in prompt
assert "Change hello to world" in prompt
assert "/test/file.py" in prompt
assert "Testing context" in prompt
assert "COMPLETE rewritten file content" in prompt
def test_extract_file_content_plain(self):
"""Test extracting content from plain LLM response."""
response = "def hello():\n print('world')"
content = self.tool._extract_file_content(response)
assert content == "def hello():\n print('world')"
def test_extract_file_content_markdown(self):
"""Test extracting content from markdown code blocks."""
response = "```python\ndef hello():\n print('world')\n```"
content = self.tool._extract_file_content(response)
assert content == "def hello():\n print('world')"
def test_extract_file_content_empty(self):
"""Test handling empty LLM response."""
content = self.tool._extract_file_content("")
assert content is None
content = self.tool._extract_file_content("```\n```")
assert content == ""
@patch('crewai.tools.agent_tools.edit_file_tool.LLM')
def test_llm_failure_handling(self, mock_llm_class):
"""Test handling of LLM failures."""
mock_llm = Mock()
mock_llm.call.side_effect = Exception("LLM error")
mock_llm_class.return_value = mock_llm
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py') as temp_file:
temp_file.write("original content")
temp_file.flush()
try:
tool = EditFileTool()
result = tool._run(
file_path=temp_file.name,
edit_instructions="Edit this file"
)
assert "Error editing file" in result
assert "LLM error" in result
finally:
os.unlink(temp_file.name)
def test_context_parameter(self):
"""Test that context parameter is properly handled."""
prompt_with_context = self.tool._build_fast_apply_prompt(
current_content="test",
edit_instructions="edit",
file_path="/test.py",
context="important context"
)
prompt_without_context = self.tool._build_fast_apply_prompt(
current_content="test",
edit_instructions="edit",
file_path="/test.py",
context=None
)
assert "important context" in prompt_with_context
assert "ADDITIONAL CONTEXT" in prompt_with_context
assert "ADDITIONAL CONTEXT" not in prompt_without_context

View File

@@ -0,0 +1,63 @@
import pytest
import tempfile
import os
from unittest.mock import patch
from crewai import Agent
from crewai.tools.agent_tools.edit_file_tool import EditFileTool
class TestEditFileToolIntegration:
def test_agent_with_edit_file_tool(self):
"""Test that agents can use the EditFileTool."""
tool = EditFileTool()
agent = Agent(
role="Code Editor",
goal="Edit files as requested",
backstory="I am an expert at editing code files",
tools=[tool],
verbose=True
)
assert tool in agent.tools
assert agent.tools[0].name == "edit_file"
def test_tool_with_different_file_types(self):
"""Test the tool works with different file types."""
tool = EditFileTool()
test_files = [
("test.py", "print('hello')", "Change hello to world"),
("test.js", "console.log('hello');", "Change hello to world"),
("test.txt", "Hello world", "Change Hello to Hi"),
("test.md", "# Title\nContent", "Change Title to New Title")
]
for filename, content, instruction in test_files:
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=filename) as temp_file:
temp_file.write(content)
temp_file.flush()
try:
with patch.object(tool.llm, 'call') as mock_call:
expected_content = content.replace('hello', 'world').replace('Hello', 'Hi').replace('Title', 'New Title')
mock_call.return_value = expected_content
result = tool._run(
file_path=temp_file.name,
edit_instructions=instruction
)
assert "Successfully edited" in result
with open(temp_file.name, 'r') as f:
edited_content = f.read()
assert edited_content == expected_content
backup_path = f"{temp_file.name}.backup"
if os.path.exists(backup_path):
os.unlink(backup_path)
finally:
os.unlink(temp_file.name)