Add conversation age limit configuration (#6763)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Robert Brennan 2025-02-20 10:50:17 -05:00 committed by GitHub
parent 42f1fc92fa
commit 52723061b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 72 additions and 35 deletions

View File

@ -76,6 +76,7 @@ class AppConfig(BaseModel):
file_uploads_allowed_extensions: list[str] = Field(default_factory=lambda: ['.*'])
runloop_api_key: SecretStr | None = Field(default=None)
cli_multiline_input: bool = Field(default=False)
conversation_max_age_seconds: int = Field(default=864000) # 10 days in seconds
defaults_dict: ClassVar[dict] = {}

View File

@ -130,8 +130,9 @@ async def _create_new_conversation(
@app.post('/conversations')
async def new_conversation(request: Request, data: InitSessionRequest):
"""Initialize a new session or join an existing one.
After successful initialization, the client should connect to the WebSocket
using the returned conversation ID
using the returned conversation ID.
"""
logger.info('Initializing new conversation')
user_id = get_user_id(request)
@ -188,10 +189,19 @@ async def search_conversations(
config, get_user_id(request)
)
conversation_metadata_result_set = await conversation_store.search(page_id, limit)
# Filter out conversations older than max_age
now = datetime.now(timezone.utc)
max_age = config.conversation_max_age_seconds
filtered_results = [
conversation for conversation in conversation_metadata_result_set.results
if hasattr(conversation, 'created_at') and
(now - conversation.created_at.replace(tzinfo=timezone.utc)).total_seconds() <= max_age
]
conversation_ids = set(
conversation.conversation_id
for conversation in conversation_metadata_result_set.results
if hasattr(conversation, 'created_at')
for conversation in filtered_results
)
running_conversations = await conversation_manager.get_running_agent_loops(
get_user_id(request), set(conversation_ids)
@ -202,7 +212,7 @@ async def search_conversations(
conversation=conversation,
is_running=conversation.conversation_id in running_conversations,
)
for conversation in conversation_metadata_result_set.results
for conversation in filtered_results
),
next_page_id=conversation_metadata_result_set.next_page_id,
)

View File

@ -1,10 +1,11 @@
import json
from contextlib import contextmanager
from datetime import datetime
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch
import pytest
from openhands.runtime.impl.docker.docker_runtime import DockerRuntime
from openhands.server.routes.manage_conversations import (
delete_conversation,
get_conversation,
@ -30,8 +31,8 @@ def _patch_store():
'selected_repository': 'foobar',
'conversation_id': 'some_conversation_id',
'github_user_id': '12345',
'created_at': '2025-01-01T00:00:00',
'last_updated_at': '2025-01-01T00:01:00',
'created_at': '2025-01-01T00:00:00+00:00',
'last_updated_at': '2025-01-01T00:01:00+00:00',
}
),
)
@ -49,22 +50,46 @@ def _patch_store():
@pytest.mark.asyncio
async def test_search_conversations():
with _patch_store():
result_set = await search_conversations(
MagicMock(state=MagicMock(github_token=''))
)
expected = ConversationInfoResultSet(
results=[
ConversationInfo(
conversation_id='some_conversation_id',
title='Some Conversation',
created_at=datetime.fromisoformat('2025-01-01T00:00:00'),
last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00'),
status=ConversationStatus.STOPPED,
selected_repository='foobar',
)
]
)
assert result_set == expected
with patch(
'openhands.server.routes.manage_conversations.config'
) as mock_config:
mock_config.conversation_max_age_seconds = 864000 # 10 days
with patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_manager:
async def mock_get_running_agent_loops(*args, **kwargs):
return set()
mock_manager.get_running_agent_loops = mock_get_running_agent_loops
with patch(
'openhands.server.routes.manage_conversations.datetime'
) as mock_datetime:
mock_datetime.now.return_value = datetime.fromisoformat(
'2025-01-01T00:00:00+00:00'
)
mock_datetime.fromisoformat = datetime.fromisoformat
mock_datetime.timezone = timezone
result_set = await search_conversations(
MagicMock(state=MagicMock(github_token=''))
)
expected = ConversationInfoResultSet(
results=[
ConversationInfo(
conversation_id='some_conversation_id',
title='Some Conversation',
created_at=datetime.fromisoformat(
'2025-01-01T00:00:00+00:00'
),
last_updated_at=datetime.fromisoformat(
'2025-01-01T00:01:00+00:00'
),
status=ConversationStatus.STOPPED,
selected_repository='foobar',
)
]
)
assert result_set == expected
@pytest.mark.asyncio
@ -76,8 +101,8 @@ async def test_get_conversation():
expected = ConversationInfo(
conversation_id='some_conversation_id',
title='Some Conversation',
created_at=datetime.fromisoformat('2025-01-01T00:00:00'),
last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00'),
created_at=datetime.fromisoformat('2025-01-01T00:00:00+00:00'),
last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00+00:00'),
status=ConversationStatus.STOPPED,
selected_repository='foobar',
)
@ -109,8 +134,8 @@ async def test_update_conversation():
expected = ConversationInfo(
conversation_id='some_conversation_id',
title='New Title',
created_at=datetime.fromisoformat('2025-01-01T00:00:00'),
last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00'),
created_at=datetime.fromisoformat('2025-01-01T00:00:00+00:00'),
last_updated_at=datetime.fromisoformat('2025-01-01T00:01:00+00:00'),
status=ConversationStatus.STOPPED,
selected_repository='foobar',
)
@ -120,11 +145,12 @@ async def test_update_conversation():
@pytest.mark.asyncio
async def test_delete_conversation():
with _patch_store():
await delete_conversation(
'some_conversation_id',
MagicMock(state=MagicMock(github_token='')),
)
conversation = await get_conversation(
'some_conversation_id', MagicMock(state=MagicMock(github_token=''))
)
assert conversation is None
with patch.object(DockerRuntime, 'delete', return_value=None):
await delete_conversation(
'some_conversation_id',
MagicMock(state=MagicMock(github_token='')),
)
conversation = await get_conversation(
'some_conversation_id', MagicMock(state=MagicMock(github_token=''))
)
assert conversation is None