Appearance
AI Intelligence Service
Cora uses an external Intelligence Service (at INTELLIGENT_SERVICE_URL) to power three AI-driven capabilities: reply generation, sentiment analysis, and voicemail generation. The service client lives in packages/lib/src/services/intelligence.service.ts.
Model
| Setting | Value |
|---|---|
| Model | GPT-4o-mini |
| Provider | Vercel AI SDK (@ai-sdk/openai) |
| Output mode | generateObject() — structured JSON only, no streaming |
| Hosted by | Separate Intelligence Service (INTELLIGENT_SERVICE_URL) |
Authentication
The Intelligence Service uses email/password auth with JWT tokens. The client handles token lifecycle transparently:
- Tokens are cached in memory with a 1-minute expiry buffer
- Falls back to DB (
AppSettings) if no in-memory token - Auto-refreshes using the refresh token (5-minute buffer before expiry)
Features
1. Reply Generation
Generates a contextually appropriate email or SMS reply for a lead based on conversation history.
Endpoint: POST /intelligence/reply
Input:
chatHistory: [{ role: "agent" | "lead", message: string }]
leadId: string
messageType: "email" | "sms"
sentimentScore: number (0–1)
sender: { firstName, lastName }
campaignId, campaignType, leadType: "BUYER" | "SELLER" | "OWNER"How chat history is built:
OUTBOUNDmessages in the DB →role: "agent"INBOUNDmessages in the DB →role: "lead"- Empty messages are filtered out
Output: Suggested reply text, or null if insufficient context.
Used in: Conversation inbox — shown as an AI-suggested draft the agent can edit before sending.
2. Sentiment Analysis
Classifies how a lead responded — crucial for deciding whether to continue or stop outreach.
Two-tier approach:
Output schema:
typescript
{
score: -1 | -0.5 | 0 | 0.5 | 1,
sentiment: "very negative" | "negative" | "neutral" | "positive" | "very positive",
optout: boolean,
explanation: string
}Score meanings:
| Score | Sentiment | Example |
|---|---|---|
1 | Very positive | "Yes I'm interested, tell me more" |
0.5 | Positive | "Maybe, what's the timeline?" |
0 | Neutral | "I'll think about it" |
-0.5 | Negative | "Not right now, too busy" |
-1 | Very negative | "Stop texting me" / opt-out |
Key rules from the system prompt:
- Polite refusals ("I'm not interested") → negative, not neutral
- Factual statements that end eligibility ("I already sold") → negative
- Future openness ("maybe next year") → positive
- Opt-outs are hard-coded via keyword matching (fast path) — never sent to the LLM
- Unrelated sentiment (e.g., "I'm having a bad day") is ignored when scoring
Used in: Automatically applied to every inbound reply. Score stored on CampaignLeads. High negative scores suppress further outreach.
3. Double Tap (Bulk Follow-up Generation)
Generates follow-up SMS messages for multiple leads in a single call — used for "double tap" sequences where a follow-up is sent if no reply was received.
Endpoint: POST /intelligence/double-tap
Input: Array of leads, each with:
- Chat history (same format as reply generation)
- Sentiment score
- Lead stage
Output: Array of follow-up messages, one per lead.
Used in: The worker cron job that identifies leads past their follow-up window.
4. Voicemail Generation
Generates voicemail copy and converts it to audio (stored in S3).
Endpoint: POST /intelligence/voicemail
Input:
lead: { firstName, lastName, phone }
brand: { name, personality, voice }
campaign: { type, leadType }
customContent?: string (optional override)Output:
- Generated voicemail script
- S3 URL of the audio file
Used in: Campaign steps of type VOICEMAIL — the worker fetches the generated audio and delivers it via Twilio voicemail drop.
Integration with Campaign Execution
The Intelligence Service is called at different points in the campaign pipeline:
Configuration
| Variable | Description |
|---|---|
INTELLIGENT_SERVICE_URL | Base URL of the Intelligence Service |
ENRICHMENT_ADMIN_EMAIL | Auth credential (shared with enrichment service) |
ENRICHMENT_ADMIN_PASSWORD | Auth credential |
OPENAI_API_KEY | Used directly for local generateObject() calls (sentiment) |