mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
4 Commits
v1-cli-add
...
openhands-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15ea9cdc9f | ||
|
|
d4d01b1a27 | ||
|
|
b7e345aa36 | ||
|
|
567ce9b049 |
@@ -12,6 +12,7 @@ from openhands.events.observation.files import (
|
||||
FileReadObservation,
|
||||
FileWriteObservation,
|
||||
)
|
||||
|
||||
from openhands.events.observation.observation import Observation
|
||||
from openhands.events.observation.reject import UserRejectObservation
|
||||
from openhands.events.observation.success import SuccessObservation
|
||||
@@ -30,4 +31,5 @@ __all__ = [
|
||||
'AgentDelegateObservation',
|
||||
'SuccessObservation',
|
||||
'UserRejectObservation',
|
||||
|
||||
]
|
||||
|
||||
@@ -323,14 +323,23 @@ async def zip_current_workspace(request: Request, background_tasks: BackgroundTa
|
||||
status_code=500,
|
||||
content={'error': f'Error zipping workspace: {e}'},
|
||||
)
|
||||
|
||||
# Get a descriptive name for the zip file
|
||||
try:
|
||||
summary = request.state.conversation.summarize_actions(request.state.llm)
|
||||
filename = f"{summary}.zip"
|
||||
except Exception as e:
|
||||
logger.warning(f'Error generating descriptive filename: {e}', exc_info=True)
|
||||
filename = 'workspace.zip'
|
||||
|
||||
response = FileResponse(
|
||||
path=zip_file,
|
||||
filename='workspace.zip',
|
||||
filename=filename,
|
||||
media_type='application/x-zip-compressed',
|
||||
)
|
||||
|
||||
# This will execute after the response is sent (So the file is not deleted before being sent)
|
||||
background_tasks.add_task(zip_file.unlink)
|
||||
background_tasks.add_task(os.unlink, zip_file)
|
||||
|
||||
return response
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from openhands.core.config import AppConfig
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.memory.condenser import MemoryCondenser
|
||||
from openhands.runtime import get_runtime_cls
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.security import SecurityAnalyzer, options
|
||||
@@ -44,3 +47,38 @@ class Conversation:
|
||||
|
||||
async def disconnect(self):
|
||||
asyncio.create_task(call_sync_from_async(self.runtime.close))
|
||||
|
||||
def summarize_actions(self, llm: LLM) -> str:
|
||||
"""Summarize the agent's actions into a short descriptive phrase.
|
||||
|
||||
Args:
|
||||
llm (LLM): The language model to use for summarization.
|
||||
|
||||
Returns:
|
||||
str: A short descriptive phrase summarizing the agent's actions.
|
||||
"""
|
||||
events = self.event_stream.get_events()
|
||||
if not events:
|
||||
return "empty-workspace"
|
||||
|
||||
# Create a prompt that asks for a short descriptive phrase
|
||||
prompt = (
|
||||
"Please summarize the following conversation into a short descriptive phrase "
|
||||
"that can be used as a filename (e.g. 'fixing-bug-in-code' or 'adding-new-feature'). "
|
||||
"The summary should be lowercase, use hyphens instead of spaces, and not include special characters.\n\n"
|
||||
"Conversation:\n"
|
||||
)
|
||||
for event in events:
|
||||
prompt += f"{event.__class__.__name__}: {str(event)}\n"
|
||||
|
||||
# Use the memory condenser to get a summary
|
||||
condenser = MemoryCondenser()
|
||||
summary = condenser.condense(prompt, llm)
|
||||
|
||||
# Clean up the summary to make it suitable for a filename
|
||||
summary = summary.strip().lower()
|
||||
summary = re.sub(r'[^a-z0-9-]', '-', summary) # Replace special chars with hyphens
|
||||
summary = re.sub(r'-+', '-', summary) # Replace multiple hyphens with single hyphen
|
||||
summary = summary.strip('-') # Remove leading/trailing hyphens
|
||||
|
||||
return summary
|
||||
|
||||
48
tests/unit/test_conversation.py
Normal file
48
tests/unit/test_conversation.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from openhands.core.config import AppConfig
|
||||
from openhands.events.action import MessageAction
|
||||
from openhands.events.observation import NullObservation
|
||||
from openhands.server.session.conversation import Conversation
|
||||
from openhands.storage.memory import InMemoryFileStore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conversation():
|
||||
config = AppConfig()
|
||||
file_store = InMemoryFileStore()
|
||||
with patch(
|
||||
'openhands.runtime.impl.eventstream.eventstream_runtime.docker'
|
||||
) as mock_docker:
|
||||
mock_client = MagicMock()
|
||||
mock_client.version.return_value = {'Version': '20.10.0'}
|
||||
mock_docker.from_env.return_value = mock_client
|
||||
return Conversation('test_sid', file_store, config)
|
||||
|
||||
|
||||
def test_summarize_actions(conversation):
|
||||
# Mock the event stream
|
||||
conversation.event_stream.get_events = MagicMock(
|
||||
return_value=[
|
||||
MessageAction('Hello'),
|
||||
NullObservation(content='Hi there'),
|
||||
MessageAction('Fix the bug'),
|
||||
NullObservation(content="I'll help fix the bug"),
|
||||
]
|
||||
)
|
||||
|
||||
# Mock the LLM
|
||||
with patch('openhands.memory.condenser.LLM') as mock_llm:
|
||||
mock_llm.completion.return_value = {
|
||||
'choices': [{'message': {'content': 'fixing-bug-in-code'}}]
|
||||
}
|
||||
summary = conversation.summarize_actions(mock_llm)
|
||||
assert summary == 'fixing-bug-in-code'
|
||||
|
||||
# Verify the prompt sent to the LLM
|
||||
messages = mock_llm.completion.call_args[1]['messages']
|
||||
assert len(messages) == 1
|
||||
assert messages[0]['role'] == 'user'
|
||||
assert 'Please summarize' in messages[0]['content']
|
||||
110
tests/unit/test_files.py
Normal file
110
tests/unit/test_files.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import tempfile
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from openhands.core.config import AppConfig
|
||||
from openhands.events.action import MessageAction
|
||||
from openhands.events.observation import NullObservation
|
||||
from openhands.server.routes.files import app
|
||||
from openhands.server.session.conversation import Conversation
|
||||
from openhands.storage.memory import InMemoryFileStore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app_instance = FastAPI()
|
||||
app_instance.include_router(app)
|
||||
|
||||
# Add middleware to inject conversation and LLM into request state
|
||||
@app_instance.middleware('http')
|
||||
async def inject_conversation(request: Request, call_next):
|
||||
request.state.conversation = request.app.state.conversation
|
||||
request.state.llm = request.app.state.llm
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
test_client = TestClient(app_instance)
|
||||
return test_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conversation():
|
||||
config = AppConfig()
|
||||
file_store = InMemoryFileStore()
|
||||
with patch(
|
||||
'openhands.runtime.impl.eventstream.eventstream_runtime.docker'
|
||||
) as mock_docker:
|
||||
mock_client = MagicMock()
|
||||
mock_client.version.return_value = {'Version': '20.10.0'}
|
||||
mock_docker.from_env.return_value = mock_client
|
||||
return Conversation('test_sid', file_store, config)
|
||||
|
||||
|
||||
def test_zip_directory_with_descriptive_name(client, conversation):
|
||||
# Create a temporary file to simulate the workspace
|
||||
with tempfile.NamedTemporaryFile() as temp_file:
|
||||
# Mock the runtime to return our temp file
|
||||
mock_runtime = MagicMock()
|
||||
mock_runtime.copy_from.return_value = temp_file.name
|
||||
mock_runtime.config.workspace_mount_path_in_sandbox = '/workspace'
|
||||
conversation.runtime = mock_runtime
|
||||
|
||||
# Mock the event stream to have some events
|
||||
conversation.event_stream.get_events = MagicMock(
|
||||
return_value=[
|
||||
MessageAction('Fix the bug'),
|
||||
NullObservation(content="I'll help fix the bug"),
|
||||
]
|
||||
)
|
||||
|
||||
# Mock the LLM
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.completion.return_value = {
|
||||
'choices': [{'message': {'content': 'fixing-bug-in-code'}}]
|
||||
}
|
||||
|
||||
# Set up the app state
|
||||
client.app.state.conversation = conversation
|
||||
client.app.state.llm = mock_llm
|
||||
|
||||
response = client.get('/api/zip-directory')
|
||||
|
||||
# Check that the response is successful
|
||||
assert response.status_code == 200
|
||||
|
||||
# Check that the filename is correct
|
||||
assert (
|
||||
response.headers['content-disposition']
|
||||
== 'attachment; filename="fixing-bug-in-code.zip"'
|
||||
)
|
||||
|
||||
|
||||
def test_zip_directory_fallback_name(client, conversation):
|
||||
# Create a temporary file to simulate the workspace
|
||||
with tempfile.NamedTemporaryFile() as temp_file:
|
||||
# Mock the runtime to return our temp file
|
||||
mock_runtime = MagicMock()
|
||||
mock_runtime.copy_from.return_value = temp_file.name
|
||||
mock_runtime.config.workspace_mount_path_in_sandbox = '/workspace'
|
||||
conversation.runtime = mock_runtime
|
||||
|
||||
# Mock the event stream to have no events
|
||||
conversation.event_stream.get_events = MagicMock(return_value=[])
|
||||
|
||||
# Set up the app state
|
||||
client.app.state.conversation = conversation
|
||||
client.app.state.llm = None # No LLM available
|
||||
|
||||
response = client.get('/api/zip-directory')
|
||||
|
||||
# Check that the response is successful
|
||||
assert response.status_code == 200
|
||||
|
||||
# Check that the filename falls back to empty-workspace.zip
|
||||
assert (
|
||||
response.headers['content-disposition']
|
||||
== 'attachment; filename="empty-workspace.zip"'
|
||||
)
|
||||
Reference in New Issue
Block a user