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.
How It Works
Section titled “How It Works”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 usageStub Detection
Section titled “Stub Detection”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
Extraction Schema
Section titled “Extraction Schema”The AI extracts a CallerInfo struct with these fields:
| Field | Type | Fill Rule |
|---|---|---|
first_name | String | Only if contact’s first_name is a stub value |
last_name | String | Only if contact’s last_name is a stub value |
email | Option<String> | Only if contact has no existing emails |
company | Option<String> | Only if company is None |
job_title | Option<String> | Only if job_title is None |
preferred_language | Option<String> | Only if preferred_language is None |
timezone | Option<String> | Only if timezone is None |
gender | String | Only if gender is None and confidence is "high" |
pipeline_stage | String | Only 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.
Validation Rules
Section titled “Validation Rules”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.
Transaction Safety
Section titled “Transaction Safety”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.
Organization Scope
Section titled “Organization Scope”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.
Configuration
Section titled “Configuration”| Setting | Value |
|---|---|
| AI model | OpenRouter (configured in core_conf) |
| API key | openrouter_api_key in core_conf table |
| Trigger | Automatic after call transcription |
No new environment variables or database migrations are required.
Key Files
Section titled “Key Files”| File | Purpose |
|---|---|
src/mods/contact/services/contact_name_ext_service.rs | Stub detection (is_stub_name_value, ContactNameExt) |
src/mods/contact/services/enrich_contact_service.rs | Call transcription enrichment |
src/mods/contact/services/ai/enrich_contact_from_messages_service.rs | SMS enrichment (also uses stub detection) |
src/mods/twilio/utils/process_twilio_recording_util.rs | Calls enrich_contact() after transcription |
Related
Section titled “Related”- 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