From 8115d82f9601cc90579019dd07802722a6299e22 Mon Sep 17 00:00:00 2001 From: Tim O'Farrell Date: Fri, 14 Nov 2025 14:08:34 +0000 Subject: [PATCH] feat: add created_at__gte filter to search_app_conversation_start_tasks (#11740) Co-authored-by: openhands --- .../v1-conversation-service.api.ts | 8 +- .../app_conversation_router.py | 10 ++ .../app_conversation_start_task_service.py | 3 + ...sql_app_conversation_start_task_service.py | 15 ++ ...sql_app_conversation_start_task_service.py | 142 ++++++++++++++++++ 5 files changed, 176 insertions(+), 2 deletions(-) diff --git a/frontend/src/api/conversation-service/v1-conversation-service.api.ts b/frontend/src/api/conversation-service/v1-conversation-service.api.ts index 717228c79f..93cd2ba85e 100644 --- a/frontend/src/api/conversation-service/v1-conversation-service.api.ts +++ b/frontend/src/api/conversation-service/v1-conversation-service.api.ts @@ -111,11 +111,11 @@ class V1ConversationService { * Search for start tasks (ongoing tasks that haven't completed yet) * Use this to find tasks that were started but the user navigated away * - * Note: Backend only supports filtering by limit. To filter by repository/trigger, + * Note: Backend supports filtering by limit and created_at__gte. To filter by repository/trigger, * filter the results client-side after fetching. * * @param limit Maximum number of tasks to return (max 100) - * @returns Array of start tasks + * @returns Array of start tasks from the last 20 minutes */ static async searchStartTasks( limit: number = 100, @@ -123,6 +123,10 @@ class V1ConversationService { const params = new URLSearchParams(); params.append("limit", limit.toString()); + // Only get tasks from the last 20 minutes + const twentyMinutesAgo = new Date(Date.now() - 20 * 60 * 1000); + params.append("created_at__gte", twentyMinutesAgo.toISOString()); + const { data } = await openHands.get( `/api/v1/app-conversations/start-tasks/search?${params.toString()}`, ); diff --git a/openhands/app_server/app_conversation/app_conversation_router.py b/openhands/app_server/app_conversation/app_conversation_router.py index 83596b64a5..997e8b6528 100644 --- a/openhands/app_server/app_conversation/app_conversation_router.py +++ b/openhands/app_server/app_conversation/app_conversation_router.py @@ -207,6 +207,10 @@ async def search_app_conversation_start_tasks( UUID | None, Query(title='Filter by conversation ID equal to this value'), ] = None, + created_at__gte: Annotated[ + datetime | None, + Query(title='Filter by created_at greater than or equal to this datetime'), + ] = None, sort_order: Annotated[ AppConversationStartTaskSortOrder, Query(title='Sort order for the results'), @@ -233,6 +237,7 @@ async def search_app_conversation_start_tasks( return ( await app_conversation_start_task_service.search_app_conversation_start_tasks( conversation_id__eq=conversation_id__eq, + created_at__gte=created_at__gte, sort_order=sort_order, page_id=page_id, limit=limit, @@ -246,6 +251,10 @@ async def count_app_conversation_start_tasks( UUID | None, Query(title='Filter by conversation ID equal to this value'), ] = None, + created_at__gte: Annotated[ + datetime | None, + Query(title='Filter by created_at greater than or equal to this datetime'), + ] = None, app_conversation_start_task_service: AppConversationStartTaskService = ( app_conversation_start_task_service_dependency ), @@ -253,6 +262,7 @@ async def count_app_conversation_start_tasks( """Count conversation start tasks matching the given filters.""" return await app_conversation_start_task_service.count_app_conversation_start_tasks( conversation_id__eq=conversation_id__eq, + created_at__gte=created_at__gte, ) diff --git a/openhands/app_server/app_conversation/app_conversation_start_task_service.py b/openhands/app_server/app_conversation/app_conversation_start_task_service.py index 05229411f5..230b26cd8f 100644 --- a/openhands/app_server/app_conversation/app_conversation_start_task_service.py +++ b/openhands/app_server/app_conversation/app_conversation_start_task_service.py @@ -1,5 +1,6 @@ import asyncio from abc import ABC, abstractmethod +from datetime import datetime from uuid import UUID from openhands.app_server.app_conversation.app_conversation_models import ( @@ -18,6 +19,7 @@ class AppConversationStartTaskService(ABC): async def search_app_conversation_start_tasks( self, conversation_id__eq: UUID | None = None, + created_at__gte: datetime | None = None, sort_order: AppConversationStartTaskSortOrder = AppConversationStartTaskSortOrder.CREATED_AT_DESC, page_id: str | None = None, limit: int = 100, @@ -28,6 +30,7 @@ class AppConversationStartTaskService(ABC): async def count_app_conversation_start_tasks( self, conversation_id__eq: UUID | None = None, + created_at__gte: datetime | None = None, ) -> int: """Count conversation start tasks.""" diff --git a/openhands/app_server/app_conversation/sql_app_conversation_start_task_service.py b/openhands/app_server/app_conversation/sql_app_conversation_start_task_service.py index 91b48ab781..4913e795bb 100644 --- a/openhands/app_server/app_conversation/sql_app_conversation_start_task_service.py +++ b/openhands/app_server/app_conversation/sql_app_conversation_start_task_service.py @@ -18,6 +18,7 @@ from __future__ import annotations import logging from dataclasses import dataclass +from datetime import datetime from typing import AsyncGenerator from uuid import UUID @@ -75,6 +76,7 @@ class SQLAppConversationStartTaskService(AppConversationStartTaskService): async def search_app_conversation_start_tasks( self, conversation_id__eq: UUID | None = None, + created_at__gte: datetime | None = None, sort_order: AppConversationStartTaskSortOrder = AppConversationStartTaskSortOrder.CREATED_AT_DESC, page_id: str | None = None, limit: int = 100, @@ -95,6 +97,12 @@ class SQLAppConversationStartTaskService(AppConversationStartTaskService): == conversation_id__eq ) + # Apply created_at__gte filter + if created_at__gte is not None: + query = query.where( + StoredAppConversationStartTask.created_at >= created_at__gte + ) + # Add sort order if sort_order == AppConversationStartTaskSortOrder.CREATED_AT: query = query.order_by(StoredAppConversationStartTask.created_at) @@ -139,6 +147,7 @@ class SQLAppConversationStartTaskService(AppConversationStartTaskService): async def count_app_conversation_start_tasks( self, conversation_id__eq: UUID | None = None, + created_at__gte: datetime | None = None, ) -> int: """Count conversation start tasks.""" query = select(func.count(StoredAppConversationStartTask.id)) @@ -156,6 +165,12 @@ class SQLAppConversationStartTaskService(AppConversationStartTaskService): == conversation_id__eq ) + # Apply created_at__gte filter + if created_at__gte is not None: + query = query.where( + StoredAppConversationStartTask.created_at >= created_at__gte + ) + result = await self.session.execute(query) count = result.scalar() return count or 0 diff --git a/tests/unit/app_server/test_sql_app_conversation_start_task_service.py b/tests/unit/app_server/test_sql_app_conversation_start_task_service.py index 017f4f1fc8..943595e141 100644 --- a/tests/unit/app_server/test_sql_app_conversation_start_task_service.py +++ b/tests/unit/app_server/test_sql_app_conversation_start_task_service.py @@ -639,3 +639,145 @@ class TestSQLAppConversationStartTaskService: user2_count = await user2_service.count_app_conversation_start_tasks() assert user2_count == 1 + + async def test_search_app_conversation_start_tasks_with_created_at_gte_filter( + self, + service: SQLAppConversationStartTaskService, + sample_request: AppConversationStartRequest, + ): + """Test search with created_at__gte filter.""" + from datetime import timedelta + + from openhands.agent_server.models import utc_now + + # Create tasks with different creation times + base_time = utc_now() + + # Task 1: created 2 hours ago + task1 = AppConversationStartTask( + id=uuid4(), + created_by_user_id='user1', + status=AppConversationStartTaskStatus.WORKING, + request=sample_request, + ) + task1.created_at = base_time - timedelta(hours=2) + await service.save_app_conversation_start_task(task1) + + # Task 2: created 1 hour ago + task2 = AppConversationStartTask( + id=uuid4(), + created_by_user_id='user1', + status=AppConversationStartTaskStatus.READY, + request=sample_request, + ) + task2.created_at = base_time - timedelta(hours=1) + await service.save_app_conversation_start_task(task2) + + # Task 3: created 30 minutes ago + task3 = AppConversationStartTask( + id=uuid4(), + created_by_user_id='user1', + status=AppConversationStartTaskStatus.WORKING, + request=sample_request, + ) + task3.created_at = base_time - timedelta(minutes=30) + await service.save_app_conversation_start_task(task3) + + # Search for tasks created in the last 90 minutes + filter_time = base_time - timedelta(minutes=90) + result = await service.search_app_conversation_start_tasks( + created_at__gte=filter_time + ) + + # Should return task2 and task3 (created within last 90 minutes) + assert len(result.items) == 2 + task_ids = [task.id for task in result.items] + assert task2.id in task_ids + assert task3.id in task_ids + assert task1.id not in task_ids + + # Test count with the same filter + count = await service.count_app_conversation_start_tasks( + created_at__gte=filter_time + ) + assert count == 2 + + # Search for tasks created in the last 45 minutes + filter_time_recent = base_time - timedelta(minutes=45) + result_recent = await service.search_app_conversation_start_tasks( + created_at__gte=filter_time_recent + ) + + # Should return only task3 + assert len(result_recent.items) == 1 + assert result_recent.items[0].id == task3.id + + # Test count with recent filter + count_recent = await service.count_app_conversation_start_tasks( + created_at__gte=filter_time_recent + ) + assert count_recent == 1 + + async def test_search_app_conversation_start_tasks_combined_filters( + self, + service: SQLAppConversationStartTaskService, + sample_request: AppConversationStartRequest, + ): + """Test search with both conversation_id and created_at__gte filters.""" + from datetime import timedelta + + from openhands.agent_server.models import utc_now + + conversation_id1 = uuid4() + conversation_id2 = uuid4() + base_time = utc_now() + + # Task 1: conversation_id1, created 2 hours ago + task1 = AppConversationStartTask( + id=uuid4(), + created_by_user_id='user1', + status=AppConversationStartTaskStatus.WORKING, + app_conversation_id=conversation_id1, + request=sample_request, + ) + task1.created_at = base_time - timedelta(hours=2) + await service.save_app_conversation_start_task(task1) + + # Task 2: conversation_id1, created 30 minutes ago + task2 = AppConversationStartTask( + id=uuid4(), + created_by_user_id='user1', + status=AppConversationStartTaskStatus.READY, + app_conversation_id=conversation_id1, + request=sample_request, + ) + task2.created_at = base_time - timedelta(minutes=30) + await service.save_app_conversation_start_task(task2) + + # Task 3: conversation_id2, created 30 minutes ago + task3 = AppConversationStartTask( + id=uuid4(), + created_by_user_id='user1', + status=AppConversationStartTaskStatus.WORKING, + app_conversation_id=conversation_id2, + request=sample_request, + ) + task3.created_at = base_time - timedelta(minutes=30) + await service.save_app_conversation_start_task(task3) + + # Search for tasks with conversation_id1 created in the last hour + filter_time = base_time - timedelta(hours=1) + result = await service.search_app_conversation_start_tasks( + conversation_id__eq=conversation_id1, created_at__gte=filter_time + ) + + # Should return only task2 (conversation_id1 and created within last hour) + assert len(result.items) == 1 + assert result.items[0].id == task2.id + assert result.items[0].app_conversation_id == conversation_id1 + + # Test count with combined filters + count = await service.count_app_conversation_start_tasks( + conversation_id__eq=conversation_id1, created_at__gte=filter_time + ) + assert count == 1