Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pyrit/memory/azure_sql_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,10 @@ def _execute_get_conversation_stats(self, *, conversation_ids: Sequence[str]) ->
created_at = datetime.fromisoformat(raw_created_at)
else:
created_at = raw_created_at
# The raw MIN(timestamp) aggregate bypasses UTCDateTime's result
# processor, so reattach the UTC offset every other read path returns.
if created_at.tzinfo is None:
created_at = created_at.replace(tzinfo=UTC)

result[conv_id] = ConversationStats(
message_count=msg_count,
Expand Down
6 changes: 5 additions & 1 deletion pyrit/memory/sqlite_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from collections.abc import Awaitable, Callable, Mapping, Sequence
from contextlib import closing
from contextvars import ContextVar
from datetime import datetime
from datetime import UTC, datetime
from pathlib import Path
from sqlite3 import Connection as SQLiteConnection
from sqlite3 import Cursor as SQLiteCursor
Expand Down Expand Up @@ -789,6 +789,10 @@ def _execute_get_conversation_stats(self, *, conversation_ids: Sequence[str]) ->
created_at = datetime.fromisoformat(raw_created_at)
else:
created_at = raw_created_at
# The raw MIN(timestamp) aggregate bypasses UTCDateTime's result
# processor, so reattach the UTC offset every other read path returns.
if created_at.tzinfo is None:
created_at = created_at.replace(tzinfo=UTC)

result[conv_id] = ConversationStats(
message_count=msg_count,
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/memory/test_sqlite_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,33 @@ async def test_get_conversation_stats_returns_empty_for_unknown_ids(sqlite_insta
assert result == {}


async def test_get_conversation_stats_returns_aware_utc_created_at(sqlite_instance):
"""The raw MIN(timestamp) aggregate must still surface aware UTC like every other read path."""
import uuid
from datetime import UTC

from pyrit.models import MessagePiece

conv_id = str(uuid.uuid4())
piece = MessagePiece(
role="user",
original_value="hello",
original_value_data_type="text",
converted_value="hello",
converted_value_data_type="text",
conversation_id=conv_id,
sequence=0,
)
sqlite_instance._insert_entry(PromptMemoryEntry(entry=piece))

result = await sqlite_instance.get_conversation_stats_async(conversation_ids=[conv_id])

created_at = result[conv_id].created_at
assert created_at is not None
assert created_at.tzinfo is not None
assert created_at.utcoffset() == UTC.utcoffset(None)


async def test_get_conversation_stats_counts_distinct_sequences(sqlite_instance, sample_conversation_entries):
"""Test that message_count reflects distinct sequence numbers, not raw rows."""
# Extract conversation IDs and sequences before inserting (entries get detached after commit)
Expand Down