diff --git a/src/langbot/pkg/api/http/controller/groups/monitoring.py b/src/langbot/pkg/api/http/controller/groups/monitoring.py index aae227e25..5cbdf56c6 100644 --- a/src/langbot/pkg/api/http/controller/groups/monitoring.py +++ b/src/langbot/pkg/api/http/controller/groups/monitoring.py @@ -24,12 +24,21 @@ def parse_iso_datetime(datetime_str: str | None) -> datetime.datetime | None: return dt -def validate_monitoring_window(start_time: datetime.datetime | None, end_time: datetime.datetime | None) -> None: +def validate_monitoring_window( + start_time: datetime.datetime | None, + end_time: datetime.datetime | None, + *, + allow_point_window: bool = False, +) -> None: """Bound explicit dashboard windows to one year without silently truncating them.""" if start_time is None: return end = end_time or datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) - if start_time >= end or end - start_time > datetime.timedelta(days=365): + if ( + start_time > end + or (start_time == end and not allow_point_window) + or end - start_time > datetime.timedelta(days=365) + ): quart.abort(400, description='Monitoring time range must be positive and no longer than 365 days') @@ -580,7 +589,8 @@ class MonitoringRouterGroup(group.RouterGroup): """Get detailed analysis for a specific session""" start_time = parse_iso_datetime(quart.request.args.get('startTime')) end_time = parse_iso_datetime(quart.request.args.get('endTime')) - validate_monitoring_window(start_time, end_time) + # Message pages can contain one timestamp; tool-call bounds are inclusive. + validate_monitoring_window(start_time, end_time, allow_point_window=True) analysis = await self.ap.monitoring_service.get_session_analysis( request_context, session_id, diff --git a/tests/unit_tests/api/service/test_monitoring_sessions.py b/tests/unit_tests/api/service/test_monitoring_sessions.py index 7451c9124..868f51589 100644 --- a/tests/unit_tests/api/service/test_monitoring_sessions.py +++ b/tests/unit_tests/api/service/test_monitoring_sessions.py @@ -183,6 +183,31 @@ async def test_analysis_fails_closed_and_scopes_statistics(service): assert result['session']['bot_id'] == 'b' +async def test_single_message_analysis_without_tool_calls(service): + from langbot.pkg.api.http.controller.groups.monitoring import validate_monitoring_window + from werkzeug.exceptions import BadRequest + + await service.record_session_start(context('a'), session_id='person_42', **resource('a')) + await service.record_message(context('a'), session_id='person_42', message_content='hi', **resource('a')) + row = (await service.ap.persistence_mgr.execute_async(sa.select(models.MonitoringMessage.timestamp))).one() + timestamp = row.timestamp + validate_monitoring_window(timestamp, timestamp, allow_point_window=True) + result = await service.get_session_analysis( + context(), 'person_42', bot_id='a', start_time=timestamp, end_time=timestamp + ) + assert result['found'] is True + assert result['message_stats']['total'] == 1 + assert result['tool_calls'] == [] + for start, end in [ + (timestamp + dt.timedelta(seconds=1), timestamp), + (timestamp, timestamp + dt.timedelta(days=366)), + ]: + with pytest.raises(BadRequest): + validate_monitoring_window(start, end, allow_point_window=True) + with pytest.raises(BadRequest): + validate_monitoring_window(timestamp, timestamp) + + async def test_activity_requires_bot_and_upsert_counts_racing_first_queries(service): for _ in range(2): await service.record_session_start(context('a'), session_id='person_42', **resource('a')) diff --git a/web/src/app/home/pipelines/PipelineMigration.tsx b/web/src/app/home/pipelines/PipelineMigration.tsx index 892022f37..d85b3931f 100644 --- a/web/src/app/home/pipelines/PipelineMigration.tsx +++ b/web/src/app/home/pipelines/PipelineMigration.tsx @@ -340,16 +340,12 @@ export default function PipelineMigration({ return ( <> - {(count > 0 || previewError || results.length > 0) && ( + {(count > 0 || results.length > 0) && (