Skip to content

Call Enrichment

Call enrichment extracts caller details from phone transcriptions after each call. When a stub contact (unnamed or “Unknown”) receives a call, the system sends the transcription to AI and fills in up to 10 fields — name, company, email, and more.

Call ends → transcription saved
→ enrich_contact(call_id, org_id)
Load call + contact (org-scoped)
→ has_stub_name()? (both names empty or "Unknown")
→ AI extracts CallerInfo from transcription
→ Validate output (reject "Unknown" names)
→ Transaction: update contact + insert email (if new)
→ Log AI usage

A contact is considered a “stub” when both names are empty or “Unknown”. The ContactNameExt::has_stub_name() trait method centralizes this check:

pub(crate) fn is_stub_name_value(name: &str) -> bool {
let trimmed = name.trim();
trimmed.is_empty() || trimmed.eq_ignore_ascii_case("unknown")
}
pub(crate) trait ContactNameExt {
fn has_stub_name(&self) -> bool;
}

is_stub_name_value handles edge cases: "Unknown", "unknown", "UNKNOWN", " unknown ", and empty strings all return true. Both enrichment services (call and SMS) share this logic.

Location: src/mods/contact/services/contact_name_ext_service.rs

The AI extracts a CallerInfo struct with these fields:

FieldTypeFill Rule
first_nameStringOnly if contact’s first_name is a stub value
last_nameStringOnly if contact’s last_name is a stub value
emailOption<String>Only if contact has no existing emails
companyOption<String>Only if company is None
job_titleOption<String>Only if job_title is None
preferred_languageOption<String>Only if preferred_language is None
timezoneOption<String>Only if timezone is None
genderStringOnly if gender is None and confidence is "high"
pipeline_stageStringOnly if pipeline_stage is None; validated against: lead, prospect, qualified, customer, churned

All fields follow a “fill empty only” pattern — existing user-entered data is never overwritten. AI-extracted values of "Unknown" or "unknown" are rejected.

Names: Rejected if is_stub_name_value() returns true (empty, “Unknown”, case-insensitive).

Email: Must contain both @ and . to be inserted. Stored lowercase, marked as preferred.

Gender: Only applied when gender_confidence is "high". Values: male, female, non-binary.

Pipeline stage: Whitelist-validated against lead, prospect, qualified, customer, churned. Invalid values are silently dropped.

Contact updates and email inserts are wrapped in a database transaction:

let txn = db.begin().await?;
if has_changes {
contact_model.update(&txn).await?;
}
if let Some(email) = valid_email {
// Optimized existence check
let has_existing = schemas::contact_email::Entity::find()
.select_only()
.column(schemas::contact_email::Column::Id)
.filter(schemas::contact_email::Column::ContactId.eq(contact_id))
.into_tuple::<Uuid>()
.one(&txn)
.await?
.is_some();
if !has_existing {
// Insert new email record
}
}
txn.commit().await?;

If either operation fails, both roll back. The email existence check uses select_only + into_tuple for minimal query overhead.

The contact lookup includes an organization filter as defense-in-depth:

let contact = schemas::contact::Entity::find_by_id(contact_id)
.filter(schemas::contact::Column::OrganizationId.eq(organization_id))
.one(&db)
.await?;

This prevents cross-organization access even if the call.contact_id reference is compromised.

SettingValue
AI modelOpenRouter (configured in core_conf)
API keyopenrouter_api_key in core_conf table
TriggerAutomatic after call transcription

No new environment variables or database migrations are required.

FilePurpose
src/mods/contact/services/contact_name_ext_service.rsStub detection (is_stub_name_value, ContactNameExt)
src/mods/contact/services/enrich_contact_service.rsCall transcription enrichment
src/mods/contact/services/ai/enrich_contact_from_messages_service.rsSMS enrichment (also uses stub detection)
src/mods/twilio/utils/process_twilio_recording_util.rsCalls enrich_contact() after transcription
  • Contact — parent module with data model and resolution flow
  • SMS Enrichment — enrichment from message history (shares stub detection logic)
  • Contact Memory — AI-synthesized memory updated after enrichment