Skip to content

Commit 8d05e95

Browse files
authored
feat(backend): enhance search_conversation_events functions with timestamp filtering support (#695)
1 parent 627da01 commit 8d05e95

File tree

4 files changed

+520
-16
lines changed

4 files changed

+520
-16
lines changed

openhands-agent-server/openhands/agent_server/event_router.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import logging
6+
from datetime import datetime
67
from typing import Annotated
78

89
from fastapi import (
@@ -31,9 +32,31 @@
3132
)
3233
logger = logging.getLogger(__name__)
3334

35+
3436
# Read methods
3537

3638

39+
def normalize_datetime_to_server_timezone(dt: datetime) -> datetime:
40+
"""
41+
Normalize datetime to server timezone for consistent comparison.
42+
43+
If the datetime has timezone info, convert to server native timezone.
44+
If it's naive (no timezone), assume it's already in server timezone.
45+
46+
Args:
47+
dt: Input datetime (may be timezone-aware or naive)
48+
49+
Returns:
50+
Datetime in server native timezone (timezone-aware)
51+
"""
52+
if dt.tzinfo is not None:
53+
# Timezone-aware: convert to server native timezone
54+
return dt.astimezone(None)
55+
else:
56+
# Naive datetime: assume it's already in server timezone
57+
return dt
58+
59+
3760
@event_router.get("/search", responses={404: {"description": "Conversation not found"}})
3861
async def search_conversation_events(
3962
page_id: Annotated[
@@ -54,12 +77,33 @@ async def search_conversation_events(
5477
EventSortOrder,
5578
Query(title="Sort order for events"),
5679
] = EventSortOrder.TIMESTAMP,
80+
timestamp__gte: Annotated[
81+
datetime | None,
82+
Query(title="Filter: event timestamp >= this datetime"),
83+
] = None,
84+
timestamp__lt: Annotated[
85+
datetime | None,
86+
Query(title="Filter: event timestamp < this datetime"),
87+
] = None,
5788
event_service: EventService = Depends(get_event_service),
5889
) -> EventPage:
5990
"""Search / List local events"""
6091
assert limit > 0
6192
assert limit <= 100
62-
return await event_service.search_events(page_id, limit, kind, sort_order)
93+
94+
# Normalize timezone-aware datetimes to server timezone
95+
normalized_gte = (
96+
normalize_datetime_to_server_timezone(timestamp__gte)
97+
if timestamp__gte
98+
else None
99+
)
100+
normalized_lt = (
101+
normalize_datetime_to_server_timezone(timestamp__lt) if timestamp__lt else None
102+
)
103+
104+
return await event_service.search_events(
105+
page_id, limit, kind, sort_order, normalized_gte, normalized_lt
106+
)
63107

64108

65109
@event_router.get("/count", responses={404: {"description": "Conversation not found"}})
@@ -70,10 +114,29 @@ async def count_conversation_events(
70114
title="Optional filter by event kind/type (e.g., ActionEvent, MessageEvent)"
71115
),
72116
] = None,
117+
timestamp__gte: Annotated[
118+
datetime | None,
119+
Query(title="Filter: event timestamp >= this datetime"),
120+
] = None,
121+
timestamp__lt: Annotated[
122+
datetime | None,
123+
Query(title="Filter: event timestamp < this datetime"),
124+
] = None,
73125
event_service: EventService = Depends(get_event_service),
74126
) -> int:
75127
"""Count local events matching the given filters"""
76-
count = await event_service.count_events(kind)
128+
# Normalize timezone-aware datetimes to server timezone
129+
normalized_gte = (
130+
normalize_datetime_to_server_timezone(timestamp__gte)
131+
if timestamp__gte
132+
else None
133+
)
134+
normalized_lt = (
135+
normalize_datetime_to_server_timezone(timestamp__lt) if timestamp__lt else None
136+
)
137+
138+
count = await event_service.count_events(kind, normalized_gte, normalized_lt)
139+
77140
return count
78141

79142

openhands-agent-server/openhands/agent_server/event_service.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
from dataclasses import dataclass, field
3+
from datetime import datetime
34
from pathlib import Path
45
from uuid import UUID
56

@@ -82,10 +83,16 @@ async def search_events(
8283
limit: int = 100,
8384
kind: str | None = None,
8485
sort_order: EventSortOrder = EventSortOrder.TIMESTAMP,
86+
timestamp__gte: datetime | None = None,
87+
timestamp__lt: datetime | None = None,
8588
) -> EventPage:
8689
if not self._conversation:
8790
raise ValueError("inactive_service")
8891

92+
# Convert datetime to ISO string for comparison (ISO strings are comparable)
93+
timestamp_gte_str = timestamp__gte.isoformat() if timestamp__gte else None
94+
timestamp_lt_str = timestamp__lt.isoformat() if timestamp__lt else None
95+
8996
# Collect all events
9097
all_events = []
9198
with self._conversation._state as state:
@@ -97,6 +104,16 @@ async def search_events(
97104
!= kind
98105
):
99106
continue
107+
108+
# Apply timestamp filters if provided (ISO string comparison)
109+
if (
110+
timestamp_gte_str is not None
111+
and event.timestamp < timestamp_gte_str
112+
):
113+
continue
114+
if timestamp_lt_str is not None and event.timestamp >= timestamp_lt_str:
115+
continue
116+
100117
all_events.append(event)
101118

102119
# Sort events based on sort_order
@@ -131,11 +148,17 @@ async def search_events(
131148
async def count_events(
132149
self,
133150
kind: str | None = None,
151+
timestamp__gte: datetime | None = None,
152+
timestamp__lt: datetime | None = None,
134153
) -> int:
135154
"""Count events matching the given filters."""
136155
if not self._conversation:
137156
raise ValueError("inactive_service")
138157

158+
# Convert datetime to ISO string for comparison (ISO strings are comparable)
159+
timestamp_gte_str = timestamp__gte.isoformat() if timestamp__gte else None
160+
timestamp_lt_str = timestamp__lt.isoformat() if timestamp__lt else None
161+
139162
count = 0
140163
with self._conversation._state as state:
141164
for event in state.events:
@@ -146,6 +169,16 @@ async def count_events(
146169
!= kind
147170
):
148171
continue
172+
173+
# Apply timestamp filters if provided (ISO string comparison)
174+
if (
175+
timestamp_gte_str is not None
176+
and event.timestamp < timestamp_gte_str
177+
):
178+
continue
179+
if timestamp_lt_str is not None and event.timestamp >= timestamp_lt_str:
180+
continue
181+
149182
count += 1
150183

151184
return count

0 commit comments

Comments
 (0)