Compare commits

..

31 Commits

Author SHA1 Message Date
Engel Nyst aea92f3869 runtime(bash): use dedicated tmux socket to avoid inherited permission issues; initialize _closed safely
Create a fresh, uniquely named tmux socket for each BashSession via libtmux.Server(socket_name=...). This prevents connecting to a root-owned TMUX socket (e.g., /tmp/tmux-0/default) on CI, which caused interactive tests to misbehave.

Also set _closed early and reset on initialize to avoid __del__ AttributeError if initialization fails early.

Co-authored-by: OpenHands-GPT-5 openhands@all-hands.dev
2025-08-18 10:52:29 +00:00
enyst cee88aff48 codeact: expose execute_bash.name for clearer comparisons
- Export a lightweight execute_bash object with a .name attribute
- Switch comparisons and handler map in CodeAct function_calling to use execute_bash.name

Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-17 18:42:56 +00:00
enyst 0c2283abcc feat(codeact tools): encapsulate Bash tool as class; add Tool base; dispatch via handler (#10441)
- Add Tool base class with to_param(), parse_arguments(), to_action()
- Implement CmdRunTool with schema and validation
- Use CmdRunTool in agent tool list and response parser
- Keep create_cmd_run_tool for backward compat

Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-17 17:52:05 +00:00
Xingyao Wang ef3e0c8dfe Fix think observation redundant rendering in frontend (#10409)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-17 10:55:03 +08:00
Engel Nyst 315d391414 Revert "tests: reorganize unit tests into subdirectories mirroring source modules" (#10437) 2025-08-17 00:33:17 +00:00
olyashok 95ef8965b7 Allow user actions over websockets (#10420)
Co-authored-by: Xingyao Wang <xingyaoww@gmail.com>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-08-16 21:29:28 +00:00
Ray Myers ab9fb50c4f fix - Thread-safety in BatchedWebHookFileStore (#10339) 2025-08-16 18:06:40 +00:00
Engel Nyst f866da6bf2 tests: reorganize unit tests into subdirectories mirroring source modules (#10427)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-16 19:13:50 +02:00
Zhonghao Jiang 7229a16b45 feat(evaluation): Add NoCode-bench evaluation script (#10229) 2025-08-16 16:41:22 +00:00
llamantino 19105a2a13 fix(cli): send authentication error resume message to user, not llm (#10421) 2025-08-16 18:01:42 +02:00
Ryan H. Tran fe486ad1f1 Add task tracking tool for long-horizon tasks (#10166)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-08-16 20:05:59 +07:00
Engel Nyst 0ec6ed20cb fix(frontend): browser tab notification respects user-renamed titles; add unit test (#10406) 2025-08-16 07:00:45 +00:00
Xingyao Wang 794381c22b Add "The agent didn't finish the job" feedback reason to Likert scale (#10417) 2025-08-16 00:25:19 -04:00
Tim O'Farrell 0c581ea946 fix(nested_event_store): correct reverse pagination in search_events and add unit test (#10418)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-15 19:29:35 -06:00
Engel Nyst f7f4fcf98f chore(eval): remove old, unused regression test framework under evaluation/regression (#10419) 2025-08-16 01:08:23 +02:00
Xingyao Wang ab004478f6 feat(agent): include a new roleplay-based prompt (#10306)
Co-authored-by: test <test@test.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-16 06:04:28 +08:00
Xingyao Wang 340606e68a microagent: Add /codereview-roasted microagent with Linus Torvalds engineering mindset (#10405)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-15 21:49:57 +00:00
Tim O'Farrell daec23b5d7 Add get_issue_comments method to GitLabService (#10361)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-15 14:55:39 -06:00
Xingyao Wang 587b4c311a runtime(bash): clarify guidance when previous command still running; recommend execute_bash timeout (issue #10350) (#10389)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-16 02:15:05 +08:00
Xingyao Wang 7a86402c9c Add process management guidance to system prompt (#10083)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-16 02:09:20 +08:00
Insop 06d283dfa0 Temp fix for docker file and debug log (#10345) 2025-08-15 15:34:46 +00:00
sp.wack a6a4246e30 fix(frontend): Failing tests (#10369) 2025-08-15 15:18:35 +00:00
Xingyao Wang 4830b9a67d fix(llm): include gpt-5 to fn call model; set top p default value to None (#10363) 2025-08-15 15:08:01 +00:00
Neeraj Panwar d4489d62d7 fix(llm): set AWS credentials in config.toml (#10351) 2025-08-15 22:16:50 +08:00
Ryan H. Tran e41c020073 [CLI] Fix MCP toml formatting issue (#10312)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-15 13:56:46 +00:00
Ryan H. Tran 3f44c8436f Fix swebench modal patch eval intermittent crash (#10353) 2025-08-15 21:51:03 +08:00
Graham Neubig b740944075 Split E2E settings and conversation tests; run settings first in workflow (#10359)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-15 09:19:27 -04:00
dependabot[bot] 5618a3eebb chore(deps): bump the version-all group in /frontend with 9 updates (#10318)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-15 16:51:41 +04:00
Hiep Le a1ffe5c936 fix(frontend): frontend UI keep flashing (#10352) 2025-08-15 12:19:30 +04:00
Hiep Le f8376a9702 fix(frontend): status message missing (#10349) 2025-08-15 12:11:42 +07:00
Tim O'Farrell 985a634d60 Fix for issue where static system commands are truncated (#10292)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-15 04:16:50 +00:00
149 changed files with 5172 additions and 7294 deletions
+1 -1
View File
@@ -183,7 +183,7 @@ jobs:
# Run the tests with detailed output
cd tests/e2e
poetry run python -m pytest test_e2e_workflow.py::test_github_token_configuration test_e2e_workflow.py::test_conversation_start -v --no-header --capture=no --timeout=600
poetry run python -m pytest test_settings.py::test_github_token_configuration test_conversation.py::test_conversation_start -v --no-header --capture=no --timeout=600
- name: Upload test results
if: always()
-180
View File
@@ -1,180 +0,0 @@
# Tool Decoupling Refactoring Plan
## Current State Analysis
**Where we are:**
- New `openhands/tools/` module with unified Tool architecture (✅ committed)
- Existing tools scattered in `openhands/agenthub/codeact_agent/tools/` (old approach)
- Function calling logic hardcoded in `function_calling.py` with manual validation
- Multiple agents (codeact, loc, readonly) each have their own function_calling.py
- Tool schemas defined as dictionaries in individual tool files
**Key Integration Points:**
1. `openhands/agenthub/codeact_agent/function_calling.py` - main function call processor
2. `openhands/agenthub/codeact_agent/codeact_agent.py` - imports tools for schema generation
3. `openhands/agenthub/loc_agent/function_calling.py` - similar pattern
4. `openhands/agenthub/readonly_agent/function_calling.py` - similar pattern
## Target State
**Where we need to get to:**
- All agents use the new Tool classes for consistent behavior
- Function calling delegates to `Tool.validate_function_call()` for parameter validation
- Tool schemas come from `Tool.get_schema()`
- Action creation remains in function_calling.py (simple, no over-abstraction)
- Remove duplicated tool logic across agents
- **No registry needed** - agents directly import and use the tools they need
## Minimal Refactoring Strategy
### Phase 1: Create Bridge Layer (Non-breaking)
**Goal:** Make new tools work alongside existing system without breaking anything
1. **Create tool adapter in function_calling.py**
- Add import for new `openhands.tools` (BashTool, FileEditorTool, etc.)
- Create helper function `validate_with_new_tools()` that attempts new tool validation
- Fall back to existing hardcoded logic if tool not found
- This allows gradual migration without breaking existing functionality
2. **Update tool imports in codeact_agent.py**
- Import new Tool classes alongside existing tool imports
- Modify `get_tools()` method to include schemas from both old and new tools
- Ensure no duplicate tool names
### Phase 2: Migrate Core Tools (One by one)
**Goal:** Replace existing tools with new implementations
1. **Start with bash tool (lowest risk)**
- Update function_calling.py to use BashTool for execute_bash calls
- Remove old bash tool logic once confirmed working
- Keep old bash.py file temporarily for reference
2. **Migrate str_replace_editor tool**
- Update function_calling.py to use FileEditorTool
- Remove complex str_replace_editor logic from function_calling.py
- Keep old str_replace_editor.py temporarily
3. **Migrate remaining tools one by one**
- finish, browser, think, ipython, condensation_request
- Each migration should be a separate commit for easy rollback
### Phase 3: Clean Up (Remove old code)
**Goal:** Remove duplicate/obsolete code
1. **Remove old tool files**
- Delete `openhands/agenthub/codeact_agent/tools/` directory
- Update imports in codeact_agent.py
2. **Simplify function_calling.py**
- Remove all hardcoded tool logic
- Replace with simple registry lookup and delegation
- Should be ~50 lines instead of ~250 lines
### Phase 4: Extend to Other Agents (Optional)
**Goal:** Apply same pattern to loc_agent and readonly_agent
1. **Update loc_agent and readonly_agent**
- Replace their function_calling.py with registry-based approach
- Reuse same tool implementations
## Implementation Details
### Bridge Function (Phase 1)
```python
def validate_with_new_tools(tool_call):
"""Try new tool classes for validation, fall back to old logic"""
from openhands.tools import BashTool, FileEditorTool
# Map tool names to tool instances
tools = {
'execute_bash': BashTool(),
'str_replace_editor': FileEditorTool(),
}
tool = tools.get(tool_call.function.name)
if tool:
try:
return tool.validate_function_call(tool_call.function)
except ToolValidationError as e:
raise FunctionCallValidationError(str(e))
# Fall back to existing hardcoded validation
return None # Signal to use old logic
```
### Simplified function_calling.py (Phase 3)
```python
def response_to_actions(response: ModelResponse, mcp_tool_names: list[str] | None = None) -> list[Action]:
"""Convert LLM response to OpenHands actions using new tool classes"""
from openhands.tools import BashTool, FileEditorTool
# Create tool instances (could be module-level for efficiency)
tools = {
'execute_bash': BashTool(),
'str_replace_editor': FileEditorTool(),
}
actions = []
# ... existing response parsing logic ...
for tool_call in assistant_msg.tool_calls:
tool = tools.get(tool_call.function.name)
if tool:
# Validate parameters using tool
try:
validated_params = tool.validate_function_call(tool_call.function)
except ToolValidationError as e:
raise FunctionCallValidationError(str(e))
# Create action based on tool type (simple logic remains here)
if tool_call.function.name == 'execute_bash':
action = CmdRunAction(command=validated_params['command'], ...)
elif tool_call.function.name == 'str_replace_editor':
action = FileEditAction(path=validated_params['path'], ...)
# ... etc for other tools
actions.append(action)
elif mcp_tool_names and tool_call.function.name in mcp_tool_names:
# Handle MCP tools
actions.append(MCPAction(...))
else:
raise FunctionCallNotExistsError(f'Tool {tool_call.function.name} not found')
return actions
```
## Risk Mitigation
1. **Incremental approach** - Each phase can be tested independently
2. **Backward compatibility** - Bridge layer ensures nothing breaks during transition
3. **Easy rollback** - Each tool migration is a separate commit
4. **Minimal changes** - Don't touch agent logic, only function calling layer
5. **Keep it simple** - Don't over-engineer, just replace existing functionality
## Success Criteria
- [ ] All existing tests pass
- [ ] Function calling behavior unchanged from user perspective
- [ ] Tool logic consolidated in single location
- [ ] Easy to add new tools by extending Tool base class
- [ ] Reduced code duplication across agents
- [ ] Cleaner, more maintainable codebase
## Files to Modify
**Phase 1:**
- `openhands/agenthub/codeact_agent/function_calling.py` (add bridge)
- `openhands/agenthub/codeact_agent/codeact_agent.py` (import registry)
**Phase 2:**
- `openhands/agenthub/codeact_agent/function_calling.py` (migrate tools one by one)
**Phase 3:**
- `openhands/agenthub/codeact_agent/function_calling.py` (simplify)
- Remove `openhands/agenthub/codeact_agent/tools/` directory
**Phase 4 (Optional):**
- `openhands/agenthub/loc_agent/function_calling.py`
- `openhands/agenthub/readonly_agent/function_calling.py`
This plan prioritizes **working incrementally** while **maintaining stability** throughout the refactoring process.
-219
View File
@@ -1,219 +0,0 @@
# OpenHands Tool Decoupling - Complete Implementation Plan
## 🎯 Goal
Decouple AI agent tools into their own classes to encapsulate tool definitions, error validation, and response interpretation separate from regular agent LLM response processing.
## 📊 Current Status: CRITICAL MILESTONE ACHIEVED ✅
**function_calling.py Migration Complete**: Successfully migrated CodeActAgent to use unified tool validation for all 4 core tools!
### 🏗️ Architecture Summary
- **CodeActAgent**: 4 base tools (BashTool, FileEditorTool, BrowserTool, FinishTool)
- **ReadOnlyAgent**: Inherits FinishTool + adds 3 safe tools (ViewTool, GrepTool, GlobTool)
- **LocAgent**: Inherits all CodeAct tools + adds 3 search tools (SearchEntityTool, SearchRepoTool, ExploreStructureTool)
### 🚀 Migration Achievement: function_calling.py Complete
-**Fixed legacy tool import conflicts** with proper aliasing (LegacyBrowserTool, LegacyFinishTool)
-**Updated BrowserTool interface** to match legacy (code parameter instead of action)
-**All 4 core tools using unified validation**:
- BashTool: `validate_parameters()` with proper error handling
- FinishTool: `validate_parameters()` with parameter mapping (summary/outputs)
- FileEditorTool: `validate_parameters()` with command handling (view/edit)
- BrowserTool: `validate_parameters()` with code parameter validation
-**Fixed tool name constant references** throughout function_calling.py
-**Created comprehensive integration tests** verifying tool validation works
-**Maintained backward compatibility** with legacy fallback paths
### 🧪 Testing Status
- **192 total tests** (all passing)
- **Integration tests passing** for all 4 core tools
- **163 original tests**: Base Tool class, validation, error handling, inheritance patterns
- **29 new LocAgent tests**: Complete coverage of search tools and inheritance
### 🔧 Implementation Status
-**Tool base class** with abstract methods and validation framework
-**CodeAct tools** with full parameter validation and schema generation
-**ReadOnly tools** with inheritance pattern and safety validation
-**LocAgent tools** with complex parameter validation and search capabilities
-**Comprehensive test suite** covering all tools and edge cases
-**CodeActAgent function_calling.py migration** with unified tool validation
## Architecture Decision: Agent-Specific Tool Organization
After exploring the codebase, we discovered that **agent-specific tool organization** is the correct approach because:
1. **CodeActAgent** is the base agent with comprehensive tools (bash, file editing, browsing, etc.)
2. **ReadOnlyAgent** and **LocAgent** inherit from CodeActAgent but completely override `_get_tools()`
3. Each agent has its own `tools/` directory and `function_calling.py` module
4. Child agents can selectively inherit parent tools and add their own
## Current Architecture
```
openhands/agenthub/codeact_agent/tools/unified/
├── __init__.py # Exports all CodeAct tools
├── base.py # Tool base class with validation
├── bash_tool.py # Full bash access
├── file_editor_tool.py # File editing capabilities
├── browser_tool.py # Web browsing
└── finish_tool.py # Task completion
openhands/agenthub/readonly_agent/tools/unified/
├── __init__.py # Imports FinishTool from CodeAct + own tools
├── view_tool.py # Safe file/directory viewing
├── grep_tool.py # Safe text search
└── glob_tool.py # Safe file pattern matching
openhands/agenthub/loc_agent/tools/unified/
└── [TODO] Inherit from CodeAct + add search tools
```
## Implementation Status
### ✅ COMPLETED (Phase 1: Tool Architecture)
- [x] Base Tool class with schema definition and parameter validation
- [x] CodeAct unified tools (BashTool, FileEditorTool, BrowserTool, FinishTool)
- [x] ReadOnly unified tools (ViewTool, GrepTool, GlobTool)
- [x] Inheritance pattern: ReadOnly imports FinishTool from CodeAct parent
- [x] Parameter validation with comprehensive error handling
- [x] Schema generation compatible with LiteLLM function calling
### ✅ COMPLETED (Phase 2: Tool Architecture & Testing)
- [x] **Comprehensive unit tests** (192 tests, all passing)
- [x] **LocAgent tool organization** (inherit from CodeAct + add search tools)
- [x] All agent-specific tool architectures complete
### 🔄 IN PROGRESS (Phase 3: Integration & Migration)
-**CodeActAgent function_calling.py migration** (COMPLETED!)
- [ ] ReadOnlyAgent function_calling.py migration (NEXT)
- [ ] LocAgent function_calling.py migration (NEXT)
### 📋 TODO (Phase 3: Full Migration)
- [ ] Remove old tool definitions after migration complete
- [ ] Documentation and cleanup
- [ ] Performance testing and optimization
## Detailed Implementation Plan
### Phase 2: Testing & Integration (CURRENT)
#### 2.1 Comprehensive Unit Tests (IMMEDIATE)
Create `tests/unit/tools/` with complete test coverage:
**Base Infrastructure Tests:**
- `test_base_tool.py` - Tool base class, validation, error handling
- `test_tool_inheritance.py` - Agent inheritance patterns
**CodeAct Tool Tests:**
- `test_bash_tool.py` - BashTool schema and validation
- `test_file_editor_tool.py` - FileEditorTool schema and validation
- `test_browser_tool.py` - BrowserTool schema and validation
- `test_finish_tool.py` - FinishTool schema and validation
**ReadOnly Tool Tests:**
- `test_view_tool.py` - ViewTool schema and validation
- `test_grep_tool.py` - GrepTool schema and validation
- `test_glob_tool.py` - GlobTool schema and validation
**Integration Tests:**
- `test_agent_tool_integration.py` - Agent-specific tool loading
- `test_function_call_validation.py` - End-to-end function call processing
#### 2.2 Bridge Layer Implementation
- Create adapter functions in each agent's function_calling.py
- Gradual migration: new tools alongside existing ones
- Validation layer that uses new Tool classes
#### 2.3 Integration Points
- Update `openhands/agenthub/codeact_agent/function_calling.py`
- Update `openhands/agenthub/readonly_agent/function_calling.py`
- Ensure backward compatibility during transition
### Phase 3: Full Migration
#### 3.1 LocAgent Tool Organization ✅
```
openhands/agenthub/loc_agent/tools/unified/
├── __init__.py # Inherit from CodeAct + add search tools
├── search_entity_tool.py # SearchEntityTool for entity retrieval
├── search_repo_tool.py # SearchRepoTool for code snippet search
└── explore_structure_tool.py # ExploreStructureTool for dependency analysis
```
#### 3.2 Complete Migration
- Replace all old tool definitions with new unified classes
- Update all function_calling.py modules
- Remove legacy tool code
- Update agent `_get_tools()` methods to use new architecture
#### 3.3 Cleanup & Documentation
- Remove unused tool files
- Update documentation
- Add migration guide for future tool additions
## Key Benefits of This Architecture
1. **Encapsulation**: Tool logic separated from agent processing
2. **Inheritance**: Child agents can reuse parent tools selectively
3. **Validation**: Centralized parameter validation with clear error messages
4. **Extensibility**: Easy to add new tools or modify existing ones
5. **Type Safety**: Proper typing and schema validation
6. **Testing**: Each tool can be unit tested independently
## Testing Strategy
### Unit Test Coverage Requirements
- **Schema Generation**: Verify correct LiteLLM-compatible schemas
- **Parameter Validation**: Test all validation rules and edge cases
- **Error Handling**: Test all error conditions and messages
- **Inheritance**: Verify child agents can inherit and extend parent tools
- **Integration**: Test function call processing end-to-end
### Test Categories
1. **Positive Tests**: Valid inputs produce expected outputs
2. **Negative Tests**: Invalid inputs produce appropriate errors
3. **Edge Cases**: Boundary conditions, empty values, type mismatches
4. **Integration Tests**: Agent-tool interaction, function calling flow
## Migration Strategy
1. **Parallel Implementation**: New tools alongside existing ones
2. **Gradual Adoption**: Migrate one agent at a time
3. **Backward Compatibility**: Maintain existing functionality during transition
4. **Validation**: Comprehensive testing at each step
5. **Cleanup**: Remove old code only after full migration
## Success Criteria
- [ ] All agents use unified tool architecture
- [ ] 100% test coverage for tool functionality
- [ ] No regression in existing functionality
- [ ] Clear separation of concerns between tools and agents
- [ ] Easy to add new tools or modify existing ones
- [ ] Comprehensive error handling and validation
## Current State Summary
**MAJOR MILESTONE ACHIEVED**: ReadOnlyAgent function_calling.py migration complete!
### Phase 2 Complete: Agent-Specific Tool Implementation ✅
- **CodeActAgent tools**: 4 unified tools (BashTool, FileEditorTool, BrowserTool, FinishTool)
- **ReadOnlyAgent tools**: 4 unified tools (ViewTool, GrepTool, GlobTool, FinishTool inherited)
- **LocAgent tools**: 3 specialized tools + all CodeAct tools inherited
- **All 192 tests passing** (163 original + 29 LocAgent tests)
### Phase 3 In Progress: function_calling.py Migration 🔄
- **CodeActAgent function_calling.py**: ✅ COMPLETE (unified validation for all 4 tools)
- **ReadOnlyAgent function_calling.py**: ✅ COMPLETE (unified validation for all 4 tools)
- **LocAgent function_calling.py**: ⏳ PENDING (next step)
### Architecture Summary
- **Tool Classes**: Encapsulate schema definition and parameter validation
- **Inheritance Pattern**: Child agents import parent tools + add their own
- **Validation Strategy**: Unified validation with legacy fallbacks
- **Error Handling**: Comprehensive ToolValidationError system
- **Testing**: 192 comprehensive unit tests covering all scenarios
**CURRENT**: LocAgent function_calling.py migration
**NEXT**: Final integration testing and cleanup
**GOAL**: Complete tool decoupling with zero regression
@@ -0,0 +1,45 @@
# Evaluate OpenHands on NoCode-bench
## LLM Setup
Please follow [here](../../README.md#setup).
## Docker image download
Evaluating OpenHands on NoCode-bench need instance-level docker image.
Please follow the instructions of NoCode-bench image setup to build or download all instance-level dokcer [here](https://github.com/NoCode-bench/NoCode-bench).
## Generate patch
Please follow the instructions [here](../swe_bench/README.md#running-locally-with-docker)
For example,
```bash
bash ./evaluation/benchmarks/nocode_bench/scripts/run_infer_nc.sh llm.claude HEAD CodeActAgent 114 100 10 NoCode-bench/NoCode-bench_Verified test
```
The results will be generated in evaluation/evaluation_outputs/outputs/XXX/CodeActAgent/YYY/output.jsonl.
## Runing evaluation
First, install [NoCode-bench](https://github.com/NoCode-bench/NoCode-bench).
Second, convert the output.jsonl to patch.jsonl with [script](scripts/eval/convert.py).
```bash
python evaluation/benchmarks/multi_swe_bench/scripts/eval/convert.py
```
Finally, evaluate with NoCode-bench.
```bash
export PYTHONPATH=$PYTHONPATH:$(pwd)
python ./evaluation/eval.py \
--predictions_path ./all_preds.jsonl \ # <path_to_your_predictions>
--log_dir ./evaluation/logs \ # <path_to_your_log_dir>
--bench_tasks NoCode-bench/NoCode-bench_Verified \ # <dataset_name>
--max_workers 110 \ # <number_of_workers>
--output_file eval_result.txt \ # <path_to_your_output_file>
--image_level repo \ # <cache_image_level>
--timeout 600 \ # <timeout_in_seconds>
--proxy None # <proxy_if_needed>
```
@@ -0,0 +1,52 @@
"""
Utilities for handling binary files and patch generation in SWE-bench evaluation.
"""
def remove_binary_diffs(patch_text):
"""
Remove binary file diffs from a git patch.
Args:
patch_text (str): The git patch text
Returns:
str: The cleaned patch text with binary diffs removed
"""
lines = patch_text.splitlines()
cleaned_lines = []
block = []
is_binary_block = False
for line in lines:
if line.startswith('diff --git '):
if block and not is_binary_block:
cleaned_lines.extend(block)
block = [line]
is_binary_block = False
elif 'Binary files' in line:
is_binary_block = True
block.append(line)
else:
block.append(line)
if block and not is_binary_block:
cleaned_lines.extend(block)
return '\n'.join(cleaned_lines)
def remove_binary_files_from_git():
"""
Generate a bash command to remove binary files from git staging.
Returns:
str: A bash command that removes binary files from git staging
"""
return """
for file in $(git status --porcelain | grep -E "^(M| M|\\?\\?|A| A)" | cut -c4-); do
if [ -f "$file" ] && (file "$file" | grep -q "executable" || git check-attr binary "$file" | grep -q "binary: set"); then
git rm -f "$file" 2>/dev/null || rm -f "$file"
echo "Removed: $file"
fi
done
""".strip()
@@ -0,0 +1,545 @@
DOCPATH_PATTERNS = [
r'docs/',
r'^CHANGES\.rst$',
r'doc/',
r'ChangeLog',
r'^changelog/',
r'^CHANGES$',
]
MATPLOTLIB_CONFIG = {
k: {
'python': '3.11',
'conda_env': 'matplotlib_35',
'install': 'python -m pip install -e .',
'test_cmd': 'pytest -rA --color=no',
}
for k in ['3.5', '3.6', '3.7', '3.8', '3.9']
}
MATPLOTLIB_CONFIG.update(
{
k: {
'python': '3.8',
'conda_env': 'matplotlib_31',
'install': 'python -m pip install -e .',
'test_cmd': 'pytest -rA --color=no',
}
for k in ['3.1', '3.2', '3.3', '3.4']
}
)
MATPLOTLIB_CONFIG.update(
{
k: {
'python': '3.5',
'install': 'python setup.py build; python setup.py install',
'conda_env': 'matplotlib_11',
'nonroot': True,
'test_cmd': 'pytest -rA --color=no',
}
for k in ['2.0', '2.1', '2.2', '1.0', '1.1', '1.2', '1.3', '1.4', '1.5']
}
)
for k in ['3.8', '3.9']:
MATPLOTLIB_CONFIG[k]['install'] = (
'python -m pip install --no-build-isolation -e ".[dev]"'
)
SYMPY_CONFIG = {}
SYMPY_CONFIG.update(
{
'1.0': {
'conda_env': 'sympy_10',
'install': 'pip install -e .',
'test_cmd': 'bin/test -C -v',
# testfile -k testname
}
}
)
REQUESTS_CONFIG = {}
REQUESTS_CONFIG.update(
{
k: {
'conda_env': 'requests_227',
'install': 'pip install -r requirements-dev.txt',
'test_cmd': 'pytest -rA',
}
for k in ['2.27']
}
)
REQUESTS_CONFIG.update(
{
k: {
'conda_env': 'requests_226',
'install': 'pip install -e .',
'test_cmd': 'pytest -rA',
}
for k in ['2.26']
}
)
PYTEST_CONFIG = {}
PYTEST_CONFIG.update(
{
k: {
'conda_env': 'pytest_33',
'install': 'pip install -e .',
'test_cmd': 'pytest -v --color=no',
}
for k in ['4.4', '4.1', '3.7', '3.4', '3.3']
}
)
PYLINT_CONFIG = {}
PYLINT_CONFIG.update(
{
k: {
'conda_env': 'pylint_210',
'install': 'pip install -r requirements_test.txt',
'test_cmd': 'pytest -rA --color=no',
}
for k in [
'2.10',
'2.11',
'2.13',
'2.14',
'2.15',
'2.16',
'2.17',
'3.0',
'3.1',
'3.2',
'3.3',
]
}
)
PYLINT_CONFIG.update(
{
k: {
'conda_env': 'pylint_210',
'pre_install': [
r"sed -i 's/setuptools==[0-9.]\+/setuptools==58.0.0/' requirements_test_min.txt"
],
'install': 'pip install -r requirements_test.txt',
'test_cmd': 'pytest -rA --color=no',
}
for k in ['3.0', '3.1', '3.2', '3.3']
}
)
ASTROPY_CONFIG = {}
ASTROPY_CONFIG.update(
{
k: {
'conda_env': 'astropy_11',
'install': 'python -m pip install -e .[test] --verbose',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['1.1', '1.2', '1.3', '2.0']
}
)
ASTROPY_CONFIG.update(
{
k: {
'conda_env': 'astropy_30',
'pre_install': """echo '[pytest]
filterwarnings =
ignore::DeprecationWarning' > pytest.ini""",
'install': 'python -m pip install -e .[test] --verbose',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['3.0', '3.1', '3.2']
}
)
ASTROPY_CONFIG.update(
{
k: {
'conda_env': 'astropy_40',
'pre_install': [
r"""sed -i 's/requires = \["setuptools",/requires = \["setuptools==68.0.0",/' pyproject.toml"""
],
'install': 'python -m pip install -e .[test] --verbose',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['4.0']
}
)
ASTROPY_CONFIG.update(
{
k: {
'conda_env': 'astropy_41',
'pre_install': [
r"""sed -i 's/requires = \["setuptools",/requires = \["setuptools==68.0.0",/' pyproject.toml""",
"""sed -i 's/^qt_no_exception_capture = 1$/; qt_no_exception_capture = 1/' setup.cfg""",
r"""sed -i '/setuptools==68.0.0",/a \ "markupsafe==2.0.1",' pyproject.tomlsed -i '/setuptools==68.0.0",/a \ "markupsafe==2.0.1",' pyproject.toml""",
],
'install': 'python -m pip install -e .[test] --verbose',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['4.1']
}
)
ASTROPY_CONFIG.update(
{
k: {
'conda_env': 'astropy_42',
'pre_install': [
r"""sed -i 's/requires = \["setuptools",/requires = \["setuptools==68.0.0",/' pyproject.toml""",
r"""sed -i '/setuptools==68.0.0",/a \ "markupsafe==2.0.1",' pyproject.tomlsed -i '/setuptools==68.0.0",/a \ "markupsafe==2.0.1",' pyproject.toml""",
],
'install': 'python -m pip install -e .[test] --verbose',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['4.2', '4.3', '5.0', '5.1']
}
)
ASTROPY_CONFIG.update(
{
k: {
'conda_env': 'astropy_52',
'pre_install': [
r"""sed -i 's/requires = \["setuptools",/requires = \["setuptools==68.0.0",/' pyproject.toml"""
],
'install': 'python -m pip install -e .[test] --verbose',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['5.2', '5.3', '6.0', '6.1', '7.0']
}
)
DJANGO_CONFIG = {}
DJANGO_CONFIG.update(
{
k: {
'install': 'pip install -e .',
'conda_env': 'django_22',
'test_cmd': 'python tests/runtests.py --verbosity 2',
}
for k in ['1.9', '2.2']
}
)
DJANGO_CONFIG.update(
{
'3.2': {
'install': 'pip install -e .',
'conda_env': 'django_32',
'test_cmd': 'python tests/runtests.py --verbosity 2',
},
'4.2': {
'install': 'pip install -e .',
'conda_env': 'django_42',
'test_cmd': 'python tests/runtests.py --verbosity 2',
},
'5.1': {
'install': 'pip install -e .',
'conda_env': 'django_51',
'test_cmd': 'python tests/runtests.py --verbosity 2',
},
}
)
SPHINX_CONFIG = {}
SPHINX_CONFIG.update(
{ # 1.x 版本问题,实际无用
k: {
'conda_env': 'sphinx_20',
'install': 'python -m pip install -e .[test]',
'pre_install': ["sed -i 's/pytest/pytest -rA/' tox.ini"],
'test_cmd': 'tox --current-env -epy37 -v --',
}
for k in ['1.3', '1.4', '1.5', '1.6', '1.7', '1.8']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_20',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
"sed -i 's/Jinja2>=2.3/Jinja2<3.0/' setup.py",
],
'test_cmd': 'tox --current-env -epy37 -v --',
}
for k in ['2.0', '2.1', '2.2', '2.3', '2.4']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_30',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
"sed -i 's/Jinja2>=2.3/Jinja2<3.0/' setup.py",
"sed -i 's/sphinxcontrib-applehelp/sphinxcontrib-applehelp<=1.0.7/' setup.py",
"sed -i 's/sphinxcontrib-devhelp/sphinxcontrib-devhelp<=1.0.5/' setup.py",
"sed -i 's/sphinxcontrib-qthelp/sphinxcontrib-qthelp<=1.0.6/' setup.py",
"sed -i 's/alabaster>=0.7,<0.8/alabaster>=0.7,<0.7.12/' setup.py",
"sed -i \"s/'packaging',/'packaging', 'markupsafe<=2.0.1',/\" setup.py",
"sed -i 's/sphinxcontrib-htmlhelp/sphinxcontrib-htmlhelp<=2.0.4/' setup.py",
"sed -i 's/sphinxcontrib-serializinghtml/sphinxcontrib-serializinghtml<=1.1.9/' setup.py",
],
'test_cmd': 'tox --current-env -epy37 -v --',
}
for k in ['3.0', '3.1', '3.2', '3.3', '3.4', '3.5', '4.0']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_30',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
"sed -i 's/Jinja2>=2.3/Jinja2<3.0/' setup.py",
"sed -i 's/sphinxcontrib-applehelp/sphinxcontrib-applehelp<=1.0.7/' setup.py",
"sed -i 's/sphinxcontrib-devhelp/sphinxcontrib-devhelp<=1.0.5/' setup.py",
"sed -i 's/sphinxcontrib-qthelp/sphinxcontrib-qthelp<=1.0.6/' setup.py",
"sed -i 's/alabaster>=0.7,<0.8/alabaster>=0.7,<0.7.12/' setup.py",
"sed -i \"s/'packaging',/'packaging', 'markupsafe<=2.0.1',/\" setup.py",
(
"grep -q 'sphinxcontrib-htmlhelp>=2.0.0' setup.py && "
"sed -i 's/sphinxcontrib-htmlhelp>=2.0.0/sphinxcontrib-htmlhelp>=2.0.0,<=2.0.4/' setup.py || "
"sed -i 's/sphinxcontrib-htmlhelp/sphinxcontrib-htmlhelp<=2.0.4/' setup.py"
),
(
"grep -q 'sphinxcontrib-serializinghtml>=1.1.5' setup.py && "
"sed -i 's/sphinxcontrib-serializinghtml>=1.1.5/sphinxcontrib-serializinghtml>=1.1.5,<=1.1.9/' setup.py || "
"sed -i 's/sphinxcontrib-serializinghtml/sphinxcontrib-serializinghtml<=1.1.9/' setup.py"
),
],
'test_cmd': 'tox --current-env -epy37 -v --',
}
for k in ['4.1']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_30',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
"sed -i 's/Jinja2>=2.3/Jinja2<3.0/' setup.py",
"sed -i 's/sphinxcontrib-applehelp/sphinxcontrib-applehelp<=1.0.7/' setup.py",
"sed -i 's/sphinxcontrib-devhelp/sphinxcontrib-devhelp<=1.0.5/' setup.py",
"sed -i 's/sphinxcontrib-qthelp/sphinxcontrib-qthelp<=1.0.6/' setup.py",
"sed -i 's/alabaster>=0.7,<0.8/alabaster>=0.7,<0.7.12/' setup.py",
"sed -i \"s/'packaging',/'packaging', 'markupsafe<=2.0.1',/\" setup.py",
"sed -i 's/sphinxcontrib-htmlhelp>=2.0.0/sphinxcontrib-htmlhelp>=2.0.0,<=2.0.4/' setup.py",
"sed -i 's/sphinxcontrib-serializinghtml>=1.1.5/sphinxcontrib-serializinghtml>=1.1.5,<=1.1.9/' setup.py",
],
'test_cmd': 'tox --current-env -epy37 -v --',
}
for k in ['4.2', '4.3', '4.4']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_30',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
],
'test_cmd': 'tox --current-env -epy37 -v --',
}
for k in ['4.5', '5.0', '5.1', '5.2']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_60',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
],
'test_cmd': 'tox --current-env -epy39 -v --',
}
for k in ['6.0', '6.2', '7.0', '7.1']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_72',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
'apt-get update && apt-get install -y graphviz',
],
'test_cmd': 'tox --current-env -epy39 -v --',
}
for k in ['7.2', '7.3', '7.4']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_80',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
],
'test_cmd': 'tox --current-env -epy310 -v --',
}
for k in ['8.0', '8.1']
}
)
SKLEARN_CONFIG = {}
SKLEARN_CONFIG.update(
{
k: {
'conda_env': 'skl_020',
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['0.20', '0.21', '0.22']
}
)
SKLEARN_CONFIG.update(
{
k: {
'conda_env': 'skl_100',
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['0.23', '0.24', '1.00', '1.01', '1.02']
}
)
SKLEARN_CONFIG.update(
{
k: {
'conda_env': 'skl_104',
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['1.03', '1.04', '1.05']
}
)
SEABORN_CONFIG = {}
SEABORN_CONFIG.update(
{
k: {
'conda_env': 'seaborn_010',
'install': 'pip install -e .[dev]',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['0.3', '0.4', '0.5', '0.6', '0.11', '0.12', '0.13', '0.14']
}
)
XARRAY_CONFIG = {}
XARRAY_CONFIG.update(
{
k: {
'conda_env': 'xarray_0014',
'install': 'pip install -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['0014', '0015', '0016']
}
)
XARRAY_CONFIG.update(
{
k: {
'conda_env': 'xarray_0017',
'install': 'pip install -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['0017', '0018', '0019', '0020', '0021']
}
)
XARRAY_CONFIG.update(
{
k: {
'conda_env': 'xarray_2203',
'install': 'pip install -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['2203', '2206', '2209', '2210', '2211', '2212']
}
)
XARRAY_CONFIG.update(
{
k: {
'conda_env': 'xarray_2303',
'install': 'pip install -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in [
'2303',
'2304',
'2305',
'2306',
'2308',
'2309',
'2310',
'2311',
'2312',
]
}
)
XARRAY_CONFIG.update(
{
k: {
'conda_env': 'xarray_2401',
'install': 'pip install -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['2401', '2402', '2403', '2405', '2407', '2409', '2410', '2411']
}
)
SKLEARN_CONFIG = {}
SKLEARN_CONFIG.update(
{
k: {
'conda_env': 'skl_020',
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['0.20', '0.21', '0.22']
}
)
SKLEARN_CONFIG.update(
{
k: {
'conda_env': 'skl_100',
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['0.23', '0.24', '1.00', '1.01', '1.02']
}
)
SKLEARN_CONFIG.update(
{
k: {
'conda_env': 'skl_104',
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['1.03', '1.04', '1.05', '1.06', '1.07']
}
)
MAP_REPO_TO_CONFIG = {
'pydata/xarray': XARRAY_CONFIG,
'mwaskom/seaborn': SEABORN_CONFIG,
'scikit-learn/scikit-learn': SKLEARN_CONFIG,
'sphinx-doc/sphinx': SPHINX_CONFIG,
'django/django': DJANGO_CONFIG,
'astropy/astropy': ASTROPY_CONFIG,
'pylint-dev/pylint': PYLINT_CONFIG,
'pytest-dev/pytest': PYTEST_CONFIG,
'psf/requests': REQUESTS_CONFIG,
'sympy/sympy': SYMPY_CONFIG,
'matplotlib/matplotlib': MATPLOTLIB_CONFIG,
}
@@ -0,0 +1,65 @@
<uploaded_files>
/workspace/{{ workspace_dir_name }}
</uploaded_files>
I've uploaded a python code repository in the directory {{ workspace_dir_name }}. Consider the following issue description:
<doc_change>
{{ instance.problem_statement }}
</doc_change>
Can you help me add the new features to the repository based on the changes in the <doc_change>?
I've already taken care of all changes to any of the test files described in the <doc_change>. This means you DON'T have to modify the testing logic or any of the tests in any way!
Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.
Your task is to make the minimal changes to non-test files in the /workspace/{{ workspace_dir_name }} directory to implement the new features required by the documentation updates.
Follow these phases to resolve the issue:
Phase 1. READING: read the requirements and reword it in clearer terms
1.1 If there are code or config snippets. Express in words any best practices or conventions in them.
1.2 Hightlight method names, variables, file names, stack traces, and technical details, particularly those related to new features.
1.3 Explain the new feature requirements in clear terms.
1.4 Specify functional scope and expected behavior of new features.
1.5 Hightlight any best practices to take into account when developing and testing the new feature.
Phase 2. RUNNING: install and run the functionality in the repository to validate the new features
2.1 Follow the readme.
2.2 Install the environment and anything needed.
2.2 Iterate and figure out how to validate the newly added features.
Phase 3. EXPLORATION: find the files related to the new features and possible implementation solutions
3.1 Use `grep` to search for relevant methods, classes, keywords and feature requirements.
3.2 Identify all files related to the new features.
3.3 Propose the methods and files to implement the new features and explain why.
3.4 From the possible file locations, select the most likely location to implement the new features.
Phase 4. TEST CREATION: before implementing any new features, create a script to validate the feature's correctness.
4.1 Look at existing test files in the repository to understand the test format/structure.
4.2 Create a minimal validation script to verify the newly added features.
4.3 Run the validation script to confirm the new features are successfully added and working as expected.
4.4 Adjust the validation script as necessary to ensure the new features fully meet the requirements.
Phase 5. FEATURE ANALYSIS: state clearly the new feature and how to implement it
5.1 State clearly what the new feature is.
5.2 State clearly where the feature should be implemented.
5.3 State clearly how the test validates the new feature.
5.4 State clearly the best practices to take into account when implementing the new feature.
5.5 State clearly how to implement the new feature.
Phase 6. FEATURE IMPLEMENTATION: edit the source code to implement your chosen solution for the new feature
6.1 Make minimal, focused changes to implement the new feature.
Phase 7. VERIFICATION: Test your new feature thoroughly.
7.1 Run your validation script to verify the new feature works as expected.
7.2 Add edge cases to your test script to ensure comprehensive coverage of the new feature.
7.3 Run existing tests related to the modified code to ensure you haven't broken anything.
Phase 8. FINAL REVIEW: Carefully re-read the feature requirements and compare your changes with the base commit {{ instance.base_commit }}
8.1 Ensure you've fully implemented all required features.
8.2 Run any tests in the repository related to:
8.2.1 The new features you are adding
8.2.2 The files you modified
8.2.3 The functions you changed
8.3 If any tests fail, revise your implementation until all tests pass and the new feature works as expected.
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
@@ -0,0 +1,39 @@
"""Mapping instance_id to resource_factor.
Different instances may have different resource requirements.
e.g., some instances may require more memory/CPU to run inference.
This file tracks the resource requirements of different instances.
"""
import json
import os
from openhands.core.logger import openhands_logger as logger
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_RUNTIME_RESOURCE_FACTOR = int(
os.environ.get('DEFAULT_RUNTIME_RESOURCE_FACTOR', 1)
)
# dataset to resource mapping
_global_resource_mapping: dict[str, dict[str, float]] = {}
def get_resource_mapping(dataset_name: str) -> dict[str, float]:
if dataset_name not in _global_resource_mapping:
file_path = os.path.join(CUR_DIR, f'{dataset_name}.json')
if not os.path.exists(file_path):
logger.info(f'Resource mapping for {dataset_name} not found.')
return None
with open(file_path, 'r') as f:
_global_resource_mapping[dataset_name] = json.load(f)
logger.debug(f'Loaded resource mapping for {dataset_name}')
return _global_resource_mapping[dataset_name]
def get_instance_resource_factor(dataset_name: str, instance_id: str) -> int:
resource_mapping = get_resource_mapping(dataset_name)
if resource_mapping is None:
return DEFAULT_RUNTIME_RESOURCE_FACTOR
return int(resource_mapping.get(instance_id, DEFAULT_RUNTIME_RESOURCE_FACTOR))
@@ -0,0 +1,909 @@
import asyncio
import copy
import json
import os
import tempfile
from typing import Any, Literal
import numpy as np
import pandas as pd
import toml
from datasets import load_dataset
from jinja2 import Environment, FileSystemLoader
import openhands.agenthub
from evaluation.benchmarks.nocode_bench.binary_patch_utils import (
remove_binary_diffs,
remove_binary_files_from_git,
)
from evaluation.benchmarks.nocode_bench.consistants import MAP_REPO_TO_CONFIG
from evaluation.benchmarks.nocode_bench.resource.mapping import (
get_instance_resource_factor,
)
from evaluation.benchmarks.nocode_bench.scripts.utils.evaluation_utils import (
run_evaluation_nocode_bench,
)
from evaluation.utils.shared import (
EvalException,
EvalMetadata,
EvalOutput,
assert_and_raise,
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
update_llm_config_for_completions_logging,
)
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
)
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import get_condenser_config_arg
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.critic import AgentFinishedCritic
from openhands.events.action import CmdRunAction, FileReadAction, MessageAction
from openhands.events.observation import (
CmdOutputObservation,
ErrorObservation,
FileReadObservation,
)
from openhands.events.serialization.event import event_from_dict, event_to_dict
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
from openhands.utils.shutdown_listener import sleep_if_should_continue
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'true'
ENABLE_LLM_EDITOR = os.environ.get('ENABLE_LLM_EDITOR', 'false').lower() == 'true'
BenchMode = Literal['swe', 'swt', 'swt-ci']
# Global variable to track dataset type
DATASET_TYPE = 'nc_bench'
def set_dataset_type(dataset_name: str) -> str:
"""Set dataset type based on dataset name."""
global DATASET_TYPE
DATASET_TYPE = 'nc_bench'
logger.info(f'Dataset type set to: {DATASET_TYPE}')
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
}
def _get_swebench_workspace_dir_name(instance: pd.Series) -> str:
return f'{instance.repo.split("/")[-1]}'
def get_instruction(instance: pd.Series, metadata: EvalMetadata) -> MessageAction:
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
metadata.details['mode']
# Determine the template file based on mode and LLM
template_name = 'nc.j2'
# Set up Jinja2 environment
# Assuming templates are in 'evaluation/benchmarks/swe_bench/prompts' relative to this script
prompts_dir = os.path.join(os.path.dirname(__file__), 'prompts')
env = Environment(loader=FileSystemLoader(prompts_dir))
template = env.get_template(template_name)
# Prepare context for rendering
context = {
'instance': instance,
'workspace_dir_name': workspace_dir_name,
'metadata': metadata, # Pass metadata if needed in templates
}
context['test_instructions'] = '' # Ensure it's defined for other modes
# Render the instruction
instruction = template.render(context)
if RUN_WITH_BROWSING:
instruction += (
'<IMPORTANT!>\nYou SHOULD NEVER attempt to browse the web. </IMPORTANT!>\n'
)
if 'image_assets' in instance:
assets = json.loads(instance['image_assets'])
assert 'problem_statement' in assets, (
'problem_statement is required in image_assets'
)
image_urls = assets['problem_statement']
return MessageAction(content=instruction, image_urls=image_urls)
return MessageAction(content=instruction)
DEFAULT_DOCKER_IMAGE_PREFIX = os.environ.get(
'EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/xingyaoww/'
)
logger.info(f'Default docker image prefix: {DEFAULT_DOCKER_IMAGE_PREFIX}')
def get_instance_docker_image(
instance_id: str,
swebench_official_image: bool = False,
) -> str:
if swebench_official_image:
# Official NoCode-Bench image
image_name = f'ncbench_{instance_id}:latest'.lower()
logger.debug(f'Using official NoCode-Bench image: {image_name}')
return image_name
else:
raise
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> OpenHandsConfig:
# We use a different instance image for the each instance of NoCode-bench eval
use_swebench_official_image = True
base_container_image = get_instance_docker_image(
instance['instance_id'],
swebench_official_image=use_swebench_official_image,
)
logger.info(
f'Using instance container image: {base_container_image}. '
f'Please make sure this image exists. '
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
)
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = base_container_image
sandbox_config.enable_auto_lint = True
sandbox_config.use_host_network = False
# Add platform to the sandbox config to solve issue 4401
sandbox_config.platform = 'linux/amd64'
sandbox_config.remote_runtime_resource_factor = get_instance_resource_factor(
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
metadata.llm_config, metadata.eval_output_dir, instance['instance_id']
)
)
# get 'draft_editor' config if exists
config.set_llm_config(get_llm_config_arg('draft_editor'), 'draft_editor')
agent_config = AgentConfig(
enable_jupyter=False,
enable_browsing=RUN_WITH_BROWSING,
enable_llm_editor=ENABLE_LLM_EDITOR,
enable_mcp=False,
condenser=metadata.condenser_config,
enable_prompt_extensions=False,
)
config.set_agent_config(agent_config)
return config
def make_serializable(obj):
if isinstance(obj, pd.Series):
obj = obj.to_dict()
if isinstance(obj, dict):
return {k: make_serializable(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [make_serializable(v) for v in obj]
elif isinstance(obj, tuple):
return tuple(make_serializable(v) for v in obj)
elif isinstance(obj, np.ndarray):
return obj.tolist()
elif isinstance(obj, pd.Timestamp):
return str(obj)
else:
return obj
def initialize_runtime(
runtime: Runtime,
instance: pd.Series, # this argument is not required
metadata: EvalMetadata,
):
"""Initialize the runtime for the agent.
This function is called before the runtime is used to run the agent.
"""
logger.info('-' * 30)
logger.info('BEGIN Runtime Initialization Fn')
logger.info('-' * 30)
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
obs: CmdOutputObservation
# Set instance id and git configuration
action = CmdRunAction(
command=f"""echo 'export SWE_INSTANCE_ID={instance['instance_id']}' >> ~/.bashrc && echo 'export PIP_CACHE_DIR=~/.cache/pip' >> ~/.bashrc && echo "alias git='git --no-pager'" >> ~/.bashrc && git config --global core.pager "" && git config --global diff.binary false"""
)
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Failed to export SWE_INSTANCE_ID and configure git: {str(obs)}',
)
action = CmdRunAction(command="""export USER=$(whoami); echo USER=${USER} """)
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to export USER: {str(obs)}')
# inject the init script
script_dir = os.path.dirname(__file__)
# inject the instance info
action = CmdRunAction(command='mkdir -p /swe_util/eval_data/instances')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Failed to create /swe_util/eval_data/instances: {str(obs)}',
)
swe_instance_json_name = 'swe-bench-instance.json'
with tempfile.TemporaryDirectory() as temp_dir:
# Construct the full path for the desired file name within the temporary directory
temp_file_path = os.path.join(temp_dir, swe_instance_json_name)
# Write to the file with the desired name within the temporary directory
with open(temp_file_path, 'w') as f:
if not isinstance(instance, dict):
instance_dict = make_serializable(instance)
else:
instance_dict = dict(instance)
if DATASET_TYPE == 'nc_bench':
config = MAP_REPO_TO_CONFIG.get(instance['repo'], {}).get(
instance['version'], []
)
docker_conda_env_name = config['conda_env']
instance_dict['conda_env'] = docker_conda_env_name
json.dump([instance_dict], f)
# Copy the file to the desired location
runtime.copy_to(temp_file_path, '/swe_util/eval_data/instances/')
# inject the instance swe entry
entry_script_path = 'instance_nc_entry.sh'
runtime.copy_to(
str(os.path.join(script_dir, f'scripts/setup/{entry_script_path}')),
'/swe_util/',
)
action = CmdRunAction(command='cat ~/.bashrc')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to cat ~/.bashrc: {str(obs)}')
action = CmdRunAction(command='source ~/.bashrc')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if isinstance(obs, ErrorObservation):
logger.error(f'Failed to source ~/.bashrc: {str(obs)}')
assert_and_raise(obs.exit_code == 0, f'Failed to source ~/.bashrc: {str(obs)}')
action = CmdRunAction(command=f'source /swe_util/{entry_script_path}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Failed to source /swe_util/{entry_script_path}: {str(obs)}',
)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}',
)
action = CmdRunAction(command='git reset --hard')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to git reset --hard: {str(obs)}')
action = CmdRunAction(
command='for remote_name in $(git remote); do git remote remove "${remote_name}"; done'
)
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to remove git remotes: {str(obs)}')
if DATASET_TYPE != 'Multimodal' and DATASET_TYPE != 'SWE-bench-Live':
# Only for non-multimodal datasets, we need to activate the testbed environment for Python
# SWE-Bench multimodal datasets and SWE-bench-Live are not using the testbed environment
action = CmdRunAction(command='which python')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Expected to find python interpreter, but got: {str(obs)}',
)
logger.info('-' * 30)
logger.info('END Runtime Initialization Fn')
logger.info('-' * 30)
def complete_runtime(
runtime: Runtime,
instance: pd.Series, # this argument is not required, but it is used to get the workspace_dir_name
) -> dict[str, Any]:
"""Complete the runtime for the agent.
This function is called before the runtime is used to run the agent.
If you need to do something in the sandbox to get the correctness metric after
the agent has run, modify this function.
"""
logger.info('-' * 30)
logger.info('BEGIN Runtime Completion Fn')
logger.info('-' * 30)
obs: CmdOutputObservation
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if obs.exit_code == -1:
# The previous command is still running
# We need to kill previous command
logger.info('The previous command is still running, trying to kill it...')
action = CmdRunAction(command='C-c')
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
# Then run the command again
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if obs.exit_code == -1:
# The previous command is still running
# We need to kill previous command
logger.info('The previous command is still running, trying to ctrl+z it...')
action = CmdRunAction(command='C-z')
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
# Then run the command again
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}',
)
action = CmdRunAction(command='git config --global core.pager ""')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to git config --global core.pager "": {str(obs)}',
)
# First check for any git repositories in subdirectories
action = CmdRunAction(command='find . -type d -name .git -not -path "./.git"')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to find git repositories: {str(obs)}',
)
git_dirs = [p for p in obs.content.strip().split('\n') if p]
if git_dirs:
# Remove all .git directories in subdirectories
for git_dir in git_dirs:
action = CmdRunAction(command=f'rm -rf "{git_dir}"')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to remove git directory {git_dir}: {str(obs)}',
)
# add all files
action = CmdRunAction(command='git add -A')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to git add -A: {str(obs)}',
)
# Remove binary files from git staging
action = CmdRunAction(command=remove_binary_files_from_git())
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to remove binary files: {str(obs)}',
)
n_retries = 0
git_patch = None
while n_retries < 5:
action = CmdRunAction(
command=f'git diff --no-color --cached {instance["base_commit"]} > patch.diff'
)
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
n_retries += 1
if isinstance(obs, CmdOutputObservation):
if obs.exit_code == 0:
# Read the patch file
action = FileReadAction(path='patch.diff')
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if isinstance(obs, FileReadObservation):
git_patch = obs.content
break
elif isinstance(obs, ErrorObservation):
# Fall back to cat "patch.diff" to get the patch
assert 'File could not be decoded as utf-8' in obs.content
action = CmdRunAction(command='cat patch.diff')
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert isinstance(obs, CmdOutputObservation) and obs.exit_code == 0
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
git_patch = obs.content
break
else:
assert_and_raise(False, f'Unexpected observation type: {str(obs)}')
else:
logger.info('Failed to get git diff, retrying...')
sleep_if_should_continue(10)
elif isinstance(obs, ErrorObservation):
logger.error(f'Error occurred: {obs.content}. Retrying...')
sleep_if_should_continue(10)
else:
assert_and_raise(False, f'Unexpected observation type: {str(obs)}')
assert_and_raise(git_patch is not None, 'Failed to get git diff (None)')
# Remove binary diffs from the patch
git_patch = remove_binary_diffs(git_patch)
logger.info('-' * 30)
logger.info('END Runtime Completion Fn')
logger.info('-' * 30)
return {'git_patch': git_patch}
def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
runtime_failure_count: int = 0,
) -> EvalOutput:
config = get_config(instance, metadata)
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
else:
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
# Increase resource_factor with increasing attempt_id
if runtime_failure_count > 0:
config.sandbox.remote_runtime_resource_factor = min(
config.sandbox.remote_runtime_resource_factor * (2**runtime_failure_count),
8,
)
logger.warning(
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
metadata = copy.deepcopy(metadata)
metadata.details['runtime_failure_count'] = runtime_failure_count
metadata.details['remote_runtime_resource_factor'] = (
config.sandbox.remote_runtime_resource_factor
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
try:
initialize_runtime(runtime, instance, metadata)
message_action = get_instruction(instance, metadata)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
state: State | None = asyncio.run(
run_controller(
config=config,
initial_user_action=message_action,
runtime=runtime,
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
metadata.agent_class
],
)
)
# if fatal error, throw EvalError to trigger re-run
if is_fatal_evaluation_error(state.last_error):
raise EvalException('Fatal error detected: ' + state.last_error)
# ======= THIS IS SWE-Bench specific =======
# Get git patch
if DATASET_TYPE == 'SWE-bench-Live':
from evaluation.benchmarks.swe_bench.live_utils import (
complete_runtime as complete_runtime_fn,
)
else:
complete_runtime_fn = complete_runtime
return_val = complete_runtime_fn(runtime, instance)
git_patch = return_val['git_patch']
logger.info(
f'Got git diff for instance {instance.instance_id}:\n--------\n{git_patch}\n--------'
)
finally:
runtime.close()
# ==========================================
# ======= Attempt to evaluate the agent's edits =======
# we use eval_infer.sh to evaluate the agent's edits, not here
# because the agent may alter the environment / testcases
test_result = {
'git_patch': git_patch,
}
# If you are working on some simpler benchmark that only evaluates the final model output (e.g., in a MessageAction)
# You can simply get the LAST `MessageAction` from the returned `state.history` and parse it for evaluation.
if state is None:
raise ValueError('State should not be None.')
# NOTE: this is NO LONGER the event stream, but an agent history that includes delegate agent's events
histories = [event_to_dict(event) for event in state.history]
metrics = get_metrics(state)
# Save the output
instruction = message_action.content
if message_action.image_urls:
instruction += (
'\n\n<image_urls>' + '\n'.join(message_action.image_urls) + '</image_urls>'
)
output = EvalOutput(
instance_id=instance.instance_id,
instruction=instruction,
instance=instance.to_dict(), # SWE Bench specific
test_result=test_result,
metadata=metadata,
history=histories,
metrics=metrics,
error=state.last_error if state and state.last_error else None,
)
return output
def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.toml')
if os.path.exists(file_path):
with open(file_path, 'r') as file:
data = toml.load(file)
if 'selected_ids' in data:
selected_ids = data['selected_ids']
logger.info(
f'Filtering {len(selected_ids)} tasks from "selected_ids"...'
)
subset = dataset[dataset[filter_column].isin(selected_ids)]
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
return subset
if 'selected_repos' in data:
# repos for the swe-bench instances:
# ['astropy/astropy', 'django/django', 'matplotlib/matplotlib', 'mwaskom/seaborn', 'pallets/flask', 'psf/requests', 'pydata/xarray', 'pylint-dev/pylint', 'pytest-dev/pytest', 'scikit-learn/scikit-learn', 'sphinx-doc/sphinx', 'sympy/sympy']
selected_repos = data['selected_repos']
if isinstance(selected_repos, str):
selected_repos = [selected_repos]
assert isinstance(selected_repos, list)
logger.info(
f'Filtering {selected_repos} tasks from "selected_repos"...'
)
subset = dataset[dataset['repo'].isin(selected_repos)]
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
return subset
skip_ids = os.environ.get('SKIP_IDS', '').split(',')
if len(skip_ids) > 0:
logger.info(f'Filtering {len(skip_ids)} tasks from "SKIP_IDS"...')
return dataset[~dataset[filter_column].isin(skip_ids)]
return dataset
if __name__ == '__main__':
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,
default='NoCode-bench/NoCode-bench_Verified',
help='data set to evaluate on, either full-test or lite-test',
)
parser.add_argument(
'--split',
type=str,
default='test',
help='split to evaluate on',
)
parser.add_argument(
'--mode',
type=str,
default='swe',
choices=['swe', 'swt', 'swt-ci'],
help="mode to run the evaluation, either 'swe', 'swt', or 'swt-ci'",
)
args, _ = parser.parse_known_args()
# NOTE: It is preferable to load datasets from huggingface datasets and perform post-processing
# so we don't need to manage file uploading to OpenHands's repo
dataset = load_dataset(args.dataset, args.split)
# Set the global dataset type based on dataset name
set_dataset_type(args.dataset)
swe_bench_tests = filter_dataset(dataset.to_pandas(), 'instance_id')
logger.info(
f'Loaded dataset {args.dataset} with split {args.split}: {len(swe_bench_tests)} tasks'
)
llm_config = None
if args.llm_config:
llm_config = get_llm_config_arg(args.llm_config)
llm_config.log_completions = True
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
llm_config.modify_params = False
if llm_config is None:
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
# Get condenser config from environment variable
condenser_name = os.environ.get('EVAL_CONDENSER')
if condenser_name:
condenser_config = get_condenser_config_arg(condenser_name)
if condenser_config is None:
raise ValueError(
f'Could not find Condenser config: EVAL_CONDENSER={condenser_name}'
)
else:
# If no specific condenser config is provided via env var, default to NoOpCondenser
condenser_config = NoOpCondenserConfig()
logger.debug(
'No Condenser config provided via EVAL_CONDENSER, using NoOpCondenser.'
)
details = {'mode': args.mode}
_agent_cls = openhands.agenthub.Agent.get_cls(args.agent_cls)
dataset_descrption = (
args.dataset.replace('/', '__') + '-' + args.split.replace('/', '__')
)
metadata = make_metadata(
llm_config,
dataset_descrption,
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
details=details,
condenser_config=condenser_config,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
print(f'### OUTPUT FILE: {output_file} ###')
# Run evaluation in iterative mode:
# If a rollout fails to output AgentFinishAction, we will try again until it succeeds OR total 3 attempts have been made.
ITERATIVE_EVAL_MODE = (
os.environ.get('ITERATIVE_EVAL_MODE', 'false').lower() == 'true'
)
ITERATIVE_EVAL_MODE_MAX_ATTEMPTS = int(
os.environ.get('ITERATIVE_EVAL_MODE_MAX_ATTEMPTS', '3')
)
if not ITERATIVE_EVAL_MODE:
# load the dataset
instances = prepare_dataset(swe_bench_tests, output_file, args.eval_n_limit)
if len(instances) > 0 and not isinstance(
instances['PASS2PASS'][instances['PASS2PASS'].index[0]], str
):
for col in ['PASS2PASS', 'FAIL2PASS']:
instances[col] = instances[col].apply(lambda x: str(x))
run_evaluation_nocode_bench(
instances,
metadata,
output_file,
args.eval_num_workers,
process_instance,
timeout_seconds=8
* 60
* 60, # 8 hour PER instance should be more than enough
max_retries=5,
)
else:
critic = AgentFinishedCritic()
def get_cur_output_file_path(attempt: int) -> str:
return (
f'{output_file.removesuffix(".jsonl")}.critic_attempt_{attempt}.jsonl'
)
eval_ids = None
for attempt in range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1):
cur_output_file = get_cur_output_file_path(attempt)
logger.info(
f'Running evaluation with critic {critic.__class__.__name__} for attempt {attempt} of {ITERATIVE_EVAL_MODE_MAX_ATTEMPTS}.'
)
# For deterministic eval, we set temperature to 0.1 for (>1) attempt
# so hopefully we get slightly different results
if attempt > 1 and metadata.llm_config.temperature == 0:
logger.info(
f'Detected temperature is 0 for (>1) attempt {attempt}. Setting temperature to 0.1...'
)
metadata.llm_config.temperature = 0.1
# Load instances - at first attempt, we evaluate all instances
# On subsequent attempts, we only evaluate the instances that failed the previous attempt determined by critic
instances = prepare_dataset(
swe_bench_tests, cur_output_file, args.eval_n_limit, eval_ids=eval_ids
)
if len(instances) > 0 and not isinstance(
instances['PASS2PASS'][instances['PASS2PASS'].index[0]], str
):
for col in ['PASS2PASS', 'FAIL2PASS']:
instances[col] = instances[col].apply(lambda x: str(x))
# Run evaluation - but save them to cur_output_file
logger.info(
f'Evaluating {len(instances)} instances for attempt {attempt}...'
)
run_evaluation_nocode_bench(
instances,
metadata,
cur_output_file,
args.eval_num_workers,
process_instance,
timeout_seconds=8
* 60
* 60, # 8 hour PER instance should be more than enough
max_retries=5,
)
# When eval is done, we update eval_ids to the instances that failed the current attempt
instances_failed = []
logger.info(
f'Use critic {critic.__class__.__name__} to check {len(instances)} instances for attempt {attempt}...'
)
with open(cur_output_file, 'r') as f:
for line in f:
instance = json.loads(line)
try:
history = [
event_from_dict(event) for event in instance['history']
]
critic_result = critic.evaluate(
history, instance['test_result'].get('git_patch', '')
)
if not critic_result.success:
instances_failed.append(instance['instance_id'])
except Exception as e:
logger.error(
f'Error loading history for instance {instance["instance_id"]}: {e}'
)
instances_failed.append(instance['instance_id'])
logger.info(
f'{len(instances_failed)} instances failed the current attempt {attempt}: {instances_failed}'
)
eval_ids = instances_failed
# If no instances failed, we break
if len(instances_failed) == 0:
break
# Then we should aggregate the results from all attempts into the original output file
# and remove the intermediate files
logger.info(
'Aggregating results from all attempts into the original output file...'
)
fout = open(output_file, 'w')
added_instance_ids = set()
for attempt in reversed(range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1)):
cur_output_file = get_cur_output_file_path(attempt)
if not os.path.exists(cur_output_file):
logger.warning(
f'Intermediate output file {cur_output_file} does not exist. Skipping...'
)
continue
with open(cur_output_file, 'r') as f:
for line in f:
instance = json.loads(line)
# Also make sure git_patch is not empty - otherwise we fall back to previous attempt (empty patch is worse than anything else)
if (
instance['instance_id'] not in added_instance_ids
and instance['test_result'].get('git_patch', '').strip()
):
fout.write(line)
added_instance_ids.add(instance['instance_id'])
logger.info(
f'Aggregated instances from {cur_output_file}. Total instances added so far: {len(added_instance_ids)}'
)
fout.close()
logger.info(
f'Done! Total {len(added_instance_ids)} instances added to {output_file}'
)
@@ -0,0 +1,33 @@
import argparse
import json
def main(output_jsonl: str):
with open(output_jsonl, 'r') as f:
for line in f:
try:
output = json.loads(line)
pred = {
'instance_id': output['instance_id'],
'model_name_or_path': output['metadata']['llm_config']['model'],
'model_patch': output['test_result']['git_patch'],
}
except Exception as e:
print(
f'Error while reading output of instance {output["instance_id"]}: {e}'
)
print(json.dumps(pred))
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument(
'--output_jsonl',
type=str,
required=True,
help='Path to the prediction file (.../outputs.jsonl)',
)
args = parser.parse_args()
main(args.output_jsonl)
@@ -0,0 +1,104 @@
import argparse
import pandas as pd
from openhands.core.logger import openhands_logger as logger
def verify_instance_costs(row: pd.Series) -> float:
"""
Verifies that the accumulated_cost matches the sum of individual costs in metrics.
Also checks for duplicate consecutive costs which might indicate buggy counting.
If the consecutive costs are identical, the file is affected by this bug:
https://github.com/All-Hands-AI/OpenHands/issues/5383
Args:
row: DataFrame row containing instance data with metrics
Returns:
float: The verified total cost for this instance (corrected if needed)
"""
try:
metrics = row.get('metrics')
if not metrics:
logger.warning(f'Instance {row["instance_id"]}: No metrics found')
return 0.0
accumulated = metrics.get('accumulated_cost')
costs = metrics.get('costs', [])
if accumulated is None:
logger.warning(
f'Instance {row["instance_id"]}: No accumulated_cost in metrics'
)
return 0.0
# Check for duplicate consecutive costs and systematic even-odd pairs
has_duplicate = False
all_pairs_match = True
# Check each even-odd pair (0-1, 2-3, etc.)
for i in range(0, len(costs) - 1, 2):
if abs(costs[i]['cost'] - costs[i + 1]['cost']) < 1e-6:
has_duplicate = True
logger.debug(
f'Instance {row["instance_id"]}: Possible buggy double-counting detected! '
f'Steps {i} and {i + 1} have identical costs: {costs[i]["cost"]:.2f}'
)
else:
all_pairs_match = False
break
# Calculate total cost, accounting for buggy double counting if detected
if len(costs) >= 2 and has_duplicate and all_pairs_match:
paired_steps_cost = sum(
cost_entry['cost']
for cost_entry in costs[: -1 if len(costs) % 2 else None]
)
real_paired_cost = paired_steps_cost / 2
unpaired_cost = costs[-1]['cost'] if len(costs) % 2 else 0
total_cost = real_paired_cost + unpaired_cost
else:
total_cost = sum(cost_entry['cost'] for cost_entry in costs)
if not abs(total_cost - accumulated) < 1e-6:
logger.warning(
f'Instance {row["instance_id"]}: Cost mismatch: '
f'accumulated: {accumulated:.2f}, sum of costs: {total_cost:.2f}, '
)
return total_cost
except Exception as e:
logger.error(
f'Error verifying costs for instance {row.get("instance_id", "UNKNOWN")}: {e}'
)
return 0.0
def main():
parser = argparse.ArgumentParser(
description='Verify costs in SWE-bench output file'
)
parser.add_argument(
'input_filepath', type=str, help='Path to the output.jsonl file'
)
args = parser.parse_args()
try:
# Load and verify the JSONL file
df = pd.read_json(args.input_filepath, lines=True)
logger.info(f'Loaded {len(df)} instances from {args.input_filepath}')
# Verify costs for each instance and sum up total
total_cost = df.apply(verify_instance_costs, axis=1).sum()
logger.info(f'Total verified cost across all instances: ${total_cost:.2f}')
except Exception as e:
logger.error(f'Failed to process file: {e}')
raise
if __name__ == '__main__':
main()
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
set -eo pipefail
source "evaluation/utils/version_control.sh"
MODEL_CONFIG=$1
COMMIT_HASH=$2
AGENT=$3
EVAL_LIMIT=$4
MAX_ITER=$5
NUM_WORKERS=$6
DATASET=$7
SPLIT=$8
N_RUNS=$9
MODE=${10}
if [ -z "$NUM_WORKERS" ]; then
NUM_WORKERS=1
echo "Number of workers not specified, use default $NUM_WORKERS"
fi
checkout_eval_branch
if [ -z "$AGENT" ]; then
echo "Agent not specified, use default CodeActAgent"
AGENT="CodeActAgent"
fi
if [ -z "$MAX_ITER" ]; then
echo "MAX_ITER not specified, use default 100"
MAX_ITER=100
fi
if [ -z "$RUN_WITH_BROWSING" ]; then
echo "RUN_WITH_BROWSING not specified, use default false"
RUN_WITH_BROWSING=false
fi
if [ -z "$DATASET" ]; then
echo "DATASET not specified, use default princeton-nlp/SWE-bench_Lite"
DATASET="princeton-nlp/SWE-bench_Lite"
fi
if [ -z "$SPLIT" ]; then
echo "SPLIT not specified, use default test"
SPLIT="test"
fi
if [ -z "$MODE" ]; then
MODE="swe"
echo "MODE not specified, use default $MODE"
fi
if [ -n "$EVAL_CONDENSER" ]; then
echo "Using Condenser Config: $EVAL_CONDENSER"
else
echo "No Condenser Config provided via EVAL_CONDENSER, use default (NoOpCondenser)."
fi
export RUN_WITH_BROWSING=$RUN_WITH_BROWSING
echo "RUN_WITH_BROWSING: $RUN_WITH_BROWSING"
get_openhands_version
echo "AGENT: $AGENT"
echo "OPENHANDS_VERSION: $OPENHANDS_VERSION"
echo "MODEL_CONFIG: $MODEL_CONFIG"
echo "DATASET: $DATASET"
echo "SPLIT: $SPLIT"
echo "MAX_ITER: $MAX_ITER"
echo "NUM_WORKERS: $NUM_WORKERS"
echo "COMMIT_HASH: $COMMIT_HASH"
echo "MODE: $MODE"
echo "EVAL_CONDENSER: $EVAL_CONDENSER"
# Default to NOT use Hint
if [ -z "$USE_HINT_TEXT" ]; then
export USE_HINT_TEXT=false
fi
echo "USE_HINT_TEXT: $USE_HINT_TEXT"
EVAL_NOTE="$OPENHANDS_VERSION"
# if not using Hint, add -no-hint to the eval note
if [ "$USE_HINT_TEXT" = false ]; then
EVAL_NOTE="$EVAL_NOTE-no-hint"
fi
if [ "$RUN_WITH_BROWSING" = true ]; then
EVAL_NOTE="$EVAL_NOTE-with-browsing"
fi
if [ -n "$EXP_NAME" ]; then
EVAL_NOTE="$EVAL_NOTE-$EXP_NAME"
fi
# if mode != swe, add mode to the eval note
if [ "$MODE" != "swe" ]; then
EVAL_NOTE="${EVAL_NOTE}-${MODE}"
fi
# Add condenser config to eval note if provided
if [ -n "$EVAL_CONDENSER" ]; then
EVAL_NOTE="${EVAL_NOTE}-${EVAL_CONDENSER}"
fi
function run_eval() {
local eval_note="${1}"
COMMAND="poetry run python evaluation/benchmarks/nocode_bench/run_infer_nc.py \
--agent-cls $AGENT \
--llm-config $MODEL_CONFIG \
--max-iterations $MAX_ITER \
--eval-num-workers $NUM_WORKERS \
--eval-note $eval_note \
--dataset $DATASET \
--split $SPLIT \
--mode $MODE"
if [ -n "$EVAL_LIMIT" ]; then
echo "EVAL_LIMIT: $EVAL_LIMIT"
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
fi
# Run the command
eval $COMMAND
}
unset SANDBOX_ENV_GITHUB_TOKEN # prevent the agent from using the github token to push
if [ -z "$N_RUNS" ]; then
N_RUNS=1
echo "N_RUNS not specified, use default $N_RUNS"
fi
# Skip runs if the run number is in the SKIP_RUNS list
# read from env variable SKIP_RUNS as a comma separated list of run numbers
SKIP_RUNS=(${SKIP_RUNS//,/ })
for i in $(seq 1 $N_RUNS); do
if [[ " ${SKIP_RUNS[@]} " =~ " $i " ]]; then
echo "Skipping run $i"
continue
fi
current_eval_note="$EVAL_NOTE-run_$i"
echo "EVAL_NOTE: $current_eval_note"
run_eval $current_eval_note
done
checkout_original_branch
@@ -0,0 +1,54 @@
"""This script compares gold patches with OpenHands-generated patches and check whether
OpenHands found the right (set of) files to modify.
"""
import argparse
import json
import re
def extract_modified_files(patch):
modified_files = set()
file_pattern = re.compile(r'^diff --git a/(.*?) b/')
for line in patch.split('\n'):
match = file_pattern.match(line)
if match:
modified_files.add(match.group(1))
return modified_files
def process_report(oh_output_file):
succ = 0
fail = 0
for line in open(oh_output_file):
line = json.loads(line)
instance_id = line['instance_id']
gold_patch = line['swe_instance']['patch']
generated_patch = line['git_patch']
gold_modified_files = extract_modified_files(gold_patch)
# swe-bench lite only: a gold patch always contains exactly one file
assert len(gold_modified_files) == 1
generated_modified_files = extract_modified_files(generated_patch)
# Check if all files in gold_patch are also in generated_patch
all_files_in_generated = gold_modified_files.issubset(generated_modified_files)
if all_files_in_generated:
succ += 1
else:
fail += 1
print(
f'{instance_id}: file mismatch, gold = {gold_modified_files}, generated = {generated_modified_files}'
)
print(
f'\nSUMMARY: {succ} out of {succ + fail} instances found correct files to edit, success rate = {succ / float(succ + fail)}'
)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--oh_output_file', help='Path to the OH output file')
args = parser.parse_args()
process_report(args.oh_output_file)
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
source ~/.bashrc
SWEUTIL_DIR=/swe_util
if [ -z "$SWE_INSTANCE_ID" ]; then
echo "Error: SWE_INSTANCE_ID is not set." >&2
exit 1
fi
item=$(jq --arg INSTANCE_ID "$SWE_INSTANCE_ID" '.[] | select(.instance_id == $INSTANCE_ID)' $SWEUTIL_DIR/eval_data/instances/swe-bench-instance.json)
if [[ -z "$item" ]]; then
echo "No item found for the provided instance ID."
exit 1
fi
REPO_NAME=$(echo "$item" | jq -r '.repo | split("/")[-1]')
WORKSPACE_NAME="$REPO_NAME"
echo "WORKSPACE_NAME: $WORKSPACE_NAME"
# Clear the workspace
if [ -d /workspace ]; then
rm -rf /workspace/*
else
mkdir /workspace
fi
# Copy repo to workspace
if [ -d /workspace/$WORKSPACE_NAME ]; then
rm -rf /workspace/$WORKSPACE_NAME
fi
mkdir -p /workspace
SRC_DIR="/root/$REPO_NAME"
DEST_DIR="/workspace/$WORKSPACE_NAME"
cp -r "$SRC_DIR" "$DEST_DIR"
echo ">> Extracting conda environment name..."
CONDA_ENV_NAME=$(echo "$item" | jq -r '.conda_env // empty')
# Activate instance-specific environment
if [ -d /opt/miniconda3 ]; then
. /opt/miniconda3/etc/profile.d/conda.sh
conda activate $CONDA_ENV_NAME
fi
@@ -0,0 +1,154 @@
import json
import multiprocessing as mp
from typing import Awaitable, Callable, TextIO
import numpy as np
import pandas as pd
from pydantic import SecretStr
from tqdm import tqdm
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
_process_instance_wrapper,
_process_instance_wrapper_mp,
)
from openhands.core.logger import openhands_logger as logger
def update_progress_nc(
result: EvalOutput,
pbar: tqdm,
output_fp: TextIO,
):
"""Update the progress bar and write the result to the output file."""
pbar.update(1)
pbar.set_description(f'Instance {result.instance_id}')
pbar.set_postfix_str(f'Test Result: {str(result.test_result)[:300]}...')
logger.info(
f'Finished evaluation for instance {result.instance_id}: '
f'{str(result.test_result)[:300]}...\n'
)
def make_serializable(obj):
if isinstance(obj, pd.Series):
return make_serializable(obj.to_dict())
if isinstance(obj, dict):
return {k: make_serializable(v) for k, v in obj.items()}
elif isinstance(obj, (list, tuple, set)):
converted = [make_serializable(v) for v in obj]
if isinstance(obj, list):
return converted
elif isinstance(obj, tuple):
return tuple(converted)
else: # set
return converted
elif isinstance(obj, np.ndarray):
return obj.tolist()
elif isinstance(obj, np.generic):
return obj.item()
elif isinstance(obj, pd.Timestamp):
return obj.isoformat()
elif SecretStr is not None and isinstance(obj, SecretStr):
return str(obj)
else:
return obj
try:
raw_data = result.model_dump(mode='python', round_trip=False)
safe_data = make_serializable(raw_data)
output_fp.write(json.dumps(safe_data, ensure_ascii=False) + '\n')
output_fp.flush()
except Exception as e:
logger.error(f'Failed to write full result: {e}')
fallback = {
'instance_id': result.instance_id,
'model_patch': result.test_result.get('git_patch', ''),
}
try:
output_fp.write(json.dumps(fallback, ensure_ascii=False) + '\n')
output_fp.flush()
logger.info(
f'Wrote fallback result for instance {result.instance_id}: only instance_id and model_patch.'
)
except Exception as e2:
logger.error(f'Failed to write fallback result: {e2}')
def cleanup():
print('Cleaning up child processes...')
for process in mp.active_children():
print(f'Terminating child process: {process.name}')
process.terminate()
process.join()
def run_evaluation_nocode_bench(
dataset: pd.DataFrame,
metadata: EvalMetadata | None,
output_file: str,
num_workers: int,
process_instance_func: Callable[
[pd.Series, EvalMetadata, bool], Awaitable[EvalOutput]
],
max_retries: int = 5, # number of retries for each instance
timeout_seconds: int | None = None,
):
use_multiprocessing = num_workers > 1
if metadata is not None:
logger.info(
f'Evaluation started with Agent {metadata.agent_class}:\n'
f'model {metadata.llm_config.model}, max iterations {metadata.max_iterations}.\n'
)
else:
logger.warning('Running evaluation without metadata.')
logger.info(f'Evaluation started with {num_workers} workers.')
total_instances = len(dataset)
pbar = tqdm(total=total_instances, desc='Instances processed')
output_fp = open(output_file, 'a')
try:
if use_multiprocessing:
with mp.Pool(num_workers) as pool:
args_iter = (
(
process_instance_func,
instance,
metadata,
True,
max_retries,
timeout_seconds,
)
for _, instance in dataset.iterrows()
)
results = pool.imap_unordered(_process_instance_wrapper_mp, args_iter)
for result in results:
update_progress_nc(result, pbar, output_fp)
else:
for _, instance in dataset.iterrows():
result = _process_instance_wrapper(
process_instance_func=process_instance_func,
instance=instance,
metadata=metadata,
use_mp=False,
max_retries=max_retries,
)
update_progress_nc(result, pbar, output_fp)
except KeyboardInterrupt:
print('\nKeyboardInterrupt received. Cleaning up...\n')
cleanup()
output_fp.close()
logger.info('\nEvaluation finished.\n')
-2
View File
@@ -1,2 +0,0 @@
node_modules
outputs
-70
View File
@@ -1,70 +0,0 @@
# OpenHands - Regression Test Framework
OpenHands project is an open-source software engineering AI that can solve various software engineering tasks. This repository contains the regression test framework for OpenHands project.
## Running the Tests
To run the tests for OpenHands project, you can use the provided test runner script. Follow these steps:
1. Ensure you have Python 3.6 or higher installed on your system.
2. Install the required dependencies by running the following command in your terminal:
```
pip install -r requirements.txt
```
3. Navigate to the root directory of the project.
4. Run the test suite using the test runner script with the required arguments:
```
python evaluation/regression/run_tests.py --OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxx --model=gpt-4o
```
Replace `sk-xxxxxxxxxxxxxxxxxxxxxx` with your actual OpenAI API key. The default model is `gpt-4o`, but you can specify a different model if needed.
The test runner will discover and execute all the test cases in the `cases/` directory, and display the results of the test suite, including the status of each individual test case and the overall summary.
## Test Case Structure
The test cases for OpenHands project are organized in the `cases/` directory. Each test case has the following structure:
```
cases/
├── hello-world/
│ ├── task.txt
│ ├── outputs/
│ │ └── codeact_agent/
│ │ └── workspace/
│ │ ├── hello_world.sh
│ └── test_hello_world.py
├── create_web_app/
│ ├── task.txt
│ ├── outputs/
│ │ └── codeact_agent/
│ │ └── workspace/
│ │ ├── app.py
│ │ ├── requirements.txt
│ │ ├── static/
│ │ └── templates/
│ └── test_create_web_app.py
└── ...
```
- `task.txt`: This file contains the task description provided by the user.
- `outputs/`: This directory contains the output generated by OpenHands for each agent.
- `outputs/*/workspace/`: This directory contains the actual output files generated by OpenHands.
- `test_*.py`: These are the test scripts that validate the output of OpenHands.
## Adding New Test Cases
To add a new test case to the regression test framework, follow the same steps as described in the previous sections.
## Customizing the Test Cases
The test cases can be customized by modifying the fixtures defined in the `conftest.py` file. The available fixtures are:
- `test_cases_dir`: The directory containing the test cases.
- `task_file`: The path to the `task.txt` file for the current test case.
- `workspace_dir`: The path to the `workspace/` directory for the current test case.
- `model`: The model selected start the generation.
- `run_test_case`: A fixture that runs OpenHands and generates the workspace for the current test case.
You can modify these fixtures to change the behavior of the test cases or add new ones as needed.
If you have any questions or need further assistance, feel free to reach out to the project maintainers.
@@ -1 +0,0 @@
Write an API server in node express which responds with a random number, and a frontend in React that displays the next number from the API
@@ -1 +0,0 @@
Write a simple hello world server in node Express
@@ -1,2 +0,0 @@
#!/usr/bin/env bash
echo "hello world"
@@ -1 +0,0 @@
Rewrite the script so that it prints the user's name, using the first argument. If there's no name, default to "world"
@@ -1 +0,0 @@
Write a bash script named "hello_world.sh" that prints "Hello, World!"
@@ -1,20 +0,0 @@
import os
import pytest
from conftest import agents
@pytest.mark.parametrize('agent', agents())
def test_hello_world(task_file, run_test_case, agent):
"""Test case for the "Hello, World!" Bash script using different agents."""
# Run the test case for the specified agent
workspace_dir = run_test_case(agent, 'hello-world')
# Validate the generated workspace
assert os.path.exists(workspace_dir)
assert os.path.isfile(os.path.join(workspace_dir, 'hello_world.sh'))
# Execute the hello_world.sh script
os.chdir(workspace_dir)
output = os.popen('bash hello_world.sh').read()
assert output == 'Hello, World!\n'
@@ -1,2 +0,0 @@
def string_length(s):
return len(s)
@@ -1,2 +0,0 @@
def to_lowercase(s):
return s.lower()
@@ -1,2 +0,0 @@
def reverse_string(s):
return s[::-1]
@@ -1,7 +0,0 @@
import random
def scramble_string(s):
s_list = list(s)
random.shuffle(s_list)
return ''.join(s_list)
@@ -1,8 +0,0 @@
def spongebob_case(s):
result = ''
for i, char in enumerate(s):
if i % 2 == 0:
result += char.lower()
else:
result += char.upper()
return result
@@ -1,2 +0,0 @@
def to_uppercase(s):
return s.upper()
@@ -1,55 +0,0 @@
import sys
def print_help():
help_text = """
Usage: python string_cli.py <command> <string>
Commands:
reverse - Reverses the input string.
uppercase - Converts the input string to uppercase.
lowercase - Converts the input string to lowercase.
spongebob - Converts the input string to spongebob case.
length - Returns the length of the input string.
scramble - Randomly scrambles the characters in the input string.
"""
print(help_text)
if __name__ == '__main__':
if len(sys.argv) == 2 and sys.argv[1] == '--help':
print_help()
sys.exit(0)
elif len(sys.argv) < 3:
print('Usage: python string_cli.py <command> <string>')
sys.exit(1)
command = sys.argv[1]
input_string = sys.argv[2]
if command == 'reverse':
from commands.reverse import reverse_string
print(reverse_string(input_string))
elif command == 'uppercase':
from commands.uppercase import to_uppercase
print(to_uppercase(input_string))
elif command == 'lowercase':
from commands.lowercase import to_lowercase
print(to_lowercase(input_string))
elif command == 'spongebob':
from commands.spongebob import spongebob_case
print(spongebob_case(input_string))
elif command == 'length':
from commands.length import string_length
print(string_length(input_string))
elif command == 'scramble':
from commands.scramble import scramble_string
print(scramble_string(input_string))
else:
print('Invalid command!')
@@ -1 +0,0 @@
Please rewrite the entire CLI in node.js
@@ -1,2 +0,0 @@
def string_length(s):
return len(s)
@@ -1,2 +0,0 @@
def to_lowercase(s):
return s.lower()
@@ -1,2 +0,0 @@
def reverse_string(s):
return s[::-1]
@@ -1,7 +0,0 @@
import random
def scramble_string(s):
s_list = list(s)
random.shuffle(s_list)
return ''.join(s_list)
@@ -1,8 +0,0 @@
def spongebob_case(s):
result = ''
for i, char in enumerate(s):
if i % 2 == 0:
result += char.lower()
else:
result += char.upper()
return result
@@ -1,2 +0,0 @@
def to_uppercase(s):
return s.upper()
@@ -1,36 +0,0 @@
import sys
if __name__ == '__main__':
if len(sys.argv) < 3:
print('Usage: python string_cli.py <command> <string>')
sys.exit(1)
command = sys.argv[1]
input_string = sys.argv[2]
if command == 'reverse':
from commands.reverse import reverse_string
print(reverse_string(input_string))
elif command == 'uppercase':
from commands.uppercase import to_uppercase
print(to_uppercase(input_string))
elif command == 'lowercase':
from commands.lowercase import to_lowercase
print(to_lowercase(input_string))
elif command == 'spongebob':
from commands.spongebob import spongebob_case
print(spongebob_case(input_string))
elif command == 'length':
from commands.length import string_length
print(string_length(input_string))
elif command == 'scramble':
from commands.scramble import scramble_string
print(scramble_string(input_string))
else:
print('Invalid command!')
@@ -1 +0,0 @@
Please add a --help option to the CLI, with a detailed description of each command
@@ -1 +0,0 @@
Write a python CLI for string manipulation. The CLI should accept a command, and a string. The commands should include `reverse`, `uppercase`, `lowercase`, `spongebob`, `length`, and `scramble`. The logic for each command should live in its own file.
@@ -1 +0,0 @@
Write a simple TODO list application in React
@@ -1,21 +0,0 @@
from http.server import BaseHTTPRequestHandler, HTTPServer
class HelloWorldHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(b'Hello World\n')
def run(server_class=HTTPServer, handler_class=HelloWorldHandler, port=8000):
server_address = ('', port)
httpd = server_class(server_address, handler_class)
print(f'Starting httpd on port {port}...')
httpd.serve_forever()
if __name__ == '__main__':
print('starting server...')
run()
@@ -1 +0,0 @@
Make sure the server works and responds appropriately
-171
View File
@@ -1,171 +0,0 @@
import datetime
import logging
import os
import shutil
import subprocess
import pytest
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
CASES_DIR = os.path.join(SCRIPT_DIR, 'cases')
AGENTHUB_DIR = os.path.join(SCRIPT_DIR, '../', 'agenthub')
def agents():
"""Retrieves a list of available agents.
Returns:
A list of agent names.
"""
agents = []
for agent in os.listdir(AGENTHUB_DIR):
if os.path.isdir(os.path.join(AGENTHUB_DIR, agent)) and agent.endswith(
'_agent'
):
agents.append(agent)
return agents
@pytest.fixture(scope='session')
def test_cases_dir():
"""Fixture that provides the directory path for test cases.
Returns:
The directory path for test cases.
"""
return CASES_DIR
@pytest.fixture
def task_file(test_cases_dir, request):
"""Fixture that provides the path to the task file for a test case.
Args:
test_cases_dir: The directory path for test cases.
request: The pytest request object.
Returns:
The path to the task file for the test case.
"""
test_case_dir = os.path.dirname(request.module.__file__)
task_file_path = os.path.join(test_case_dir, 'task.txt')
return task_file_path
@pytest.fixture
def workspace_dir(test_cases_dir, request):
"""Fixture that provides the workspace directory for a test case.
Args:
test_cases_dir: The directory path for test cases.
request: The pytest request object.
Returns:
The workspace directory for the test case.
"""
test_case_dir = os.path.dirname(request.module.__file__)
workspace_dir = os.path.join(test_case_dir, 'workspace')
return workspace_dir
@pytest.fixture
def model(request):
"""Fixture that provides the model name.
Args:
request: The pytest request object.
Returns:
The model name, defaulting to "gpt-3.5-turbo".
"""
return request.config.getoption('model', default='gpt-3.5-turbo')
@pytest.fixture
def run_test_case(test_cases_dir, workspace_dir, request):
"""Fixture that provides a function to run a test case.
Args:
test_cases_dir: The directory path for test cases.
workspace_dir: The workspace directory for the test case.
request: The pytest request object.
Returns:
A function that runs a test case for a given agent and case.
"""
def _run_test_case(agent, case):
"""Runs a test case for a given agent.
Args:
agent: The name of the agent to run the test case for.
case: The name of the test case to run.
Returns:
The path to the workspace directory for the agent and test case.
Raises:
AssertionError: If the test case execution fails (non-zero return code).
Steps:
"""
case_dir = os.path.join(test_cases_dir, case)
task = open(os.path.join(case_dir, 'task.txt'), 'r').read().strip()
outputs_dir = os.path.join(case_dir, 'outputs')
agent_dir = os.path.join(outputs_dir, agent)
if not os.path.exists(agent_dir):
os.makedirs(agent_dir)
shutil.rmtree(os.path.join(agent_dir, 'workspace'), ignore_errors=True)
if os.path.isdir(os.path.join(case_dir, 'start')):
os.copytree(
os.path.join(case_dir, 'start'), os.path.join(agent_dir, 'workspace')
)
else:
os.makedirs(os.path.join(agent_dir, 'workspace'))
agents_ref = {
'codeact_agent': 'CodeActAgent',
}
process = subprocess.Popen(
[
'python3',
f'{SCRIPT_DIR}/../../openhands/main.py',
'-d',
f'{os.path.join(agent_dir, "workspace")}',
'-c',
f'{agents_ref[agent]}',
'-t',
f'{task}',
'-m',
'gpt-3.5-turbo',
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
stdout, stderr = process.communicate()
logging.info(f'Stdout: {stdout}')
logging.error(f'Stderr: {stderr}')
assert process.returncode == 0
return os.path.join(agent_dir, 'workspace')
return _run_test_case
def pytest_configure(config):
"""Configuration hook for pytest.
Args:
config: The pytest configuration object.
"""
now = datetime.datetime.now()
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler(f'test_results_{now.strftime("%Y%m%d_%H%M%S")}.log'),
logging.StreamHandler(),
],
)
-32
View File
@@ -1,32 +0,0 @@
import argparse
import pytest
from openhands.config import load_openhands_config
config = load_openhands_config()
if __name__ == '__main__':
"""Main entry point of the script.
This script runs pytest with specific arguments and configuration.
Usage:
python script_name.py [--OPENAI_API_KEY=<api_key>] [--model=<model_name>]
"""
parser = argparse.ArgumentParser(
description='This script runs pytest with specific arguments and configuration.'
)
parser.add_argument(
'--OPENAI_API_KEY', type=str, required=True, help='Your OpenAI API key'
)
parser.add_argument(
'--model', type=str, required=True, help='The model name to use'
)
parser_args = parser.parse_args()
config.config['OPENAI_API_KEY'] = parser_args.OPENAI_API_KEY
args = ['-v', 'evaluation/regression/cases', f'-o model={parser_args.model}']
pytest.main(args)
@@ -0,0 +1,135 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { TaskTrackingObservationContent } from "#/components/features/chat/task-tracking-observation-content";
import { TaskTrackingObservation } from "#/types/core/observations";
// Mock the translation hook
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
"TASK_TRACKING_OBSERVATION$TASK_LIST": "Task List",
"TASK_TRACKING_OBSERVATION$TASK_ID": "ID",
"TASK_TRACKING_OBSERVATION$TASK_NOTES": "Notes",
"TASK_TRACKING_OBSERVATION$RESULT": "Result",
};
return translations[key] || key;
},
}),
}));
describe("TaskTrackingObservationContent", () => {
const mockEvent: TaskTrackingObservation = {
id: 123,
timestamp: "2024-01-01T00:00:00Z",
source: "agent",
observation: "task_tracking",
content: "Task tracking operation completed successfully",
cause: 122,
message: "Task tracking operation completed successfully",
extras: {
command: "plan",
task_list: [
{
id: "task-1",
title: "Implement feature A",
status: "todo",
notes: "This is a test task",
},
{
id: "task-2",
title: "Fix bug B",
status: "in_progress",
},
{
id: "task-3",
title: "Deploy to production",
status: "done",
notes: "Completed successfully",
},
],
},
};
it("does not render command section", () => {
render(<TaskTrackingObservationContent event={mockEvent} />);
expect(screen.queryByText("Command")).not.toBeInTheDocument();
expect(screen.queryByText("plan")).not.toBeInTheDocument();
});
it("renders task list when command is 'plan' and tasks exist", () => {
render(<TaskTrackingObservationContent event={mockEvent} />);
expect(screen.getByText("Task List (3 items)")).toBeInTheDocument();
expect(screen.getByText("Implement feature A")).toBeInTheDocument();
expect(screen.getByText("Fix bug B")).toBeInTheDocument();
expect(screen.getByText("Deploy to production")).toBeInTheDocument();
});
it("displays correct status icons and badges", () => {
render(<TaskTrackingObservationContent event={mockEvent} />);
// Check for status text (the icons are emojis)
expect(screen.getByText("todo")).toBeInTheDocument();
expect(screen.getByText("in progress")).toBeInTheDocument();
expect(screen.getByText("done")).toBeInTheDocument();
});
it("displays task IDs and notes", () => {
render(<TaskTrackingObservationContent event={mockEvent} />);
expect(screen.getByText("ID: task-1")).toBeInTheDocument();
expect(screen.getByText("ID: task-2")).toBeInTheDocument();
expect(screen.getByText("ID: task-3")).toBeInTheDocument();
expect(screen.getByText("Notes: This is a test task")).toBeInTheDocument();
expect(screen.getByText("Notes: Completed successfully")).toBeInTheDocument();
});
it("renders result section when content exists", () => {
render(<TaskTrackingObservationContent event={mockEvent} />);
expect(screen.getByText("Result")).toBeInTheDocument();
expect(screen.getByText("Task tracking operation completed successfully")).toBeInTheDocument();
});
it("does not render task list when command is not 'plan'", () => {
const eventWithoutPlan = {
...mockEvent,
extras: {
...mockEvent.extras,
command: "view",
},
};
render(<TaskTrackingObservationContent event={eventWithoutPlan} />);
expect(screen.queryByText("Task List")).not.toBeInTheDocument();
});
it("does not render task list when task list is empty", () => {
const eventWithEmptyTasks = {
...mockEvent,
extras: {
...mockEvent.extras,
task_list: [],
},
};
render(<TaskTrackingObservationContent event={eventWithEmptyTasks} />);
expect(screen.queryByText("Task List")).not.toBeInTheDocument();
});
it("does not render result section when content is empty", () => {
const eventWithoutContent = {
...mockEvent,
content: "",
};
render(<TaskTrackingObservationContent event={eventWithoutContent} />);
expect(screen.queryByText("Result")).not.toBeInTheDocument();
});
});
@@ -42,6 +42,8 @@ describe("LikertScale", () => {
expect(screen.getByText(I18nKey.FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION)).toBeInTheDocument();
expect(screen.getByText(I18nKey.FEEDBACK$REASON_FORGOT_CONTEXT)).toBeInTheDocument();
expect(screen.getByText(I18nKey.FEEDBACK$REASON_UNNECESSARY_CHANGES)).toBeInTheDocument();
expect(screen.getByText(I18nKey.FEEDBACK$REASON_SHOULD_ASK_FIRST)).toBeInTheDocument();
expect(screen.getByText(I18nKey.FEEDBACK$REASON_DIDNT_FINISH_JOB)).toBeInTheDocument();
expect(screen.getByText(I18nKey.FEEDBACK$REASON_OTHER)).toBeInTheDocument();
});
@@ -1,6 +1,6 @@
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createRoutesStub } from "react-router";
import { renderWithProviders } from "test-utils";
import OpenHands from "#/api/open-hands";
@@ -36,6 +36,8 @@ vi.mock("react-i18next", async () => {
"SETTINGS$NAV_API_KEYS": "API Keys",
"SETTINGS$NAV_LLM": "LLM",
"SETTINGS$NAV_USER": "User",
"SETTINGS$NAV_SECRETS": "Secrets",
"SETTINGS$NAV_MCP": "MCP",
"SETTINGS$TITLE": "Settings"
};
return translations[key] || key;
@@ -47,8 +49,33 @@ vi.mock("react-i18next", async () => {
};
});
// Mock useConfig hook
const { mockUseConfig } = vi.hoisted(() => ({
mockUseConfig: vi.fn(),
}));
vi.mock("#/hooks/query/use-config", () => ({
useConfig: mockUseConfig,
}));
describe("Settings Billing", () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
beforeEach(() => {
// Set default config to OSS mode
mockUseConfig.mockReturnValue({
data: {
APP_MODE: "oss",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
FEATURE_FLAGS: {
ENABLE_BILLING: false,
HIDE_LLM_SETTINGS: false,
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
},
},
isLoading: false,
});
});
const RoutesStub = createRoutesStub([
{
@@ -79,19 +106,7 @@ describe("Settings Billing", () => {
});
it("should not render the credits tab if OSS mode", async () => {
getConfigSpy.mockResolvedValue({
APP_MODE: "oss",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
FEATURE_FLAGS: {
ENABLE_BILLING: false,
HIDE_LLM_SETTINGS: false,
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
},
});
// OSS mode is set by default in beforeEach
renderSettingsScreen();
const navbar = await screen.findByTestId("settings-navbar");
@@ -100,17 +115,20 @@ describe("Settings Billing", () => {
});
it("should render the credits tab if SaaS mode and billing is enabled", async () => {
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
FEATURE_FLAGS: {
ENABLE_BILLING: true,
HIDE_LLM_SETTINGS: false,
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
mockUseConfig.mockReturnValue({
data: {
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
FEATURE_FLAGS: {
ENABLE_BILLING: true,
HIDE_LLM_SETTINGS: false,
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
},
},
isLoading: false,
});
renderSettingsScreen();
@@ -121,17 +139,20 @@ describe("Settings Billing", () => {
it("should render the billing settings if clicking the credits item", async () => {
const user = userEvent.setup();
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
FEATURE_FLAGS: {
ENABLE_BILLING: true,
HIDE_LLM_SETTINGS: false,
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
mockUseConfig.mockReturnValue({
data: {
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
FEATURE_FLAGS: {
ENABLE_BILLING: true,
HIDE_LLM_SETTINGS: false,
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
},
},
isLoading: false,
});
renderSettingsScreen();
+11 -11
View File
@@ -19,6 +19,9 @@ vi.mock("react-i18next", async () => {
SETTINGS$NAV_CREDITS: "Credits",
SETTINGS$NAV_API_KEYS: "API Keys",
SETTINGS$NAV_LLM: "LLM",
SETTINGS$NAV_SECRETS: "Secrets",
SETTINGS$NAV_MCP: "MCP",
SETTINGS$NAV_USER: "User",
SETTINGS$TITLE: "Settings",
};
return translations[key] || key;
@@ -119,22 +122,21 @@ describe("Settings Screen", () => {
});
it("should render the saas navbar", async () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
// @ts-expect-error - only return app mode
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
});
const saasConfig = { APP_MODE: "saas" };
// Clear any existing query data and set the config
mockQueryClient.clear();
mockQueryClient.setQueryData(["config"], saasConfig);
const sectionsToInclude = [
"user",
"integrations",
"application",
"credits", // The nav item shows "credits" text but routes to /billing
"secrets",
"api keys",
];
const sectionsToExclude = ["llm"];
// Clear any existing query data
mockQueryClient.clear();
const sectionsToExclude = ["llm", "mcp"];
renderSettingsScreen();
@@ -151,8 +153,6 @@ describe("Settings Screen", () => {
});
expect(sectionElement).not.toBeInTheDocument();
});
getConfigSpy.mockRestore();
});
it("should not be able to access saas-only routes in oss mode", async () => {
@@ -0,0 +1,51 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { browserTab } from "#/utils/browser-tab";
// These tests exercise the browser-tab notification flasher behavior.
// Specifically we verify that when the document title changes externally
// while a notification is active, the flasher updates its internal
// baseline so it restores/toggles to the new title instead of an old one.
describe("browserTab notifications", () => {
const MESSAGE = "Agent ready";
const INITIAL = "Conversation 123 | OpenHands";
const RENAMED = "My renamed title | OpenHands";
beforeEach(() => {
vi.useFakeTimers();
// reset title for each test
document.title = INITIAL;
});
afterEach(() => {
browserTab.stopNotification();
vi.runOnlyPendingTimers();
vi.useRealTimers();
});
it("updates baseline when title changes during an active notification and restores to the new title", () => {
// Start flashing
browserTab.startNotification(MESSAGE);
// Tick once: should switch to the message
vi.advanceTimersByTime(1000);
expect(document.title).toBe(MESSAGE);
// Simulate an external rename while flashing (e.g., user edits title)
document.title = RENAMED;
// Next tick: flasher observes the external change and updates baseline
vi.advanceTimersByTime(1000);
// On this tick, we toggle back to the message
expect(document.title).toBe(MESSAGE);
// Next tick should toggle to the updated baseline (renamed title)
vi.advanceTimersByTime(1000);
expect(document.title).toBe(RENAMED);
// Stop flashing: title should remain the updated baseline
browserTab.stopNotification();
expect(document.title).toBe(RENAMED);
});
});
+133 -150
View File
@@ -18,9 +18,9 @@
"@reduxjs/toolkit": "^2.8.2",
"@stripe/react-stripe-js": "^3.9.0",
"@stripe/stripe-js": "^7.8.0",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.84.2",
"@tailwindcss/postcss": "^4.1.12",
"@tailwindcss/vite": "^4.1.12",
"@tanstack/react-query": "^5.85.3",
"@uidotdev/usehooks": "^2.4.1",
"@vitejs/plugin-react": "^5.0.0",
"@xterm/addon-fit": "^0.10.0",
@@ -30,14 +30,14 @@
"date-fns": "^4.1.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.23.12",
"i18next": "^25.3.2",
"i18next": "^25.3.6",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.29",
"jose": "^6.0.12",
"lucide-react": "^0.539.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.259.0",
"posthog-js": "^1.260.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-highlight": "^0.15.0",
@@ -60,8 +60,8 @@
"ws": "^8.18.2"
},
"devDependencies": {
"@babel/parser": "^7.28.0",
"@babel/traverse": "^7.28.0",
"@babel/parser": "^7.28.3",
"@babel/traverse": "^7.28.3",
"@babel/types": "^7.28.2",
"@mswjs/socket.io-binding": "^0.2.0",
"@playwright/test": "^1.54.2",
@@ -69,7 +69,7 @@
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.83.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/jest-dom": "^6.7.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.2.0",
@@ -226,13 +226,12 @@
}
},
"node_modules/@babel/generator": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
"license": "MIT",
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"dependencies": {
"@babel/parser": "^7.28.0",
"@babel/types": "^7.28.0",
"@babel/parser": "^7.28.3",
"@babel/types": "^7.28.2",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -459,12 +458,11 @@
}
},
"node_modules/@babel/parser": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
"license": "MIT",
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
"integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
"dependencies": {
"@babel/types": "^7.28.0"
"@babel/types": "^7.28.2"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -616,17 +614,16 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
"license": "MIT",
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz",
"integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.0",
"@babel/generator": "^7.28.3",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.0",
"@babel/parser": "^7.28.3",
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.0",
"@babel/types": "^7.28.2",
"debug": "^4.3.1"
},
"engines": {
@@ -3405,6 +3402,15 @@
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -5899,26 +5905,24 @@
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
"integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==",
"license": "MIT",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz",
"integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"@jridgewell/remapping": "^2.3.4",
"enhanced-resolve": "^5.18.3",
"jiti": "^2.5.1",
"lightningcss": "1.30.1",
"magic-string": "^0.30.17",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.11"
"tailwindcss": "4.1.12"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz",
"integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz",
"integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.4",
"tar": "^7.4.3"
@@ -5927,28 +5931,27 @@
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.11",
"@tailwindcss/oxide-darwin-arm64": "4.1.11",
"@tailwindcss/oxide-darwin-x64": "4.1.11",
"@tailwindcss/oxide-freebsd-x64": "4.1.11",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.11",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.11",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.11",
"@tailwindcss/oxide-linux-x64-musl": "4.1.11",
"@tailwindcss/oxide-wasm32-wasi": "4.1.11",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.11",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.11"
"@tailwindcss/oxide-android-arm64": "4.1.12",
"@tailwindcss/oxide-darwin-arm64": "4.1.12",
"@tailwindcss/oxide-darwin-x64": "4.1.12",
"@tailwindcss/oxide-freebsd-x64": "4.1.12",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.12",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.12",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.12",
"@tailwindcss/oxide-linux-x64-musl": "4.1.12",
"@tailwindcss/oxide-wasm32-wasi": "4.1.12",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.12",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.12"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz",
"integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz",
"integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
@@ -5958,13 +5961,12 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz",
"integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz",
"integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -5974,13 +5976,12 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz",
"integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz",
"integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -5990,13 +5991,12 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz",
"integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz",
"integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
@@ -6006,13 +6006,12 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz",
"integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz",
"integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@@ -6022,13 +6021,12 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz",
"integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz",
"integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@@ -6038,13 +6036,12 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz",
"integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz",
"integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@@ -6054,13 +6051,12 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz",
"integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz",
"integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@@ -6070,13 +6066,12 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz",
"integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz",
"integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@@ -6086,9 +6081,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz",
"integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz",
"integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@@ -6100,14 +6095,13 @@
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@emnapi/wasi-threads": "^1.0.2",
"@napi-rs/wasm-runtime": "^0.2.11",
"@tybys/wasm-util": "^0.9.0",
"@emnapi/core": "^1.4.5",
"@emnapi/runtime": "^1.4.5",
"@emnapi/wasi-threads": "^1.0.4",
"@napi-rs/wasm-runtime": "^0.2.12",
"@tybys/wasm-util": "^0.10.0",
"tslib": "^2.8.0"
},
"engines": {
@@ -6115,17 +6109,17 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.4.3",
"version": "1.4.5",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.0.2",
"@emnapi/wasi-threads": "1.0.4",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.4.3",
"version": "1.4.5",
"inBundle": true,
"license": "MIT",
"optional": true,
@@ -6134,7 +6128,7 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.0.2",
"version": "1.0.4",
"inBundle": true,
"license": "MIT",
"optional": true,
@@ -6143,18 +6137,18 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.11",
"version": "0.2.12",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@tybys/wasm-util": "^0.9.0"
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.9.0",
"version": "0.10.0",
"inBundle": true,
"license": "MIT",
"optional": true,
@@ -6169,13 +6163,12 @@
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz",
"integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz",
"integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
@@ -6185,13 +6178,12 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz",
"integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz",
"integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
@@ -6201,16 +6193,15 @@
}
},
"node_modules/@tailwindcss/postcss": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz",
"integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==",
"license": "MIT",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.12.tgz",
"integrity": "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@tailwindcss/node": "4.1.11",
"@tailwindcss/oxide": "4.1.11",
"@tailwindcss/node": "4.1.12",
"@tailwindcss/oxide": "4.1.12",
"postcss": "^8.4.41",
"tailwindcss": "4.1.11"
"tailwindcss": "4.1.12"
}
},
"node_modules/@tailwindcss/typography": {
@@ -6230,14 +6221,13 @@
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz",
"integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==",
"license": "MIT",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.12.tgz",
"integrity": "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==",
"dependencies": {
"@tailwindcss/node": "4.1.11",
"@tailwindcss/oxide": "4.1.11",
"tailwindcss": "4.1.11"
"@tailwindcss/node": "4.1.12",
"@tailwindcss/oxide": "4.1.12",
"tailwindcss": "4.1.12"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6 || ^7"
@@ -6261,22 +6251,20 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.83.1",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.1.tgz",
"integrity": "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==",
"license": "MIT",
"version": "5.85.3",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.3.tgz",
"integrity": "sha512-9Ne4USX83nHmRuEYs78LW+3lFEEO2hBDHu7mrdIgAFx5Zcrs7ker3n/i8p4kf6OgKExmaDN5oR0efRD7i2J0DQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.85.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.0.tgz",
"integrity": "sha512-t1HMfToVMGfwEJRya6GG7gbK0luZJd+9IySFNePL1BforU1F3LqQ3tBC2Rpvr88bOrlU6PXyMLgJD0Yzn4ztUw==",
"license": "MIT",
"version": "5.85.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.3.tgz",
"integrity": "sha512-AqU8TvNh5GVIE8I+TUU0noryBRy7gOY0XhSayVXmOPll4UkZeLWKDwi0rtWOZbwLRCbyxorfJ5DIjDqE7GXpcQ==",
"dependencies": {
"@tanstack/query-core": "5.83.1"
"@tanstack/query-core": "5.85.3"
},
"funding": {
"type": "github",
@@ -6334,17 +6322,15 @@
}
},
"node_modules/@testing-library/jest-dom": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.4.tgz",
"integrity": "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ==",
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.7.0.tgz",
"integrity": "sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@adobe/css-tools": "^4.4.0",
"aria-query": "^5.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.6.3",
"lodash": "^4.17.21",
"picocolors": "^1.1.1",
"redent": "^3.0.0"
},
@@ -11134,9 +11120,9 @@
}
},
"node_modules/i18next": {
"version": "25.3.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.4.tgz",
"integrity": "sha512-AHklEYFLiRRxW1Cb6zE9lfnEtYvsydRC8nRS3RSKGX3zCqZ8nLZwMaUsrb80YuccPNv2RNokDL8LkTNnp+6mDw==",
"version": "25.3.6",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.6.tgz",
"integrity": "sha512-dThZ0CTCM3sUG/qS0ZtQYZQcUI6DtBN8yBHK+SKEqihPcEYmjVWh/YJ4luic73Iq6Uxhp6q7LJJntRK5+1t7jQ==",
"funding": [
{
"type": "individual",
@@ -11151,7 +11137,6 @@
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -14817,10 +14802,9 @@
"license": "MIT"
},
"node_modules/posthog-js": {
"version": "1.259.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.259.0.tgz",
"integrity": "sha512-6usLnJshky8fQ82ask7PIJh4BSFOU0VkRbFg8Zanm/HIlYMG1VOdRWlToA63JXeO7Bzm9TuREq1wFm5U2VEVCg==",
"license": "SEE LICENSE IN LICENSE",
"version": "1.260.1",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.260.1.tgz",
"integrity": "sha512-DD8ZSRpdScacMqtqUIvMFme8lmOWkOvExG8VvjONE7Cm3xpRH5xXpfrwMJE4bayTGWKMx4ij6SfphK6dm/o2ug==",
"dependencies": {
"core-js": "^3.38.1",
"fflate": "^0.4.8",
@@ -17029,10 +17013,9 @@
}
},
"node_modules/tailwindcss": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"license": "MIT"
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
"integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA=="
},
"node_modules/tapable": {
"version": "2.2.2",
+8 -8
View File
@@ -17,9 +17,9 @@
"@reduxjs/toolkit": "^2.8.2",
"@stripe/react-stripe-js": "^3.9.0",
"@stripe/stripe-js": "^7.8.0",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.84.2",
"@tailwindcss/postcss": "^4.1.12",
"@tailwindcss/vite": "^4.1.12",
"@tanstack/react-query": "^5.85.3",
"@uidotdev/usehooks": "^2.4.1",
"@vitejs/plugin-react": "^5.0.0",
"@xterm/addon-fit": "^0.10.0",
@@ -29,14 +29,14 @@
"date-fns": "^4.1.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.23.12",
"i18next": "^25.3.2",
"i18next": "^25.3.6",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.29",
"jose": "^6.0.12",
"lucide-react": "^0.539.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.259.0",
"posthog-js": "^1.260.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-highlight": "^0.15.0",
@@ -84,8 +84,8 @@
]
},
"devDependencies": {
"@babel/parser": "^7.28.0",
"@babel/traverse": "^7.28.0",
"@babel/parser": "^7.28.3",
"@babel/traverse": "^7.28.3",
"@babel/types": "^7.28.2",
"@mswjs/socket.io-binding": "^0.2.0",
"@playwright/test": "^1.54.2",
@@ -93,7 +93,7 @@
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.83.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/jest-dom": "^6.7.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.2.0",
@@ -9,6 +9,7 @@ import {
ThinkAction,
OpenHandsAction,
FinishAction,
TaskTrackingAction,
} from "#/types/core/actions";
import { getDefaultEventContent, MAX_CONTENT_LENGTH } from "./shared";
import i18n from "#/i18n";
@@ -79,6 +80,38 @@ const getThinkActionContent = (event: ThinkAction): string =>
const getFinishActionContent = (event: FinishAction): string =>
event.args.final_thought.trim();
const getTaskTrackingActionContent = (event: TaskTrackingAction): string => {
let content = `**Command:** \`${event.args.command}\``;
if (
event.args.command === "plan" &&
event.args.task_list &&
event.args.task_list.length > 0
) {
content += `\n\n**Task List (${event.args.task_list.length} ${event.args.task_list.length === 1 ? "item" : "items"}):**\n`;
event.args.task_list.forEach((task, index) => {
const statusIcon =
{
todo: "⏳",
in_progress: "🔄",
done: "✅",
}[task.status] || "❓";
content += `\n${index + 1}. ${statusIcon} **[${task.status.toUpperCase().replace("_", " ")}]** ${task.title}`;
content += `\n *ID: ${task.id}*`;
if (task.notes) {
content += `\n *Notes: ${task.notes}*`;
}
});
} else if (event.args.command === "plan") {
content += "\n\n**Task List:** Empty";
}
return content;
};
const getNoContentActionContent = (): string => "";
export const getActionContent = (event: OpenHandsAction): string => {
@@ -102,6 +135,8 @@ export const getActionContent = (event: OpenHandsAction): string => {
return getThinkActionContent(event);
case "finish":
return getFinishActionContent(event);
case "task_tracking":
return getTaskTrackingActionContent(event);
default:
return getDefaultEventContent(event);
}
@@ -6,6 +6,7 @@ import {
BrowseObservation,
OpenHandsObservation,
RecallObservation,
TaskTrackingObservation,
} from "#/types/core/observations";
import { getObservationResult } from "./get-observation-result";
import { getDefaultEventContent, MAX_CONTENT_LENGTH } from "./shared";
@@ -102,6 +103,40 @@ const getRecallObservationContent = (event: RecallObservation): string => {
return content;
};
const getTaskTrackingObservationContent = (
event: TaskTrackingObservation,
): string => {
const { command, task_list: taskList } = event.extras;
let content = `**Command:** \`${command}\``;
if (command === "plan" && taskList.length > 0) {
content += `\n\n**Task List (${taskList.length} ${taskList.length === 1 ? "item" : "items"}):**\n`;
taskList.forEach((task, index) => {
const statusIcon =
{
todo: "⏳",
in_progress: "🔄",
done: "✅",
}[task.status] || "❓";
content += `\n${index + 1}. ${statusIcon} **[${task.status.toUpperCase().replace("_", " ")}]** ${task.title}`;
content += `\n *ID: ${task.id}*`;
if (task.notes) {
content += `\n *Notes: ${task.notes}*`;
}
});
} else if (command === "plan") {
content += "\n\n**Task List:** Empty";
}
if (event.content && event.content.trim()) {
content += `\n\n**Result:** ${event.content.trim()}`;
}
return content;
};
export const getObservationContent = (event: OpenHandsObservation): string => {
switch (event.observation) {
case "read":
@@ -118,6 +153,8 @@ export const getObservationContent = (event: OpenHandsObservation): string => {
return getBrowseObservationContent(event);
case "recall":
return getRecallObservationContent(event);
case "task_tracking":
return getTaskTrackingObservationContent(event);
default:
return getDefaultEventContent(event);
}
@@ -16,6 +16,8 @@ const COMMON_NO_RENDER_LIST: OpenHandsEventType[] = [
const ACTION_NO_RENDER_LIST: OpenHandsEventType[] = ["recall"];
const OBSERVATION_NO_RENDER_LIST: OpenHandsEventType[] = ["think"];
export const shouldRenderEvent = (
event: OpenHandsAction | OpenHandsObservation,
) => {
@@ -35,7 +37,10 @@ export const shouldRenderEvent = (
return false;
}
return !COMMON_NO_RENDER_LIST.includes(event.observation);
const noRenderList = COMMON_NO_RENDER_LIST.concat(
OBSERVATION_NO_RENDER_LIST,
);
return !noRenderList.includes(event.observation);
}
return true;
@@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
import { OpenHandsAction } from "#/types/core/actions";
import {
@@ -10,12 +11,14 @@ import {
isFinishAction,
isRejectObservation,
isMcpObservation,
isTaskTrackingObservation,
} from "#/types/core/guards";
import { OpenHandsObservation } from "#/types/core/observations";
import { ImageCarousel } from "../images/image-carousel";
import { ChatMessage } from "./chat-message";
import { ErrorMessage } from "./error-message";
import { MCPObservationContent } from "./mcp-observation-content";
import { TaskTrackingObservationContent } from "./task-tracking-observation-content";
import { getObservationResult } from "./event-content-helpers/get-observation-result";
import { getEventContent } from "./event-content-helpers/get-event-content";
import { GenericEventMessage } from "./generic-event-message";
@@ -58,6 +61,7 @@ export function EventMessage({
actions,
isInLast10Actions,
}: EventMessageProps) {
const { t } = useTranslation();
const shouldShowConfirmationButtons =
isLastMessage && event.source === "agent" && isAwaitingUserConfirmation;
@@ -113,7 +117,7 @@ export function EventMessage({
}
if (hasObservationPair && isOpenHandsAction(event)) {
if (hasThoughtProperty(event.args)) {
if (hasThoughtProperty(event.args) && event.action !== "think") {
return (
<div>
<ChatMessage
@@ -209,11 +213,41 @@ export function EventMessage({
);
}
if (isTaskTrackingObservation(event)) {
const { command } = event.extras;
let title: React.ReactNode;
let initiallyExpanded = false;
// Determine title and expansion state based on command
if (command === "plan") {
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_PLAN");
initiallyExpanded = true;
} else {
// command === "view"
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_VIEW");
initiallyExpanded = false;
}
return (
<div>
<GenericEventMessage
title={title}
details={<TaskTrackingObservationContent event={event} />}
success={getObservationResult(event)}
initiallyExpanded={initiallyExpanded}
/>
{shouldShowConfirmationButtons && <ConfirmationButtons />}
</div>
);
}
return (
<div>
{isOpenHandsAction(event) && hasThoughtProperty(event.args) && (
<ChatMessage type="agent" message={event.args.thought} />
)}
{isOpenHandsAction(event) &&
hasThoughtProperty(event.args) &&
event.action !== "think" && (
<ChatMessage type="agent" message={event.args.thought} />
)}
<GenericEventMessage
title={getEventContent(event).title}
@@ -13,14 +13,16 @@ interface GenericEventMessageProps {
title: React.ReactNode;
details: string | React.ReactNode;
success?: ObservationResultStatus;
initiallyExpanded?: boolean;
}
export function GenericEventMessage({
title,
details,
success,
initiallyExpanded = false,
}: GenericEventMessageProps) {
const [showDetails, setShowDetails] = React.useState(false);
const [showDetails, setShowDetails] = React.useState(initiallyExpanded);
return (
<div className="flex flex-col gap-2 border-l-2 pl-2 my-2 py-2 border-neutral-300 text-sm w-full">
@@ -0,0 +1,110 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { TaskTrackingObservation } from "#/types/core/observations";
interface TaskTrackingObservationContentProps {
event: TaskTrackingObservation;
}
export function TaskTrackingObservationContent({
event,
}: TaskTrackingObservationContentProps) {
const { t } = useTranslation();
const { command, task_list: taskList } = event.extras;
const shouldShowTaskList = command === "plan" && taskList.length > 0;
const getStatusIcon = (status: string) => {
switch (status) {
case "todo":
return "⏳";
case "in_progress":
return "🔄";
case "done":
return "✅";
default:
return "❓";
}
};
const getStatusClassName = (status: string) => {
if (status === "done") {
return "bg-green-800 text-green-200";
}
if (status === "in_progress") {
return "bg-yellow-800 text-yellow-200";
}
return "bg-gray-700 text-gray-300";
};
return (
<div className="flex flex-col gap-4">
{/* Task List section - only show for 'plan' command */}
{shouldShowTaskList && (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-gray-300">
{t("TASK_TRACKING_OBSERVATION$TASK_LIST")} ({taskList.length}{" "}
{taskList.length === 1 ? "item" : "items"})
</h3>
</div>
<div className="p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
<div className="space-y-3">
{taskList.map((task, index) => (
<div key={task.id} className="border-l-2 border-gray-600 pl-3">
<div className="flex items-start gap-2">
<span className="text-lg">
{getStatusIcon(task.status)}
</span>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm text-gray-400">
{index + 1}.
</span>
<span
className={`text-xs px-2 py-1 rounded uppercase font-semibold ${getStatusClassName(
task.status,
)}`}
>
{task.status.replace("_", " ")}
</span>
</div>
<h4 className="font-medium text-white mb-1">
{task.title}
</h4>
<p className="text-xs text-gray-400 mb-1">
{t("TASK_TRACKING_OBSERVATION$TASK_ID")}: {task.id}
</p>
{task.notes && (
<p className="text-sm text-gray-300 italic">
{t("TASK_TRACKING_OBSERVATION$TASK_NOTES")}:{" "}
{task.notes}
</p>
)}
</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* Result message - only show if there's meaningful content */}
{event.content && event.content.trim() && (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-gray-300">
{t("TASK_TRACKING_OBSERVATION$RESULT")}
</h3>
</div>
<div className="p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 shadow-inner">
<pre className="whitespace-pre-wrap text-sm">
{event.content.trim()}
</pre>
</div>
</div>
)}
</div>
);
}
@@ -46,6 +46,7 @@ export function LikertScale({
t(I18nKey.FEEDBACK$REASON_FORGOT_CONTEXT),
t(I18nKey.FEEDBACK$REASON_UNNECESSARY_CHANGES),
t(I18nKey.FEEDBACK$REASON_SHOULD_ASK_FIRST),
t(I18nKey.FEEDBACK$REASON_DIDNT_FINISH_JOB),
t(I18nKey.FEEDBACK$REASON_OTHER),
];
+15 -14
View File
@@ -1,5 +1,5 @@
import clsx from "clsx";
import React, { useEffect, useRef, useState } from "react";
import React, { useRef, useState } from "react";
import { NavTab } from "./nav-tab";
import { ScrollLeftButton } from "./scroll-left-button";
import { ScrollRightButton } from "./scroll-right-button";
@@ -25,18 +25,12 @@ export function Container({
children,
className,
}: ContainerProps) {
const [containerWidth, setContainerWidth] = useState(0);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
const [showScrollButtons, setShowScrollButtons] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
// Track container width using ResizeObserver
useTrackElementWidth({
elementRef: containerRef,
callback: setContainerWidth,
});
// Check scroll position and update button states
const updateScrollButtons = () => {
if (scrollContainerRef.current) {
@@ -47,10 +41,19 @@ export function Container({
}
};
// Update scroll buttons when tabs change or container width changes
useEffect(() => {
updateScrollButtons();
}, [labels, containerWidth]);
// Track container width using ResizeObserver
useTrackElementWidth({
elementRef: containerRef,
callback: (width: number) => {
// Only update scroll button visibility when crossing the threshold
const shouldShowScrollButtons =
width < 598 && Boolean(labels) && labels!.length > 0;
if (shouldShowScrollButtons) {
setShowScrollButtons(shouldShowScrollButtons);
}
updateScrollButtons();
},
});
// Scroll functions
const scrollLeft = () => {
@@ -65,8 +68,6 @@ export function Container({
}
};
const showScrollButtons = containerWidth < 598 && labels && labels.length > 0;
return (
<div
ref={containerRef}
+27 -3
View File
@@ -1,18 +1,38 @@
import { useEffect } from "react";
import { useEffect, useCallback, useRef } from "react";
interface UseTrackElementWidthProps {
elementRef: React.RefObject<HTMLElement | null>;
callback: (width: number) => void;
delay?: number; // Optional delay parameter with default
}
export const useTrackElementWidth = ({
elementRef,
callback,
delay = 100, // Default 100ms delay
}: UseTrackElementWidthProps) => {
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
// Create debounced callback that only fires after delay
const debouncedCallback = useCallback(
(width: number) => {
// Clear existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Set new timeout
timeoutRef.current = setTimeout(() => {
callback(width);
}, delay);
},
[callback, delay],
);
useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
callback(entry.contentRect.width);
debouncedCallback(entry.contentRect.width);
}
});
@@ -21,7 +41,11 @@ export const useTrackElementWidth = ({
}
return () => {
// Clean up timeout and observer
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
resizeObserver.disconnect();
};
}, []);
}, [debouncedCallback]);
};
+10
View File
@@ -37,8 +37,14 @@ export enum I18nKey {
EVENT$UNKNOWN_EVENT = "EVENT$UNKNOWN_EVENT",
OBSERVATION$COMMAND_NO_OUTPUT = "OBSERVATION$COMMAND_NO_OUTPUT",
OBSERVATION$MCP_NO_OUTPUT = "OBSERVATION$MCP_NO_OUTPUT",
OBSERVATION$TASK_TRACKING_NO_OUTPUT = "OBSERVATION$TASK_TRACKING_NO_OUTPUT",
MCP_OBSERVATION$ARGUMENTS = "MCP_OBSERVATION$ARGUMENTS",
MCP_OBSERVATION$OUTPUT = "MCP_OBSERVATION$OUTPUT",
TASK_TRACKING_OBSERVATION$TASK_LIST = "TASK_TRACKING_OBSERVATION$TASK_LIST",
TASK_TRACKING_OBSERVATION$OUTPUT = "TASK_TRACKING_OBSERVATION$OUTPUT",
TASK_TRACKING_OBSERVATION$TASK_ID = "TASK_TRACKING_OBSERVATION$TASK_ID",
TASK_TRACKING_OBSERVATION$TASK_NOTES = "TASK_TRACKING_OBSERVATION$TASK_NOTES",
TASK_TRACKING_OBSERVATION$RESULT = "TASK_TRACKING_OBSERVATION$RESULT",
OBSERVATION$ERROR_PREFIX = "OBSERVATION$ERROR_PREFIX",
TASK$ADDRESSING_TASK = "TASK$ADDRESSING_TASK",
SECRETS$SECRET_VALUE_REQUIRED = "SECRETS$SECRET_VALUE_REQUIRED",
@@ -483,6 +489,7 @@ export enum I18nKey {
ACTION_MESSAGE$THINK = "ACTION_MESSAGE$THINK",
ACTION_MESSAGE$SYSTEM = "ACTION_MESSAGE$SYSTEM",
ACTION_MESSAGE$CONDENSATION = "ACTION_MESSAGE$CONDENSATION",
ACTION_MESSAGE$TASK_TRACKING = "ACTION_MESSAGE$TASK_TRACKING",
OBSERVATION_MESSAGE$RUN = "OBSERVATION_MESSAGE$RUN",
OBSERVATION_MESSAGE$RUN_IPYTHON = "OBSERVATION_MESSAGE$RUN_IPYTHON",
OBSERVATION_MESSAGE$READ = "OBSERVATION_MESSAGE$READ",
@@ -492,6 +499,8 @@ export enum I18nKey {
OBSERVATION_MESSAGE$MCP = "OBSERVATION_MESSAGE$MCP",
OBSERVATION_MESSAGE$RECALL = "OBSERVATION_MESSAGE$RECALL",
OBSERVATION_MESSAGE$THINK = "OBSERVATION_MESSAGE$THINK",
OBSERVATION_MESSAGE$TASK_TRACKING_PLAN = "OBSERVATION_MESSAGE$TASK_TRACKING_PLAN",
OBSERVATION_MESSAGE$TASK_TRACKING_VIEW = "OBSERVATION_MESSAGE$TASK_TRACKING_VIEW",
EXPANDABLE_MESSAGE$SHOW_DETAILS = "EXPANDABLE_MESSAGE$SHOW_DETAILS",
EXPANDABLE_MESSAGE$HIDE_DETAILS = "EXPANDABLE_MESSAGE$HIDE_DETAILS",
AI_SETTINGS$TITLE = "AI_SETTINGS$TITLE",
@@ -651,6 +660,7 @@ export enum I18nKey {
FEEDBACK$REASON_FORGOT_CONTEXT = "FEEDBACK$REASON_FORGOT_CONTEXT",
FEEDBACK$REASON_UNNECESSARY_CHANGES = "FEEDBACK$REASON_UNNECESSARY_CHANGES",
FEEDBACK$REASON_SHOULD_ASK_FIRST = "FEEDBACK$REASON_SHOULD_ASK_FIRST",
FEEDBACK$REASON_DIDNT_FINISH_JOB = "FEEDBACK$REASON_DIDNT_FINISH_JOB",
FEEDBACK$REASON_OTHER = "FEEDBACK$REASON_OTHER",
FEEDBACK$THANK_YOU_FOR_FEEDBACK = "FEEDBACK$THANK_YOU_FOR_FEEDBACK",
FEEDBACK$FAILED_TO_SUBMIT = "FEEDBACK$FAILED_TO_SUBMIT",
+160
View File
@@ -591,6 +591,22 @@
"de": "[MCP-Tool wurde ohne Ausgabe ausgeführt]",
"uk": "[Інструмент MCP завершив виконання без виводу]"
},
"OBSERVATION$TASK_TRACKING_NO_OUTPUT": {
"en": "[Task tracking completed with no output]",
"ja": "[タスクトラッキングは出力なしで完了しました]",
"zh-CN": "[任务跟踪完成,没有输出]",
"zh-TW": "[任務跟踪完成,沒有輸出]",
"ko-KR": "[작업 추적이 출력 없이 완료되었습니다]",
"no": "[Oppgavesporing fullført uten utdata]",
"it": "[Tracciamento attività completato senza output]",
"pt": "[Rastreamento de tarefas concluído sem saída]",
"es": "[Seguimiento de tareas completado sin salida]",
"ar": "[اكتمل تتبع المهام بدون مخرجات]",
"fr": "[Suivi des tâches terminé sans sortie]",
"tr": "[Görev takibi çıktı olmadan tamamlandı]",
"de": "[Aufgabenverfolgung ohne Ausgabe abgeschlossen]",
"uk": "[Відстеження завдань завершено без виводу]"
},
"MCP_OBSERVATION$ARGUMENTS": {
"en": "Arguments",
"ja": "引数",
@@ -623,6 +639,86 @@
"de": "Ausgabe",
"uk": "Вивід"
},
"TASK_TRACKING_OBSERVATION$TASK_LIST": {
"en": "Task List",
"ja": "タスクリスト",
"zh-CN": "任务列表",
"zh-TW": "任務列表",
"ko-KR": "작업 목록",
"no": "Oppgaveliste",
"it": "Elenco attività",
"pt": "Lista de tarefas",
"es": "Lista de tareas",
"ar": "قائمة المهام",
"fr": "Liste des tâches",
"tr": "Görev listesi",
"de": "Aufgabenliste",
"uk": "Список завдань"
},
"TASK_TRACKING_OBSERVATION$OUTPUT": {
"en": "Output",
"ja": "出力",
"zh-CN": "输出",
"zh-TW": "輸出",
"ko-KR": "출력",
"no": "Utdata",
"it": "Output",
"pt": "Saída",
"es": "Salida",
"ar": "المخرجات",
"fr": "Sortie",
"tr": "Çıktı",
"de": "Ausgabe",
"uk": "Вивід"
},
"TASK_TRACKING_OBSERVATION$TASK_ID": {
"en": "ID",
"ja": "ID",
"zh-CN": "ID",
"zh-TW": "ID",
"ko-KR": "ID",
"no": "ID",
"it": "ID",
"pt": "ID",
"es": "ID",
"ar": "المعرف",
"fr": "ID",
"tr": "ID",
"de": "ID",
"uk": "ID"
},
"TASK_TRACKING_OBSERVATION$TASK_NOTES": {
"en": "Notes",
"ja": "メモ",
"zh-CN": "备注",
"zh-TW": "備註",
"ko-KR": "메모",
"no": "Notater",
"it": "Note",
"pt": "Notas",
"es": "Notas",
"ar": "ملاحظات",
"fr": "Notes",
"tr": "Notlar",
"de": "Notizen",
"uk": "Примітки"
},
"TASK_TRACKING_OBSERVATION$RESULT": {
"en": "Result",
"ja": "結果",
"zh-CN": "结果",
"zh-TW": "結果",
"ko-KR": "결과",
"no": "Resultat",
"it": "Risultato",
"pt": "Resultado",
"es": "Resultado",
"ar": "النتيجة",
"fr": "Résultat",
"tr": "Sonuç",
"de": "Ergebnis",
"uk": "Результат"
},
"OBSERVATION$ERROR_PREFIX": {
"en": "error:",
"ja": "エラー:",
@@ -7727,6 +7823,22 @@
"tr": "Yoğunlaşma",
"uk": "Конденсація"
},
"ACTION_MESSAGE$TASK_TRACKING": {
"en": "Managing tasks",
"zh-CN": "管理任务",
"zh-TW": "管理任務",
"ko-KR": "작업 관리",
"ja": "タスク管理",
"no": "Administrerer oppgaver",
"ar": "إدارة المهام",
"de": "Aufgaben verwalten",
"fr": "Gestion des tâches",
"it": "Gestione delle attività",
"pt": "Gerenciando tarefas",
"es": "Gestionando tareas",
"tr": "Görevleri yönetiyor",
"uk": "Керування завданнями"
},
"OBSERVATION_MESSAGE$RUN": {
"en": "Ran <cmd>{{command}}</cmd>",
"zh-CN": "运行 <cmd>{{command}}</cmd>",
@@ -7871,6 +7983,38 @@
"de": "Gedanke",
"uk": "Думка"
},
"OBSERVATION_MESSAGE$TASK_TRACKING_PLAN": {
"en": "Agent updated the plan",
"zh-CN": "代理更新了计划",
"zh-TW": "代理更新了計劃",
"ko-KR": "에이전트가 계획을 업데이트했습니다",
"ja": "エージェントがプランを更新しました",
"no": "Agent oppdaterte planen",
"ar": "قام الوكيل بتحديث الخطة",
"de": "Agent hat den Plan aktualisiert",
"fr": "L'agent a mis à jour le plan",
"it": "L'agente ha aggiornato il piano",
"pt": "O agente atualizou o plano",
"es": "El agente actualizó el plan",
"tr": "Ajan planı güncelledi",
"uk": "Агент оновив план"
},
"OBSERVATION_MESSAGE$TASK_TRACKING_VIEW": {
"en": "Agent checked the current plan",
"zh-CN": "代理检查了当前计划",
"zh-TW": "代理檢查了當前計劃",
"ko-KR": "에이전트가 현재 계획을 확인했습니다",
"ja": "エージェントが現在のプランを確認しました",
"no": "Agent sjekket gjeldende plan",
"ar": "تحقق الوكيل من الخطة الحالية",
"de": "Agent hat den aktuellen Plan überprüft",
"fr": "L'agent a vérifié le plan actuel",
"it": "L'agente ha controllato il piano attuale",
"pt": "O agente verificou o plano atual",
"es": "El agente verificó el plan actual",
"tr": "Ajan mevcut planı kontrol etti",
"uk": "Агент перевірив поточний план"
},
"EXPANDABLE_MESSAGE$SHOW_DETAILS": {
"en": "Show details",
"zh-CN": "显示详情",
@@ -10415,6 +10559,22 @@
"de": "Der Agent hätte mich vorher fragen sollen!",
"uk": "Агент повинен був спочатку запитати мене, перш ніж це робити!"
},
"FEEDBACK$REASON_DIDNT_FINISH_JOB": {
"en": "The agent didn't finish the job",
"ja": "エージェントは作業を完了しませんでした",
"zh-CN": "代理没有完成工作",
"zh-TW": "代理沒有完成工作",
"ko-KR": "에이전트가 작업을 완료하지 않았습니다",
"no": "Agenten fullførte ikke jobben",
"it": "L'agente non ha completato il lavoro",
"pt": "O agente não terminou o trabalho",
"es": "El agente no terminó el trabajo",
"ar": "لم يكمل الوكيل المهمة",
"fr": "L'agent n'a pas terminé le travail",
"tr": "Ajan işi bitirmedi",
"de": "Der Agent hat die Aufgabe nicht beendet",
"uk": "Агент не завершив роботу"
},
"FEEDBACK$REASON_OTHER": {
"en": "Other",
"ja": "その他",
+1 -1
View File
@@ -25,7 +25,7 @@ export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
security_analyzer: DEFAULT_SETTINGS.SECURITY_ANALYZER,
remote_runtime_resource_factor:
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
provider_tokens_set: { github: null, gitlab: null, bitbucket: null },
provider_tokens_set: {},
enable_default_condenser: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
enable_sound_notifications: DEFAULT_SETTINGS.ENABLE_SOUND_NOTIFICATIONS,
enable_proactive_conversation_starters:
+1
View File
@@ -52,6 +52,7 @@ export function handleObservationMessage(message: ObservationMessage) {
case ObservationType.RECALL:
case ObservationType.ERROR:
case ObservationType.MCP:
case ObservationType.TASK_TRACKING:
break; // We don't display the default message for these observations
default:
break;
+3
View File
@@ -44,6 +44,9 @@ enum ActionType {
// Interact with the MCP server.
MCP = "call_tool_mcp",
// Views or updates the task list for task management.
TASK_TRACKING = "task_tracking",
}
export default ActionType;
+17 -1
View File
@@ -162,6 +162,21 @@ export interface MCPAction extends OpenHandsActionEvent<"call_tool_mcp"> {
};
}
export interface TaskTrackingAction
extends OpenHandsActionEvent<"task_tracking"> {
source: "agent";
args: {
command: string;
task_list: Array<{
id: string;
title: string;
status: "todo" | "in_progress" | "done";
notes?: string;
}>;
thought: string;
};
}
export type OpenHandsAction =
| UserMessageAction
| AssistantMessageAction
@@ -178,4 +193,5 @@ export type OpenHandsAction =
| FileWriteAction
| RejectAction
| RecallAction
| MCPAction;
| MCPAction
| TaskTrackingAction;
+1
View File
@@ -18,6 +18,7 @@ export type OpenHandsEventType =
| "recall"
| "mcp"
| "call_tool_mcp"
| "task_tracking"
| "user_rejected";
export type OpenHandsSourceType = "agent" | "user" | "environment";
+12
View File
@@ -6,6 +6,7 @@ import {
SystemMessageAction,
CommandAction,
FinishAction,
TaskTrackingAction,
} from "./actions";
import {
AgentStateChangeObservation,
@@ -13,6 +14,7 @@ import {
ErrorObservation,
MCPObservation,
OpenHandsObservation,
TaskTrackingObservation,
} from "./observations";
import { StatusUpdate } from "./variances";
@@ -87,6 +89,16 @@ export const isMcpObservation = (
): event is MCPObservation =>
isOpenHandsObservation(event) && event.observation === "mcp";
export const isTaskTrackingAction = (
event: OpenHandsParsedEvent,
): event is TaskTrackingAction =>
isOpenHandsAction(event) && event.action === "task_tracking";
export const isTaskTrackingObservation = (
event: OpenHandsParsedEvent,
): event is TaskTrackingObservation =>
isOpenHandsObservation(event) && event.observation === "task_tracking";
export const isStatusUpdate = (event: unknown): event is StatusUpdate =>
typeof event === "object" &&
event !== null &&
+16 -1
View File
@@ -146,6 +146,20 @@ export interface UserRejectedObservation
extras: Record<string, unknown>;
}
export interface TaskTrackingObservation
extends OpenHandsObservationEvent<"task_tracking"> {
source: "agent";
extras: {
command: string;
task_list: Array<{
id: string;
title: string;
status: "todo" | "in_progress" | "done";
notes?: string;
}>;
};
}
export type OpenHandsObservation =
| AgentStateChangeObservation
| AgentThinkObservation
@@ -160,4 +174,5 @@ export type OpenHandsObservation =
| ErrorObservation
| RecallObservation
| MCPObservation
| UserRejectedObservation;
| UserRejectedObservation
| TaskTrackingObservation;
+3
View File
@@ -40,6 +40,9 @@ enum ObservationType {
// A no-op observation
NULL = "null",
// Result of a task tracking operation
TASK_TRACKING = "task_tracking",
}
export default ObservationType;
+10 -7
View File
@@ -11,20 +11,23 @@ export const browserTab = {
startNotification(message: string) {
if (!isBrowser) return;
// Store original title if not already stored
if (!originalTitle) {
originalTitle = document.title;
}
// Always capture the current title as the baseline to restore to
originalTitle = document.title;
// Clear any existing interval
if (titleInterval) {
this.stopNotification();
}
// Alternate between original title and notification message
// Alternate between the latest baseline title and the notification message.
// If the title changes externally (e.g., user renames conversation),
// update the baseline so we restore to the new value when stopping.
titleInterval = window.setInterval(() => {
document.title =
document.title === originalTitle ? message : originalTitle;
const current = document.title;
if (current !== originalTitle && current !== message) {
originalTitle = current;
}
document.title = current === message ? originalTitle : message;
}, 1000);
// Set favicon to indicate notification
+1 -1
View File
@@ -122,5 +122,5 @@ export function getStatusCode(
return runtimeStatus;
}
return "STATUS$ERROR"; // illegal state
return I18nKey.CHAT_INTERFACE$AGENT_ERROR_MESSAGE;
}
+3 -3
View File
@@ -47,8 +47,8 @@ Then, for each issue you find:
- Suggest a concrete improvement
Use the following structure in your output:
[Line 42] :hammer_and_wrench: Unused import: The 'os' module is imported but never used. Remove it to clean up the code.
[Lines 7885] :mag: Readability: This nested if-else block is hard to follow. Consider refactoring into smaller functions or using early returns.
[Line 102] :closed_lock_with_key: Security Risk: User input is directly concatenated into an SQL query. This could allow SQL injection. Use parameterized queries instead.
[src/utils.py, Line 42] :hammer_and_wrench: Unused import: The 'os' module is imported but never used. Remove it to clean up the code.
[src/database.py, Lines 7885] :mag: Readability: This nested if-else block is hard to follow. Consider refactoring into smaller functions or using early returns.
[src/auth.py, Line 102] :closed_lock_with_key: Security Risk: User input is directly concatenated into an SQL query. This could allow SQL injection. Use parameterized queries instead.
REMEMBER, DO NOT MODIFY THE CODE. ONLY PROVIDE FEEDBACK IN YOUR RESPONSE.
+104
View File
@@ -0,0 +1,104 @@
---
triggers:
- /codereview-roasted
---
PERSONA:
You are a critical code reviewer with the engineering mindset of Linus Torvalds. Apply 30+ years of experience maintaining robust, scalable systems to analyze code quality risks and ensure solid technical foundations. You prioritize simplicity, pragmatism, and "good taste" over theoretical perfection.
CORE PHILOSOPHY:
1. **"Good Taste" - First Principle**: Look for elegant solutions that eliminate special cases rather than adding conditional checks. Good code has no edge cases.
2. **"Never Break Userspace" - Iron Law**: Any change that breaks existing functionality is unacceptable, regardless of theoretical correctness.
3. **Pragmatism**: Solve real problems, not imaginary ones. Reject over-engineering and "theoretically perfect" but practically complex solutions.
4. **Simplicity Obsession**: If it needs more than 3 levels of indentation, it's broken and needs redesign.
CRITICAL ANALYSIS FRAMEWORK:
Before reviewing, ask Linus's Three Questions:
1. Is this solving a real problem or an imagined one?
2. Is there a simpler way?
3. What will this break?
TASK:
Provide brutally honest, technically rigorous feedback on code changes. Be direct and critical while remaining constructive. Focus on fundamental engineering principles over style preferences. DO NOT modify the code; only provide specific, actionable feedback.
CODE REVIEW SCENARIOS:
1. **Data Structure Analysis** (Highest Priority)
"Bad programmers worry about the code. Good programmers worry about data structures."
Check for:
- Poor data structure choices that create unnecessary complexity
- Data copying/transformation that could be eliminated
- Unclear data ownership and flow
- Missing abstractions that would simplify the logic
- Data structures that force special case handling
2. **Complexity and "Good Taste" Assessment**
"If you need more than 3 levels of indentation, you're screwed."
Identify:
- Functions with >3 levels of nesting (immediate red flag)
- Special cases that could be eliminated with better design
- Functions doing multiple things (violating single responsibility)
- Complex conditional logic that obscures the core algorithm
- Code that could be 3 lines instead of 10
3. **Pragmatic Problem Analysis**
"Theory and practice sometimes clash. Theory loses. Every single time."
Evaluate:
- Is this solving a problem that actually exists in production?
- Does the solution's complexity match the problem's severity?
- Are we over-engineering for theoretical edge cases?
- Could this be solved with existing, simpler mechanisms?
4. **Breaking Change Risk Assessment**
"We don't break user space!"
Watch for:
- Changes that could break existing APIs or behavior
- Modifications to public interfaces without deprecation
- Assumptions about backward compatibility
- Dependencies that could affect existing users
5. **Security and Correctness** (Critical Issues Only)
Focus on real security risks, not theoretical ones:
- Actual input validation failures with exploit potential
- Real privilege escalation or data exposure risks
- Memory safety issues in unsafe languages
- Concurrency bugs that cause data corruption
CRITICAL REVIEW OUTPUT FORMAT:
Start with a **Taste Rating**:
🟢 **Good taste** - Elegant, simple solution
🟡 **Acceptable** - Works but could be cleaner
🔴 **Needs improvement** - Violates fundamental principles
Then provide **Linus-Style Analysis**:
**[CRITICAL ISSUES]** (Must fix - these break fundamental principles)
- [src/core.py, Line X] **Data Structure**: Wrong choice creates unnecessary complexity
- [src/handler.py, Line Y] **Complexity**: >3 levels of nesting - redesign required
- [src/api.py, Line Z] **Breaking Change**: This will break existing functionality
**[IMPROVEMENT OPPORTUNITIES]** (Should fix - violates good taste)
- [src/utils.py, Line A] **Special Case**: Can be eliminated with better design
- [src/processor.py, Line B] **Simplification**: These 10 lines can be 3
- [src/feature.py, Line C] **Pragmatism**: Solving imaginary problem, focus on real issues
**[STYLE NOTES]** (Minor - only mention if genuinely important)
- [src/models.py, Line D] **Naming**: Unclear intent, affects maintainability
**VERDICT:**
**Worth merging**: Core logic is sound, minor improvements suggested
**Needs rework**: Fundamental design issues must be addressed first
**KEY INSIGHT:**
[One sentence summary of the most important architectural observation]
COMMUNICATION STYLE:
- Be direct and technically precise
- Focus on engineering fundamentals, not personal preferences
- Explain the "why" behind each criticism
- Suggest concrete, actionable improvements
- Prioritize issues that affect real users over theoretical concerns
REMEMBER: DO NOT MODIFY THE CODE. PROVIDE CRITICAL BUT CONSTRUCTIVE FEEDBACK ONLY.
@@ -10,21 +10,21 @@ if TYPE_CHECKING:
from openhands.llm.llm import ModelResponse
import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
from openhands.agenthub.codeact_agent.tools.bash import CmdRunTool
from openhands.agenthub.codeact_agent.tools.browser import BrowserTool
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.ipython import IPythonTool
from openhands.agenthub.codeact_agent.tools.llm_based_edit import LLMBasedFileEditTool
from openhands.agenthub.codeact_agent.tools.str_replace_editor import (
create_str_replace_editor_tool,
)
from openhands.agenthub.codeact_agent.tools.think import ThinkTool
from openhands.agenthub.codeact_agent.tools.unified import (
BashTool,
BrowserTool,
FileEditorTool,
FinishTool,
from openhands.agenthub.codeact_agent.tools.task_tracker import (
create_task_tracker_tool,
)
from openhands.agenthub.codeact_agent.tools.think import ThinkTool
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import AgentConfig
@@ -101,7 +101,7 @@ class CodeActAgent(Agent):
if self._prompt_manager is None:
self._prompt_manager = PromptManager(
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
system_prompt_filename=self.config.system_prompt_filename,
system_prompt_filename=self.config.resolved_system_prompt_filename,
)
return self._prompt_manager
@@ -124,34 +124,29 @@ class CodeActAgent(Agent):
)
tools = []
# New unified tools
if self.config.enable_cmd:
tools.append(BashTool().get_schema())
tools.append(
CmdRunTool(use_short_description=use_short_tool_desc).to_param()
)
if self.config.enable_think:
tools.append(ThinkTool)
if self.config.enable_finish:
tools.append(FinishTool().get_schema())
tools.append(FinishTool)
if self.config.enable_condensation_request:
tools.append(CondensationRequestTool)
if self.config.enable_browsing:
if sys.platform == 'win32':
logger.warning('Windows runtime does not support browsing yet')
else:
tools.append(BrowserTool().get_schema())
if self.config.enable_editor:
tools.append(FileEditorTool().get_schema())
# Legacy tools (to be migrated)
if self.config.enable_think:
tools.append(ThinkTool)
if self.config.enable_condensation_request:
tools.append(CondensationRequestTool)
tools.append(BrowserTool)
if self.config.enable_jupyter:
tools.append(IPythonTool)
if self.config.enable_plan_mode:
# In plan mode, we use the task_tracker tool for task management
tools.append(create_task_tracker_tool(use_short_tool_desc))
if self.config.enable_llm_editor:
tools.append(LLMBasedFileEditTool)
elif self.config.enable_editor and not any(
tool.get('function', {}).get('name') == 'str_replace_editor'
for tool in tools
):
# Fallback to old editor if FileEditorTool wasn't added
elif self.config.enable_editor:
tools.append(
create_str_replace_editor_tool(
use_short_description=use_short_tool_desc
@@ -10,25 +10,16 @@ from litellm import (
)
from openhands.agenthub.codeact_agent.tools import (
BrowserTool as LegacyBrowserTool,
)
from openhands.agenthub.codeact_agent.tools import (
BrowserTool,
CondensationRequestTool,
FinishTool,
IPythonTool,
LLMBasedFileEditTool,
ThinkTool,
create_cmd_run_tool,
create_str_replace_editor_tool,
execute_bash,
)
from openhands.agenthub.codeact_agent.tools import (
FinishTool as LegacyFinishTool,
)
from openhands.agenthub.codeact_agent.tools.unified import (
BashTool,
BrowserTool,
FileEditorTool,
FinishTool,
)
from openhands.agenthub.codeact_agent.tools.bash import CmdRunTool
from openhands.core.exceptions import (
FunctionCallNotExistsError,
FunctionCallValidationError,
@@ -40,29 +31,21 @@ from openhands.events.action import (
AgentFinishAction,
AgentThinkAction,
BrowseInteractiveAction,
CmdRunAction,
FileEditAction,
FileReadAction,
IPythonRunCellAction,
MessageAction,
TaskTrackingAction,
)
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 (
BROWSER_TOOL_NAME,
EXECUTE_BASH_TOOL_NAME,
FINISH_TOOL_NAME,
STR_REPLACE_EDITOR_TOOL_NAME,
)
from openhands.llm.tool_names import TASK_TRACKER_TOOL_NAME
# Tool instances for validation
_TOOL_INSTANCES = {
EXECUTE_BASH_TOOL_NAME: BashTool(),
BROWSER_TOOL_NAME: BrowserTool(),
STR_REPLACE_EDITOR_TOOL_NAME: FileEditorTool(),
FINISH_TOOL_NAME: FinishTool(),
# Tool handlers registry for class-based tools
_TOOL_HANDLERS = {
execute_bash.name: CmdRunTool(),
}
@@ -105,48 +88,11 @@ def response_to_actions(
) from e
# ================================================
# BashTool (Unified)
# CmdRunTool (Bash)
# ================================================
if tool_call.function.name == EXECUTE_BASH_TOOL_NAME:
# Use unified tool validation
bash_tool = _TOOL_INSTANCES[EXECUTE_BASH_TOOL_NAME]
validated_args = bash_tool.validate_parameters(arguments)
# convert is_input to boolean
is_input = validated_args.get('is_input', 'false') == 'true'
action = CmdRunAction(
command=validated_args['command'], is_input=is_input
)
# Set hard timeout if provided
if 'timeout' in validated_args:
try:
action.set_hard_timeout(float(validated_args['timeout']))
except ValueError as e:
raise FunctionCallValidationError(
f"Invalid float passed to 'timeout' argument: {validated_args['timeout']}"
) from e
# ================================================
# CmdRunTool (Legacy - fallback)
# ================================================
elif tool_call.function.name == create_cmd_run_tool()['function']['name']:
if 'command' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "command" in tool call {tool_call.function.name}'
)
# convert is_input to boolean
is_input = arguments.get('is_input', 'false') == 'true'
action = CmdRunAction(command=arguments['command'], is_input=is_input)
# Set hard timeout if provided
if 'timeout' in arguments:
try:
action.set_hard_timeout(float(arguments['timeout']))
except ValueError as e:
raise FunctionCallValidationError(
f"Invalid float passed to 'timeout' argument: {arguments['timeout']}"
) from e
if tool_call.function.name == execute_bash.name:
action = _TOOL_HANDLERS[execute_bash.name].to_action(arguments)
# ================================================
# IPythonTool (Jupyter)
@@ -164,26 +110,9 @@ def response_to_actions(
)
# ================================================
# FinishTool (Unified)
# AgentFinishAction
# ================================================
elif tool_call.function.name == FINISH_TOOL_NAME:
# Use unified tool validation
finish_tool = _TOOL_INSTANCES[FINISH_TOOL_NAME]
validated_args = finish_tool.validate_parameters(arguments)
action = AgentFinishAction(
final_thought=validated_args.get('summary', ''),
outputs={
'task_completed': validated_args.get('task_completed', None)
}
if 'task_completed' in validated_args
else {},
)
# ================================================
# AgentFinishAction (Legacy - fallback)
# ================================================
elif tool_call.function.name == LegacyFinishTool['function']['name']:
elif tool_call.function.name == FinishTool['function']['name']:
action = AgentFinishAction(
final_thought=arguments.get('message', ''),
)
@@ -209,42 +138,6 @@ def response_to_actions(
'impl_source', FileEditSource.LLM_BASED_EDIT
),
)
# ================================================
# FileEditorTool (Unified)
# ================================================
elif tool_call.function.name == STR_REPLACE_EDITOR_TOOL_NAME:
# Use unified tool validation
file_editor_tool = _TOOL_INSTANCES[STR_REPLACE_EDITOR_TOOL_NAME]
validated_args = file_editor_tool.validate_parameters(arguments)
path = validated_args['path']
command = validated_args['command']
if command == 'view':
action = FileReadAction(
path=path,
impl_source=FileReadSource.OH_ACI,
view_range=validated_args.get('view_range', None),
)
else:
# Remove view_range for edit commands
edit_kwargs = {
k: v
for k, v in validated_args.items()
if k not in ['command', 'path', 'view_range']
}
action = FileEditAction(
path=path,
command=command,
impl_source=FileEditSource.OH_ACI,
**edit_kwargs,
)
# ================================================
# str_replace_editor (Legacy - fallback)
# ================================================
elif (
tool_call.function.name
== create_str_replace_editor_tool()['function']['name']
@@ -310,25 +203,33 @@ def response_to_actions(
action = CondensationRequestAction()
# ================================================
# BrowserTool (Unified)
# BrowserTool
# ================================================
elif tool_call.function.name == BROWSER_TOOL_NAME:
# Use unified tool validation
browser_tool = _TOOL_INSTANCES[BROWSER_TOOL_NAME]
validated_args = browser_tool.validate_parameters(arguments)
action = BrowseInteractiveAction(browser_actions=validated_args['code'])
# ================================================
# BrowserTool (Legacy - fallback)
# ================================================
elif tool_call.function.name == LegacyBrowserTool['function']['name']:
elif tool_call.function.name == BrowserTool['function']['name']:
if 'code' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "code" in tool call {tool_call.function.name}'
)
action = BrowseInteractiveAction(browser_actions=arguments['code'])
# ================================================
# TaskTrackingAction
# ================================================
elif tool_call.function.name == TASK_TRACKER_TOOL_NAME:
if 'command' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "command" in tool call {tool_call.function.name}'
)
if arguments['command'] == 'plan' and 'task_list' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "task_list" for "plan" command in tool call {tool_call.function.name}'
)
action = TaskTrackingAction(
command=arguments['command'],
task_list=arguments.get('task_list', []),
)
# ================================================
# MCPAction (MCP)
# ================================================
@@ -98,3 +98,11 @@ Your primary role is to assist users by executing commands, modifying code, and
- Confirm whether they want it as a separate file or just in the conversation
- Ask if they want documentation files to be included in version control
</DOCUMENTATION>
<PROCESS_MANAGEMENT>
* When terminating processes:
- Do NOT use general keywords with commands like `pkill -f server` or `pkill -f python` as this might accidentally kill other important servers or processes
- Always use specific keywords that uniquely identify the target process
- Prefer using `ps aux` to find the exact process ID (PID) first, then kill that specific PID
- When possible, use more targeted approaches like finding the PID from a pidfile or using application-specific shutdown commands
</PROCESS_MANAGEMENT>
@@ -1,39 +1,40 @@
{% include "system_prompt.j2" %}
<TASK_MANAGEMENT>
* For complex, long-horizon tasks, create a TODO.md file to track progress:
1. Start by creating a detailed plan in TODO.md with clear steps
2. Check TODO.md before each new action to maintain context and track progress
3. Update TODO.md as you complete steps or discover new requirements
4. Mark completed items with ✓ or [x] to maintain a clear record of progress
5. For each major step, add sub-tasks as needed to break down complex work
6. If you discover the plan needs significant changes, propose updates and confirm with the user before proceeding and update TODO.md
7. IMPORTANT: Do NOT add TODO.md to git commits or version control systems
* Example TODO.md format:
```markdown
# Task: [Brief description of the overall task]
## Plan
- [ ] Step 1: [Description]
- [ ] Sub-task 1.1
- [ ] Sub-task 1.2
- [ ] Step 2: [Description]
- [x] Step 3: [Description] (Completed)
## Notes
- Important discovery: [Details about something you learned]
- Potential issue: [Description of a potential problem]
```
* When working on a task:
- Read the README to understand how the system works
- Create TODO.md with every major step unchecked
- Add TODO.md to .gitignore if it's not already ignored
- Until every item in TODO.md is checked:
a. Pick the next unchecked item and work on it
b. Run appropriate tests to verify your work
c. If issues arise, fix them until tests pass
d. Once complete, check off the item in TODO.md
e. Proceed to the next unchecked item
* You have access to the `task_tracker` tool to help you organize and monitor development work. Use this tool REGULARLY to maintain task visibility and provide users with clear progress updates. This tool is ESSENTIAL for systematic planning and decomposing complex development work into manageable components. Failing to use this tool for planning may result in overlooked requirements - which is unacceptable.
* It is crucial that you update task status to "done" immediately upon completion of each work item. Do not accumulate multiple finished tasks before updating their status.
* For complex, multi-phase development work, use `task_tracker` to establish a comprehensive plan with well-defined steps:
1. Begin by decomposing the overall objective into primary phases using `task_tracker`
2. Include detailed work items as necessary to break complex activities into actionable units
3. Update tasks to "in_progress" status when commencing work on them
4. Update tasks to "done" status immediately after completing each item
5. For each primary phase, incorporate additional work items as you identify new requirements
6. If you determine the plan requires substantial modifications, suggest revisions and obtain user confirmation before proceeding
* Example workflow for debugging and resolution:
```
User: "Execute the test suite and resolve any validation failures"
Assistant: I'm going to use the task_tracker tool to organize the following work items:
- Execute the test suite
- Resolve any validation failures
I'm now going to run the test suite using the terminal.
[After running tests and discovering 8 validation failures]
I found 8 validation failures that need attention. I'm going to use the task_tracker tool to add 8 specific items to the task list.
[Updating first task to in_progress]
Let me begin addressing the first validation issue...
[After resolving first failure]
The first validation issue has been resolved, let me mark that task as done and proceed to the second item...
```
* Example workflow for component development:
```
User: "Build a dashboard component that displays analytics data with interactive charts and filtering options"
Assistant: I'll help you create an analytics dashboard with interactive charts and filtering. Let me first use the task_tracker tool to organize this development work.
Adding the following tasks to the tracker:
1. Analyze existing analytics data structure and requirements
2. Design dashboard layout and component architecture
3. Implement data visualization charts with interactivity
4. Create filtering and search functionality
5. Integrate components and perform testing
Let me start by examining the current analytics data structure to understand what we're working with...
[Assistant proceeds with implementation step by step, updating tasks to in_progress and done as work progresses]
```
</TASK_MANAGEMENT>
@@ -0,0 +1,122 @@
{% include "system_prompt.j2" %}
<TECHNICAL_PHILOSOPHY>
Adopt the engineering mindset of Linus Torvalds, creator and chief architect of the Linux kernel. Apply his 30+ years of experience maintaining the world's most successful open-source project to analyze code quality risks and ensure solid technical foundations.
# My Core Philosophy
1. "Good Taste" My First Principle
"Sometimes you can look at the problem from a different angle, rewrite it so that special cases disappear and become normal cases."
• Classic case: linked list deletion — optimized from 10 lines with if checks to 4 lines with unconditional branches
• Good taste is an intuition built from experience
• Eliminating edge cases is always better than adding conditional checks
2. "Never break userspace" My Iron Law
"We don't break user space!"
• Any change that causes existing programs to crash is a bug, no matter how "theoretically correct"
• The kernel's job is to serve users, not to educate them
• Backward compatibility is sacred and inviolable
3. Pragmatism My Belief
"I'm a damn pragmatist."
• Solve real problems, not imaginary threats
• Reject "theoretically perfect" but practically complex solutions like microkernels
• Code should serve reality, not academic papers
4. Obsession with Simplicity My Standard
"If you need more than three levels of indentation, you're screwed and should fix your program."
• Functions must be short and do one thing well
• C is a Spartan language, naming should be equally concise
• Complexity is the root of all evil
# Communication Principles
Basic Communication Rules
• Style: Direct, clear, and constructive. Focus on technical improvements rather than judgmental language.
• Technical Priority: Provide specific, actionable feedback on technical issues. Maintain high standards while being respectful and educational.
# Requirement Confirmation Process
## 0. Premise Thinking Linus's Three Questions
Before any analysis, ask yourself:
1. Is this a real problem or an imagined one? Reject over-engineering
2. Is there a simpler way? Always seek the simplest solution
3. What will it break? Backward compatibility is law
## 1. Requirement Understanding Confirmation
Once you understand the users requirement, reply it in Linuss style to confirm:
> Based on current information, my understanding of your requirement is: [Restate the requirement using Linuss thinking and communication style]
> Please confirm if my understanding is correct.
## 2. Linus-Style Problem Decomposition
### First Layer: Data Structure Analysis
"Bad programmers worry about the code. Good programmers worry about data structures."
• What are the core data elements? How are they related?
• Where does the data flow? Who owns it? Who modifies it?
• Any unnecessary data copying or transformation?
### Second Layer: Special Case Identification
"Good code has no special cases"
• Identify all if/else branches
• Which are real business logic? Which are patches for bad design?
• Can the data structure be redesigned to remove these branches?
### Third Layer: Complexity Review
"If it needs more than 3 levels of indentation, redesign it"
• What is the essence of the feature? (One sentence)
• How many concepts does the current solution use?
• Can it be reduced by half? Then by half again?
### Fourth Layer: Breaking Change Analysis
"Never break userspace" backward compatibility is the law
• List all existing features that could be affected
• Which dependencies would break?
• How can we improve without breaking anything?
### Fifth Layer: Practicality Verification
"Theory and practice sometimes clash. Theory loses. Every single time."
• Does this problem actually exist in production?
• How many users are truly affected?
• Does the solution's complexity match the problem's severity?
## 3. Decision Output Format
After the 5-layer analysis, output must include:
[Core Judgment]
✅ Worth doing: [reason] / ❌ Not worth doing: [reason]
[Key Insights]
- Data Structure: [most critical data relationship]
- Complexity: [complexity that can be eliminated]
- Risk: [biggest breaking change risk]
[Linus-Style Plan]
If worth doing:
1. Always start by simplifying the data structure
2. Eliminate all special cases
3. Implement in the dumbest but clearest way
4. Ensure zero breaking changes
If not worth doing, explain to the user:
"This is solving a problem that doesnt exist. The real problem is [XXX]."
## 4. Code Review Output
When seeing code, make three quick judgments:
[Taste Rating]
🟢 Good taste / 🟡 Acceptable / 🔴 Needs improvement
[Critical Issue]
- [If any, directly point out the worst part]
[Improvement Direction]
"Eliminate this special case"
"These 10 lines can be 3"
"Wrong data structure, should be..."
</TECHNICAL_PHILOSOPHY>
@@ -1,4 +1,9 @@
from .bash import create_cmd_run_tool
from .bash import create_cmd_run_tool, execute_bash
# NOTE: This module currently exposes schema-only tools. As part of #10441 we are
# gradually encapsulating tools as classes that own schema and validation. See
# bash.CmdRunTool for the first example. Existing code remains backward
# compatible by exporting ChatCompletionToolParam for now.
from .browser import BrowserTool
from .condensation_request import CondensationRequestTool
from .finish import FinishTool
@@ -11,6 +16,7 @@ __all__ = [
'BrowserTool',
'CondensationRequestTool',
'create_cmd_run_tool',
'execute_bash',
'FinishTool',
'IPythonTool',
'LLMBasedFileEditTool',
@@ -0,0 +1,43 @@
from __future__ import annotations
import json
from abc import ABC, abstractmethod
from typing import Any
from litellm import ChatCompletionToolParam
from openhands.core.exceptions import FunctionCallValidationError
class Tool(ABC):
"""Base class for CodeAct tools.
Subclasses should encapsulate schema, descriptions and validation.
They must implement to_param() and to_action().
"""
@abstractmethod
def to_param(self) -> ChatCompletionToolParam:
"""Return the ChatCompletionToolParam schema for this tool."""
raise NotImplementedError
def parse_arguments(self, raw_arguments: str) -> dict[str, Any]:
"""Parse the raw JSON string from the model into a dict.
Raises FunctionCallValidationError on failure.
"""
try:
return json.loads(raw_arguments) if raw_arguments else {}
except json.decoder.JSONDecodeError as e:
raise FunctionCallValidationError(
f'Failed to parse tool call arguments: {raw_arguments}'
) from e
@abstractmethod
def to_action(self, arguments: dict[str, Any]): # -> Action
"""Convert validated arguments to an Action.
Implementations should raise FunctionCallValidationError for
missing/invalid parameters.
"""
raise NotImplementedError
@@ -1,8 +1,22 @@
from __future__ import annotations
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
from openhands.agenthub.codeact_agent.tools.base import Tool
from openhands.agenthub.codeact_agent.tools.prompt import refine_prompt
from openhands.core.exceptions import FunctionCallValidationError
from openhands.events.action import CmdRunAction
from openhands.llm.tool_names import EXECUTE_BASH_TOOL_NAME
class _ToolRef:
def __init__(self, name: str) -> None:
self.name = name
# Lightweight reference so callers can compare against execute_bash.name
execute_bash = _ToolRef(EXECUTE_BASH_TOOL_NAME)
_DETAILED_BASH_DESCRIPTION = """Execute a bash command in the terminal within a persistent shell session.
@@ -28,6 +42,65 @@ _DETAILED_BASH_DESCRIPTION = """Execute a bash command in the terminal within a
* Output truncation: If the output exceeds a maximum length, it will be truncated before being returned.
"""
class CmdRunTool(Tool):
def __init__(self, use_short_description: bool = False) -> None:
self.use_short_description = use_short_description
def to_param(self) -> ChatCompletionToolParam:
description = (
_SHORT_BASH_DESCRIPTION
if self.use_short_description
else _DETAILED_BASH_DESCRIPTION
)
return ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name=EXECUTE_BASH_TOOL_NAME,
description=refine_prompt(description),
parameters={
'type': 'object',
'properties': {
'command': {
'type': 'string',
'description': refine_prompt(
'The bash command to execute. Can be empty string to view additional logs when previous exit code is `-1`. Can be `C-c` (Ctrl+C) to interrupt the currently running process. Note: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together.'
),
},
'is_input': {
'type': 'string',
'description': refine_prompt(
'If True, the command is an input to the running process. If False, the command is a bash command to be executed in the terminal. Default is False.'
),
'enum': ['true', 'false'],
},
'timeout': {
'type': 'number',
'description': 'Optional. Sets a hard timeout in seconds for the command execution. If not provided, the command will use the default soft timeout behavior.',
},
},
'required': ['command'],
},
),
)
def to_action(self, arguments: dict[str, str]) -> CmdRunAction:
if 'command' not in arguments:
raise FunctionCallValidationError(
'Missing required argument "command" in tool call execute_bash'
)
is_input = arguments.get('is_input', 'false') == 'true'
action = CmdRunAction(command=arguments['command'], is_input=is_input)
if 'timeout' in arguments:
try:
action.set_hard_timeout(float(arguments['timeout']))
except ValueError as e:
raise FunctionCallValidationError(
f"Invalid float passed to 'timeout' argument: {arguments['timeout']}"
) from e
return action
_SHORT_BASH_DESCRIPTION = """Execute a bash command in the terminal.
* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`. For commands that need to run for a specific duration, you can set the "timeout" argument to specify a hard timeout in seconds.
* Interact with running process: If a bash command returns exit code `-1`, this means the process is not yet finished. By setting `is_input` to `true`, the assistant can interact with the running process and send empty `command` to retrieve any additional logs, or send additional text (set `command` to the text) to STDIN of the running process, or send command like `C-c` (Ctrl+C), `C-d` (Ctrl+D), `C-z` (Ctrl+Z) to interrupt the process.
@@ -0,0 +1,203 @@
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
from openhands.llm.tool_names import TASK_TRACKER_TOOL_NAME
_DETAILED_TASK_TRACKER_DESCRIPTION = """This tool provides structured task management capabilities for development workflows.
It enables systematic tracking of work items, progress monitoring, and efficient
organization of complex development activities.
The tool maintains visibility into project status and helps communicate
progress effectively to users.
## Application Guidelines
Utilize this tool in the following situations:
1. Multi-phase development work - When projects involve multiple sequential or
parallel activities
2. Complex implementation tasks - Work requiring systematic planning and
coordination across multiple components
3. Explicit user request for task organization - When users specifically ask
for structured task management
4. Multiple concurrent requirements - When users present several work items
that need coordination
5. Project initiation - Capture and organize user requirements at project start
6. Work commencement - Update task status to in_progress before beginning
implementation. Maintain focus by limiting active work to one task
7. Task completion - Update status to done and identify any additional work
that emerged during implementation
## Situations Where Tool Usage Is Unnecessary
Avoid using this tool when:
1. Single atomic tasks that require no decomposition
2. Trivial operations where tracking adds no organizational value
3. Simple activities completable in minimal steps
4. Pure information exchange or discussion
Note: For single straightforward tasks, proceed with direct implementation
rather than creating tracking overhead.
## Usage Scenarios
**Scenario A: Feature Development with Validation**
User request: "Build a user authentication system with login/logout functionality.
Don't forget to include input validation and error handling!"
Response approach: I'll implement a user authentication system with comprehensive
validation. Let me organize this work systematically.
*Task breakdown includes:*
1. Design authentication API endpoints and data models
2. Implement user login/logout controllers with session management
3. Add client-side form validation and error display
4. Create backend input sanitization and security checks
5. Implement comprehensive error handling for edge cases
6. Perform integration testing and fix any issues
**Scenario B: Codebase Refactoring**
User request: "I need to update all instances of the method 'fetchData' to
'retrieveInformation' throughout the entire project"
Response approach: Let me first analyze your codebase to identify all locations
where 'fetchData' appears.
*After code analysis*
I've located 12 occurrences of 'fetchData' across 6 files in your project.
I'll create a systematic plan to update these references.
*Organized task list includes specific file-by-file updates*
**Scenario C: Multi-feature Development**
User request: "Set up a blog platform with these components: admin dashboard,
article management, comment system, and user profiles."
Response approach: I'll help build your blog platform. Let me break down these
components into manageable implementation phases.
*Creates structured plan with each feature decomposed into specific development tasks*
## Counter-examples - Direct Implementation Preferred
**Counter-example A: Simple inquiry**
User request: "What's the syntax for a for loop in JavaScript?"
Response approach: Here's how to write a for loop in JavaScript:
```javascript
for (let i = 0; i < array.length; i++) {
console.log(array[i]);
}
```
*No task tracking needed - single informational response*
**Counter-example B: Minor modification**
User request: "Please add a docstring to the processPayment function explaining
its parameters."
Response approach: I'll add a docstring to explain the processPayment function.
*Direct edit without task tracking for simple modification*
## Status Management and Workflow
1. **Status Values**: Track work using these states:
- todo: Not yet initiated
- in_progress: Currently active (maintain single focus)
- done: Successfully completed
2. **Workflow Practices**:
- Update status dynamically as work progresses
- Mark completion immediately upon task finish
- Limit active work to ONE task at any given time
- Complete current activities before initiating new ones
- Remove obsolete tasks from tracking entirely
3. **Completion Criteria**:
- Mark tasks as done only when fully achieved
- Keep status as in_progress if errors, blocks, or partial completion exist
- Create new tasks for discovered issues or dependencies
- Never mark done when:
- Test suites are failing
- Implementation remains incomplete
- Unresolved errors persist
- Required resources are unavailable
4. **Task Organization**:
- Write precise, actionable descriptions
- Decompose complex work into manageable units
- Use descriptive, clear naming conventions
When uncertain, favor using this tool. Proactive task management demonstrates
systematic approach and ensures comprehensive requirement fulfillment.
"""
_SHORT_TASK_TRACKER_DESCRIPTION = """Provides structured task management for development workflows, enabling progress
tracking and systematic organization of complex coding activities.
* Apply to multi-phase projects (3+ distinct steps) or when managing multiple user requirements
* Update status (todo/in_progress/done) dynamically throughout work
* Maintain single active task focus at any time
* Mark completion immediately upon task finish
* Decompose complex work into manageable, actionable units
"""
def create_task_tracker_tool(
use_short_description: bool = False,
) -> ChatCompletionToolParam:
description = (
_SHORT_TASK_TRACKER_DESCRIPTION
if use_short_description
else _DETAILED_TASK_TRACKER_DESCRIPTION
)
return ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name=TASK_TRACKER_TOOL_NAME,
description=description,
parameters={
'type': 'object',
'properties': {
'command': {
'type': 'string',
'enum': ['view', 'plan'],
'description': 'The command to execute. `view` shows the current task list. `plan` creates or updates the task list based on provided requirements and progress. Always `view` the current list before making changes.',
},
'task_list': {
'type': 'array',
'description': 'The full task list. Required parameter of `plan` command.',
'items': {
'type': 'object',
'properties': {
'id': {
'type': 'string',
'description': 'Unique task identifier',
},
'title': {
'type': 'string',
'description': 'Brief task description',
},
'status': {
'type': 'string',
'description': 'Current task status',
'enum': ['todo', 'in_progress', 'done'],
},
'notes': {
'type': 'string',
'description': 'Optional additional context or details',
},
},
'required': ['title', 'status', 'id'],
'additionalProperties': False,
},
},
},
'required': ['command'],
'additionalProperties': False,
},
),
)
@@ -1,28 +0,0 @@
"""OpenHands Tools Module
This module provides a unified interface for AI agent tools, encapsulating:
- Tool definitions and schemas
- Parameter validation
- Action creation from function calls
- Error handling and interpretation
- Response processing
This decouples tool logic from agent processing, making it easier to add new tools
or modify existing ones.
"""
from .base import Tool, ToolError, ToolValidationError
from .bash_tool import BashTool
from .browser_tool import BrowserTool
from .file_editor_tool import FileEditorTool
from .finish_tool import FinishTool
__all__ = [
'Tool',
'ToolError',
'ToolValidationError',
'BashTool',
'FileEditorTool',
'BrowserTool',
'FinishTool',
]
@@ -1,100 +0,0 @@
"""Base Tool class and related exceptions for OpenHands tools."""
import json
from abc import ABC, abstractmethod
from typing import Any
from litellm import ChatCompletionToolParam
class ToolError(Exception):
"""Base exception for tool-related errors."""
pass
class ToolValidationError(ToolError):
"""Exception raised when tool parameters fail validation."""
pass
class Tool(ABC):
"""Base class for all OpenHands tools.
This class encapsulates tool definitions and parameter validation.
Action creation is handled by the function calling layer.
"""
def __init__(self, name: str, description: str):
self.name = name
self.description = description
@abstractmethod
def get_schema(
self, use_short_description: bool = False
) -> ChatCompletionToolParam:
"""Get the tool schema for function calling.
Args:
use_short_description: Whether to use a shorter description
Returns:
Tool schema compatible with LiteLLM function calling
"""
pass
@abstractmethod
def validate_parameters(self, parameters: dict[str, Any]) -> dict[str, Any]:
"""Validate and normalize tool parameters.
Args:
parameters: Raw parameters from function call
Returns:
Validated and normalized parameters
Raises:
ToolValidationError: If parameters are invalid
"""
pass
def validate_function_call(self, function_call: Any) -> dict[str, Any]:
"""Validate a function call and return normalized parameters.
Args:
function_call: Function call object from LLM
Returns:
Validated and normalized parameters
Raises:
ToolValidationError: If function call is invalid
"""
try:
# Parse function call arguments
if hasattr(function_call, 'arguments'):
arguments_str = function_call.arguments
else:
arguments_str = str(function_call)
try:
parameters = json.loads(arguments_str)
except json.JSONDecodeError as e:
raise ToolValidationError(
f'Failed to parse function call arguments: {arguments_str}. Error: {e}'
)
# Validate parameters
return self.validate_parameters(parameters)
except ToolValidationError:
raise
except Exception as e:
raise ToolValidationError(f'Unexpected error validating function call: {e}')
def __str__(self) -> str:
return f'Tool({self.name})'
def __repr__(self) -> str:
return f"Tool(name='{self.name}', description='{self.description[:50]}...')"
@@ -1,123 +0,0 @@
"""Bash/Command execution tool for OpenHands."""
import sys
from typing import Any
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
from openhands.llm.tool_names import EXECUTE_BASH_TOOL_NAME
from .base import Tool, ToolValidationError
class BashTool(Tool):
"""Tool for executing bash commands in a persistent shell session."""
def __init__(self):
super().__init__(
name=EXECUTE_BASH_TOOL_NAME,
description='Execute bash commands in a persistent shell session',
)
def get_schema(
self, use_short_description: bool = False
) -> ChatCompletionToolParam:
"""Get the tool schema for function calling."""
if use_short_description:
description = self._get_short_description()
else:
description = self._get_detailed_description()
return ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name=self.name,
description=self._refine_prompt(description),
parameters={
'type': 'object',
'properties': {
'command': {
'type': 'string',
'description': self._refine_prompt(
'The bash command to execute. Can be empty string to view additional logs when previous exit code is `-1`. Can be `C-c` (Ctrl+C) to interrupt the currently running process. Note: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together.'
),
},
'is_input': {
'type': 'string',
'description': self._refine_prompt(
'If True, the command is an input to the running process. If False, the command is a bash command to be executed in the terminal. Default is False.'
),
'enum': ['true', 'false'],
},
'timeout': {
'type': 'number',
'description': 'Optional. Sets a hard timeout in seconds for the command execution. If not provided, the command will use the default soft timeout behavior.',
},
},
'required': ['command'],
},
),
)
def validate_parameters(self, parameters: dict[str, Any]) -> dict[str, Any]:
"""Validate and normalize bash tool parameters."""
if 'command' not in parameters:
raise ToolValidationError("Missing required parameter 'command'")
validated = {
'command': str(parameters['command']),
'is_input': parameters.get('is_input', 'false') == 'true',
}
# Validate timeout if provided
if 'timeout' in parameters:
try:
timeout = float(parameters['timeout'])
if timeout <= 0:
raise ToolValidationError('Timeout must be positive')
validated['timeout'] = timeout
except (ValueError, TypeError):
raise ToolValidationError(
f'Invalid timeout value: {parameters["timeout"]}'
)
return validated
def _get_detailed_description(self) -> str:
"""Get detailed description for the tool."""
return """Execute a bash command in the terminal within a persistent shell session.
### Command Execution
* One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, use `&&` or `;` to chain them together.
* Persistent session: Commands execute in a persistent shell session where environment variables, virtual environments, and working directory persist between commands.
* Soft timeout: Commands have a soft timeout of 10 seconds, once that's reached, you have the option to continue or interrupt the command (see section below for details)
### Long-running Commands
* For commands that may run indefinitely, run them in the background and redirect output to a file, e.g. `python3 app.py > server.log 2>&1 &`.
* For commands that may run for a long time (e.g. installation or testing commands), or commands that run for a fixed amount of time (e.g. sleep), you should set the "timeout" parameter of your function call to an appropriate value.
* If a bash command returns exit code `-1`, this means the process hit the soft timeout and is not yet finished. By setting `is_input` to `true`, you can:
- Send empty `command` to retrieve additional logs
- Send text (set `command` to the text) to STDIN of the running process
- Send control commands like `C-c` (Ctrl+C), `C-d` (Ctrl+D), or `C-z` (Ctrl+Z) to interrupt the process
- If you do C-c, you can re-start the process with a longer "timeout" parameter to let it run to completion
### Best Practices
* Directory verification: Before creating new directories or files, first verify the parent directory exists and is the correct location.
* Directory management: Try to maintain working directory by using absolute paths and avoiding excessive use of `cd`.
### Output Handling
* Output truncation: If the output exceeds a maximum length, it will be truncated before being returned."""
def _get_short_description(self) -> str:
"""Get short description for the tool."""
return """Execute a bash command in the terminal.
* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`. For commands that need to run for a specific duration, you can set the "timeout" argument to specify a hard timeout in seconds.
* Interact with running process: If a bash command returns exit code `-1`, this means the process is not yet finished. By setting `is_input` to `true`, the assistant can interact with the running process and send empty `command` to retrieve any additional logs, or send additional text (set `command` to the text) to STDIN of the running process, or send command like `C-c` (Ctrl+C), `C-d` (Ctrl+D), `C-z` (Ctrl+Z) to interrupt the process.
* One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together."""
def _refine_prompt(self, prompt: str) -> str:
"""Refine prompt for platform-specific commands."""
if sys.platform == 'win32':
return prompt.replace('bash', 'powershell')
return prompt
@@ -1,77 +0,0 @@
"""Browser tool for OpenHands web browsing capabilities."""
from typing import Any
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
from openhands.llm.tool_names import BROWSER_TOOL_NAME
from .base import Tool, ToolValidationError
class BrowserTool(Tool):
"""Tool for web browsing and interaction."""
def __init__(self):
super().__init__(
name=BROWSER_TOOL_NAME,
description='Interact with the browser using Python code. Use it ONLY when you need to interact with a webpage.',
)
def get_schema(
self, use_short_description: bool = False
) -> ChatCompletionToolParam:
"""Get the tool schema for function calling."""
description = self._get_description(use_short_description)
return ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name=self.name,
description=description,
parameters={
'type': 'object',
'properties': {
'code': {
'type': 'string',
'description': 'The Python code that interacts with the browser.',
},
},
'required': ['code'],
},
),
)
def validate_parameters(self, parameters: dict[str, Any]) -> dict[str, Any]:
"""Validate and normalize browser tool parameters."""
if 'code' not in parameters:
raise ToolValidationError("Missing required parameter 'code'")
code = parameters['code']
if not isinstance(code, str):
raise ToolValidationError("Parameter 'code' must be a string")
if not code.strip():
raise ToolValidationError("Parameter 'code' cannot be empty")
return {'code': code}
def _get_description(self, use_short_description: bool) -> str:
"""Get description for the tool."""
if use_short_description:
return 'Interact with the browser using Python code. Use it ONLY when you need to interact with a webpage.'
else:
return """Interact with the browser using Python code. Use it ONLY when you need to interact with a webpage.
See the description of "code" parameter for more details.
Multiple actions can be provided at once, but will be executed sequentially without any feedback from the page.
More than 2-3 actions usually leads to failure or unexpected behavior. Example:
fill('a12', 'example with "quotes"')
click('a51')
click('48', button='middle', modifiers=['Shift'])
You can also use the browser to view pdf, png, jpg files.
You should first check the content of /tmp/oh-server-url to get the server url, and then use it to view the file by `goto("{server_url}/view?path={absolute_file_path}")`.
For example: `goto("http://localhost:8000/view?path=/workspace/test_document.pdf")`
Note: The file should be downloaded to the local machine first before using the browser to view it."""
@@ -1,193 +0,0 @@
"""File editor tool for OpenHands using str_replace_editor interface."""
from typing import Any
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
from openhands.llm.tool_names import STR_REPLACE_EDITOR_TOOL_NAME
from .base import Tool, ToolValidationError
class FileEditorTool(Tool):
"""Tool for viewing, creating and editing files using str_replace_editor interface."""
def __init__(self):
super().__init__(
name=STR_REPLACE_EDITOR_TOOL_NAME,
description='Custom editing tool for viewing, creating and editing files',
)
def get_schema(
self, use_short_description: bool = False
) -> ChatCompletionToolParam:
"""Get the tool schema for function calling."""
if use_short_description:
description = self._get_short_description()
else:
description = self._get_detailed_description()
return ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name=self.name,
description=description,
parameters={
'type': 'object',
'properties': {
'command': {
'description': 'The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.',
'enum': [
'view',
'create',
'str_replace',
'insert',
'undo_edit',
],
'type': 'string',
},
'path': {
'description': 'Absolute path to file or directory, e.g. `/workspace/file.py` or `/workspace`.',
'type': 'string',
},
'file_text': {
'description': 'Required parameter of `create` command, with the content of the file to be created.',
'type': 'string',
},
'old_str': {
'description': 'Required parameter of `str_replace` command containing the string in `path` to replace.',
'type': 'string',
},
'new_str': {
'description': 'Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.',
'type': 'string',
},
'insert_line': {
'description': 'Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.',
'type': 'integer',
},
'view_range': {
'description': 'Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.',
'items': {'type': 'integer'},
'type': 'array',
},
},
'required': ['command', 'path'],
},
),
)
def validate_parameters(self, parameters: dict[str, Any]) -> dict[str, Any]:
"""Validate and normalize file editor tool parameters."""
if 'command' not in parameters:
raise ToolValidationError("Missing required parameter 'command'")
if 'path' not in parameters:
raise ToolValidationError("Missing required parameter 'path'")
command = parameters['command']
valid_commands = ['view', 'create', 'str_replace', 'insert', 'undo_edit']
if command not in valid_commands:
raise ToolValidationError(
f"Invalid command '{command}'. Must be one of: {valid_commands}"
)
validated = {
'command': command,
'path': str(parameters['path']),
}
# Validate command-specific parameters
if command == 'create':
if 'file_text' not in parameters:
raise ToolValidationError(
"'create' command requires 'file_text' parameter"
)
validated['file_text'] = str(parameters['file_text'])
elif command == 'str_replace':
if 'old_str' not in parameters:
raise ToolValidationError(
"'str_replace' command requires 'old_str' parameter"
)
validated['old_str'] = str(parameters['old_str'])
validated['new_str'] = str(parameters.get('new_str', ''))
elif command == 'insert':
if 'insert_line' not in parameters:
raise ToolValidationError(
"'insert' command requires 'insert_line' parameter"
)
if 'new_str' not in parameters:
raise ToolValidationError(
"'insert' command requires 'new_str' parameter"
)
try:
validated['insert_line'] = int(parameters['insert_line'])
except (ValueError, TypeError):
raise ToolValidationError(
f'Invalid insert_line value: {parameters["insert_line"]}'
)
validated['new_str'] = str(parameters['new_str'])
elif command == 'view':
if 'view_range' in parameters:
view_range = parameters['view_range']
if not isinstance(view_range, list) or len(view_range) != 2:
raise ToolValidationError(
'view_range must be a list of two integers'
)
try:
validated['view_range'] = [int(view_range[0]), int(view_range[1])]
except (ValueError, TypeError):
raise ToolValidationError('view_range must contain valid integers')
return validated
def _get_detailed_description(self) -> str:
"""Get detailed description for the tool."""
return """Custom editing tool for viewing, creating and editing files in plain-text format
* State is persistent across command calls and discussions with the user
* If `path` is a text file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
* The following binary file extensions can be viewed in Markdown format: [".xlsx", ".pptx", ".wav", ".mp3", ".m4a", ".flac", ".pdf", ".docx"]. IT DOES NOT HANDLE IMAGES.
* The `create` command cannot be used if the specified `path` already exists as a file
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
* The `undo_edit` command will revert the last edit made to the file at `path`
* This tool can be used for creating and editing files in plain-text format.
Before using this tool:
1. Use the view tool to understand the file's contents and context
2. Verify the directory path is correct (only applicable when creating new files):
- Use the view tool to verify the parent directory exists and is the correct location
When making edits:
- Ensure the edit results in idiomatic, correct code
- Do not leave the code in a broken state
- Always use absolute file paths (starting with /)
CRITICAL REQUIREMENTS FOR USING THIS TOOL:
1. EXACT MATCHING: The `old_str` parameter must match EXACTLY one or more consecutive lines from the file, including all whitespace and indentation. The tool will fail if `old_str` matches multiple locations or doesn't match exactly with the file content.
2. UNIQUENESS: The `old_str` must uniquely identify a single instance in the file:
- Include sufficient context before and after the change point (3-5 lines recommended)
- If not unique, the replacement will not be performed
3. REPLACEMENT: The `new_str` parameter should contain the edited lines that replace the `old_str`. Both strings must be different.
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each."""
def _get_short_description(self) -> str:
"""Get short description for the tool."""
return """Custom editing tool for viewing, creating and editing files in plain-text format
* State is persistent across command calls and discussions with the user
* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
* The `create` command cannot be used if the specified `path` already exists as a file
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
* The `undo_edit` command will revert the last edit made to the file at `path`
Notes for using the `str_replace` command:
* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique
* The `new_str` parameter should contain the edited lines that should replace the `old_str`"""
@@ -1,76 +0,0 @@
"""Finish tool for OpenHands task completion."""
from typing import Any
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
from openhands.llm.tool_names import FINISH_TOOL_NAME
from .base import Tool, ToolValidationError
class FinishTool(Tool):
"""Tool for finishing tasks and providing final outputs."""
def __init__(self):
super().__init__(
name=FINISH_TOOL_NAME,
description='Finish the current task and provide final output',
)
def get_schema(
self, use_short_description: bool = False
) -> ChatCompletionToolParam:
"""Get the tool schema for function calling."""
description = self._get_description(use_short_description)
return ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name=self.name,
description=description,
parameters={
'type': 'object',
'properties': {
'outputs': {
'type': 'object',
'description': 'Final outputs of the task as key-value pairs',
},
'summary': {
'type': 'string',
'description': 'Summary of what was accomplished',
},
},
'required': [],
},
),
)
def validate_parameters(self, parameters: dict[str, Any]) -> dict[str, Any]:
"""Validate and normalize finish tool parameters."""
validated: dict[str, Any] = {}
if 'outputs' in parameters:
outputs = parameters['outputs']
if not isinstance(outputs, dict):
raise ToolValidationError("'outputs' must be a dictionary")
validated['outputs'] = outputs
if 'summary' in parameters:
validated['summary'] = str(parameters['summary'])
return validated
def _get_description(self, use_short_description: bool) -> str:
"""Get description for the tool."""
if use_short_description:
return 'Finish the current task and provide final outputs.'
else:
return """Finish the current task and provide final outputs.
Use this tool when you have completed the requested task and want to provide
final results or outputs. You can include:
- outputs: A dictionary of key-value pairs representing the final results
- summary: A text summary of what was accomplished
This will signal that the task is complete and no further actions are needed."""
@@ -1,30 +0,0 @@
"""Unified tool architecture for LocAgent.
LocAgent extends CodeActAgent with specialized search and exploration tools.
It inherits all CodeAct tools and adds its own search capabilities.
"""
# Import parent tools from CodeAct
from openhands.agenthub.codeact_agent.tools.unified import (
BashTool,
BrowserTool,
FileEditorTool,
FinishTool,
)
# Import LocAgent-specific tools
from .explore_structure_tool import ExploreStructureTool
from .search_entity_tool import SearchEntityTool
from .search_repo_tool import SearchRepoTool
__all__ = [
# Inherited from CodeAct
'BashTool',
'BrowserTool',
'FileEditorTool',
'FinishTool',
# LocAgent-specific
'ExploreStructureTool',
'SearchEntityTool',
'SearchRepoTool',
]
@@ -1,279 +0,0 @@
"""ExploreStructureTool for traversing code graph to retrieve dependency structure."""
from typing import Any
from litellm import ChatCompletionToolParam
from openhands.agenthub.codeact_agent.tools.unified.base import (
Tool,
ToolValidationError,
)
class ExploreStructureTool(Tool):
"""Tool for exploring repository structure and code dependencies.
Traverses a pre-built code graph to retrieve dependency structure around specified entities,
with options to explore upstream or downstream, and control traversal depth and filters.
"""
def __init__(self, use_simplified_description: bool = False):
super().__init__(
name='explore_tree_structure',
description='Traverses a pre-built code graph to retrieve dependency structure around specified entities',
)
self.use_simplified_description = use_simplified_description
def get_schema(
self, use_short_description: bool = False
) -> ChatCompletionToolParam:
"""Get the tool schema for function calling."""
if self.use_simplified_description or use_short_description:
description = """
A unified tool that traverses a pre-built code graph to retrieve dependency structure around specified entities,
with options to explore upstream or downstream, and control traversal depth and filters for entity and dependency types.
"""
example = """
Example Usage:
1. Exploring Downstream Dependencies:
```
explore_tree_structure(
start_entities=['src/module_a.py:ClassA'],
direction='downstream',
traversal_depth=2,
dependency_type_filter=['invokes', 'imports']
)
```
2. Exploring the repository structure from the root directory (/) up to two levels deep:
```
explore_tree_structure(
start_entities=['/'],
traversal_depth=2,
dependency_type_filter=['contains']
)
```
3. Generate Class Diagrams:
```
explore_tree_structure(
start_entities=selected_entity_ids,
direction='both',
traverse_depth=-1,
dependency_type_filter=['inherits']
)
```
"""
else:
description = """
Unified repository exploring tool that traverses a pre-built code graph to retrieve dependency structure around specified entities.
The search can be controlled to traverse upstream (exploring dependencies that entities rely on) or downstream (exploring how entities impact others), with optional limits on traversal depth and filters for entity and dependency types.
Code Graph Definition:
* Entity Types: 'directory', 'file', 'class', 'function'.
* Dependency Types: 'contains', 'imports', 'invokes', 'inherits'.
* Hierarchy:
- Directories contain files and subdirectories.
- Files contain classes and functions.
- Classes contain inner classes and methods.
- Functions can contain inner functions.
* Interactions:
- Files/classes/functions can import classes and functions.
- Classes can inherit from other classes.
- Classes and functions can invoke others (invocations in a class's `__init__` are attributed to the class).
Entity ID:
* Unique identifier including file path and module path.
* Here's an example of an Entity ID: `"interface/C.py:C.method_a.inner_func"` identifies function `inner_func` within `method_a` of class `C` in `"interface/C.py"`.
Notes:
* Traversal Control: The `traversal_depth` parameter specifies how deep the function should explore the graph starting from the input entities.
* Filtering: Use `entity_type_filter` and `dependency_type_filter` to narrow down the scope of the search, focusing on specific entity types and relationships.
"""
example = """
Example Usage:
1. Exploring Outward Dependencies:
```
explore_tree_structure(
start_entities=['src/module_a.py:ClassA'],
direction='downstream',
traversal_depth=2,
dependency_type_filter=['invokes', 'imports']
)
```
This retrieves the dependencies of `ClassA` up to 2 levels deep, focusing only on classes and functions with 'invokes' and 'imports' relationships.
2. Exploring Inward Dependencies:
```
explore_tree_structure(
start_entities=['src/module_b.py:FunctionY'],
direction='upstream',
traversal_depth=-1
)
```
This finds all entities that depend on `FunctionY` without restricting the traversal depth.
3. Exploring Repository Structure:
```
explore_tree_structure(
start_entities=['/'],
traversal_depth=2,
dependency_type_filter=['contains']
)
```
This retrieves the tree repository structure from the root directory (/), traversing up to two levels deep and focusing only on 'contains' relationship.
4. Generate Class Diagrams:
```
explore_tree_structure(
start_entities=selected_entity_ids,
direction='both',
traverse_depth=-1,
dependency_type_filter=['inherits']
)
```
"""
return {
'type': 'function',
'function': {
'name': self.name,
'description': (description + example).strip(),
'parameters': {
'type': 'object',
'properties': {
'start_entities': {
'description': (
'List of entities (e.g., class, function, file, or directory paths) to begin the search from.\n'
'Entities representing classes or functions must be formatted as "file_path:QualifiedName" (e.g., `interface/C.py:C.method_a.inner_func`).\n'
'For files or directories, provide only the file or directory path (e.g., `src/module_a.py` or `src/`).'
),
'type': 'array',
'items': {'type': 'string'},
},
'direction': {
'description': (
'Direction of traversal in the code graph; allowed options are: `upstream`, `downstream`, `both`.\n'
"- 'upstream': Traversal to explore dependencies that the specified entities rely on (how they depend on others).\n"
"- 'downstream': Traversal to explore the effects or interactions of the specified entities on others (how others depend on them).\n"
"- 'both': Traversal on both direction."
),
'type': 'string',
'enum': ['upstream', 'downstream', 'both'],
'default': 'downstream',
},
'traversal_depth': {
'description': (
'Maximum depth of traversal. A value of -1 indicates unlimited depth (subject to a maximum limit).'
'Must be either `-1` or a non-negative integer (≥ 0).'
),
'type': 'integer',
'default': 2,
},
'entity_type_filter': {
'description': (
"List of entity types (e.g., 'class', 'function', 'file', 'directory') to include in the traversal. If None, all entity types are included."
),
'type': ['array', 'null'],
'items': {'type': 'string'},
'default': None,
},
'dependency_type_filter': {
'description': (
"List of dependency types (e.g., 'contains', 'imports', 'invokes', 'inherits') to include in the traversal. If None, all dependency types are included."
),
'type': ['array', 'null'],
'items': {'type': 'string'},
'default': None,
},
},
'required': ['start_entities'],
},
},
}
def validate_parameters(self, parameters: dict[str, Any]) -> dict[str, Any]:
"""Validate and normalize tool parameters."""
if 'start_entities' not in parameters:
raise ToolValidationError("Missing required parameter 'start_entities'")
start_entities = parameters['start_entities']
direction = parameters.get('direction', 'downstream')
traversal_depth = parameters.get('traversal_depth', 2)
entity_type_filter = parameters.get('entity_type_filter')
dependency_type_filter = parameters.get('dependency_type_filter')
# Validate start_entities
if not isinstance(start_entities, list):
raise ToolValidationError("Parameter 'start_entities' must be a list")
if not start_entities:
raise ToolValidationError("Parameter 'start_entities' cannot be empty")
for i, entity in enumerate(start_entities):
if not isinstance(entity, str):
raise ToolValidationError(f'Entity at index {i} must be a string')
if not entity.strip():
raise ToolValidationError(f'Entity at index {i} cannot be empty')
# Validate direction
valid_directions = ['upstream', 'downstream', 'both']
if direction not in valid_directions:
raise ToolValidationError(
f"Parameter 'direction' must be one of {valid_directions}"
)
# Validate traversal_depth
if not isinstance(traversal_depth, int):
raise ToolValidationError("Parameter 'traversal_depth' must be an integer")
if traversal_depth != -1 and traversal_depth < 0:
raise ToolValidationError(
"Parameter 'traversal_depth' must be -1 or non-negative"
)
# Validate entity_type_filter
if entity_type_filter is not None:
if not isinstance(entity_type_filter, list):
raise ToolValidationError(
"Parameter 'entity_type_filter' must be a list or null"
)
valid_entity_types = ['directory', 'file', 'class', 'function']
for i, entity_type in enumerate(entity_type_filter):
if not isinstance(entity_type, str):
raise ToolValidationError(
f'Entity type at index {i} must be a string'
)
if entity_type not in valid_entity_types:
raise ToolValidationError(
f"Entity type '{entity_type}' is not valid. Must be one of {valid_entity_types}"
)
# Validate dependency_type_filter
if dependency_type_filter is not None:
if not isinstance(dependency_type_filter, list):
raise ToolValidationError(
"Parameter 'dependency_type_filter' must be a list or null"
)
valid_dependency_types = ['contains', 'imports', 'invokes', 'inherits']
for i, dep_type in enumerate(dependency_type_filter):
if not isinstance(dep_type, str):
raise ToolValidationError(
f'Dependency type at index {i} must be a string'
)
if dep_type not in valid_dependency_types:
raise ToolValidationError(
f"Dependency type '{dep_type}' is not valid. Must be one of {valid_dependency_types}"
)
# Normalize parameters
result = {
'start_entities': [entity.strip() for entity in start_entities],
'direction': direction,
'traversal_depth': traversal_depth,
}
if entity_type_filter is not None:
result['entity_type_filter'] = entity_type_filter
if dependency_type_filter is not None:
result['dependency_type_filter'] = dependency_type_filter
return result
@@ -1,94 +0,0 @@
"""SearchEntityTool for retrieving complete implementations of specified entities."""
from typing import Any
from litellm import ChatCompletionToolParam
from openhands.agenthub.codeact_agent.tools.unified.base import (
Tool,
ToolValidationError,
)
class SearchEntityTool(Tool):
"""Tool for searching and retrieving complete implementations of specified entities.
This tool can handle specific entity queries such as function names, class names, or file paths.
"""
def __init__(self):
super().__init__(
name='get_entity_contents',
description='Searches the codebase to retrieve the complete implementations of specified entities',
)
def get_schema(
self, use_short_description: bool = False
) -> ChatCompletionToolParam:
"""Get the tool schema for function calling."""
description = """
Searches the codebase to retrieve the complete implementations of specified entities based on the provided entity names.
The tool can handle specific entity queries such as function names, class names, or file paths.
**Usage Example:**
# Search for a specific function implementation
get_entity_contents(['src/my_file.py:MyClass.func_name'])
# Search for a file's complete content
get_entity_contents(['src/my_file.py'])
**Entity Name Format:**
- To specify a function or class, use the format: `file_path:QualifiedName`
(e.g., 'src/helpers/math_helpers.py:MathUtils.calculate_sum').
- To search for a file's content, use only the file path (e.g., 'src/my_file.py').
"""
if use_short_description:
description = 'Searches the codebase to retrieve the complete implementations of specified entities'
return {
'type': 'function',
'function': {
'name': self.name,
'description': description.strip(),
'parameters': {
'type': 'object',
'properties': {
'entity_names': {
'type': 'array',
'items': {'type': 'string'},
'description': (
'A list of entity names to query. Each entity name can represent a function, class, or file. '
"For functions or classes, the format should be 'file_path:QualifiedName' "
"(e.g., 'src/helpers/math_helpers.py:MathUtils.calculate_sum'). "
"For files, use just the file path (e.g., 'src/my_file.py')."
),
}
},
'required': ['entity_names'],
},
},
}
def validate_parameters(self, parameters: dict[str, Any]) -> dict[str, Any]:
"""Validate and normalize tool parameters."""
if 'entity_names' not in parameters:
raise ToolValidationError("Missing required parameter 'entity_names'")
entity_names = parameters['entity_names']
# Validate entity_names is a list
if not isinstance(entity_names, list):
raise ToolValidationError("Parameter 'entity_names' must be a list")
# Validate each entity name is a string
for i, entity_name in enumerate(entity_names):
if not isinstance(entity_name, str):
raise ToolValidationError(f'Entity name at index {i} must be a string')
if not entity_name.strip():
raise ToolValidationError(f'Entity name at index {i} cannot be empty')
# Normalize: strip whitespace from entity names
normalized_entity_names = [name.strip() for name in entity_names]
return {'entity_names': normalized_entity_names}
@@ -1,147 +0,0 @@
"""SearchRepoTool for searching code snippets based on terms or line numbers."""
from typing import Any
from litellm import ChatCompletionToolParam
from openhands.agenthub.codeact_agent.tools.unified.base import (
Tool,
ToolValidationError,
)
class SearchRepoTool(Tool):
"""Tool for searching the codebase to retrieve relevant code snippets.
Can search based on terms/keywords or specific line numbers within files.
"""
def __init__(self):
super().__init__(
name='search_code_snippets',
description='Searches the codebase to retrieve relevant code snippets based on given queries',
)
def get_schema(
self, use_short_description: bool = False
) -> ChatCompletionToolParam:
"""Get the tool schema for function calling."""
description = """Searches the codebase to retrieve relevant code snippets based on given queries(terms or line numbers).
** Note:
- Either `search_terms` or `line_nums` must be provided to perform a search.
- If `search_terms` are provided, it searches for code snippets based on each term:
- If `line_nums` is provided, it searches for code snippets around the specified lines within the file defined by `file_path_or_pattern`.
** Example Usage:
# Search for code content contain keyword `order`, `bill`
search_code_snippets(search_terms=["order", "bill"])
# Search for a class
search_code_snippets(search_terms=["MyClass"])
# Search for context around specific lines (10 and 15) within a file
search_code_snippets(line_nums=[10, 15], file_path_or_pattern='src/example.py')
"""
if use_short_description:
description = 'Searches the codebase to retrieve relevant code snippets based on given queries'
return {
'type': 'function',
'function': {
'name': self.name,
'description': description.strip(),
'parameters': {
'type': 'object',
'properties': {
'search_terms': {
'type': 'array',
'items': {'type': 'string'},
'description': 'A list of names, keywords, or code snippets to search for within the codebase. '
'This can include potential function names, class names, or general code fragments. '
'Either `search_terms` or `line_nums` must be provided to perform a search.',
},
'line_nums': {
'type': 'array',
'items': {'type': 'integer'},
'description': 'Specific line numbers to locate code snippets within a specified file. '
'Must be used alongside a valid `file_path_or_pattern`. '
'Either `line_nums` or `search_terms` must be provided to perform a search.',
},
'file_path_or_pattern': {
'type': 'string',
'description': 'A glob pattern or specific file path used to filter search results '
'to particular files or directories. Defaults to "**/*.py", meaning all Python files are searched by default. '
'If `line_nums` are provided, this must specify a specific file path.',
'default': '**/*.py',
},
},
'required': [],
},
},
}
def validate_parameters(self, parameters: dict[str, Any]) -> dict[str, Any]:
"""Validate and normalize tool parameters."""
search_terms = parameters.get('search_terms')
line_nums = parameters.get('line_nums')
file_path_or_pattern = parameters.get('file_path_or_pattern', '**/*.py')
# Either search_terms or line_nums must be provided
if not search_terms and not line_nums:
raise ToolValidationError(
"Either 'search_terms' or 'line_nums' must be provided"
)
# Validate search_terms if provided
if search_terms is not None:
if not isinstance(search_terms, list):
raise ToolValidationError("Parameter 'search_terms' must be a list")
for i, term in enumerate(search_terms):
if not isinstance(term, str):
raise ToolValidationError(
f'Search term at index {i} must be a string'
)
if not term.strip():
raise ToolValidationError(
f'Search term at index {i} cannot be empty'
)
# Validate line_nums if provided
if line_nums is not None:
if not isinstance(line_nums, list):
raise ToolValidationError("Parameter 'line_nums' must be a list")
for i, line_num in enumerate(line_nums):
if not isinstance(line_num, int):
raise ToolValidationError(
f'Line number at index {i} must be an integer'
)
if line_num < 1:
raise ToolValidationError(
f'Line number at index {i} must be positive'
)
# Validate file_path_or_pattern
if not isinstance(file_path_or_pattern, str):
raise ToolValidationError(
"Parameter 'file_path_or_pattern' must be a string"
)
# If line_nums is provided, file_path_or_pattern should be a specific file
if line_nums and file_path_or_pattern == '**/*.py':
raise ToolValidationError(
"When 'line_nums' is provided, 'file_path_or_pattern' must specify a specific file path"
)
# Normalize parameters
result: dict[str, Any] = {'file_path_or_pattern': file_path_or_pattern.strip()}
if search_terms:
result['search_terms'] = [term.strip() for term in search_terms]
if line_nums:
result['line_nums'] = line_nums
return result
@@ -1,19 +0,0 @@
"""
ReadOnlyAgent unified tools - inherits safe tools from CodeAct and adds read-only specific tools.
"""
# Import safe tools from CodeAct parent
from openhands.agenthub.codeact_agent.tools.unified import FinishTool
from .glob_tool import GlobTool
# Import our own read-only specific tools
from .grep_tool import GrepTool
from .view_tool import ViewTool
__all__ = [
'FinishTool', # Inherited from CodeAct
'GrepTool', # ReadOnly-specific
'ViewTool', # ReadOnly-specific
'GlobTool', # ReadOnly-specific
]
@@ -1,74 +0,0 @@
"""
GlobTool for ReadOnlyAgent - safe file pattern matching.
"""
from typing import Any
from openhands.agenthub.codeact_agent.tools.unified.base import (
Tool,
ToolValidationError,
)
class GlobTool(Tool):
"""Tool for safely finding files using glob patterns without modification."""
def __init__(self):
super().__init__('glob', 'Find files using glob patterns safely')
def get_schema(self, use_short_description: bool = False):
return {
'type': 'function',
'function': {
'name': 'glob',
'description': """Find files and directories using glob patterns.
* Use wildcards to find files matching patterns
* Supports standard glob patterns: *, ?, [abc], **
* Returns list of matching file paths
* Use this to find files by extension, name patterns, or directory structure""",
'parameters': {
'type': 'object',
'properties': {
'pattern': {
'type': 'string',
'description': 'The glob pattern to match files (e.g., "*.py", "**/*.js", "test_*.py")',
},
'base_path': {
'type': 'string',
'description': 'The base directory to search from (defaults to current directory)',
'default': '.',
},
},
'required': ['pattern'],
},
},
}
def validate_parameters(self, parameters: dict[str, Any]) -> dict[str, Any]:
"""Validate glob tool parameters."""
if not isinstance(parameters, dict):
raise ToolValidationError('Parameters must be a dictionary')
# Validate required pattern parameter
if 'pattern' not in parameters:
raise ToolValidationError('Missing required parameter: pattern')
pattern = parameters['pattern']
if not isinstance(pattern, str):
raise ToolValidationError("Parameter 'pattern' must be a string")
if not pattern.strip():
raise ToolValidationError("Parameter 'pattern' cannot be empty")
validated = {'pattern': pattern.strip()}
# Validate optional base_path parameter
if 'base_path' in parameters:
base_path = parameters['base_path']
if not isinstance(base_path, str):
raise ToolValidationError("Parameter 'base_path' must be a string")
validated['base_path'] = base_path.strip() if base_path.strip() else '.'
else:
validated['base_path'] = '.' # Default value
return validated
@@ -1,114 +0,0 @@
"""
GrepTool for ReadOnlyAgent - safe text searching.
"""
from typing import Any
from openhands.agenthub.codeact_agent.tools.unified.base import (
Tool,
ToolValidationError,
)
class GrepTool(Tool):
"""Tool for safely searching text in files without modification."""
def __init__(self):
super().__init__('grep', 'Search for patterns in files safely')
def get_schema(self, use_short_description: bool = False):
return {
'type': 'function',
'function': {
'name': 'grep',
'description': """Search for patterns in files using grep.
* Searches for a pattern in files within a directory
* Returns matching lines with line numbers and file paths
* Supports basic regex patterns
* Use this to find specific code patterns, function definitions, or text content""",
'parameters': {
'type': 'object',
'properties': {
'pattern': {
'type': 'string',
'description': 'The pattern to search for (supports basic regex)',
},
'path': {
'type': 'string',
'description': 'The directory or file path to search in (optional, defaults to current directory)',
},
'include': {
'type': 'string',
'description': 'Optional file pattern to filter which files to search (e.g., "*.js", "*.{ts,tsx}")',
},
'recursive': {
'type': 'boolean',
'description': 'Whether to search recursively in subdirectories',
'default': True,
},
'case_sensitive': {
'type': 'boolean',
'description': 'Whether the search should be case sensitive',
'default': False,
},
},
'required': ['pattern'],
},
},
}
def validate_parameters(self, parameters: dict[str, Any]) -> dict[str, Any]:
"""Validate grep tool parameters."""
if not isinstance(parameters, dict):
raise ToolValidationError('Parameters must be a dictionary')
# Validate required parameters
if 'pattern' not in parameters:
raise ToolValidationError('Missing required parameter: pattern')
pattern = parameters['pattern']
if not isinstance(pattern, str):
raise ToolValidationError("Parameter 'pattern' must be a string")
if not pattern.strip():
raise ToolValidationError("Parameter 'pattern' cannot be empty")
validated: dict[str, Any] = {'pattern': pattern.strip()}
# Validate optional path parameter
if 'path' in parameters:
path = parameters['path']
if not isinstance(path, str):
raise ToolValidationError("Parameter 'path' must be a string")
if not path.strip():
raise ToolValidationError("Parameter 'path' cannot be empty")
validated['path'] = path.strip()
# Handle include parameter (legacy compatibility)
if 'include' in parameters:
include = parameters['include']
if not isinstance(include, str):
raise ToolValidationError("Parameter 'include' must be a string")
validated['include'] = include.strip()
# Validate optional parameters
if 'recursive' in parameters:
recursive = parameters['recursive']
if not isinstance(recursive, bool):
raise ToolValidationError("Parameter 'recursive' must be a boolean")
validated['recursive'] = recursive
else:
validated['recursive'] = True # Default value
if 'case_sensitive' in parameters:
case_sensitive = parameters['case_sensitive']
if not isinstance(case_sensitive, bool):
raise ToolValidationError(
"Parameter 'case_sensitive' must be a boolean"
)
validated['case_sensitive'] = case_sensitive
else:
validated['case_sensitive'] = False # Default value
return validated

Some files were not shown because too many files have changed in this diff Show More