prism-models 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of prism-models might be problematic. Click here for more details.

Files changed (38) hide show
  1. prism_models/__init__.py +45 -0
  2. prism_models/agent_profile.py +101 -0
  3. prism_models/base.py +75 -0
  4. prism_models/chat.py +349 -0
  5. prism_models/config.py +37 -0
  6. prism_models/content.py +250 -0
  7. prism_models/feedback.py +145 -0
  8. prism_models/migration/README +1 -0
  9. prism_models/migration/__init__.py +0 -0
  10. prism_models/migration/env.py +131 -0
  11. prism_models/migration/script.py.mako +28 -0
  12. prism_models/migration/versions/2025_09_11_1516_161f8829d93f_initial_schema.py +492 -0
  13. prism_models/migration/versions/2025_09_11_1558_5e011849ea76_changes_for_feedback.py +79 -0
  14. prism_models/migration/versions/2025_09_14_2243_059af231c2b2_profile_entities.py +108 -0
  15. prism_models/migration/versions/2025_09_15_1646_3219fec0bb10_agent_changes.py +32 -0
  16. prism_models/migration/versions/2025_09_16_1627_f2013b08daac_rename_metadata_to_additional_data.py +37 -0
  17. prism_models/migration/versions/2025_09_17_1147_327febbf555f_display_name_added.py +34 -0
  18. prism_models/migration/versions/2025_09_18_1106_b0bcb7ca1dc9_add_support_for_profile_on_create_convo.py +41 -0
  19. prism_models/migration/versions/2025_09_18_1511_bbc1955191e6_preview_mode.py +36 -0
  20. prism_models/migration/versions/2025_09_26_1115_6eb70e848451_added_publish_status_to_document.py +38 -0
  21. prism_models/migration/versions/2025_09_26_1240_f8b0ea2e743c_drop_unique_title_version_on_document.py +40 -0
  22. prism_models/migration/versions/2025_09_26_1505_07dc8c2589e0_added_chunk_id_and_vector_embeddings_to_.py +44 -0
  23. prism_models/migration/versions/2025_09_29_1220_46ba2693b883_add_markdown_markdown_file_path_s3_.py +32 -0
  24. prism_models/migration/versions/2025_10_02_1520_bf1472a9b021_removed_doc_id_from_config_table_and_.py +34 -0
  25. prism_models/migration/versions/2025_10_02_1525_6c0e63e0fef8_removed_doc_id_from_config_table_.py +34 -0
  26. prism_models/migration/versions/2025_10_02_1608_1b3eb48f5017_config_id_on_delete_will_be_set_to_null.py +34 -0
  27. prism_models/migration/versions/2025_10_03_1109_ac85b606d8a4_added_docling_hybrid_to_chunkstrategy.py +32 -0
  28. prism_models/migration/versions/2025_10_03_1204_7d1cb343a63f_added_s3_bucket_and_s3_dir_in_source_.py +42 -0
  29. prism_models/migration/versions/2025_10_03_1452_f9c750ec2a0b_1_to_1_relationship_between_collection_.py +52 -0
  30. prism_models/migration/versions/2025_10_07_1722_5cfa0c462948_added_travel_advisory_enum.py +38 -0
  31. prism_models/migration/versions/2025_10_08_1304_c91eb8e38cc7_added_destination_report_and_event_.py +38 -0
  32. prism_models/migration/versions/2025_10_09_1308_796b720ea35f_added_qa_id_to_vovetor.py +42 -0
  33. prism_models/migration/versions/2025_10_16_1611_663c66268631_added_sharepoint_drive_item_id_as_an_.py +32 -0
  34. prism_models/qdrant.py +97 -0
  35. prism_models-0.1.0.dist-info/METADATA +10 -0
  36. prism_models-0.1.0.dist-info/RECORD +38 -0
  37. prism_models-0.1.0.dist-info/WHEEL +5 -0
  38. prism_models-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,45 @@
1
+ """Data models for the Prism RAG system."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+
6
+ from prism_models.agent_profile import Agent, AgentProfile, AgentProfileStatus, Profile, ProfileCollectionAccess
7
+ from prism_models.base import POSTGRES_NAMING_CONVENTION, Base, BaseModel, TimestampMixin
8
+ from prism_models.chat import Contact, Conversation, ConversationMessage, ConversationMessageMetadata
9
+ from prism_models.content import Chunk, ChunkConfig, Collection, CollectionDocument, Document, IntegrationConfig, QAPair, Source, Vector
10
+ from prism_models.feedback import Augmentation, Feedback, FeedbackAnalysis, FeedbackConfidence, FeedbackStatus, FeedbackType
11
+ from prism_models.qdrant import QdrantVectorPayload, DestinationVectorPayload, PydanticType
12
+
13
+ __all__ = [
14
+ "POSTGRES_NAMING_CONVENTION",
15
+ "Agent",
16
+ "AgentProfile",
17
+ "AgentProfileStatus",
18
+ "Augmentation",
19
+ "Base",
20
+ "BaseModel",
21
+ "Chunk",
22
+ "ChunkConfig",
23
+ "Collection",
24
+ "CollectionDocument",
25
+ "Contact",
26
+ "Conversation",
27
+ "ConversationMessage",
28
+ "ConversationMessageMetadata",
29
+ "DestinationVectorPayload",
30
+ "Document",
31
+ "Feedback",
32
+ "FeedbackAnalysis",
33
+ "FeedbackConfidence",
34
+ "FeedbackStatus",
35
+ "FeedbackType",
36
+ "IntegrationConfig",
37
+ "Profile",
38
+ "ProfileCollectionAccess",
39
+ "PydanticType",
40
+ "QAPair",
41
+ "QdrantVectorPayload",
42
+ "Source",
43
+ "TimestampMixin",
44
+ "Vector",
45
+ ]
@@ -0,0 +1,101 @@
1
+ """Profile model for storing user profile information."""
2
+
3
+ from datetime import datetime
4
+ from enum import Enum
5
+
6
+ from sqlalchemy import TIMESTAMP, Boolean
7
+ from sqlalchemy import Enum as SAEnum
8
+ from sqlalchemy import ForeignKey, Integer, String, UniqueConstraint, func
9
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
10
+ from sqlalchemy.sql.sqltypes import Text
11
+
12
+ from prism_models.base import BaseModel
13
+ from prism_models.content import Collection
14
+
15
+
16
+ class Profile(BaseModel):
17
+ """
18
+ Profile model for organizing prompt variants by team/use case.
19
+
20
+ Profiles allow admins to create different prompt variations for different
21
+ teams or scenarios (e.g., "marketing_aggressive", "support_friendly", "default").
22
+ Each profile allows configuration of multiple agents.
23
+
24
+ Relationships:
25
+ - profile_prompts: One-to-many with ProfilePrompt
26
+ - agent_profiles: One-to-many with AgentProfile
27
+ - conversation_profiles: One-to-many with ConversationProfile
28
+ """
29
+
30
+ name: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
31
+ description: Mapped[str | None] = mapped_column(String(1024))
32
+ is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
33
+
34
+ __table_args__ = (UniqueConstraint("name", name="uq_profile_name"),)
35
+
36
+ def __repr__(self) -> str:
37
+ return f"<Profile(id={self.id}, name='{self.name}', active={self.is_active})>"
38
+
39
+
40
+ class Agent(BaseModel):
41
+ """Agent model for storing agent information."""
42
+
43
+ name: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
44
+ display_name: Mapped[str | None] = mapped_column(String(255))
45
+ description: Mapped[str | None] = mapped_column(String(1024))
46
+
47
+ __table_args__ = (
48
+ UniqueConstraint("name", name="uq_agent_name"),
49
+ UniqueConstraint("display_name", name="uq_agent_display_name"),
50
+ )
51
+
52
+ def __repr__(self) -> str:
53
+ return f"<Agent(id={self.id}, name='{self.name}')>"
54
+
55
+
56
+ class AgentProfileStatus(str, Enum):
57
+ ACTIVE = "active"
58
+ INACTIVE = "inactive"
59
+ PREVIEW = "preview"
60
+
61
+
62
+ class AgentProfile(BaseModel):
63
+ """AgentProfile model for storing agent profile information."""
64
+
65
+ agent_id: Mapped[int] = mapped_column(Integer, ForeignKey("agent.id"), nullable=False, index=True)
66
+ profile_id: Mapped[int] = mapped_column(Integer, ForeignKey("profile.id"), nullable=False, index=True)
67
+ version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
68
+ status: Mapped[AgentProfileStatus] = mapped_column(
69
+ SAEnum(AgentProfileStatus, name="agent_profile_status"),
70
+ default=AgentProfileStatus.PREVIEW,
71
+ nullable=False,
72
+ )
73
+ is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
74
+ prompt: Mapped[str | None] = mapped_column(Text)
75
+
76
+ # NOT SURE IF WE NEED THIS could lead to N+1 queries
77
+ # TODO: Add relationship if needed
78
+ # agent: Mapped["Agent"] = relationship(back_populates="profile")
79
+ # profile: Mapped["Profile"] = relationship(back_populates="agent")
80
+
81
+
82
+ class ProfileCollectionAccess(BaseModel):
83
+ """ProfileCollectionAccess model for storing profile collection access information."""
84
+
85
+ profile_id: Mapped[int] = mapped_column(
86
+ Integer,
87
+ ForeignKey("profile.id", ondelete="CASCADE"),
88
+ nullable=False,
89
+ index=True,
90
+ )
91
+ collection_id: Mapped[int] = mapped_column(
92
+ Integer,
93
+ ForeignKey("collection.id", ondelete="CASCADE"),
94
+ nullable=False,
95
+ index=True,
96
+ )
97
+ access_granted_at: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), server_default=func.now(), nullable=True)
98
+
99
+ collection: Mapped["Collection"] = relationship()
100
+
101
+ __table_args__ = (UniqueConstraint("profile_id", "collection_id", name="uq_profile_collection"),)
prism_models/base.py ADDED
@@ -0,0 +1,75 @@
1
+ from datetime import UTC, datetime
2
+
3
+ from sqlalchemy import Column, DateTime, Integer, MetaData, func
4
+ from sqlalchemy.orm import Mapped, declarative_base, declared_attr, mapped_column
5
+
6
+ # PostgreSQL naming convention for consistent constraint names
7
+ POSTGRES_NAMING_CONVENTION = {
8
+ "ix": "%(column_0_label)s_idx",
9
+ "uq": "%(table_name)s_%(column_0_name)s_key",
10
+ "ck": "%(table_name)s_%(constraint_name)s_check",
11
+ "fk": "%(table_name)s_%(column_0_name)s_fkey",
12
+ "pk": "%(table_name)s_pkey",
13
+ }
14
+
15
+ metadata = MetaData(naming_convention=POSTGRES_NAMING_CONVENTION)
16
+ Base = declarative_base(metadata=metadata)
17
+
18
+
19
+ class TimestampMixin:
20
+ """
21
+ Mixin for automatic timestamp fields using database-level defaults.
22
+ """
23
+
24
+ # Use server_default to make the database the single source of truth.
25
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
26
+
27
+ updated_at: Mapped[datetime] = mapped_column(
28
+ DateTime(timezone=True),
29
+ server_default=func.now(),
30
+ onupdate=func.now(),
31
+ server_onupdate=func.now(),
32
+ nullable=False,
33
+ )
34
+
35
+
36
+ class SoftDeleteMixin:
37
+ """
38
+ Mixin to add soft delete functionality to a model.
39
+ """
40
+
41
+ deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
42
+
43
+ def soft_delete(self):
44
+ """Mark the object as deleted."""
45
+ if self.deleted_at is None:
46
+ self.deleted_at = datetime.now(UTC)
47
+
48
+ def undelete(self):
49
+ """Mark the object as not deleted."""
50
+ self.deleted_at = None
51
+
52
+
53
+ class ChatSchemaMixin:
54
+ pass
55
+
56
+
57
+ class BaseModel(Base, TimestampMixin, SoftDeleteMixin):
58
+ """Base model with common fields for all entities."""
59
+
60
+ __abstract__ = True
61
+
62
+ id = Column(Integer, primary_key=True, index=True)
63
+
64
+ @declared_attr.directive
65
+ def __tablename__(cls):
66
+ """Auto-generate table name from class name in snake_case (singular)."""
67
+ import re
68
+
69
+ # Convert CamelCase to snake_case
70
+ name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", cls.__name__)
71
+ return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
72
+
73
+ def __repr__(self):
74
+ """String representation for debugging."""
75
+ return f"<{self.__class__.__name__}(id={self.id})>"
prism_models/chat.py ADDED
@@ -0,0 +1,349 @@
1
+ import uuid
2
+ from enum import Enum as PyEnum
3
+
4
+ from sqlalchemy import Boolean, CheckConstraint, Column, Enum, ForeignKey, Index, Integer, String, Text, UniqueConstraint
5
+ from sqlalchemy.dialects.postgresql import JSONB, UUID
6
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
7
+
8
+ from prism_models.base import BaseModel, ChatSchemaMixin
9
+
10
+
11
+ class Gender(PyEnum):
12
+ MALE = "MALE"
13
+ FEMALE = "FEMALE"
14
+
15
+
16
+ class ContactStatus(PyEnum):
17
+ ACTIVE = "ACTIVE"
18
+ INACTIVE = "INACTIVE"
19
+ APPROVAL_PENDING = "APPROVAL_PENDING"
20
+ MERGED = "MERGED"
21
+
22
+
23
+ class ContactSource(PyEnum):
24
+ GRID = "GRID"
25
+ CRM = "CRM"
26
+ EXTERNAL = "EXTERNAL"
27
+
28
+
29
+ class ConversationType(PyEnum):
30
+ TRAVEL_GUIDE = "TRAVEL_GUIDE"
31
+ MSA_CHAT = "MSA_CHAT"
32
+
33
+
34
+ class MessageRole(PyEnum):
35
+ USER = "USER"
36
+ ASSISTANT = "ASSISTANT"
37
+ SYSTEM = "SYSTEM"
38
+
39
+
40
+ class MessageStatus(PyEnum):
41
+ PENDING = "PENDING"
42
+ PROCESSING = "PROCESSING"
43
+ COMPLETED = "COMPLETED"
44
+ FAILED = "FAILED"
45
+
46
+
47
+ class MessageType(PyEnum):
48
+ TEXT = "TEXT"
49
+
50
+
51
+ class Contact(ChatSchemaMixin, BaseModel):
52
+ """
53
+ Contact model for chat participants.
54
+
55
+ Represents individuals who can participate in conversations within the chat system.
56
+ Each contact can have multiple conversations and maintains their profile information
57
+ including personal details and CRM integration data.
58
+
59
+ Relationships:
60
+ - conversations: One-to-many relationship with Conversation model
61
+ """
62
+
63
+ email = Column(String(300), nullable=True)
64
+
65
+ first_name = Column(String(100), nullable=False)
66
+ middle_name = Column(String(100), nullable=True)
67
+ last_name = Column(String(100), nullable=False)
68
+
69
+ primary_phone = Column(String(45), nullable=True)
70
+
71
+ gender: Mapped[Gender | None] = mapped_column(Enum(Gender), nullable=True)
72
+
73
+ status: Mapped[ContactStatus | None] = mapped_column(Enum(ContactStatus), nullable=True)
74
+ source: Mapped[ContactSource] = mapped_column(Enum(ContactSource), nullable=False)
75
+
76
+ grid_contact_id = Column(Integer, nullable=True)
77
+
78
+ crm_contact_guid = Column(String(36), nullable=True)
79
+
80
+ account_id = Column(String(36), nullable=True)
81
+
82
+ conversations = relationship("Conversation", back_populates="contact", cascade="all, delete-orphan")
83
+
84
+
85
+ class Conversation(ChatSchemaMixin, BaseModel):
86
+ """
87
+ Conversation model for chat sessions.
88
+
89
+ Represents a conversation thread between a contact and the system.
90
+ Each conversation contains multiple messages and maintains session state.
91
+
92
+ Relationships:
93
+ - contact: Many-to-one relationship with Contact model
94
+ - messages: One-to-many relationship with ConversationMessage model
95
+ """
96
+
97
+ uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, nullable=False, unique=True, index=True)
98
+ title = Column(String(255), nullable=True)
99
+ contact_id = Column(Integer, ForeignKey("contact.id"), nullable=False)
100
+ is_active = Column(Boolean, default=True, nullable=False)
101
+ conversation_type: Mapped[ConversationType] = mapped_column(Enum(ConversationType), nullable=False)
102
+
103
+ is_audio_analysis = Column(Boolean, default=False, nullable=False)
104
+
105
+ profile_id = Column(Integer, ForeignKey("profile.id"), nullable=True)
106
+ preview_mode = Column(Boolean, default=False, nullable=False)
107
+
108
+ # Storage links for simulation artifacts (optional)
109
+ audio_file_url = Column(String(1024), nullable=True)
110
+ transcription_json_url = Column(String(1024), nullable=True)
111
+ repaired_json_url = Column(String(1024), nullable=True)
112
+
113
+ contact = relationship("Contact", back_populates="conversations")
114
+ messages = relationship(
115
+ "ConversationMessage",
116
+ back_populates="conversation",
117
+ cascade="all, delete-orphan",
118
+ )
119
+
120
+ conversation_comparisons = relationship(
121
+ "ConversationCompare",
122
+ back_populates="conversation",
123
+ cascade="all, delete-orphan",
124
+ order_by="ConversationCompare.question_index",
125
+ )
126
+
127
+ profile = relationship("Profile")
128
+
129
+
130
+ class ConversationMessage(ChatSchemaMixin, BaseModel):
131
+ """
132
+ ConversationMessage model for individual chat messages.
133
+
134
+ Represents individual messages within a conversation thread. Messages are ordered
135
+ by sequence number and can be from users, assistants, or system. Each message
136
+ tracks its processing status and can contain LLM response data.
137
+
138
+ Relationships:
139
+ - conversation: Many-to-one relationship with Conversation model
140
+ - message_metadata: One-to-one relationship with ConversationMessageMetadata model
141
+
142
+ Constraints:
143
+ - Unique constraint on (conversation_id, sequence_number) ensures proper ordering
144
+ - Check constraint ensures sequence_number >= 0
145
+ """
146
+
147
+ content = Column(Text, nullable=False)
148
+ conversation_id = Column(Integer, ForeignKey("conversation.id"), nullable=False)
149
+ role: Mapped[MessageRole] = mapped_column(Enum(MessageRole), nullable=False)
150
+ status: Mapped[MessageStatus] = mapped_column(Enum(MessageStatus), default=MessageStatus.COMPLETED, nullable=False)
151
+ message_type: Mapped[MessageType] = mapped_column(Enum(MessageType), default=MessageType.TEXT, nullable=False)
152
+ sequence_number = Column(
153
+ Integer,
154
+ CheckConstraint("sequence_number >= 0", name="ck_sequence_number_non_negative"),
155
+ nullable=False,
156
+ )
157
+ llm_response_data = Column(JSONB(), nullable=True)
158
+ is_upvoted = Column(Boolean, nullable=True, default=None)
159
+ is_resolved = Column(Boolean, nullable=True, default=None)
160
+
161
+ # Relationships
162
+ conversation = relationship("Conversation", back_populates="messages")
163
+ message_metadata = relationship(
164
+ "ConversationMessageMetadata",
165
+ back_populates="message",
166
+ uselist=False,
167
+ cascade="all, delete-orphan",
168
+ )
169
+ components = relationship(
170
+ "MessageComponent",
171
+ back_populates="message",
172
+ cascade="all, delete-orphan",
173
+ order_by="MessageComponent.position",
174
+ )
175
+
176
+ __table_args__ = (
177
+ Index(
178
+ "ix_conversation_message_conversation_sequence",
179
+ "conversation_id",
180
+ "sequence_number",
181
+ ),
182
+ Index(
183
+ "ix_conversation_message_conversation_created",
184
+ "conversation_id",
185
+ "created_at",
186
+ ),
187
+ Index(
188
+ "ix_conversation_message_llm_response_gin",
189
+ "llm_response_data",
190
+ postgresql_using="gin",
191
+ ),
192
+ UniqueConstraint(
193
+ "conversation_id",
194
+ "sequence_number",
195
+ name="uq_conversation_message_sequence",
196
+ ),
197
+ )
198
+
199
+ def is_user_message(self) -> bool:
200
+ """Check if message is from user."""
201
+ return self.role == MessageRole.USER
202
+
203
+ def is_assistant_message(self) -> bool:
204
+ """Check if message is from AI assistant."""
205
+ return self.role == MessageRole.ASSISTANT
206
+
207
+ def is_system_message(self) -> bool:
208
+ """Check if message is a system message."""
209
+ return self.role == MessageRole.SYSTEM
210
+
211
+ def is_completed(self) -> bool:
212
+ """Check if message processing is completed."""
213
+ return self.status == MessageStatus.COMPLETED
214
+
215
+ def is_failed(self) -> bool:
216
+ """Check if message processing failed."""
217
+ return self.status == MessageStatus.FAILED
218
+
219
+
220
+ class MessageComponent(ChatSchemaMixin, BaseModel):
221
+ """
222
+ MessageComponent represents a UI-renderable block (e.g., entry requirement, place list)
223
+ attached to a single ConversationMessage.
224
+ Allows each message to contain one or more UI components, rendered in order.
225
+ """
226
+
227
+ component_type: Mapped[str] = mapped_column(String, nullable=False)
228
+ payload: Mapped[dict] = mapped_column(JSONB(), nullable=False)
229
+ position: Mapped[int] = mapped_column(Integer, nullable=False)
230
+
231
+ message_id: Mapped[int] = mapped_column(
232
+ Integer,
233
+ ForeignKey("conversation_message.id", ondelete="CASCADE"),
234
+ nullable=False,
235
+ )
236
+
237
+ message = relationship("ConversationMessage", back_populates="components")
238
+
239
+ __table_args__ = (
240
+ Index("ix_message_component_message_id", "message_id"),
241
+ Index("ix_message_component_position", "message_id", "position"),
242
+ UniqueConstraint(
243
+ "message_id",
244
+ "position",
245
+ name="uq_message_component_position",
246
+ ),
247
+ )
248
+
249
+
250
+ class ConversationMessageMetadata(ChatSchemaMixin, BaseModel):
251
+ """
252
+ Metadata model for RAG-specific message analytics and processing information.
253
+
254
+ Stores performance metrics, model information, and retrieval context for each message.
255
+ This data is essential for monitoring system performance, token usage, and
256
+ improving RAG system effectiveness.
257
+
258
+ Relationships:
259
+ - message: One-to-one relationship with ConversationMessage model
260
+
261
+ Constraints:
262
+ - Check constraints ensure non-negative values for token counts and processing time
263
+ - Unique constraint on message_id ensures one metadata record per message
264
+ """
265
+
266
+ message_id = Column(Integer, ForeignKey("conversation_message.id"), nullable=False, unique=True)
267
+ model_name = Column(String(100), nullable=True)
268
+ token_count_input = Column(
269
+ Integer,
270
+ CheckConstraint("token_count_input >= 0", name="ck_token_count_input_non_negative"),
271
+ nullable=True,
272
+ )
273
+ token_count_output = Column(
274
+ Integer,
275
+ CheckConstraint("token_count_output >= 0", name="ck_token_count_output_non_negative"),
276
+ nullable=True,
277
+ )
278
+ processing_time_ms = Column(
279
+ Integer,
280
+ CheckConstraint("processing_time_ms >= 0", name="ck_processing_time_non_negative"),
281
+ nullable=True,
282
+ )
283
+ retrieval_context = Column(JSONB(), nullable=True)
284
+
285
+ # Relationships
286
+ message = relationship("ConversationMessage", back_populates="message_metadata")
287
+
288
+ __table_args__ = (
289
+ Index(
290
+ "ix_conversation_message_metadata_retrieval_gin",
291
+ "retrieval_context",
292
+ postgresql_using="gin",
293
+ ),
294
+ )
295
+
296
+ def calculate_total_tokens(self) -> int:
297
+ """Calculate total tokens used (input + output)."""
298
+ input_tokens = self.token_count_input or 0
299
+ output_tokens = self.token_count_output or 0
300
+ return int(input_tokens + output_tokens)
301
+
302
+
303
+ class ConversationCompare(ChatSchemaMixin, BaseModel):
304
+ """
305
+ Stores a single QA comparison for a conversation:
306
+ - the user's question (normalized),
307
+ - the MSA agent's answer (ground truth from transcript),
308
+ - the AI assistant's answer (simulated reply).
309
+
310
+ Uniqueness:
311
+ One row per (conversation_id, question_index).
312
+ """
313
+
314
+ conversation_id: Mapped[int] = mapped_column(
315
+ Integer,
316
+ ForeignKey("conversation.id", ondelete="CASCADE"),
317
+ nullable=False,
318
+ )
319
+
320
+ question_index: Mapped[int] = mapped_column(
321
+ Integer,
322
+ CheckConstraint("question_index >= 0", name="ck_sim_compare_qidx_non_negative"),
323
+ nullable=False,
324
+ )
325
+
326
+ question_text: Mapped[str] = mapped_column(Text, nullable=False)
327
+ msa_answer_text: Mapped[str | None] = mapped_column(Text, nullable=True)
328
+ ai_answer_text: Mapped[str | None] = mapped_column(Text, nullable=True)
329
+ msa_speaker_label: Mapped[str | None] = mapped_column(String(32), nullable=True)
330
+
331
+ conversation = relationship("Conversation", back_populates="conversation_comparisons")
332
+
333
+ __table_args__ = (
334
+ UniqueConstraint(
335
+ "conversation_id",
336
+ "question_index",
337
+ name="uq_sim_compare_conversation_qidx",
338
+ ),
339
+ Index(
340
+ "ix_sim_compare_conversation_created",
341
+ "conversation_id",
342
+ "created_at",
343
+ ),
344
+ Index(
345
+ "ix_sim_compare_conversation_sequence",
346
+ "conversation_id",
347
+ "question_index",
348
+ ),
349
+ )
prism_models/config.py ADDED
@@ -0,0 +1,37 @@
1
+ from enum import Enum
2
+ from pydantic_settings import BaseSettings, SettingsConfigDict
3
+
4
+
5
+ class Environment(str, Enum):
6
+ """Environment types."""
7
+
8
+ DEVELOPMENT = "development"
9
+ STAGING = "staging"
10
+ PRODUCTION = "production"
11
+
12
+
13
+ class Settings(BaseSettings):
14
+ """Minimal configuration for prism-models database operations."""
15
+
16
+ model_config = SettingsConfigDict(
17
+ env_file=".env",
18
+ env_file_encoding="utf-8",
19
+ case_sensitive=False,
20
+ extra="ignore",
21
+ )
22
+
23
+ PG_DATABASE_URL: str = "postgresql+asyncpg://user:password@host"
24
+ environment: Environment = Environment.DEVELOPMENT
25
+
26
+ @property
27
+ def PG_DATABASE_URL_PRISM(self) -> str:
28
+ """Get the Prism database URL."""
29
+ return self.PG_DATABASE_URL + "/prism"
30
+
31
+ @property
32
+ def is_development(self) -> bool:
33
+ """Check if running in development environment."""
34
+ return self.environment == Environment.DEVELOPMENT
35
+
36
+
37
+ settings = Settings()