Conversation Model¶
Represents a complete AI conversation with messages and metadata.
Overview¶
The Conversation model is the primary data structure for representing AI conversations. It includes metadata (title, timestamps) and a collection of messages organized in a tree structure.
API Reference¶
Conversation
¶
Bases: BaseModel
Immutable conversation structure with tree navigation (per FR-222, FR-227, FR-278).
This model represents a complete AI conversation with metadata and all messages. Messages form a tree structure via parent_id references, enabling branching dialogue paths and multi-turn interactions.
Immutability
This model is FROZEN - attempting to modify fields will raise ValidationError. Use .model_copy(update={...}) to create modified instances.
Example
from datetime import datetime, UTC
messages = [
Message(id="1", content="Hello", role="user", timestamp=datetime.now(UTC), parent_id=None),
Message(id="2", content="Hi!", role="assistant", timestamp=datetime.now(UTC), parent_id="1"),
Message(id="3", content="Alt response", role="assistant", timestamp=datetime.now(UTC), parent_id="1"),
]
conversation = Conversation(
id="conv-001",
title="Greeting",
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
messages=messages
)
# Tree navigation
roots = conversation.get_root_messages() # [msg-1]
children = conversation.get_children("1") # [msg-2, msg-3]
threads = conversation.get_all_threads() # [[msg-1, msg-2], [msg-1, msg-3]]
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Unique conversation identifier (non-empty string) |
title |
str
|
Conversation title (non-empty, any UTF-8) |
created_at |
datetime
|
Conversation creation timestamp (timezone-aware UTC, REQUIRED) |
updated_at |
datetime | None
|
Last modification timestamp (timezone-aware UTC, None if never updated) |
messages |
list[Message]
|
All messages in conversation (non-empty list) |
metadata |
dict[str, Any]
|
Provider-specific fields (e.g., moderation_results, plugin_ids) |
Computed Properties
updated_at_or_created: Returns updated_at if set, else created_at (never None) message_count: Number of messages in conversation
updated_at_or_created
property
¶
Get the last update timestamp, falling back to created_at if not set.
This property ensures downstream code always has a valid "last modified" timestamp without needing to handle Optional[datetime]. If the conversation has never been updated (updated_at is None), returns created_at.
Returns:
| Type | Description |
|---|---|
datetime
|
Last update timestamp (updated_at if set, else created_at) |
Example
Usage Notes
- Prefer this property over direct
updated_ataccess for display/sorting - Use direct
updated_atfield when you need to distinguish null vs. set - Guaranteed non-null return value (mypy --strict compliant)
message_count
property
¶
Get the total number of messages in the conversation.
Returns:
| Type | Description |
|---|---|
int
|
Number of messages in the conversation |
Example
Requirements
- FR-018: Metadata includes message count
validate_created_at_timezone_aware
classmethod
¶
Ensure created_at is timezone-aware and normalized to UTC.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
v
|
datetime
|
Timestamp value to validate |
required |
Returns:
| Type | Description |
|---|---|
datetime
|
Timezone-aware datetime normalized to UTC |
Raises:
| Type | Description |
|---|---|
ValueError
|
If timestamp is timezone-naive |
Requirements
- FR-244: Timestamps must be timezone-aware
- FR-245: Timestamps normalized to UTC
- FR-246: Validation enforced at parse time
Source code in src/echomine/models/conversation.py
validate_updated_at_timezone_aware
classmethod
¶
Ensure updated_at is timezone-aware and >= created_at (if provided).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
v
|
datetime | None
|
updated_at value to validate (may be None) |
required |
info
|
Any
|
Field validation info containing created_at |
required |
Returns:
| Type | Description |
|---|---|
datetime | None
|
Timezone-aware datetime normalized to UTC, or None if not provided |
Raises:
| Type | Description |
|---|---|
ValueError
|
If timestamp is timezone-naive or < created_at |
Requirements
- FR-244: Timestamps must be timezone-aware (when provided)
- FR-245: Timestamps normalized to UTC
- FR-246: Validation enforced at parse time
- FR-273: updated_at must be >= created_at (when provided)
Source code in src/echomine/models/conversation.py
get_message_by_id
¶
Find message by ID.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message_id
|
str
|
Message identifier to find |
required |
Returns:
| Type | Description |
|---|---|
Message | None
|
Message if found, None otherwise |
Requirements
- FR-278: Support message lookup by ID
Source code in src/echomine/models/conversation.py
get_root_messages
¶
Get all root messages (parent_id is None).
Root messages are entry points for conversation tree traversal.
Returns:
| Type | Description |
|---|---|
list[Message]
|
List of root messages (may be empty if malformed data) |
Example
Requirements
- FR-278: Support identifying conversation entry points
Source code in src/echomine/models/conversation.py
get_children
¶
Get all direct children of a message.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message_id
|
str
|
Parent message identifier |
required |
Returns:
| Type | Description |
|---|---|
list[Message]
|
List of messages with parent_id == message_id (may be empty) |
Example
Requirements
- FR-278: Support tree navigation via parent-child relationships
Source code in src/echomine/models/conversation.py
get_thread
¶
Get message and all ancestors up to root.
Returns messages in chronological order (oldest first), representing the conversation path from root to the specified message.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message_id
|
str
|
Target message identifier |
required |
Returns:
| Type | Description |
|---|---|
list[Message]
|
List of messages from root to target (empty if message_id not found) |
Example
Requirements
- FR-278: Support retrieving conversation context for a message
Source code in src/echomine/models/conversation.py
get_all_threads
¶
Get all conversation threads (root-to-leaf paths).
Returns list of threads, where each thread is a list of messages from root to leaf in chronological order. Useful for understanding all conversation branches.
Returns:
| Type | Description |
|---|---|
list[list[Message]]
|
List of message threads (each thread is a list of messages) |
Example
# Conversation structure:
# msg-1 (user: "Hello")
# ├── msg-2 (assistant: "Hi! How can I help?")
# └── msg-3 (assistant: "Alternative response")
#
# Result: [[msg-1, msg-2], [msg-1, msg-3]]
threads = conversation.get_all_threads()
for i, thread in enumerate(threads):
print(f"Thread {i+1}: {len(thread)} messages")
Requirements
- FR-278: Support comprehensive tree traversal
- FR-280: Enable analysis of all conversation branches
Source code in src/echomine/models/conversation.py
flatten_messages
¶
Flatten all message content to single string for search.
Concatenates all message content with space separation, useful for full-text search operations.
Returns:
| Type | Description |
|---|---|
str
|
Single string containing all message content |
Example
Requirements
- FR-322: Enable full-text search across conversation content
Source code in src/echomine/models/conversation.py
Usage Examples¶
Basic Access¶
from echomine import OpenAIAdapter
from pathlib import Path
adapter = OpenAIAdapter()
conversation = adapter.get_conversation_by_id(Path("export.json"), "conv-abc123")
# Access metadata
print(f"Title: {conversation.title}")
print(f"Created: {conversation.created_at}")
print(f"Messages: {len(conversation.messages)}")
# Access messages
for message in conversation.messages:
print(f"{message.role}: {message.content[:50]}...")
Message Tree Navigation¶
Conversations can have branching message trees (e.g., regenerated AI responses):
# Get all threads (root-to-leaf paths)
threads = conversation.get_all_threads()
print(f"Conversation has {len(threads)} branches")
for i, thread in enumerate(threads, 1):
print(f"\nThread {i} ({len(thread)} messages):")
for msg in thread:
print(f" {msg.role}: {msg.content[:50]}...")
# Get specific thread by leaf message ID
thread = conversation.get_thread("msg-xyz-789")
# Get root messages (conversation starters)
roots = conversation.get_root_messages()
# Get children of a specific message
children = conversation.get_children("msg-abc-123")
# Check if message has children (branches)
has_branches = conversation.has_children("msg-abc-123")
Immutability¶
Models are frozen (immutable) to prevent accidental data corruption:
from pydantic import ValidationError
# ❌ This raises ValidationError (frozen model)
try:
conversation.title = "New Title"
except ValidationError:
print("Error: Cannot modify frozen model")
# ✅ Create modified copy instead
updated = conversation.model_copy(update={"title": "New Title"})
print(f"Original: {conversation.title}")
print(f"Updated: {updated.title}")
Validation¶
All fields are strictly validated:
from echomine.models import Conversation
from datetime import datetime, timezone
from pydantic import ValidationError
# ❌ Invalid: missing required fields
try:
invalid = Conversation(
id="conv-123",
title="Test"
# Missing: created_at, messages
)
except ValidationError as e:
print(f"Validation error: {e}")
# ✅ Valid: all required fields provided
valid = Conversation(
id="conv-123",
title="Test Conversation",
created_at=datetime.now(timezone.utc),
messages=[],
metadata={}
)
Model Fields¶
Required Fields¶
- id (
str): Unique conversation identifier - title (
str): Conversation title (1-2000 characters) - created_at (
datetime): Creation timestamp (UTC, timezone-aware) - messages (
list[Message]): List of conversation messages
Optional Fields¶
- updated_at (
datetime | None): Last update timestamp (UTC, timezone-aware),Noneif never updated - metadata (
dict[str, Any]): Provider-specific metadata (default: empty dict)
Computed Properties¶
- message_count (
int): Number of messages in conversation
Message Tree Structure¶
Conversations are organized as trees, not linear sequences:
- Each message has an optional
parent_idpointing to its predecessor - Messages with
parent_id=Noneare root messages (conversation starters) - Messages can have multiple children (branches from regenerated responses)
Example Tree:
Root (parent_id=None)
├── Child 1 (parent_id=Root)
│ └── Child 1.1 (parent_id=Child 1)
└── Child 2 (parent_id=Root) # Branch from regeneration
└── Child 2.1 (parent_id=Child 2)
Thread Extraction¶
Thread: A root-to-leaf path through the message tree.
# Get all threads (all possible conversation paths)
threads = conversation.get_all_threads()
# Each thread is a list of messages in chronological order
for thread in threads:
for msg in thread:
print(f"{msg.role}: {msg.content}")
Metadata¶
Provider-specific data is stored in the metadata dictionary:
# OpenAI-specific metadata
conversation.metadata.get("openai_model", "unknown")
conversation.metadata.get("openai_conversation_template_id")
conversation.metadata.get("openai_plugin_ids", [])
# Check if metadata exists
if "openai_model" in conversation.metadata:
print(f"Model: {conversation.metadata['openai_model']}")
Related Models¶
- Message: Individual message model
- SearchResult: Search result containing a conversation