11from datetime import datetime
2- from datetime import timezone
32from typing import List
43from typing import Optional
54
109
1110
1211try :
13- from sqlalchemy import JSON
1412 from sqlalchemy import Column
1513 from sqlalchemy import DateTime
1614 from sqlalchemy import Integer
@@ -35,12 +33,11 @@ class Base(DeclarativeBase):
3533class EventModel (Base ):
3634 """
3735 SQLAlchemy model representing an event record.
38- Storing:
39- - an auto-increment primary key,
40- - the `event_id` (from your domain event),
41- - the `event_type`,
42- - a JSON field for the entire event data,
43- - a creation timestamp.
36+ Stores event data with proper indexing for efficient queries.
37+
38+ Note: Timestamps are stored as timezone-naive UTC datetimes.
39+ Event timestamps are created as timezone-aware (UTC) but converted
40+ to naive for storage to avoid timezone comparison issues.
4441 """
4542
4643 __tablename__ = "events"
@@ -49,10 +46,10 @@ class EventModel(Base):
4946 event_id = Column (String , unique = True , index = True , nullable = False )
5047 conversation_id = Column (String , index = True , nullable = False )
5148 assistant_request_id = Column (String , index = True , nullable = False )
52- event_type = Column (String , nullable = False )
53- event_context = Column (JSONB , nullable = False )
54- data = Column (JSON , nullable = False )
55- timestamp = Column (DateTime , default = datetime . now ( timezone . utc ), nullable = False )
49+ event_type = Column (String , index = True , nullable = False )
50+ event_version = Column (String , nullable = False , default = "1.0" )
51+ event_data = Column (JSONB , nullable = False )
52+ timestamp = Column (DateTime , nullable = False ) # Stored as naive UTC
5653
5754
5855class EventStorePostgres (EventStore ):
@@ -77,24 +74,38 @@ def __init__(self, db_url: str):
7774 self .async_engine , class_ = AsyncSession , expire_on_commit = False
7875 )
7976
77+ async def clear_events (self ) -> None :
78+ """Clear all events from the database."""
79+ pass
80+
81+ async def get_events (self ) -> List [Event ]:
82+ """Get all events from the database."""
83+ pass
84+
8085 async def record_event (self , event : Event ) -> None :
8186 """Record a single event into the database asynchronously."""
8287 async with self .AsyncSession () as session :
8388 try :
8489 # Convert Event object to dict
8590 event_dict = event .to_dict ()
8691
92+ # Extract conversation_id from invoke_context
93+ # The invoke_context is nested inside event_context
94+ event_context = event_dict .get ("event_context" , {})
95+ invoke_context = event_context .get ("invoke_context" , {})
96+ conversation_id = invoke_context .get ("conversation_id" , "" )
97+
8798 # Create SQLAlchemy model instance
8899 model = EventModel (
89100 event_id = event_dict ["event_id" ],
90- conversation_id = event_dict ["event_context" ]["invoke_context" ][
91- "conversation_id"
92- ],
101+ conversation_id = conversation_id ,
93102 assistant_request_id = event_dict ["assistant_request_id" ],
94103 event_type = event_dict ["event_type" ],
95- event_context = event_dict ["event_context" ],
96- data = event_dict ["data" ],
97- timestamp = event_dict ["timestamp" ],
104+ event_version = event_dict .get ("event_version" , "1.0" ),
105+ event_data = event_dict ,
106+ timestamp = datetime .fromisoformat (event_dict ["timestamp" ]).replace (
107+ tzinfo = None
108+ ),
98109 )
99110 session .add (model )
100111 await session .commit ()
@@ -110,17 +121,23 @@ async def record_events(self, events: List[Event]) -> None:
110121 models = []
111122 for event in events :
112123 event_dict = event .to_dict ()
124+
125+ # Extract conversation_id from invoke_context
126+ event_context = event_dict .get ("event_context" , {})
127+ invoke_context = event_context .get ("invoke_context" , {})
128+ conversation_id = invoke_context .get ("conversation_id" , "" )
129+
113130 models .append (
114131 EventModel (
115132 event_id = event_dict ["event_id" ],
116- conversation_id = event_dict ["event_context" ][
117- "invoke_context"
118- ]["conversation_id" ],
133+ conversation_id = conversation_id ,
119134 assistant_request_id = event_dict ["assistant_request_id" ],
120135 event_type = event_dict ["event_type" ],
121- event_context = event_dict ["event_context" ],
122- data = event_dict ["data" ],
123- timestamp = event_dict ["timestamp" ],
136+ event_version = event_dict .get ("event_version" , "1.0" ),
137+ event_data = event_dict ,
138+ timestamp = datetime .fromisoformat (
139+ event_dict ["timestamp" ]
140+ ).replace (tzinfo = None ),
124141 )
125142 )
126143 session .add_all (models )
@@ -142,15 +159,7 @@ async def get_event(self, event_id: str) -> Optional[Event]:
142159 if not row :
143160 return None
144161
145- event_data = {
146- "event_id" : row .event_id ,
147- "assistant_request_id" : row .assistant_request_id ,
148- "event_type" : row .event_type ,
149- "event_context" : row .event_context ,
150- "data" : row .data ,
151- "timestamp" : str (row .timestamp ),
152- }
153- return self ._create_event_from_dict (event_data )
162+ return self ._create_event_from_dict (row .event_data )
154163 except Exception as e :
155164 logger .error (f"Failed to get event { event_id } : { e } " )
156165 raise e
@@ -160,9 +169,9 @@ async def get_agent_events(self, assistant_request_id: str) -> List[Event]:
160169 async with self .AsyncSession () as session :
161170 try :
162171 result = await session .execute (
163- select (EventModel ). where (
164- EventModel .assistant_request_id == assistant_request_id
165- )
172+ select (EventModel )
173+ . where ( EventModel .assistant_request_id == assistant_request_id )
174+ . order_by ( EventModel . timestamp )
166175 )
167176 rows = result .scalars ().all ()
168177
@@ -171,15 +180,7 @@ async def get_agent_events(self, assistant_request_id: str) -> List[Event]:
171180
172181 events : List [Event ] = []
173182 for r in rows :
174- event_data = {
175- "event_id" : r .event_id ,
176- "assistant_request_id" : r .assistant_request_id ,
177- "event_type" : r .event_type ,
178- "event_context" : r .event_context ,
179- "data" : r .data ,
180- "timestamp" : str (r .timestamp ),
181- }
182- event = self ._create_event_from_dict (event_data )
183+ event = self ._create_event_from_dict (r .event_data )
183184 if event :
184185 events .append (event )
185186
@@ -193,9 +194,9 @@ async def get_conversation_events(self, conversation_id: str) -> List[Event]:
193194 async with self .AsyncSession () as session :
194195 try :
195196 result = await session .execute (
196- select (EventModel ). where (
197- EventModel .conversation_id == conversation_id
198- )
197+ select (EventModel )
198+ . where ( EventModel .conversation_id == conversation_id )
199+ . order_by ( EventModel . timestamp )
199200 )
200201 rows = result .scalars ().all ()
201202
@@ -204,15 +205,7 @@ async def get_conversation_events(self, conversation_id: str) -> List[Event]:
204205
205206 events : List [Event ] = []
206207 for r in rows :
207- event_data = {
208- "event_id" : r .event_id ,
209- "assistant_request_id" : r .assistant_request_id ,
210- "event_type" : r .event_type ,
211- "event_context" : r .event_context ,
212- "data" : r .data ,
213- "timestamp" : str (r .timestamp ),
214- }
215- event = self ._create_event_from_dict (event_data )
208+ event = self ._create_event_from_dict (r .event_data )
216209 if event :
217210 events .append (event )
218211
@@ -231,20 +224,27 @@ async def get_topic_events(self, name: str, offsets: List[int]) -> List[Event]:
231224 async with self .AsyncSession () as session :
232225 try :
233226 # Use JSONB operators for efficient filtering at the database level
227+ # The topic name and offset are now in event_context
234228 stmt = (
235229 select (EventModel ).where (
236230 # Filter by event type
237- EventModel .event_type .in_ (["PublishToTopic" , "OutputTopic" ]),
238- # Use JSONB ->> operator to extract name and compare
239- EventModel .event_context .op ("->>" )("name" ) == name ,
240- # Use JSONB -> operator to extract offset and check if it's in our list
241- EventModel .event_context .op ("->" )("offset" )
231+ EventModel .event_type .in_ (["PublishToTopic" , "TopicEvent" ]),
232+ # Use JSONB -> operator to navigate to event_context -> name
233+ EventModel .event_data .op ("->" )('"event_context"' ).op ("->>" )(
234+ '"name"'
235+ )
236+ == name ,
237+ # Use JSONB -> operator to navigate to event_context -> offset
238+ EventModel .event_data .op ("->" )('"event_context"' )
239+ .op ("->" )('"offset"' )
242240 .astext .cast (Integer )
243241 .in_ (offsets ),
244242 )
245243 # Order by offset for consistent results
246244 .order_by (
247- EventModel .event_context .op ("->" )("offset" ).astext .cast (Integer )
245+ EventModel .event_data .op ("->" )('"event_context"' )
246+ .op ("->" )('"offset"' )
247+ .astext .cast (Integer )
248248 )
249249 )
250250
@@ -253,15 +253,7 @@ async def get_topic_events(self, name: str, offsets: List[int]) -> List[Event]:
253253
254254 events : List [Event ] = []
255255 for r in rows :
256- event_data = {
257- "event_id" : r .event_id ,
258- "assistant_request_id" : r .assistant_request_id ,
259- "event_type" : r .event_type ,
260- "event_context" : r .event_context ,
261- "data" : r .data ,
262- "timestamp" : str (r .timestamp ),
263- }
264- event = self ._create_event_from_dict (event_data )
256+ event = self ._create_event_from_dict (r .event_data )
265257 if event :
266258 events .append (event )
267259
0 commit comments