Last Updated: 2026-04-03 Status: Draft
1. Core Entities & Aggregates
1.1 Contact Domain
1.1.1 Contact
The root entity. Represents a single end-user interacting with a tenant's AI bot.
| Field | Type | Required | Description |
|---|---|---|---|
id |
UUID | ✅ | Primary key |
tenant_id |
UUID | ✅ | Owning business |
external_id |
String | ✅ | Channel-specific identifier (phone number, widget session ID) |
channel |
Enum | ✅ | whatsapp, telegram, widget |
display_name |
String | ❌ | Name extracted from first interaction or channel profile |
avatar_url |
String | ❌ | Profile picture URL (from channel, if available) |
language |
String | ❌ | Detected language (ISO 639-1, e.g., en, hi) |
timezone |
String | ❌ | Inferred from activity patterns (e.g., Asia/Kolkata) |
outreach_disabled |
Boolean | ✅ | If true, NO proactive messages ever. Set when contact says "stop". Default false. |
created_at |
Timestamp | ✅ | First interaction |
updated_at |
Timestamp | ✅ | Last profile modification |
Unique constraint: (tenant_id, external_id, channel) — one contact per identity per channel per tenant.
1.1.2 ContactState
The hot, frequently-updated state of a contact. One row per contact, updated on every interaction.
| Field | Type | Required | Description |
|---|---|---|---|
id |
UUID | ✅ | Primary key |
tenant_id |
UUID | ✅ | Owning business |
contact_id |
UUID | ✅ | FK → Contact (unique per contact) |
mood |
Enum | ✅ | happy, sad, anxious, excited, neutral, angry, frustrated, flirty, bored, grateful |
energy |
Enum | ✅ | high, medium, low |
conversation_style |
Enum | ✅ | playful, deep, casual, supportive, romantic, venting |
relationship_stage |
Enum | ✅ | new, building, established, deep, fading, dormant |
active_streak |
Integer | ✅ | Consecutive days with at least one message |
total_messages |
Integer | ✅ | Lifetime message count |
total_sessions |
Integer | ✅ | Lifetime session count (session = gap of 30+ min between messages) |
avg_session_duration |
Integer | ✅ | Average session length in minutes |
avg_messages_per_session |
Integer | ✅ | Average messages per session |
preferred_hours |
Text[] | ❌ | Most active hours (e.g., ["22:00", "23:00"]) |
churn_risk |
Float | ✅ | 0.0–1.0, computed from engagement signals |
last_active_at |
Timestamp | ✅ | Last message timestamp |
last_mood_at |
Timestamp | ✅ | When mood was last classified |
first_contact_at |
Timestamp | ✅ | Very first interaction ever |
updated_at |
Timestamp | ✅ | Last state update |
1.2 Memory Domain
1.2.1 ContactMemory
A stored piece of knowledge about a specific end-user. Same architecture as AI Brain's ai_memory, but scoped to contact_id.
| Field | Type | Required | Description |
|---|---|---|---|
id |
UUID | ✅ | Primary key |
tenant_id |
UUID | ✅ | Owning business |
contact_id |
UUID | ✅ | FK → Contact — the end-user this memory belongs to |
memory_type |
Enum | ✅ | fact, preference, episode, pattern |
content |
Text | ✅ | The memory itself (natural language) |
embedding |
vector(1536) | ❌ | Semantic embedding (null until deferred processing completes) |
importance |
Float | ✅ | 0.0–1.0, default varies by type |
access_count |
Integer | ✅ | Times retrieved for context |
decay_rate |
Float | ✅ | Importance decay per day. Default 0.005 (slower than Brain's 0.01 — relationships are long-term) |
entity_ids |
Text[] | ❌ | Linked entities (e.g., ["dog:bruno", "workplace:infosys"]) |
source_type |
Enum | ✅ | conversation, observation, consolidation |
source_id |
UUID | ❌ | Reference to originating conversation/interaction |
accessed_at |
Timestamp | ✅ | Last retrieval timestamp |
created_at |
Timestamp | ✅ | Record creation |
Key difference from AI Brain memory: decay_rate is 0.005 vs 0.01. A contact's dog name should persist for months. Business metrics need faster refresh.
1.2.2 ContactEntity
An entity node in the contact's memory graph.
| Field | Type | Required | Description |
|---|---|---|---|
id |
String | ✅ | Entity identifier (e.g., dog:bruno, workplace:infosys) |
tenant_id |
UUID | ✅ | Owning business |
contact_id |
UUID | ✅ | FK → Contact |
entity_type |
Enum | ✅ | person, pet, place, workplace, hobby, event, preference, topic |
display_name |
String | ✅ | Human-readable name (e.g., "Bruno") |
metadata |
JSONB | ❌ | Additional structured data (e.g., { "breed": "golden retriever" }) |
memory_count |
Integer | ✅ | Number of memories linked to this entity |
created_at |
Timestamp | ✅ | When first recognized |
1.3 Outreach Domain
1.3.1 OutreachTrigger
A scheduled proactive message to be sent to a contact.
| Field | Type | Required | Description |
|---|---|---|---|
id |
UUID | ✅ | Primary key |
tenant_id |
UUID | ✅ | Owning business |
contact_id |
UUID | ✅ | FK → Contact |
trigger_type |
Enum | ✅ | scheduled, inactivity, milestone, recurring |
channel |
Enum | ✅ | whatsapp, telegram, widget |
message_template |
Text | ❌ | Static message text (can contain {name}, {memory_ref}) |
generate_with_llm |
Boolean | ✅ | If true, generate a contextual message at send time using contact memory |
llm_context |
Text | ❌ | Additional context for LLM generation (e.g., "Arjun was upset yesterday") |
scheduled_at |
Timestamp | ❌ | For scheduled type — when to send |
cron_expression |
String | ❌ | For recurring type (e.g., 0 8 * * * = daily 8am) |
inactivity_days |
Integer | ❌ | For inactivity type — trigger after N days of silence |
milestone_type |
String | ❌ | For milestone type (e.g., 30_day_anniversary, 100_messages, birthday) |
status |
Enum | ✅ | pending, sent, failed, cancelled, skipped |
attempt_count |
Integer | ✅ | Number of send attempts (max 2 for inactivity) |
sent_at |
Timestamp | ❌ | When the message was actually delivered |
sent_message |
Text | ❌ | The actual message text that was sent (stored after generation/delivery) |
created_at |
Timestamp | ✅ | Record creation |
1.3.2 OutreachConfig
Tenant-level outreach configuration. Controls what types of proactive messaging are enabled.
| Field | Type | Required | Description |
|---|---|---|---|
id |
UUID | ✅ | Primary key |
tenant_id |
UUID | ✅ | Owning business (unique per tenant) |
inactivity_enabled |
Boolean | ✅ | Allow inactivity re-engagement messages. Default true. |
inactivity_days |
Integer | ✅ | Days of silence before triggering. Default 3. |
inactivity_max_attempts |
Integer | ✅ | Max re-engagement messages per inactive period. Default 2. |
milestone_enabled |
Boolean | ✅ | Allow milestone celebration messages. Default true. |
recurring_enabled |
Boolean | ✅ | Allow recurring messages (good morning, etc.). Default false. |
recurring_cron |
String | ❌ | Cron expression for recurring messages |
quiet_hours_start |
String | ❌ | Don't send during these hours (e.g., "23:00") |
quiet_hours_end |
String | ❌ | (e.g., "07:00") |
updated_at |
Timestamp | ✅ | Last modification |
1.4 Interaction Domain
1.4.1 InteractionLog
A lightweight log of every message exchange. Not the full conversation (that's in Chat Engine's chat_messages) — just the signals Contact Intelligence extracted.
| Field | Type | Required | Description |
|---|---|---|---|
id |
UUID | ✅ | Primary key |
tenant_id |
UUID | ✅ | Owning business |
contact_id |
UUID | ✅ | FK → Contact |
detected_mood |
Enum | ✅ | Mood classified from user message |
detected_energy |
Enum | ✅ | Energy level classified |
memories_extracted |
Integer | ✅ | Number of memories created from this interaction |
memories_retrieved |
Integer | ✅ | Number of memories used for context |
session_id |
UUID | ✅ | Groups messages into sessions (30min gap = new session) |
created_at |
Timestamp | ✅ | Interaction timestamp |
2. Entity Relationships
3. State Machines in Detail
3.1 RelationshipStage Lifecycle
3.2 OutreachTrigger.status Lifecycle
3.3 Mood Transition Tracking
Mood doesn't have a formal state machine — it's set on every message by the classifier. But transitions are tracked in InteractionLog for pattern detection:
Pattern detection:
If mood transitions from happy/neutral → sad/anxious for 3+ consecutive messages
→ Create trigger: check-in message next session
→ Update memory: "contact went through a difficult period around {date}"
If mood is consistently "bored" for 5+ messages
→ Signal to AI: change conversation style, introduce new topics
→ Flag churn_risk increase4. Key Domain Rules & Invariants
-
One ContactState per Contact. The
(tenant_id, contact_id)pair oncontact_statehas a UNIQUE constraint. State is upserted, never duplicated. If a row doesn't exist, the first interaction creates it with defaults. -
Outreach respects "stop" absolutely. When
contact.outreach_disabled = true, NO outreach triggers fire. The outreach engine checks this BEFORE generating any message. There is no override, no exception, no "just this once." This is a legal and trust requirement. -
Memories belong to contacts, not conversations. A memory's lifecycle is independent of the conversation it was extracted from. Deleting a conversation (in Chat Engine) does NOT delete the memories extracted from it. Memories are only deleted by explicit "forget me" request or importance decay.
-
Inactivity outreach has hard limits. Max
outreach_config.inactivity_max_attemptsmessages per inactive period (default 2). After the limit, outreach stops until the contact re-engages. No configuration allows unlimited inactivity messages. -
Quiet hours are enforced. If
outreach_config.quiet_hours_startandquiet_hours_endare set, no outreach is delivered during those hours (in the contact's inferred timezone). Triggers scheduled during quiet hours are delayed to the next valid window. -
Contact deletion cascades completely. Deleting a Contact deletes: ContactState, all ContactMemory, all ContactEntity, all OutreachTrigger, all InteractionLog. No orphaned data.
-
Mood classification never blocks response. If the mood classifier is slow or fails, the Chat Engine receives the previous mood from KV state. The user experience is never degraded by mood classification latency.
-
Memory importance floor is 0.1. Memories below 0.1 importance are pruned by periodic consolidation. They are effectively "forgotten." This prevents unbounded memory growth while keeping meaningful knowledge.