prism-models 0.1.0__tar.gz
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.
- prism_models-0.1.0/PKG-INFO +10 -0
- prism_models-0.1.0/README.md +0 -0
- prism_models-0.1.0/prism_models/__init__.py +45 -0
- prism_models-0.1.0/prism_models/agent_profile.py +101 -0
- prism_models-0.1.0/prism_models/base.py +75 -0
- prism_models-0.1.0/prism_models/chat.py +349 -0
- prism_models-0.1.0/prism_models/config.py +37 -0
- prism_models-0.1.0/prism_models/content.py +250 -0
- prism_models-0.1.0/prism_models/feedback.py +145 -0
- prism_models-0.1.0/prism_models/migration/README +1 -0
- prism_models-0.1.0/prism_models/migration/__init__.py +0 -0
- prism_models-0.1.0/prism_models/migration/env.py +131 -0
- prism_models-0.1.0/prism_models/migration/script.py.mako +28 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_09_11_1516_161f8829d93f_initial_schema.py +492 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_09_11_1558_5e011849ea76_changes_for_feedback.py +79 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_09_14_2243_059af231c2b2_profile_entities.py +108 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_09_15_1646_3219fec0bb10_agent_changes.py +32 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_09_16_1627_f2013b08daac_rename_metadata_to_additional_data.py +37 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_09_17_1147_327febbf555f_display_name_added.py +34 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_09_18_1106_b0bcb7ca1dc9_add_support_for_profile_on_create_convo.py +41 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_09_18_1511_bbc1955191e6_preview_mode.py +36 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_09_26_1115_6eb70e848451_added_publish_status_to_document.py +38 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_09_26_1240_f8b0ea2e743c_drop_unique_title_version_on_document.py +40 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_09_26_1505_07dc8c2589e0_added_chunk_id_and_vector_embeddings_to_.py +44 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_09_29_1220_46ba2693b883_add_markdown_markdown_file_path_s3_.py +32 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_10_02_1520_bf1472a9b021_removed_doc_id_from_config_table_and_.py +34 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_10_02_1525_6c0e63e0fef8_removed_doc_id_from_config_table_.py +34 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_10_02_1608_1b3eb48f5017_config_id_on_delete_will_be_set_to_null.py +34 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_10_03_1109_ac85b606d8a4_added_docling_hybrid_to_chunkstrategy.py +32 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_10_03_1204_7d1cb343a63f_added_s3_bucket_and_s3_dir_in_source_.py +42 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_10_03_1452_f9c750ec2a0b_1_to_1_relationship_between_collection_.py +52 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_10_07_1722_5cfa0c462948_added_travel_advisory_enum.py +38 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_10_08_1304_c91eb8e38cc7_added_destination_report_and_event_.py +38 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_10_09_1308_796b720ea35f_added_qa_id_to_vovetor.py +42 -0
- prism_models-0.1.0/prism_models/migration/versions/2025_10_16_1611_663c66268631_added_sharepoint_drive_item_id_as_an_.py +32 -0
- prism_models-0.1.0/prism_models/qdrant.py +97 -0
- prism_models-0.1.0/prism_models.egg-info/PKG-INFO +10 -0
- prism_models-0.1.0/prism_models.egg-info/SOURCES.txt +41 -0
- prism_models-0.1.0/prism_models.egg-info/dependency_links.txt +1 -0
- prism_models-0.1.0/prism_models.egg-info/requires.txt +6 -0
- prism_models-0.1.0/prism_models.egg-info/top_level.txt +1 -0
- prism_models-0.1.0/pyproject.toml +22 -0
- prism_models-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prism-models
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Requires-Python: >=3.12
|
|
5
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.0
|
|
6
|
+
Requires-Dist: alembic>=1.16.0
|
|
7
|
+
Requires-Dist: asyncpg>=0.29.0
|
|
8
|
+
Requires-Dist: pydantic>=2.0.0
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.1.0
|
|
10
|
+
Requires-Dist: uuid7>=0.1.0
|
|
File without changes
|
|
@@ -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"),)
|
|
@@ -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})>"
|
|
@@ -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
|
+
)
|
|
@@ -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()
|