Skip to content

Commit 844b1af

Browse files
Gl/update postgres (#83)
* fix lint * update postgres
1 parent 3376a13 commit 844b1af

2 files changed

Lines changed: 65 additions & 72 deletions

File tree

grafi/common/event_stores/event_store_postgres.py

Lines changed: 64 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from datetime import datetime
2-
from datetime import timezone
32
from typing import List
43
from typing import Optional
54

@@ -10,7 +9,6 @@
109

1110

1211
try:
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):
3533
class 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

5855
class 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

tests_integration/event_store_postgres/simple_llm_assistant_postgres_integration.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def get_invoke_context() -> InvokeContext:
5757

5858
async def test_simple_llm_assistant() -> None:
5959
invoke_context = get_invoke_context()
60+
await postgres_event_store.initialize()
6061
assistant = (
6162
SimpleLLMAssistant.builder()
6263
.name("SimpleLLMAssistant")

0 commit comments

Comments
 (0)