mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 13:52:43 +08:00
feat(backend): exclude sub-conversations when searching for conversations (#11733)
This commit is contained in:
parent
2841e35f24
commit
833aae1833
@ -26,6 +26,7 @@ class AppConversationInfoService(ABC):
|
||||
sort_order: AppConversationSortOrder = AppConversationSortOrder.CREATED_AT_DESC,
|
||||
page_id: str | None = None,
|
||||
limit: int = 100,
|
||||
include_sub_conversations: bool = False,
|
||||
) -> AppConversationInfoPage:
|
||||
"""Search for sandboxed conversations."""
|
||||
|
||||
|
||||
@ -99,6 +99,12 @@ async def search_app_conversations(
|
||||
lte=100,
|
||||
),
|
||||
] = 100,
|
||||
include_sub_conversations: Annotated[
|
||||
bool,
|
||||
Query(
|
||||
title='If True, include sub-conversations in the results. If False (default), exclude all sub-conversations.'
|
||||
),
|
||||
] = False,
|
||||
app_conversation_service: AppConversationService = (
|
||||
app_conversation_service_dependency
|
||||
),
|
||||
@ -114,6 +120,7 @@ async def search_app_conversations(
|
||||
updated_at__lt=updated_at__lt,
|
||||
page_id=page_id,
|
||||
limit=limit,
|
||||
include_sub_conversations=include_sub_conversations,
|
||||
)
|
||||
|
||||
|
||||
@ -193,7 +200,8 @@ async def stream_app_conversation_start(
|
||||
user_context: UserContext = user_context_dependency,
|
||||
) -> list[AppConversationStartTask]:
|
||||
"""Start an app conversation start task and stream updates from it.
|
||||
Leaves the connection open until either the conversation starts or there was an error"""
|
||||
Leaves the connection open until either the conversation starts or there was an error
|
||||
"""
|
||||
response = StreamingResponse(
|
||||
_stream_app_conversation_start(request, user_context),
|
||||
media_type='application/json',
|
||||
|
||||
@ -30,6 +30,7 @@ class AppConversationService(ABC):
|
||||
sort_order: AppConversationSortOrder = AppConversationSortOrder.CREATED_AT_DESC,
|
||||
page_id: str | None = None,
|
||||
limit: int = 100,
|
||||
include_sub_conversations: bool = False,
|
||||
) -> AppConversationPage:
|
||||
"""Search for sandboxed conversations."""
|
||||
|
||||
|
||||
@ -105,6 +105,7 @@ class LiveStatusAppConversationService(GitAppConversationService):
|
||||
sort_order: AppConversationSortOrder = AppConversationSortOrder.CREATED_AT_DESC,
|
||||
page_id: str | None = None,
|
||||
limit: int = 20,
|
||||
include_sub_conversations: bool = False,
|
||||
) -> AppConversationPage:
|
||||
"""Search for sandboxed conversations."""
|
||||
page = await self.app_conversation_info_service.search_app_conversation_info(
|
||||
@ -116,6 +117,7 @@ class LiveStatusAppConversationService(GitAppConversationService):
|
||||
sort_order=sort_order,
|
||||
page_id=page_id,
|
||||
limit=limit,
|
||||
include_sub_conversations=include_sub_conversations,
|
||||
)
|
||||
conversations: list[AppConversation] = await self._build_app_conversations(
|
||||
page.items
|
||||
|
||||
@ -111,10 +111,18 @@ class SQLAppConversationInfoService(AppConversationInfoService):
|
||||
sort_order: AppConversationSortOrder = AppConversationSortOrder.CREATED_AT_DESC,
|
||||
page_id: str | None = None,
|
||||
limit: int = 100,
|
||||
include_sub_conversations: bool = False,
|
||||
) -> AppConversationInfoPage:
|
||||
"""Search for sandboxed conversations without permission checks."""
|
||||
query = await self._secure_select()
|
||||
|
||||
# Conditionally exclude sub-conversations based on the parameter
|
||||
if not include_sub_conversations:
|
||||
# Exclude sub-conversations (only include top-level conversations)
|
||||
query = query.where(
|
||||
StoredConversationMetadata.parent_conversation_id.is_(None)
|
||||
)
|
||||
|
||||
query = self._apply_filters(
|
||||
query=query,
|
||||
title__contains=title__contains,
|
||||
|
||||
@ -6,9 +6,10 @@ import os
|
||||
import re
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Annotated
|
||||
|
||||
import base62
|
||||
from fastapi import APIRouter, Depends, Request, status
|
||||
from fastapi import APIRouter, Depends, Query, Request, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
@ -309,6 +310,12 @@ async def search_conversations(
|
||||
limit: int = 20,
|
||||
selected_repository: str | None = None,
|
||||
conversation_trigger: ConversationTrigger | None = None,
|
||||
include_sub_conversations: Annotated[
|
||||
bool,
|
||||
Query(
|
||||
title='If True, include sub-conversations in the results. If False (default), exclude all sub-conversations.'
|
||||
),
|
||||
] = False,
|
||||
conversation_store: ConversationStore = Depends(get_conversation_store),
|
||||
app_conversation_service: AppConversationService = app_conversation_service_dependency,
|
||||
) -> ConversationInfoResultSet:
|
||||
@ -343,6 +350,7 @@ async def search_conversations(
|
||||
limit=limit,
|
||||
# Apply age filter at the service level if possible
|
||||
created_at__gte=age_filter_date,
|
||||
include_sub_conversations=include_sub_conversations,
|
||||
)
|
||||
|
||||
# Convert V1 conversations to ConversationInfo format
|
||||
@ -1187,6 +1195,7 @@ async def _fetch_v1_conversations_safe(
|
||||
app_conversation_service: App conversation service for V1
|
||||
v1_page_id: Page ID for V1 pagination
|
||||
limit: Maximum number of results
|
||||
include_sub_conversations: If True, include sub-conversations in results
|
||||
|
||||
Returns:
|
||||
Tuple of (v1_conversations, v1_next_page_id)
|
||||
|
||||
@ -623,3 +623,383 @@ class TestSQLAppConversationInfoService:
|
||||
created_at__gte=start_time, created_at__lt=end_time
|
||||
)
|
||||
assert count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_excludes_sub_conversations_by_default(
|
||||
self,
|
||||
service: SQLAppConversationInfoService,
|
||||
):
|
||||
"""Test that search excludes sub-conversations by default."""
|
||||
# Create a parent conversation
|
||||
parent_id = uuid4()
|
||||
parent_info = AppConversationInfo(
|
||||
id=parent_id,
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_parent',
|
||||
title='Parent Conversation',
|
||||
created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 12, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
# Create sub-conversations
|
||||
sub_info_1 = AppConversationInfo(
|
||||
id=uuid4(),
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_sub1',
|
||||
title='Sub Conversation 1',
|
||||
parent_conversation_id=parent_id,
|
||||
created_at=datetime(2024, 1, 1, 13, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 13, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
sub_info_2 = AppConversationInfo(
|
||||
id=uuid4(),
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_sub2',
|
||||
title='Sub Conversation 2',
|
||||
parent_conversation_id=parent_id,
|
||||
created_at=datetime(2024, 1, 1, 14, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 14, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
# Save all conversations
|
||||
await service.save_app_conversation_info(parent_info)
|
||||
await service.save_app_conversation_info(sub_info_1)
|
||||
await service.save_app_conversation_info(sub_info_2)
|
||||
|
||||
# Search without include_sub_conversations (default False)
|
||||
page = await service.search_app_conversation_info()
|
||||
|
||||
# Should only return the parent conversation
|
||||
assert len(page.items) == 1
|
||||
assert page.items[0].id == parent_id
|
||||
assert page.items[0].title == 'Parent Conversation'
|
||||
assert page.items[0].parent_conversation_id is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_includes_sub_conversations_when_flag_true(
|
||||
self,
|
||||
service: SQLAppConversationInfoService,
|
||||
):
|
||||
"""Test that search includes sub-conversations when include_sub_conversations=True."""
|
||||
# Create a parent conversation
|
||||
parent_id = uuid4()
|
||||
parent_info = AppConversationInfo(
|
||||
id=parent_id,
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_parent',
|
||||
title='Parent Conversation',
|
||||
created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 12, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
# Create sub-conversations
|
||||
sub_info_1 = AppConversationInfo(
|
||||
id=uuid4(),
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_sub1',
|
||||
title='Sub Conversation 1',
|
||||
parent_conversation_id=parent_id,
|
||||
created_at=datetime(2024, 1, 1, 13, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 13, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
sub_info_2 = AppConversationInfo(
|
||||
id=uuid4(),
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_sub2',
|
||||
title='Sub Conversation 2',
|
||||
parent_conversation_id=parent_id,
|
||||
created_at=datetime(2024, 1, 1, 14, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 14, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
# Save all conversations
|
||||
await service.save_app_conversation_info(parent_info)
|
||||
await service.save_app_conversation_info(sub_info_1)
|
||||
await service.save_app_conversation_info(sub_info_2)
|
||||
|
||||
# Search with include_sub_conversations=True
|
||||
page = await service.search_app_conversation_info(
|
||||
include_sub_conversations=True
|
||||
)
|
||||
|
||||
# Should return all conversations (1 parent + 2 sub-conversations)
|
||||
assert len(page.items) == 3
|
||||
|
||||
# Verify all conversations are present
|
||||
conversation_ids = {item.id for item in page.items}
|
||||
assert parent_id in conversation_ids
|
||||
assert sub_info_1.id in conversation_ids
|
||||
assert sub_info_2.id in conversation_ids
|
||||
|
||||
# Verify parent conversation has no parent_conversation_id
|
||||
parent_item = next(item for item in page.items if item.id == parent_id)
|
||||
assert parent_item.parent_conversation_id is None
|
||||
|
||||
# Verify sub-conversations have parent_conversation_id set
|
||||
sub_item_1 = next(item for item in page.items if item.id == sub_info_1.id)
|
||||
assert sub_item_1.parent_conversation_id == parent_id
|
||||
|
||||
sub_item_2 = next(item for item in page.items if item.id == sub_info_2.id)
|
||||
assert sub_item_2.parent_conversation_id == parent_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_sub_conversations_with_filters(
|
||||
self,
|
||||
service: SQLAppConversationInfoService,
|
||||
):
|
||||
"""Test that include_sub_conversations works correctly with other filters."""
|
||||
# Create a parent conversation
|
||||
parent_id = uuid4()
|
||||
parent_info = AppConversationInfo(
|
||||
id=parent_id,
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_parent',
|
||||
title='Parent Conversation',
|
||||
created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 12, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
# Create sub-conversations with different titles
|
||||
sub_info_1 = AppConversationInfo(
|
||||
id=uuid4(),
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_sub1',
|
||||
title='Sub Conversation Alpha',
|
||||
parent_conversation_id=parent_id,
|
||||
created_at=datetime(2024, 1, 1, 13, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 13, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
sub_info_2 = AppConversationInfo(
|
||||
id=uuid4(),
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_sub2',
|
||||
title='Sub Conversation Beta',
|
||||
parent_conversation_id=parent_id,
|
||||
created_at=datetime(2024, 1, 1, 14, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 14, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
# Save all conversations
|
||||
await service.save_app_conversation_info(parent_info)
|
||||
await service.save_app_conversation_info(sub_info_1)
|
||||
await service.save_app_conversation_info(sub_info_2)
|
||||
|
||||
# Search with title filter and include_sub_conversations=False (default)
|
||||
page = await service.search_app_conversation_info(title__contains='Alpha')
|
||||
# Should only find parent if it matches, but parent doesn't have "Alpha"
|
||||
# So should find nothing or only sub if we include them
|
||||
assert len(page.items) == 0
|
||||
|
||||
# Search with title filter and include_sub_conversations=True
|
||||
page = await service.search_app_conversation_info(
|
||||
title__contains='Alpha', include_sub_conversations=True
|
||||
)
|
||||
# Should find the sub-conversation with "Alpha" in title
|
||||
assert len(page.items) == 1
|
||||
assert page.items[0].title == 'Sub Conversation Alpha'
|
||||
assert page.items[0].parent_conversation_id == parent_id
|
||||
|
||||
# Search with title filter for "Parent" and include_sub_conversations=True
|
||||
page = await service.search_app_conversation_info(
|
||||
title__contains='Parent', include_sub_conversations=True
|
||||
)
|
||||
# Should find the parent conversation
|
||||
assert len(page.items) == 1
|
||||
assert page.items[0].title == 'Parent Conversation'
|
||||
assert page.items[0].parent_conversation_id is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_sub_conversations_with_date_filters(
|
||||
self,
|
||||
service: SQLAppConversationInfoService,
|
||||
):
|
||||
"""Test that include_sub_conversations works correctly with date filters."""
|
||||
# Create a parent conversation
|
||||
parent_id = uuid4()
|
||||
parent_info = AppConversationInfo(
|
||||
id=parent_id,
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_parent',
|
||||
title='Parent Conversation',
|
||||
created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 12, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
# Create sub-conversations at different times
|
||||
sub_info_1 = AppConversationInfo(
|
||||
id=uuid4(),
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_sub1',
|
||||
title='Sub Conversation 1',
|
||||
parent_conversation_id=parent_id,
|
||||
created_at=datetime(2024, 1, 1, 13, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 13, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
sub_info_2 = AppConversationInfo(
|
||||
id=uuid4(),
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_sub2',
|
||||
title='Sub Conversation 2',
|
||||
parent_conversation_id=parent_id,
|
||||
created_at=datetime(2024, 1, 1, 14, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 14, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
# Save all conversations
|
||||
await service.save_app_conversation_info(parent_info)
|
||||
await service.save_app_conversation_info(sub_info_1)
|
||||
await service.save_app_conversation_info(sub_info_2)
|
||||
|
||||
# Search with date filter and include_sub_conversations=False (default)
|
||||
cutoff_time = datetime(2024, 1, 1, 13, 30, 0, tzinfo=timezone.utc)
|
||||
page = await service.search_app_conversation_info(created_at__gte=cutoff_time)
|
||||
# Should only return parent if it matches the filter, but parent is at 12:00
|
||||
assert len(page.items) == 0
|
||||
|
||||
# Search with date filter and include_sub_conversations=True
|
||||
page = await service.search_app_conversation_info(
|
||||
created_at__gte=cutoff_time, include_sub_conversations=True
|
||||
)
|
||||
# Should find sub-conversations created after cutoff (sub_info_2 at 14:00)
|
||||
assert len(page.items) == 1
|
||||
assert page.items[0].id == sub_info_2.id
|
||||
assert page.items[0].parent_conversation_id == parent_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_multiple_parents_with_sub_conversations(
|
||||
self,
|
||||
service: SQLAppConversationInfoService,
|
||||
):
|
||||
"""Test search with multiple parent conversations and their sub-conversations."""
|
||||
# Create first parent conversation
|
||||
parent1_id = uuid4()
|
||||
parent1_info = AppConversationInfo(
|
||||
id=parent1_id,
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_parent1',
|
||||
title='Parent 1',
|
||||
created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 12, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
# Create second parent conversation
|
||||
parent2_id = uuid4()
|
||||
parent2_info = AppConversationInfo(
|
||||
id=parent2_id,
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_parent2',
|
||||
title='Parent 2',
|
||||
created_at=datetime(2024, 1, 1, 13, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 13, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
# Create sub-conversations for parent1
|
||||
sub1_1 = AppConversationInfo(
|
||||
id=uuid4(),
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_sub1_1',
|
||||
title='Sub 1-1',
|
||||
parent_conversation_id=parent1_id,
|
||||
created_at=datetime(2024, 1, 1, 14, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 14, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
# Create sub-conversations for parent2
|
||||
sub2_1 = AppConversationInfo(
|
||||
id=uuid4(),
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_sub2_1',
|
||||
title='Sub 2-1',
|
||||
parent_conversation_id=parent2_id,
|
||||
created_at=datetime(2024, 1, 1, 15, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 15, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
# Save all conversations
|
||||
await service.save_app_conversation_info(parent1_info)
|
||||
await service.save_app_conversation_info(parent2_info)
|
||||
await service.save_app_conversation_info(sub1_1)
|
||||
await service.save_app_conversation_info(sub2_1)
|
||||
|
||||
# Search without include_sub_conversations (default False)
|
||||
page = await service.search_app_conversation_info()
|
||||
# Should only return the 2 parent conversations
|
||||
assert len(page.items) == 2
|
||||
conversation_ids = {item.id for item in page.items}
|
||||
assert parent1_id in conversation_ids
|
||||
assert parent2_id in conversation_ids
|
||||
assert sub1_1.id not in conversation_ids
|
||||
assert sub2_1.id not in conversation_ids
|
||||
|
||||
# Search with include_sub_conversations=True
|
||||
page = await service.search_app_conversation_info(
|
||||
include_sub_conversations=True
|
||||
)
|
||||
# Should return all 4 conversations (2 parents + 2 sub-conversations)
|
||||
assert len(page.items) == 4
|
||||
conversation_ids = {item.id for item in page.items}
|
||||
assert parent1_id in conversation_ids
|
||||
assert parent2_id in conversation_ids
|
||||
assert sub1_1.id in conversation_ids
|
||||
assert sub2_1.id in conversation_ids
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_sub_conversations_with_pagination(
|
||||
self,
|
||||
service: SQLAppConversationInfoService,
|
||||
):
|
||||
"""Test that include_sub_conversations works correctly with pagination."""
|
||||
# Create a parent conversation
|
||||
parent_id = uuid4()
|
||||
parent_info = AppConversationInfo(
|
||||
id=parent_id,
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id='sandbox_parent',
|
||||
title='Parent Conversation',
|
||||
created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 12, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
# Create multiple sub-conversations
|
||||
sub_conversations = []
|
||||
for i in range(5):
|
||||
sub_info = AppConversationInfo(
|
||||
id=uuid4(),
|
||||
created_by_user_id='test_user_123',
|
||||
sandbox_id=f'sandbox_sub{i}',
|
||||
title=f'Sub Conversation {i}',
|
||||
parent_conversation_id=parent_id,
|
||||
created_at=datetime(2024, 1, 1, 13 + i, 0, 0, tzinfo=timezone.utc),
|
||||
updated_at=datetime(2024, 1, 1, 13 + i, 30, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
sub_conversations.append(sub_info)
|
||||
await service.save_app_conversation_info(sub_info)
|
||||
|
||||
# Save parent
|
||||
await service.save_app_conversation_info(parent_info)
|
||||
|
||||
# Search with include_sub_conversations=True and pagination
|
||||
page1 = await service.search_app_conversation_info(
|
||||
include_sub_conversations=True, limit=3
|
||||
)
|
||||
# Should return 3 items (1 parent + 2 sub-conversations)
|
||||
assert len(page1.items) == 3
|
||||
assert page1.next_page_id is not None
|
||||
|
||||
# Get next page
|
||||
page2 = await service.search_app_conversation_info(
|
||||
include_sub_conversations=True, limit=3, page_id=page1.next_page_id
|
||||
)
|
||||
# Should return remaining items
|
||||
assert len(page2.items) == 3
|
||||
assert page2.next_page_id is None
|
||||
|
||||
# Verify all conversations are present across pages
|
||||
all_ids = {item.id for item in page1.items} | {item.id for item in page2.items}
|
||||
assert parent_id in all_ids
|
||||
for sub_info in sub_conversations:
|
||||
assert sub_info.id in all_ids
|
||||
|
||||
@ -13,6 +13,7 @@ from openhands.app_server.app_conversation.app_conversation_info_service import
|
||||
from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
AgentType,
|
||||
AppConversationInfo,
|
||||
AppConversationPage,
|
||||
AppConversationStartRequest,
|
||||
AppConversationStartTask,
|
||||
AppConversationStartTaskStatus,
|
||||
@ -22,6 +23,9 @@ from openhands.app_server.app_conversation.app_conversation_service import (
|
||||
)
|
||||
from openhands.microagent.microagent import KnowledgeMicroagent, RepoMicroagent
|
||||
from openhands.microagent.types import MicroagentMetadata, MicroagentType
|
||||
from openhands.server.data_models.conversation_info_result_set import (
|
||||
ConversationInfoResultSet,
|
||||
)
|
||||
from openhands.server.routes.conversation import (
|
||||
AddMessageRequest,
|
||||
add_message,
|
||||
@ -29,11 +33,15 @@ from openhands.server.routes.conversation import (
|
||||
)
|
||||
from openhands.server.routes.manage_conversations import (
|
||||
UpdateConversationRequest,
|
||||
search_conversations,
|
||||
update_conversation,
|
||||
)
|
||||
from openhands.server.session.conversation import ServerConversation
|
||||
from openhands.storage.conversation.conversation_store import ConversationStore
|
||||
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
|
||||
from openhands.storage.data_models.conversation_metadata import (
|
||||
ConversationMetadata,
|
||||
ConversationTrigger,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -1200,3 +1208,254 @@ async def test_create_sub_conversation_with_planning_agent():
|
||||
assert task.request.parent_conversation_id == parent_conversation_id
|
||||
assert task.sandbox_id == sandbox_id
|
||||
break
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_conversations_include_sub_conversations_default_false():
|
||||
"""Test that include_sub_conversations defaults to False when not provided."""
|
||||
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=[])
|
||||
)
|
||||
|
||||
# Create a mock app conversation service
|
||||
mock_app_conversation_service = AsyncMock()
|
||||
mock_app_conversation_service.search_app_conversations.return_value = (
|
||||
AppConversationPage(items=[])
|
||||
)
|
||||
|
||||
# Call search_conversations without include_sub_conversations parameter
|
||||
await search_conversations(
|
||||
page_id=None,
|
||||
limit=20,
|
||||
selected_repository=None,
|
||||
conversation_trigger=None,
|
||||
conversation_store=mock_store,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
)
|
||||
|
||||
# Verify that search_app_conversations was called with include_sub_conversations=False (default)
|
||||
mock_app_conversation_service.search_app_conversations.assert_called_once()
|
||||
call_kwargs = (
|
||||
mock_app_conversation_service.search_app_conversations.call_args[1]
|
||||
)
|
||||
assert call_kwargs.get('include_sub_conversations') is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_conversations_include_sub_conversations_explicit_false():
|
||||
"""Test that include_sub_conversations=False is properly passed through."""
|
||||
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=[])
|
||||
)
|
||||
|
||||
# Create a mock app conversation service
|
||||
mock_app_conversation_service = AsyncMock()
|
||||
mock_app_conversation_service.search_app_conversations.return_value = (
|
||||
AppConversationPage(items=[])
|
||||
)
|
||||
|
||||
# Call search_conversations with include_sub_conversations=False
|
||||
await search_conversations(
|
||||
page_id=None,
|
||||
limit=20,
|
||||
selected_repository=None,
|
||||
conversation_trigger=None,
|
||||
include_sub_conversations=False,
|
||||
conversation_store=mock_store,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
)
|
||||
|
||||
# Verify that search_app_conversations was called with include_sub_conversations=False
|
||||
mock_app_conversation_service.search_app_conversations.assert_called_once()
|
||||
call_kwargs = (
|
||||
mock_app_conversation_service.search_app_conversations.call_args[1]
|
||||
)
|
||||
assert call_kwargs.get('include_sub_conversations') is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_conversations_include_sub_conversations_explicit_true():
|
||||
"""Test that include_sub_conversations=True is properly passed through."""
|
||||
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=[])
|
||||
)
|
||||
|
||||
# Create a mock app conversation service
|
||||
mock_app_conversation_service = AsyncMock()
|
||||
mock_app_conversation_service.search_app_conversations.return_value = (
|
||||
AppConversationPage(items=[])
|
||||
)
|
||||
|
||||
# Call search_conversations with include_sub_conversations=True
|
||||
await search_conversations(
|
||||
page_id=None,
|
||||
limit=20,
|
||||
selected_repository=None,
|
||||
conversation_trigger=None,
|
||||
include_sub_conversations=True,
|
||||
conversation_store=mock_store,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
)
|
||||
|
||||
# Verify that search_app_conversations was called with include_sub_conversations=True
|
||||
mock_app_conversation_service.search_app_conversations.assert_called_once()
|
||||
call_kwargs = (
|
||||
mock_app_conversation_service.search_app_conversations.call_args[1]
|
||||
)
|
||||
assert call_kwargs.get('include_sub_conversations') is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_conversations_include_sub_conversations_with_other_filters():
|
||||
"""Test that include_sub_conversations works correctly with other filters."""
|
||||
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=[])
|
||||
)
|
||||
|
||||
# Create a mock app conversation service
|
||||
mock_app_conversation_service = AsyncMock()
|
||||
mock_app_conversation_service.search_app_conversations.return_value = (
|
||||
AppConversationPage(items=[])
|
||||
)
|
||||
|
||||
# Create a valid base64-encoded page_id for testing
|
||||
import base64
|
||||
|
||||
page_id_data = json.dumps({'v0': None, 'v1': 'test_v1_page_id'})
|
||||
encoded_page_id = base64.b64encode(page_id_data.encode()).decode()
|
||||
|
||||
# Call search_conversations with include_sub_conversations and other filters
|
||||
await search_conversations(
|
||||
page_id=encoded_page_id,
|
||||
limit=50,
|
||||
selected_repository='test/repo',
|
||||
conversation_trigger=ConversationTrigger.GUI,
|
||||
include_sub_conversations=True,
|
||||
conversation_store=mock_store,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
)
|
||||
|
||||
# Verify that search_app_conversations was called with all parameters including include_sub_conversations=True
|
||||
mock_app_conversation_service.search_app_conversations.assert_called_once()
|
||||
call_kwargs = (
|
||||
mock_app_conversation_service.search_app_conversations.call_args[1]
|
||||
)
|
||||
assert call_kwargs.get('include_sub_conversations') is True
|
||||
assert call_kwargs.get('page_id') == 'test_v1_page_id'
|
||||
assert call_kwargs.get('limit') == 50
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user