feat(backend): Develop an API to fetch conversations by repository and conversation_trigger. (#9764)

This commit is contained in:
Hiep Le 2025-07-18 22:44:24 +07:00 committed by GitHub
parent 793786130a
commit dc41e0e90c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 449 additions and 8 deletions

View File

@ -220,20 +220,43 @@ async def new_conversation(
async def search_conversations(
page_id: str | None = None,
limit: int = 20,
selected_repository: str | None = None,
conversation_trigger: ConversationTrigger | None = None,
conversation_store: ConversationStore = Depends(get_conversation_store),
) -> ConversationInfoResultSet:
conversation_metadata_result_set = await conversation_store.search(page_id, limit)
# Filter out conversations older than max_age
# Apply filters at API level
filtered_results = []
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
]
for conversation in conversation_metadata_result_set.results:
# Skip conversations without created_at or older than max_age
if not hasattr(conversation, 'created_at'):
continue
age_seconds = (
now - conversation.created_at.replace(tzinfo=timezone.utc)
).total_seconds()
if age_seconds > max_age:
continue
# Apply repository filter
if (
selected_repository is not None
and conversation.selected_repository != selected_repository
):
continue
# Apply conversation trigger filter
if (
conversation_trigger is not None
and conversation.trigger != conversation_trigger
):
continue
filtered_results.append(conversation)
conversation_ids = set(
conversation.conversation_id for conversation in filtered_results

View File

@ -159,6 +159,8 @@ async def test_search_conversations():
result_set = await search_conversations(
page_id=None,
limit=20,
selected_repository=None,
conversation_trigger=None,
conversation_store=mock_store,
)
@ -183,6 +185,422 @@ async def test_search_conversations():
assert result_set == expected
@pytest.mark.asyncio
async def test_search_conversations_with_repository_filter():
"""Test searching conversations with repository filter."""
with _patch_store():
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()
async def mock_get_connections(*args, **kwargs):
return {}
async def get_agent_loop_info(*args, **kwargs):
return []
mock_manager.get_running_agent_loops = mock_get_running_agent_loops
mock_manager.get_connections = mock_get_connections
mock_manager.get_agent_loop_info = get_agent_loop_info
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
# Mock the conversation store
mock_store = MagicMock()
mock_store.search = AsyncMock(
return_value=ConversationInfoResultSet(
results=[
ConversationMetadata(
conversation_id='conversation_1',
title='Conversation 1',
created_at=datetime.fromisoformat(
'2025-01-01T00:00:00+00:00'
),
last_updated_at=datetime.fromisoformat(
'2025-01-01T00:01:00+00:00'
),
selected_repository='test/repo',
user_id='12345',
)
]
)
)
result_set = await search_conversations(
page_id=None,
limit=20,
selected_repository='test/repo',
conversation_trigger=None,
conversation_store=mock_store,
)
# Verify that search was called with only pagination parameters (filtering is done at API level)
mock_store.search.assert_called_once_with(None, 20)
# Verify the result contains only conversations from the specified repository
assert len(result_set.results) == 1
assert result_set.results[0].selected_repository == 'test/repo'
@pytest.mark.asyncio
async def test_search_conversations_with_trigger_filter():
"""Test searching conversations with conversation trigger filter."""
with _patch_store():
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()
async def mock_get_connections(*args, **kwargs):
return {}
async def get_agent_loop_info(*args, **kwargs):
return []
mock_manager.get_running_agent_loops = mock_get_running_agent_loops
mock_manager.get_connections = mock_get_connections
mock_manager.get_agent_loop_info = get_agent_loop_info
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
# Mock the conversation store
mock_store = MagicMock()
mock_store.search = AsyncMock(
return_value=ConversationInfoResultSet(
results=[
ConversationMetadata(
conversation_id='conversation_1',
title='Conversation 1',
created_at=datetime.fromisoformat(
'2025-01-01T00:00:00+00:00'
),
last_updated_at=datetime.fromisoformat(
'2025-01-01T00:01:00+00:00'
),
selected_repository='test/repo',
trigger=ConversationTrigger.GUI,
user_id='12345',
)
]
)
)
result_set = await search_conversations(
page_id=None,
limit=20,
selected_repository=None,
conversation_trigger=ConversationTrigger.GUI,
conversation_store=mock_store,
)
# Verify that search was called with only pagination parameters (filtering is done at API level)
mock_store.search.assert_called_once_with(None, 20)
# Verify the result contains only conversations with the specified trigger
assert len(result_set.results) == 1
assert result_set.results[0].trigger == ConversationTrigger.GUI
@pytest.mark.asyncio
async def test_search_conversations_with_both_filters():
"""Test searching conversations with both repository and trigger filters."""
with _patch_store():
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()
async def mock_get_connections(*args, **kwargs):
return {}
async def get_agent_loop_info(*args, **kwargs):
return []
mock_manager.get_running_agent_loops = mock_get_running_agent_loops
mock_manager.get_connections = mock_get_connections
mock_manager.get_agent_loop_info = get_agent_loop_info
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
# Mock the conversation store
mock_store = MagicMock()
mock_store.search = AsyncMock(
return_value=ConversationInfoResultSet(
results=[
ConversationMetadata(
conversation_id='conversation_1',
title='Conversation 1',
created_at=datetime.fromisoformat(
'2025-01-01T00:00:00+00:00'
),
last_updated_at=datetime.fromisoformat(
'2025-01-01T00:01:00+00:00'
),
selected_repository='test/repo',
trigger=ConversationTrigger.SUGGESTED_TASK,
user_id='12345',
)
]
)
)
result_set = await search_conversations(
page_id=None,
limit=20,
selected_repository='test/repo',
conversation_trigger=ConversationTrigger.SUGGESTED_TASK,
conversation_store=mock_store,
)
# Verify that search was called with only pagination parameters (filtering is done at API level)
mock_store.search.assert_called_once_with(None, 20)
# Verify the result contains only conversations matching both filters
assert len(result_set.results) == 1
result = result_set.results[0]
assert result.selected_repository == 'test/repo'
assert result.trigger == ConversationTrigger.SUGGESTED_TASK
@pytest.mark.asyncio
async def test_search_conversations_with_pagination():
"""Test searching conversations with pagination."""
with _patch_store():
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()
async def mock_get_connections(*args, **kwargs):
return {}
async def get_agent_loop_info(*args, **kwargs):
return []
mock_manager.get_running_agent_loops = mock_get_running_agent_loops
mock_manager.get_connections = mock_get_connections
mock_manager.get_agent_loop_info = get_agent_loop_info
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
# Mock the conversation store
mock_store = MagicMock()
mock_store.search = AsyncMock(
return_value=ConversationInfoResultSet(
results=[
ConversationMetadata(
conversation_id='conversation_1',
title='Conversation 1',
created_at=datetime.fromisoformat(
'2025-01-01T00:00:00+00:00'
),
last_updated_at=datetime.fromisoformat(
'2025-01-01T00:01:00+00:00'
),
selected_repository='test/repo',
user_id='12345',
)
],
next_page_id='next_page_123',
)
)
result_set = await search_conversations(
page_id='page_123',
limit=10,
selected_repository=None,
conversation_trigger=None,
conversation_store=mock_store,
)
# Verify that search was called with pagination parameters (filtering is done at API level)
mock_store.search.assert_called_once_with('page_123', 10)
# Verify the result includes pagination info
assert result_set.next_page_id == 'next_page_123'
@pytest.mark.asyncio
async def test_search_conversations_with_filters_and_pagination():
"""Test searching conversations with filters and pagination."""
with _patch_store():
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()
async def mock_get_connections(*args, **kwargs):
return {}
async def get_agent_loop_info(*args, **kwargs):
return []
mock_manager.get_running_agent_loops = mock_get_running_agent_loops
mock_manager.get_connections = mock_get_connections
mock_manager.get_agent_loop_info = get_agent_loop_info
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
# Mock the conversation store
mock_store = MagicMock()
mock_store.search = AsyncMock(
return_value=ConversationInfoResultSet(
results=[
ConversationMetadata(
conversation_id='conversation_1',
title='Conversation 1',
created_at=datetime.fromisoformat(
'2025-01-01T00:00:00+00:00'
),
last_updated_at=datetime.fromisoformat(
'2025-01-01T00:01:00+00:00'
),
selected_repository='test/repo',
trigger=ConversationTrigger.GUI,
user_id='12345',
)
],
next_page_id='next_page_456',
)
)
result_set = await search_conversations(
page_id='page_456',
limit=5,
selected_repository='test/repo',
conversation_trigger=ConversationTrigger.GUI,
conversation_store=mock_store,
)
# Verify that search was called with only pagination parameters (filtering is done at API level)
mock_store.search.assert_called_once_with('page_456', 5)
# Verify the result includes pagination info
assert result_set.next_page_id == 'next_page_456'
assert len(result_set.results) == 1
result = result_set.results[0]
assert result.selected_repository == 'test/repo'
assert result.trigger == ConversationTrigger.GUI
@pytest.mark.asyncio
async def test_search_conversations_empty_results():
"""Test searching conversations that returns empty results."""
with _patch_store():
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()
async def mock_get_connections(*args, **kwargs):
return {}
async def get_agent_loop_info(*args, **kwargs):
return []
mock_manager.get_running_agent_loops = mock_get_running_agent_loops
mock_manager.get_connections = mock_get_connections
mock_manager.get_agent_loop_info = get_agent_loop_info
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
# Mock the conversation store
mock_store = MagicMock()
mock_store.search = AsyncMock(
return_value=ConversationInfoResultSet(
results=[], next_page_id=None
)
)
result_set = await search_conversations(
page_id=None,
limit=20,
selected_repository='nonexistent/repo',
conversation_trigger=ConversationTrigger.GUI,
conversation_store=mock_store,
)
# Verify that search was called with only pagination parameters (filtering is done at API level)
mock_store.search.assert_called_once_with(None, 20)
# Verify the result is empty
assert len(result_set.results) == 0
assert result_set.next_page_id is None
@pytest.mark.asyncio
async def test_get_conversation():
with _patch_store():