Skip to main content

AI Agent Memory — Memory DWH Schema & Data Spec for DSAI

Audience: DSAI PM / engineering. Purpose: the Memory DWH table schemas that will hold the AI Agent's "customer memory" in Phase 1, and the read contract we're asking DSAI to build against. Shared by BOT squad (Dimas Fauzi Hidayat).

1. Context in three sentences

The AI Agent Memory initiative gives the autonomous agent customer-level context before it responds: past conversations, profile, deals, and tickets for the customer it is talking to. This quarter the data is served from a dedicated Memory DWH (not CEBE — CEBE is the long-term backbone; every column below carries its CEBE event-field lineage so the later cutover changes the source, not the schema). DSAI is the consumer: it reads the serving view once at conversation start and injects it into the agent's reasoning.

2. Read contract at a glance

AspectContract
TriggerOnce per conversation, at the first agent response — never per turn
Key(organization_id, qontak_customer_id) — if identity is unresolved (null), skip memory entirely; no fuzzy matching
Latency budgetHard 2,000 ms on the read; on timeout the agent answers without memory (auto-skip) — the reply path is never blocked
CachingFetched context is thread-cached; turns 2..n reuse it, no re-query
FlagOrg-level ai_agent_memory (default OFF) — flag state arrives in the prediction payload; no read when OFF
DegradationTimeout / error / empty profile / null identity → answer exactly as today; failure reason recorded in telemetry
Data minimizationDSAI receives only allow-listed, masked fields — never raw payment data or government IDs; allow-list to be finalized together (§6)
TelemetryEvery response reports memory_status (hit / miss_no_identity / miss_empty_profile / skipped_flag_off / skipped_timeout / error) + read latency + modules present, returned in the response envelope

Full behavior spec: the Phase-1 PRD (MEM-S01..S04) — linked in §7.

3. Schema conventions (all tables)

ConventionValue
Standard columns on every tableorganization_id STRING · company_sso_id STRING · qontak_customer_id STRING (FK → dim_customer) · source_module STRING · ingested_at TIMESTAMP · _source_pk STRING (lineage to the source row)
TenancyEvery query is scoped by organization_id; cluster/sort keys = (organization_id, qontak_customer_id)
PartitioningFact tables partitioned by their primary date column (noted per table)
TypesWarehouse-generic: STRING / TEXT / INTEGER / NUMERIC / BOOLEAN / TIMESTAMP / DATE / JSON / ARRAY
LineageThe Source column per field = CEBE event.field it mirrors (schema origin: CEBE ANCHOR §6) + this quarter's interim source system

4. Table schemas — the four domains

4.1 dim_customer — customer profile (grain: 1 row per org + customer)

ColumnTypeNullSource
qontak_customer_idSTRINGNO — natural key w/ organization_idcustomer.created.qontak_customer_id / hub-core contacts.qontak_customer_id
organization_idSTRINGNOenvelope
contact_handler_idSTRINGYEShub-core contacts.contact_handler_id (cross-channel unification)
full_nameSTRINGYEScustomer.created.default_fields[] / hub-core contacts.full_name
phone_numberSTRINGYES (PII)default_fields[] / contacts.phone_number
emailSTRINGYES (PII)default_fields[] / contacts.extra.email
usernameSTRINGYEScontacts.extra.username
channelsARRAY<STRING>YEShub-core contact_handlers.channels[]
is_blockedBOOLEANNO, default falsehub-core contacts.is_blocked
identity_confidenceSTRING (resolved | channel_only)NOderived — channel_only rows are excluded from serving
created_at / created_byTIMESTAMP / STRINGYEScustomer.created
updated_at / updated_byTIMESTAMP / STRINGYEScustomer.updated
first_seen_at / last_seen_atTIMESTAMPYESderived from fact_conversation

4.2 dim_customer_attribute — custom fields (grain: 1 row per customer × field)

ColumnTypeNullSource
qontak_customer_id, organization_idSTRINGNOFK
field_idSTRINGNOhub-core contact_fields.contact_custom_field_id (carries cdp_field_property_id)
field_nameSTRINGNOcustom_fields[] / contact_custom_fields.name
field_typeSTRINGNOcontact_custom_fields.type
valueJSONYEScustom_fields[] / contact_fields.value
date_valueDATEYEScontact_fields.date_value
updated_atTIMESTAMPNOcustomer.updated.change_fields[]

4.3 dim_customer_segment — segment membership (grain: customer × segment)

ColumnTypeNullSource
qontak_customer_id, organization_idSTRINGNOFK
segment_id / segment_nameSTRINGNOsegment.entered
entered_atTIMESTAMPNOsegment.entered
exited_atTIMESTAMPYES — NULL = currently activesegment.exited

4.4 fact_conversation — past conversations (grain: 1 row per room; partition: started_at)

ColumnTypeNullSource
room_idSTRINGNO — PKconversation.started.room_id / hub-core rooms.id
qontak_customer_id, organization_idSTRINGNOenvelope (via participants → identity spine)
channelSTRINGNOconversation.started.channel
started_atTIMESTAMPNO — partition keyconversation.started / rooms.created_at
assigned_at / agent_id / assignment_type (human | bot)TIMESTAMP / STRING / STRINGYESconversation.assigned
resolved_at / resolved_byTIMESTAMP / STRINGYESconversation.resolved / rooms.resolved_at
resolution_time_secondsINTEGERYESconversation.resolved
handled_bySTRING (human | bot)YESconversation.resolved.handled_by / chatbot rooms.closed_reason
containment_flag / bot_flow_idBOOLEAN / STRINGYESbot.contained / chatbot closed_reason = RESOLVE_AI
escalation_reason / bot_session_idSTRINGYESconversation.escalated / closed_reason = ASSIGN_AGENT_AI
csat_score / csat_commentNUMERIC / STRINGYEScsat.submitted
nps_scoreNUMERICYESnps.submitted
summaryTEXTYES (PII)conversation.summarized extension / omnichannel_room_summaries.summarythe primary memory artifact
sentimentSTRINGYESsame / omnichannel_room_summaries.sentiment
summarized_atTIMESTAMPYESomnichannel_room_summaries.summarized_at
message_countINTEGERYESderived from messages

4.5 fact_messageoptional, Phase 2 of the DWH (grain: 1 row per customer message; partition: created_at)

ColumnTypeNullSource
message_idSTRINGNO — PKmessage.received.message_id
room_id, qontak_customer_id, organization_idSTRINGNOFK
message_typeSTRINGNOmessage.received.message_type
content_previewSTRING (truncated)YES (PII)message.received.content_previewfull bodies are never ingested
sender_typeSTRINGNOhub-core messages.sender_type
created_atTIMESTAMPNO — partition keyhub-core messages.created_at

4.6 fact_deal — associated deals (grain: 1 row per deal, current state; partition: created_at)

ColumnTypeNullSource
deal_idSTRINGNO — PKdeal.created.deal_id
qontak_customer_id, organization_idSTRINGNOcustomer_association (CRM crm_lead_ids → identity spine)
pipeline_id / stage_idSTRINGNO / NO — current stagedeal.created + latest deal.stage_changed.new_stage
deal_size / currencyNUMERIC / STRINGYESdeal.created.deal_size
owner_idSTRINGYESdeal.created.owner_id
statusSTRING (open | won | lost)NOderived from deal.won / deal.lost
close_dateDATEYESdeal.won.close_date
lost_date / loss_reasonDATE / STRINGYESdeal.lost
product_idsARRAY<STRING>YESproduct_association
other_fieldsJSONYES{other_fields} (CRM custom fields)
created_at / updated_atTIMESTAMPNO / NOdeal.created / latest event

Optional companion: fact_deal_stage_history (1 row per deal.stage_changed: deal_id, old_stage, new_stage, days_in_stage, changed_by, changed_at) — analytics-grade; not read by the serving view.

4.7 fact_ticket — associated tickets (grain: 1 row per ticket, current state; partition: created_at)

ColumnTypeNullSource
ticket_idSTRINGNO — PKticket.created.ticket_id
qontak_customer_id, organization_idSTRINGNOcustomer_association / hub-core room_tickets.account_uniq_id → identity spine
room_idSTRINGYES — originating conversationhub-core room_tickets.room_id
pipeline_id / stage_idSTRINGNO / NO — current stageticket.created + latest ticket.stage_changed.new_stage
priority_id / category_idSTRINGYESticket.created.{other_fields}
assigned_id / team_id / assigned_atSTRING / STRING / TIMESTAMPYESticket.assigned
creator_idSTRINGYESticket.created.creator_id
statusSTRING (open | resolved)NOderived from ticket.resolved
resolution_typeSTRING (first_contact | escalated)YESticket.resolved.resolution_type
resolved_at / due_dateTIMESTAMP / DATEYESticket.resolved / ticket.created
sla_breached / sla_type / breach_duration_secondsBOOLEAN / STRING / INTEGERYESticket.sla_breached — lets the agent acknowledge a delayed ticket proactively
other_fieldsJSONYES{other_fields}
created_atTIMESTAMPNO — partition keyticket.created

(Campaign/Ads, Commerce/Booking, and Loyalty are out of scope for memory Phase 1.)

5. The serving view DSAI reads — customer_memory_profile

One read → one object per (organization_id, qontak_customer_id), assembled from the tables above:

BlockShapeBuilt from
profileSTRUCT: name, channels, segments, allow-listed attributesdim_customer + dim_customer_attribute + dim_customer_segment
conversation_historyARRAY<STRUCT>, last N (default 5), newest first: summary, sentiment, handled_by, containment_flag, csat_score, started_at, channelfact_conversation
open_dealsARRAY<STRUCT>: deal_id, stage_id, deal_size, status, updated_at (open first, then recent won/lost)fact_deal
open_ticketsARRAY<STRUCT>: ticket_id, stage_id, status, sla_breached, due_date, created_atfact_ticket
statsSTRUCT: total_conversations, containment_rate, avg_csat, tickets_open, tickets_resolved, total_won_valueaggregates over the facts
modules_presentARRAY<STRING> — which domains had data (partial profiles are normal; the agent must not fabricate absent domains)derived

6. What we need from DSAI

  1. Memory client + context injection — read customer_memory_profile at conversation start within the 2,000 ms budget, inject into the agent's reasoning, thread-cache for later turns. Injection mechanics (system-context block vs. retrieval tool) = your call, decided in the RFC.
  2. Telemetry in the response envelopememory_status, read latency, modules_present, cached per response (feeds the adoption metric and rollout gates).
  3. Field allow-list review — confirm which columns from §4/§5 the model may receive (PII-masking policy; PII-flagged columns above are the review set), and the thread retention policy for the cached profile.
  4. A written contract + Q4 capacity commitment — interface, latency SLO, timeline. This is the BUILD entry gate (G0) on our side: no build starts without it.

7. References

  • Phase-1 PRD (behavior spec MEM-S01..S04): Confluence · repo chatbot/ai-agent-memory/prds/phase-1-read-side-cebe-memory.md
  • Initiative ANCHOR: Confluence
  • CEBE event catalog (schema lineage origin): CEBE ANCHOR §6
  • Full ingestion catalog (sources + identity spine, BOT-internal): repo chatbot/ai-agent-memory/documentations/memory-dwh-ingestion-catalog.md