mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat(backend): Develop an API to fetch conversations by repository and conversation_trigger. (#9764)
This commit is contained in:
parent
793786130a
commit
dc41e0e90c
@ -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
|
||||
|
||||
@ -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():
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user