mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
2 Commits
feat/gemin
...
fix-filere
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44e052e0b8 | ||
|
|
d6c60bd480 |
@@ -1,302 +0,0 @@
|
||||
# Gemini-Optimized Tools Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the plan to implement Gemini-optimized file editing tools for OpenHands, inspired by the excellent design patterns found in Google's Gemini-CLI project.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### OpenHands CodeActAgent Tools
|
||||
- **str_replace_editor**: Uses exact string matching with `old_str`/`new_str` parameters
|
||||
- **Limitations**:
|
||||
- Requires very precise matching (whitespace, indentation)
|
||||
- No built-in error correction
|
||||
- Fails if string appears multiple times or doesn't match exactly
|
||||
- Not optimized for Gemini's capabilities
|
||||
|
||||
### Gemini-CLI Tools (Superior Design)
|
||||
- **replace tool**: Uses `old_string`/`new_string` with smart correction
|
||||
- **write_file tool**: Writes entire file content with validation
|
||||
- **read_file tool**: Reads files with offset/limit support
|
||||
- **Key advantages**:
|
||||
- Uses Gemini itself for content correction via `ensureCorrectEdit`
|
||||
- Better error handling and user feedback
|
||||
- IDE integration for diff previews
|
||||
- Automatic content validation and correction
|
||||
- More forgiving of minor formatting differences
|
||||
|
||||
### openhands-aci
|
||||
- Provides the runtime execution environment inside Docker containers
|
||||
- Has OHEditor class for file operations
|
||||
- Needs to be extended to support new Gemini-optimized tools
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Core Tool Implementation in openhands-aci
|
||||
|
||||
#### 1.1 Create Gemini-Optimized Editor Tools
|
||||
**Location**: `openhands-aci/openhands_aci/editor/gemini_tools.py`
|
||||
|
||||
Implement three core tools mirroring Gemini-CLI's approach:
|
||||
|
||||
1. **GeminiReplaceEditor**
|
||||
- Similar to Gemini-CLI's `replace` tool
|
||||
- Uses LLM-assisted content correction
|
||||
- Parameters: `file_path`, `old_string`, `new_string`, `expected_replacements`
|
||||
- Smart error recovery and suggestions
|
||||
|
||||
2. **GeminiWriteFileEditor**
|
||||
- Similar to Gemini-CLI's `write_file` tool
|
||||
- Writes entire file content with validation
|
||||
- Parameters: `file_path`, `content`
|
||||
- Content validation and correction
|
||||
|
||||
3. **GeminiReadFileEditor**
|
||||
- Enhanced file reading with range support
|
||||
- Parameters: `absolute_path`, `offset`, `limit`
|
||||
- Better error messages and content handling
|
||||
|
||||
#### 1.2 Content Correction System
|
||||
**Location**: `openhands-aci/openhands_aci/editor/content_corrector.py`
|
||||
|
||||
Implement LLM-assisted content correction similar to Gemini-CLI's `ensureCorrectEdit`:
|
||||
- Analyze edit context and suggest corrections
|
||||
- Handle whitespace/indentation issues
|
||||
- Provide helpful error messages
|
||||
- Validate edit feasibility
|
||||
|
||||
#### 1.3 Enhanced Error Handling
|
||||
**Location**: `openhands-aci/openhands_aci/editor/gemini_exceptions.py`
|
||||
|
||||
Create Gemini-specific error types and handling:
|
||||
- More descriptive error messages
|
||||
- Suggestions for fixing common issues
|
||||
- Context-aware error reporting
|
||||
|
||||
### Phase 2: OpenHands Integration
|
||||
|
||||
#### 2.1 Create Gemini Tool Definitions
|
||||
**Location**: `openhands/agenthub/codeact_agent/tools/gemini_tools.py`
|
||||
|
||||
Create tool definitions that interface with the openhands-aci implementations:
|
||||
|
||||
1. **create_gemini_replace_tool()**
|
||||
2. **create_gemini_write_file_tool()**
|
||||
3. **create_gemini_read_file_tool()**
|
||||
|
||||
#### 2.2 Model-Specific Tool Selection
|
||||
**Location**: `openhands/agenthub/codeact_agent/codeact_agent.py`
|
||||
|
||||
Modify CodeActAgent to use Gemini tools when Gemini models are detected:
|
||||
- Check if current LLM is a Gemini model
|
||||
- Switch to Gemini-optimized tools automatically
|
||||
- Maintain backward compatibility with existing tools
|
||||
|
||||
#### 2.3 Tool Registry Updates
|
||||
**Location**: `openhands/llm/tool_names.py`
|
||||
|
||||
Add new tool names:
|
||||
- `GEMINI_REPLACE_TOOL_NAME`
|
||||
- `GEMINI_WRITE_FILE_TOOL_NAME`
|
||||
- `GEMINI_READ_FILE_TOOL_NAME`
|
||||
|
||||
### Phase 3: Advanced Features
|
||||
|
||||
#### 3.1 Content Validation
|
||||
- Implement syntax validation for code files
|
||||
- Use Gemini for content quality checks
|
||||
- Provide suggestions for improvements
|
||||
|
||||
#### 3.2 Diff Generation and Preview
|
||||
- Generate unified diffs for changes
|
||||
- Better visualization of edits
|
||||
- Integration with IDE preview (future)
|
||||
|
||||
#### 3.3 Smart Context Handling
|
||||
- Automatically include relevant context around edits
|
||||
- Handle large files intelligently
|
||||
- Optimize token usage
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
### 1. Gemini-First Approach
|
||||
- Leverage Gemini's strengths in understanding context and intent
|
||||
- Use Gemini for content correction and validation
|
||||
- Design prompts optimized for Gemini's capabilities
|
||||
|
||||
### 2. Graceful Degradation
|
||||
- Provide helpful error messages and suggestions
|
||||
- Attempt automatic correction when possible
|
||||
- Fall back gracefully when corrections aren't possible
|
||||
|
||||
### 3. Developer Experience
|
||||
- Clear, actionable error messages
|
||||
- Intuitive parameter names and descriptions
|
||||
- Consistent behavior across tools
|
||||
|
||||
### 4. Performance Optimization
|
||||
- Minimize unnecessary LLM calls
|
||||
- Cache validation results when appropriate
|
||||
- Efficient handling of large files
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Tool Parameter Design
|
||||
|
||||
#### Gemini Replace Tool
|
||||
```python
|
||||
{
|
||||
"file_path": str, # Absolute path to file
|
||||
"old_string": str, # Text to replace (more forgiving than current)
|
||||
"new_string": str, # Replacement text
|
||||
"expected_replacements": int = 1, # Number of expected replacements
|
||||
}
|
||||
```
|
||||
|
||||
#### Gemini Write File Tool
|
||||
```python
|
||||
{
|
||||
"file_path": str, # Absolute path to file
|
||||
"content": str, # Complete file content
|
||||
}
|
||||
```
|
||||
|
||||
#### Gemini Read File Tool
|
||||
```python
|
||||
{
|
||||
"absolute_path": str, # Absolute path to file
|
||||
"offset": int = None, # Starting line number
|
||||
"limit": int = None, # Number of lines to read
|
||||
}
|
||||
```
|
||||
|
||||
### Content Correction Algorithm
|
||||
|
||||
1. **Parse Edit Request**: Extract file path, old content, new content
|
||||
2. **Validate Context**: Check if old_string exists and is unique enough
|
||||
3. **LLM Correction**: If issues found, use Gemini to suggest corrections
|
||||
4. **Apply Edit**: Execute the corrected edit
|
||||
5. **Validate Result**: Ensure edit was successful and makes sense
|
||||
|
||||
### Error Recovery Strategies
|
||||
|
||||
1. **Fuzzy Matching**: If exact match fails, try fuzzy matching with suggestions
|
||||
2. **Context Expansion**: Automatically include more context if needed
|
||||
3. **Alternative Suggestions**: Provide multiple correction options
|
||||
4. **Rollback Support**: Easy undo functionality
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- Test each tool individually
|
||||
- Mock LLM responses for consistent testing
|
||||
- Cover error cases and edge conditions
|
||||
|
||||
### Integration Tests
|
||||
- Test with real Gemini models
|
||||
- Verify tool selection logic
|
||||
- Test backward compatibility
|
||||
|
||||
### Performance Tests
|
||||
- Measure token usage efficiency
|
||||
- Test with large files
|
||||
- Benchmark against current tools
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Phase 1: Parallel Implementation
|
||||
- Implement new tools alongside existing ones
|
||||
- Use feature flags to control rollout
|
||||
- Gather feedback from early adopters
|
||||
|
||||
### Phase 2: Gradual Rollout
|
||||
- Enable for specific Gemini models first
|
||||
- Monitor performance and error rates
|
||||
- Collect user feedback
|
||||
|
||||
### Phase 3: Full Deployment
|
||||
- Make Gemini tools default for Gemini models
|
||||
- Maintain existing tools for other models
|
||||
- Document differences and migration guide
|
||||
|
||||
## Success Metrics
|
||||
|
||||
1. **Reduced Edit Failures**: Lower rate of failed str_replace operations
|
||||
2. **Improved User Experience**: Better error messages and suggestions
|
||||
3. **Higher Success Rate**: More successful file edits on first attempt
|
||||
4. **Token Efficiency**: Optimal use of Gemini's capabilities
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **IDE Integration**: Real-time diff previews
|
||||
2. **Multi-file Operations**: Batch editing across multiple files
|
||||
3. **Semantic Understanding**: Context-aware editing suggestions
|
||||
4. **Version Control Integration**: Git-aware editing operations
|
||||
|
||||
## Conclusion
|
||||
|
||||
This implementation will significantly improve the file editing experience for Gemini users in OpenHands by leveraging Gemini's strengths and following proven patterns from Gemini-CLI. The modular design ensures compatibility while providing superior functionality for Gemini models.
|
||||
|
||||
---
|
||||
|
||||
## ✅ IMPLEMENTATION COMPLETE
|
||||
|
||||
**Status: FULLY IMPLEMENTED AND TESTED** 🎉
|
||||
|
||||
The Gemini-optimized tools have been successfully implemented and integrated into OpenHands!
|
||||
|
||||
### What was implemented:
|
||||
|
||||
1. **Enhanced File Operations** (openhands-aci):
|
||||
- `GeminiFileEditor` with intelligent content correction
|
||||
- `ContentCorrector` for automatic edit fixing
|
||||
- Enhanced error handling with user-friendly messages
|
||||
- Content validation and syntax checking
|
||||
|
||||
2. **Tool Definitions** (OpenHands):
|
||||
- `gemini_read_file` - Enhanced file reading with range support
|
||||
- `gemini_write_file` - File writing with validation
|
||||
- `gemini_replace` - Intelligent text replacement with correction
|
||||
|
||||
3. **Runtime Integration**:
|
||||
- Action classes: `GeminiReadFileAction`, `GeminiWriteFileAction`, `GeminiReplaceAction`
|
||||
- Function calling integration in `function_calling.py`
|
||||
- Runtime execution handlers in `action_execution_server.py`
|
||||
|
||||
4. **Automatic Model Detection**:
|
||||
- CodeActAgent automatically uses Gemini tools when 'gemini' is detected in model name
|
||||
- Seamless fallback to standard tools for other models
|
||||
|
||||
### Key Features Implemented:
|
||||
- ✅ Intelligent content correction (whitespace normalization, context expansion, fuzzy matching)
|
||||
- ✅ Enhanced error messages with suggestions and similar file detection
|
||||
- ✅ Content validation for syntax issues
|
||||
- ✅ Range-based file reading (offset/limit support)
|
||||
- ✅ Expected replacement count validation
|
||||
- ✅ Automatic model detection and tool selection
|
||||
- ✅ Full backward compatibility with existing tools
|
||||
|
||||
### Testing Results:
|
||||
- ✅ All tool definitions working correctly
|
||||
- ✅ Action creation and dispatch functioning
|
||||
- ✅ File operations (read/write/replace) working
|
||||
- ✅ Error handling providing helpful feedback
|
||||
- ✅ Content correction capabilities verified
|
||||
|
||||
### Files Modified/Created:
|
||||
|
||||
**openhands-aci:**
|
||||
- `openhands_aci/editor/content_corrector.py` (NEW)
|
||||
- `openhands_aci/editor/gemini_exceptions.py` (NEW)
|
||||
- `openhands_aci/editor/gemini_tools.py` (NEW)
|
||||
|
||||
**OpenHands:**
|
||||
- `openhands/llm/tool_names.py` (MODIFIED - added Gemini tool names)
|
||||
- `openhands/events/action/files.py` (MODIFIED - added Gemini action classes)
|
||||
- `openhands/events/action/__init__.py` (MODIFIED - exported new actions)
|
||||
- `openhands/agenthub/codeact_agent/tools/gemini_tools.py` (NEW)
|
||||
- `openhands/agenthub/codeact_agent/codeact_agent.py` (MODIFIED - added model detection)
|
||||
- `openhands/agenthub/codeact_agent/function_calling.py` (MODIFIED - added Gemini tool handling)
|
||||
- `openhands/runtime/action_execution_server.py` (MODIFIED - added runtime handlers)
|
||||
|
||||
The implementation is production-ready and will automatically activate when using Gemini models!
|
||||
@@ -16,7 +16,6 @@ from openhands.agenthub.codeact_agent.tools.condensation_request import (
|
||||
CondensationRequestTool,
|
||||
)
|
||||
from openhands.agenthub.codeact_agent.tools.finish import FinishTool
|
||||
from openhands.agenthub.codeact_agent.tools.gemini_tools import get_gemini_tools
|
||||
from openhands.agenthub.codeact_agent.tools.ipython import IPythonTool
|
||||
from openhands.agenthub.codeact_agent.tools.llm_based_edit import LLMBasedFileEditTool
|
||||
from openhands.agenthub.codeact_agent.tools.str_replace_editor import (
|
||||
@@ -140,24 +139,11 @@ class CodeActAgent(Agent):
|
||||
if self.config.enable_llm_editor:
|
||||
tools.append(LLMBasedFileEditTool)
|
||||
elif self.config.enable_editor:
|
||||
# Check if we should use Gemini-optimized tools
|
||||
use_gemini_tools = (
|
||||
self.llm is not None and 'gemini' in self.llm.config.model.lower()
|
||||
tools.append(
|
||||
create_str_replace_editor_tool(
|
||||
use_short_description=use_short_tool_desc
|
||||
)
|
||||
)
|
||||
|
||||
if use_gemini_tools:
|
||||
# Use Gemini-optimized tools for better performance
|
||||
tools.extend(get_gemini_tools())
|
||||
logger.info(
|
||||
f'Using Gemini-optimized tools for model: {self.llm.config.model}'
|
||||
)
|
||||
else:
|
||||
# Use standard str_replace_editor tool
|
||||
tools.append(
|
||||
create_str_replace_editor_tool(
|
||||
use_short_description=use_short_tool_desc
|
||||
)
|
||||
)
|
||||
return tools
|
||||
|
||||
def reset(self) -> None:
|
||||
|
||||
@@ -33,9 +33,6 @@ from openhands.events.action import (
|
||||
CmdRunAction,
|
||||
FileEditAction,
|
||||
FileReadAction,
|
||||
GeminiReadFileAction,
|
||||
GeminiReplaceAction,
|
||||
GeminiWriteFileAction,
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
)
|
||||
@@ -43,11 +40,6 @@ from openhands.events.action.agent import CondensationRequestAction
|
||||
from openhands.events.action.mcp import MCPAction
|
||||
from openhands.events.event import FileEditSource, FileReadSource
|
||||
from openhands.events.tool import ToolCallMetadata
|
||||
from openhands.llm.tool_names import (
|
||||
GEMINI_READ_FILE_TOOL_NAME,
|
||||
GEMINI_REPLACE_TOOL_NAME,
|
||||
GEMINI_WRITE_FILE_TOOL_NAME,
|
||||
)
|
||||
|
||||
|
||||
def combine_thought(action: Action, thought: str) -> Action:
|
||||
@@ -228,52 +220,6 @@ def response_to_actions(
|
||||
)
|
||||
action = BrowseInteractiveAction(browser_actions=arguments['code'])
|
||||
|
||||
# ================================================
|
||||
# Gemini-optimized tools
|
||||
# ================================================
|
||||
elif tool_call.function.name == GEMINI_READ_FILE_TOOL_NAME:
|
||||
if 'absolute_path' not in arguments:
|
||||
raise FunctionCallValidationError(
|
||||
f'Missing required argument "absolute_path" in tool call {tool_call.function.name}'
|
||||
)
|
||||
action = GeminiReadFileAction(
|
||||
absolute_path=arguments['absolute_path'],
|
||||
offset=arguments.get('offset'),
|
||||
limit=arguments.get('limit'),
|
||||
)
|
||||
elif tool_call.function.name == GEMINI_WRITE_FILE_TOOL_NAME:
|
||||
if 'file_path' not in arguments:
|
||||
raise FunctionCallValidationError(
|
||||
f'Missing required argument "file_path" in tool call {tool_call.function.name}'
|
||||
)
|
||||
if 'content' not in arguments:
|
||||
raise FunctionCallValidationError(
|
||||
f'Missing required argument "content" in tool call {tool_call.function.name}'
|
||||
)
|
||||
action = GeminiWriteFileAction(
|
||||
file_path=arguments['file_path'],
|
||||
content=arguments['content'],
|
||||
)
|
||||
elif tool_call.function.name == GEMINI_REPLACE_TOOL_NAME:
|
||||
if 'file_path' not in arguments:
|
||||
raise FunctionCallValidationError(
|
||||
f'Missing required argument "file_path" in tool call {tool_call.function.name}'
|
||||
)
|
||||
if 'old_string' not in arguments:
|
||||
raise FunctionCallValidationError(
|
||||
f'Missing required argument "old_string" in tool call {tool_call.function.name}'
|
||||
)
|
||||
if 'new_string' not in arguments:
|
||||
raise FunctionCallValidationError(
|
||||
f'Missing required argument "new_string" in tool call {tool_call.function.name}'
|
||||
)
|
||||
action = GeminiReplaceAction(
|
||||
file_path=arguments['file_path'],
|
||||
old_string=arguments['old_string'],
|
||||
new_string=arguments['new_string'],
|
||||
expected_replacements=arguments.get('expected_replacements', 1),
|
||||
)
|
||||
|
||||
# ================================================
|
||||
# MCPAction (MCP)
|
||||
# ================================================
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
"""
|
||||
Gemini-optimized tools for OpenHands CodeActAgent.
|
||||
These tools are designed to work better with Gemini models by providing
|
||||
enhanced error handling, content correction, and more intuitive interfaces.
|
||||
"""
|
||||
|
||||
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
|
||||
|
||||
from openhands.llm.tool_names import (
|
||||
GEMINI_READ_FILE_TOOL_NAME,
|
||||
GEMINI_REPLACE_TOOL_NAME,
|
||||
GEMINI_WRITE_FILE_TOOL_NAME,
|
||||
)
|
||||
|
||||
# Detailed descriptions for Gemini tools
|
||||
_GEMINI_READ_FILE_DESCRIPTION = """Enhanced file reading tool optimized for Gemini models.
|
||||
|
||||
Key features:
|
||||
- Read files with optional line range support (offset and limit)
|
||||
- Better error messages with suggestions for similar files
|
||||
- Automatic handling of binary files and large files
|
||||
- Directory listing when path points to a directory
|
||||
|
||||
Parameters:
|
||||
- absolute_path: Absolute path to the file to read
|
||||
- offset: Optional starting line number (1-based indexing)
|
||||
- limit: Optional number of lines to read from offset
|
||||
|
||||
This tool provides more helpful error messages and suggestions compared to the standard file reader."""
|
||||
|
||||
_GEMINI_WRITE_FILE_DESCRIPTION = """Enhanced file writing tool optimized for Gemini models.
|
||||
|
||||
Key features:
|
||||
- Write complete file content with automatic validation
|
||||
- Create parent directories if they don't exist
|
||||
- Content validation for common syntax issues
|
||||
- Better error messages with actionable suggestions
|
||||
- Automatic encoding handling
|
||||
|
||||
Parameters:
|
||||
- file_path: Absolute path to the file to write
|
||||
- content: Complete content to write to the file
|
||||
|
||||
This tool is ideal for creating new files or completely rewriting existing files.
|
||||
For partial edits, use the gemini_replace tool instead."""
|
||||
|
||||
_GEMINI_REPLACE_DESCRIPTION = """Enhanced text replacement tool optimized for Gemini models.
|
||||
|
||||
Key features:
|
||||
- Intelligent content correction when exact matches fail
|
||||
- Better error messages with suggestions and similar content
|
||||
- Support for expected replacement counts
|
||||
- Automatic whitespace and formatting normalization
|
||||
- Context expansion for better matching
|
||||
|
||||
Parameters:
|
||||
- file_path: Absolute path to the file to edit
|
||||
- old_string: Text to replace (more forgiving than standard str_replace)
|
||||
- new_string: Replacement text
|
||||
- expected_replacements: Number of replacements expected (default: 1)
|
||||
|
||||
This tool is much more forgiving than the standard str_replace tool and will attempt
|
||||
to correct minor formatting differences automatically. It's designed to work better
|
||||
with Gemini's natural language understanding capabilities."""
|
||||
|
||||
|
||||
def create_gemini_read_file_tool() -> ChatCompletionToolParam:
|
||||
"""Create the Gemini-optimized file reading tool."""
|
||||
return ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name=GEMINI_READ_FILE_TOOL_NAME,
|
||||
description=_GEMINI_READ_FILE_DESCRIPTION,
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'absolute_path': {
|
||||
'description': 'Absolute path to the file to read (e.g., /workspace/file.py)',
|
||||
'type': 'string',
|
||||
},
|
||||
'offset': {
|
||||
'description': 'Optional starting line number (1-based). If specified, reading starts from this line.',
|
||||
'type': 'integer',
|
||||
'minimum': 1,
|
||||
},
|
||||
'limit': {
|
||||
'description': 'Optional number of lines to read from the offset. If not specified, reads to end of file.',
|
||||
'type': 'integer',
|
||||
'minimum': 1,
|
||||
},
|
||||
},
|
||||
'required': ['absolute_path'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def create_gemini_write_file_tool() -> ChatCompletionToolParam:
|
||||
"""Create the Gemini-optimized file writing tool."""
|
||||
return ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name=GEMINI_WRITE_FILE_TOOL_NAME,
|
||||
description=_GEMINI_WRITE_FILE_DESCRIPTION,
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'file_path': {
|
||||
'description': 'Absolute path to the file to write (e.g., /workspace/file.py)',
|
||||
'type': 'string',
|
||||
},
|
||||
'content': {
|
||||
'description': 'Complete content to write to the file',
|
||||
'type': 'string',
|
||||
},
|
||||
},
|
||||
'required': ['file_path', 'content'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def create_gemini_replace_tool() -> ChatCompletionToolParam:
|
||||
"""Create the Gemini-optimized text replacement tool."""
|
||||
return ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name=GEMINI_REPLACE_TOOL_NAME,
|
||||
description=_GEMINI_REPLACE_DESCRIPTION,
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'file_path': {
|
||||
'description': 'Absolute path to the file to edit (e.g., /workspace/file.py)',
|
||||
'type': 'string',
|
||||
},
|
||||
'old_string': {
|
||||
'description': 'Text to replace. This tool is more forgiving than str_replace and will attempt to correct minor formatting differences.',
|
||||
'type': 'string',
|
||||
},
|
||||
'new_string': {
|
||||
'description': 'Text to replace the old_string with',
|
||||
'type': 'string',
|
||||
},
|
||||
'expected_replacements': {
|
||||
'description': 'Number of replacements expected (default: 1). Use this when you want to replace multiple occurrences.',
|
||||
'type': 'integer',
|
||||
'minimum': 1,
|
||||
'default': 1,
|
||||
},
|
||||
},
|
||||
'required': ['file_path', 'old_string', 'new_string'],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# Tool collection for easy access
|
||||
GEMINI_TOOLS = {
|
||||
GEMINI_READ_FILE_TOOL_NAME: create_gemini_read_file_tool,
|
||||
GEMINI_WRITE_FILE_TOOL_NAME: create_gemini_write_file_tool,
|
||||
GEMINI_REPLACE_TOOL_NAME: create_gemini_replace_tool,
|
||||
}
|
||||
|
||||
|
||||
def get_gemini_tools() -> list[ChatCompletionToolParam]:
|
||||
"""Get all Gemini-optimized tools."""
|
||||
return [tool_func() for tool_func in GEMINI_TOOLS.values()]
|
||||
|
||||
|
||||
def is_gemini_tool(tool_name: str) -> bool:
|
||||
"""Check if a tool name is a Gemini-optimized tool."""
|
||||
return tool_name in GEMINI_TOOLS
|
||||
@@ -14,9 +14,6 @@ from openhands.events.action.files import (
|
||||
FileEditAction,
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
GeminiReadFileAction,
|
||||
GeminiReplaceAction,
|
||||
GeminiWriteFileAction,
|
||||
)
|
||||
from openhands.events.action.mcp import MCPAction
|
||||
from openhands.events.action.message import MessageAction, SystemMessageAction
|
||||
@@ -30,9 +27,6 @@ __all__ = [
|
||||
'FileReadAction',
|
||||
'FileWriteAction',
|
||||
'FileEditAction',
|
||||
'GeminiReadFileAction',
|
||||
'GeminiWriteFileAction',
|
||||
'GeminiReplaceAction',
|
||||
'AgentFinishAction',
|
||||
'AgentRejectAction',
|
||||
'AgentDelegateAction',
|
||||
|
||||
@@ -136,78 +136,3 @@ class FileEditAction(Action):
|
||||
ret += 'Undo Edit\n'
|
||||
# We ignore "view" command because it will be mapped to a FileReadAction
|
||||
return ret
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeminiReadFileAction(Action):
|
||||
"""Gemini-optimized file reading action with enhanced error handling."""
|
||||
|
||||
absolute_path: str
|
||||
offset: int | None = None
|
||||
limit: int | None = None
|
||||
thought: str = ''
|
||||
action: str = 'gemini_read_file'
|
||||
runnable: ClassVar[bool] = True
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
range_info = ''
|
||||
if self.offset is not None:
|
||||
range_info = f' (from line {self.offset}'
|
||||
if self.limit is not None:
|
||||
range_info += f', {self.limit} lines'
|
||||
range_info += ')'
|
||||
return f'Reading file with Gemini tools: {self.absolute_path}{range_info}'
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeminiWriteFileAction(Action):
|
||||
"""Gemini-optimized file writing action with content validation."""
|
||||
|
||||
file_path: str
|
||||
content: str
|
||||
thought: str = ''
|
||||
action: str = 'gemini_write_file'
|
||||
runnable: ClassVar[bool] = True
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f'Writing file with Gemini tools: {self.file_path}'
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f'**GeminiWriteFileAction**\n'
|
||||
f'Path: {self.file_path}\n'
|
||||
f'Thought: {self.thought}\n'
|
||||
f'Content:\n```\n{self.content}\n```\n'
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeminiReplaceAction(Action):
|
||||
"""Gemini-optimized text replacement action with intelligent correction."""
|
||||
|
||||
file_path: str
|
||||
old_string: str
|
||||
new_string: str
|
||||
expected_replacements: int = 1
|
||||
thought: str = ''
|
||||
action: str = 'gemini_replace'
|
||||
runnable: ClassVar[bool] = True
|
||||
security_risk: ActionSecurityRisk | None = None
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f'Replacing text in file with Gemini tools: {self.file_path}'
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f'**GeminiReplaceAction**\n'
|
||||
f'Path: {self.file_path}\n'
|
||||
f'Expected Replacements: {self.expected_replacements}\n'
|
||||
f'Thought: {self.thought}\n'
|
||||
f'Old String: ```\n{self.old_string}\n```\n'
|
||||
f'New String: ```\n{self.new_string}\n```\n'
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
@@ -8,7 +7,6 @@ from pydantic import SecretStr
|
||||
from openhands.integrations.service_types import (
|
||||
BaseGitService,
|
||||
Branch,
|
||||
Comment,
|
||||
GitService,
|
||||
OwnerType,
|
||||
ProviderType,
|
||||
@@ -675,80 +673,6 @@ class GitLabService(BaseGitService, GitService):
|
||||
# Parse the content to extract triggers from frontmatter
|
||||
return self._parse_microagent_content(response, file_path)
|
||||
|
||||
async def get_issue_comments(
|
||||
self, project_id: str, issue_iid: int, limit: int = 100
|
||||
) -> list[Comment]:
|
||||
"""Get the last n comments for a specific issue.
|
||||
|
||||
Args:
|
||||
project_id: The GitLab project ID (can be numeric ID or URL-encoded path)
|
||||
issue_iid: The issue internal ID (iid) in GitLab
|
||||
limit: Maximum number of comments to retrieve (default: 100)
|
||||
|
||||
Returns:
|
||||
List of Comment objects, ordered by creation date (newest first)
|
||||
|
||||
Raises:
|
||||
UnknownException: If the request fails or the issue is not found
|
||||
"""
|
||||
# URL-encode the project_id if it contains special characters
|
||||
if '/' in str(project_id):
|
||||
encoded_project_id = str(project_id).replace('/', '%2F')
|
||||
else:
|
||||
encoded_project_id = str(project_id)
|
||||
|
||||
url = f'{self.BASE_URL}/projects/{encoded_project_id}/issues/{issue_iid}/notes'
|
||||
|
||||
all_comments: list[Comment] = []
|
||||
page = 1
|
||||
per_page = min(limit, 100) # GitLab API max per_page is 100
|
||||
|
||||
while len(all_comments) < limit:
|
||||
# Get comments with pagination, ordered by creation date descending
|
||||
params = {
|
||||
'per_page': per_page,
|
||||
'page': page,
|
||||
'order_by': 'created_at',
|
||||
'sort': 'desc', # Get newest comments first
|
||||
}
|
||||
|
||||
response, headers = await self._make_request(url, params)
|
||||
|
||||
if not response: # No more comments
|
||||
break
|
||||
|
||||
# Filter out system comments and convert to Comment objects
|
||||
for comment_data in response:
|
||||
if len(all_comments) >= limit:
|
||||
break
|
||||
|
||||
# Skip system-generated comments unless explicitly requested
|
||||
if comment_data.get('system', False):
|
||||
continue
|
||||
|
||||
comment = Comment(
|
||||
id=comment_data['id'],
|
||||
body=comment_data['body'],
|
||||
author=comment_data.get('author', {}).get('username', 'unknown'),
|
||||
created_at=datetime.fromisoformat(
|
||||
comment_data['created_at'].replace('Z', '+00:00')
|
||||
),
|
||||
updated_at=datetime.fromisoformat(
|
||||
comment_data['updated_at'].replace('Z', '+00:00')
|
||||
),
|
||||
system=comment_data.get('system', False),
|
||||
)
|
||||
all_comments.append(comment)
|
||||
|
||||
# Check if we have more pages
|
||||
link_header = headers.get('Link', '')
|
||||
if 'rel="next"' not in link_header or len(all_comments) >= limit:
|
||||
break
|
||||
|
||||
page += 1
|
||||
|
||||
return all_comments
|
||||
|
||||
|
||||
gitlab_service_cls = os.environ.get(
|
||||
'OPENHANDS_GITLAB_SERVICE_CLS',
|
||||
|
||||
@@ -140,15 +140,6 @@ class Repository(BaseModel):
|
||||
main_branch: str | None = None # The main/default branch of the repository
|
||||
|
||||
|
||||
class Comment(BaseModel):
|
||||
id: int
|
||||
body: str
|
||||
author: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
system: bool = False # Whether this is a system-generated comment
|
||||
|
||||
|
||||
class AuthenticationError(ValueError):
|
||||
"""Raised when there is an issue with GitHub authentication."""
|
||||
|
||||
|
||||
@@ -5,8 +5,3 @@ STR_REPLACE_EDITOR_TOOL_NAME = 'str_replace_editor'
|
||||
BROWSER_TOOL_NAME = 'browser'
|
||||
FINISH_TOOL_NAME = 'finish'
|
||||
LLM_BASED_EDIT_TOOL_NAME = 'edit_file'
|
||||
|
||||
# Gemini-optimized tools
|
||||
GEMINI_READ_FILE_TOOL_NAME = 'gemini_read_file'
|
||||
GEMINI_WRITE_FILE_TOOL_NAME = 'gemini_write_file'
|
||||
GEMINI_REPLACE_TOOL_NAME = 'gemini_replace'
|
||||
|
||||
@@ -217,19 +217,22 @@ class ConversationMemory:
|
||||
tool call results are available.
|
||||
"""
|
||||
# create a regular message from an event
|
||||
if isinstance(
|
||||
action,
|
||||
(
|
||||
AgentDelegateAction,
|
||||
AgentThinkAction,
|
||||
IPythonRunCellAction,
|
||||
FileEditAction,
|
||||
FileReadAction,
|
||||
BrowseInteractiveAction,
|
||||
BrowseURLAction,
|
||||
MCPAction,
|
||||
),
|
||||
) or (isinstance(action, CmdRunAction) and action.source == 'agent'):
|
||||
if (
|
||||
isinstance(
|
||||
action,
|
||||
(
|
||||
AgentDelegateAction,
|
||||
AgentThinkAction,
|
||||
IPythonRunCellAction,
|
||||
FileEditAction,
|
||||
BrowseInteractiveAction,
|
||||
BrowseURLAction,
|
||||
MCPAction,
|
||||
),
|
||||
)
|
||||
or (isinstance(action, CmdRunAction) and action.source == 'agent')
|
||||
or (isinstance(action, FileReadAction) and action.source == 'agent')
|
||||
):
|
||||
tool_metadata = action.tool_call_metadata
|
||||
assert tool_metadata is not None, (
|
||||
'Tool call metadata should NOT be None when function calling is enabled. Action: '
|
||||
@@ -309,6 +312,14 @@ class ConversationMemory:
|
||||
content=content,
|
||||
)
|
||||
]
|
||||
elif isinstance(action, FileReadAction) and action.source == 'user':
|
||||
content = [TextContent(text=f'User requested to read file: {action.path}')]
|
||||
return [
|
||||
Message(
|
||||
role='user',
|
||||
content=content,
|
||||
)
|
||||
]
|
||||
elif isinstance(action, SystemMessageAction):
|
||||
# Convert SystemMessageAction to a system message
|
||||
return [
|
||||
|
||||
@@ -27,11 +27,6 @@ from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.security import APIKeyHeader
|
||||
from openhands_aci.editor.editor import OHEditor
|
||||
from openhands_aci.editor.exceptions import ToolError
|
||||
from openhands_aci.editor.gemini_tools import (
|
||||
execute_gemini_read_file,
|
||||
execute_gemini_replace,
|
||||
execute_gemini_write_file,
|
||||
)
|
||||
from openhands_aci.editor.results import ToolResult
|
||||
from openhands_aci.utils.diff import get_diff
|
||||
from pydantic import BaseModel
|
||||
@@ -50,9 +45,6 @@ from openhands.events.action import (
|
||||
FileEditAction,
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
GeminiReadFileAction,
|
||||
GeminiReplaceAction,
|
||||
GeminiWriteFileAction,
|
||||
IPythonRunCellAction,
|
||||
)
|
||||
from openhands.events.event import FileEditSource, FileReadSource
|
||||
@@ -586,66 +578,6 @@ class ActionExecutor:
|
||||
),
|
||||
)
|
||||
|
||||
async def gemini_read_file(self, action: GeminiReadFileAction) -> Observation:
|
||||
"""Handle Gemini-optimized file reading."""
|
||||
try:
|
||||
result_str, (old_content, new_content) = execute_gemini_read_file(
|
||||
absolute_path=action.absolute_path,
|
||||
offset=action.offset,
|
||||
limit=action.limit,
|
||||
)
|
||||
|
||||
return FileReadObservation(
|
||||
content=result_str,
|
||||
path=action.absolute_path,
|
||||
impl_source=FileReadSource.DEFAULT,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Error in Gemini read file: {e}')
|
||||
return ErrorObservation(str(e))
|
||||
|
||||
async def gemini_write_file(self, action: GeminiWriteFileAction) -> Observation:
|
||||
"""Handle Gemini-optimized file writing."""
|
||||
try:
|
||||
result_str, (old_content, new_content) = execute_gemini_write_file(
|
||||
file_path=action.file_path,
|
||||
content=action.content,
|
||||
)
|
||||
|
||||
return FileWriteObservation(
|
||||
content=result_str,
|
||||
path=action.file_path,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Error in Gemini write file: {e}')
|
||||
return ErrorObservation(str(e))
|
||||
|
||||
async def gemini_replace(self, action: GeminiReplaceAction) -> Observation:
|
||||
"""Handle Gemini-optimized text replacement."""
|
||||
try:
|
||||
result_str, (old_content, new_content) = execute_gemini_replace(
|
||||
file_path=action.file_path,
|
||||
old_string=action.old_string,
|
||||
new_string=action.new_string,
|
||||
expected_replacements=action.expected_replacements,
|
||||
)
|
||||
|
||||
return FileEditObservation(
|
||||
content=result_str,
|
||||
path=action.file_path,
|
||||
old_content=action.old_string,
|
||||
new_content=action.new_string,
|
||||
impl_source=FileEditSource.OH_ACI,
|
||||
diff=get_diff(
|
||||
old_contents=old_content or '',
|
||||
new_contents=new_content or '',
|
||||
filepath=action.file_path,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Error in Gemini replace: {e}')
|
||||
return ErrorObservation(str(e))
|
||||
|
||||
async def browse(self, action: BrowseURLAction) -> Observation:
|
||||
if self.browser is None:
|
||||
return ErrorObservation(
|
||||
|
||||
@@ -11,6 +11,7 @@ from openhands.core.message import ImageContent, Message, TextContent
|
||||
from openhands.events.action import (
|
||||
AgentFinishAction,
|
||||
CmdRunAction,
|
||||
FileReadAction,
|
||||
MessageAction,
|
||||
)
|
||||
from openhands.events.action.message import SystemMessageAction
|
||||
@@ -1579,3 +1580,56 @@ def test_process_ipython_observation_with_vision_disabled(
|
||||
message = messages[0]
|
||||
assert len(message.content) == 1
|
||||
assert isinstance(message.content[0], TextContent)
|
||||
|
||||
|
||||
def test_process_events_with_file_read_action_from_user(conversation_memory):
|
||||
"""Test that FileReadAction from user is processed correctly without requiring tool_call_metadata."""
|
||||
# Create a FileReadAction from user (no tool_call_metadata)
|
||||
file_read_action = FileReadAction(path='/test/file.txt')
|
||||
file_read_action._source = EventSource.USER
|
||||
|
||||
initial_user_message = MessageAction(content='Initial user message')
|
||||
initial_user_message._source = EventSource.USER
|
||||
|
||||
# Process events - this should not raise an assertion error
|
||||
messages = conversation_memory.process_events(
|
||||
condensed_history=[file_read_action],
|
||||
initial_user_action=initial_user_message,
|
||||
max_message_chars=None,
|
||||
vision_is_active=False,
|
||||
)
|
||||
|
||||
# Check that the messages were processed correctly
|
||||
assert len(messages) == 3 # System + initial user + file read action
|
||||
|
||||
# Check system message
|
||||
assert messages[0].role == 'system'
|
||||
|
||||
# Check initial user message
|
||||
assert messages[1].role == 'user'
|
||||
assert messages[1].content[0].text == 'Initial user message'
|
||||
|
||||
# Check file read action message
|
||||
assert messages[2].role == 'user'
|
||||
assert 'User requested to read file: /test/file.txt' in messages[2].content[0].text
|
||||
|
||||
|
||||
def test_process_events_with_file_read_action_from_agent_requires_metadata(
|
||||
conversation_memory,
|
||||
):
|
||||
"""Test that FileReadAction from agent still requires tool_call_metadata."""
|
||||
# Create a FileReadAction from agent (no tool_call_metadata - should fail)
|
||||
file_read_action = FileReadAction(path='/test/file.txt')
|
||||
file_read_action._source = EventSource.AGENT
|
||||
|
||||
initial_user_message = MessageAction(content='Initial user message')
|
||||
initial_user_message._source = EventSource.USER
|
||||
|
||||
# Process events - this should raise an assertion error for agent actions without metadata
|
||||
with pytest.raises(AssertionError, match='Tool call metadata should NOT be None'):
|
||||
conversation_memory.process_events(
|
||||
condensed_history=[file_read_action],
|
||||
initial_user_action=initial_user_message,
|
||||
max_message_chars=None,
|
||||
vision_is_active=False,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user