phone-agent

Physical name: morpheus-sam-phone-agent. Source: lambdas/phone_agent/.

Three unrelated scheduled jobs share this function because they share its integrations: Twilio, Slack, and the two outbound queues.

Aspect Value

Handler

handler.lambda_handler

Timeout

120 s

Memory

256 MB

Reserved concurrency

2

Layers

common, data-layer, bigquery

Triggers

3 EventBridge schedules, all enabled

It has no SQS trigger, but the dispatcher supports the SQS path so one can be added without touching handler.py.

Registered events

detail-type Handler Purpose

Phone.LineIntelPending

LineIntelPendingHandler

Twilio line-type intelligence for pending target_phone_numbers

Phone.TaskDeadlineReminder

TaskDeadlineReminderHandler

Slack reminders for tasks due within 24 h, plus the escalation ladder

Phone.CampaignTargetReEngagement

CampaignReEngagementHandler

Amazon seller campaign re-engagement

Phone.LineIntelPending

Selects target_phone_numbers rows where syncTwilio is false, capped at PHONE_AGENT_BATCH_SIZE. Per number:

  1. GET Twilio Lookup v2 with Fields=line_type_intelligence,line_status.

  2. Update the row with country code, carrier, MCC/MNC, line type, risk level, isBlocked, and syncTwilio = True.

  3. Build HubSpot properties phone_carrier_name and phone_line_type.

  4. Resolve a hubspotContactId through the row’s contactId.

  5. Publish Contact.Update to CrmSyncQueue with target_phone_number_id set, so morpheus-hubspot stamps the row after a successful PATCH.

Returns immediately with an ERROR when CRM_SYNC_QUEUE_URL is unset.

Risk tiers above medium — nonFixedVoip (high), premium, voicemail, pager (critical) — set isBlocked = True, keeping the number out of future dialling.

syncTwilio is the idempotence marker: a number is looked up exactly once, which matters because Twilio Lookup is billed per request.

Phone.TaskDeadlineReminder

Full rules: escalation ladder.

Two phases per invocation, both gated on SLACK_BOT_TOKEN and SALES_SLACK_CHANNEL_ID being set:

Phase 1 — reminders

Pending tasks due within 24 hours, grouped by rep, posted as one Slack channel message. An Activity row of type reminder is inserted per task as a best-effort audit trail.

Phase 2 — escalation

Tasks past their deadline by a day or more, escalated one stage per run with a 3-second sleep between tasks for Slack rate limits:

Stage Action Next

0

Public channel warning mentioning the rep

1

1

Slack DM to the rep + Email.SendInternal to EmailAgentQueue

2

2

Mark the task failed

3

3

Terminal — return

—

At stage 3, a FollowUpTaskSyncEvent with status failed is published to CrmSyncQueue when the task has a hubspotTaskId.

A per-task failure is logged and the loop continues.

Phone.CampaignTargetReEngagement

Full rules: Amazon seller campaign.

Queries v_amazon_seller_contact_engagement_summary in BigQuery, computes priority in Python, resolves ownership, and publishes one Contact.Update per row to CrmSyncQueue.

Priority:

Condition call_priority

AI-owned last engagement within REENGAGEMENT_STALE_DAYS (3)

4

AI-owned last engagement older, or none

3

Last engagement owner is not the AI

1

This is the only handler in the platform that aborts the whole batch when round-robin returns no rep, rather than continuing — every remaining row would otherwise be enrolled with no assignee.

Returns early with a warning when BIGQUERY_CREDENTIALS or CRM_SYNC_QUEUE_URL is unset.

Services

Module Responsibility

services/twilio_lookup.py

Twilio Lookup v2 via urllib with Basic auth; maps line type → LineTypes enum and → RiskLevel; returns a frozen TwilioLineIntelResult

services/slack_client.py

send_message, send_dm, resolve_user_id(email), mention(email, name)

services/sqs_publisher.py

publish_to_standard_queue() — for EmailAgentQueue, which is not FIFO and so cannot use morpheus_common.publish_event()

utils/task_blocks.py

group_by_rep() and build_reminder_blocks() — Slack Block Kit construction

utils/constants.py

AI_OWNER_ID, REENGAGEMENT_STALE_DAYS, TASK_ACTIVE_HOURS, the BigQuery query, the outreach_list_reason string

Two publishers exist because the two queues differ. morpheus_common.publish_event() sets MessageGroupId and MessageDeduplicationId, which are FIFO-only and would be rejected by the standard EmailAgentQueue.

Configuration

Setting Default Purpose

AGENTCORE_SECRET_ARN

(template)

Shared secret

SUBSCRIPTION_ID

(template)

Tenant scope

CRM_SYNC_QUEUE_URL

""

Required by line-intel and campaign re-engagement

EMAIL_AGENT_QUEUE_URL

""

Stage-1 escalation email; skipped with a warning when unset

PHONE_AGENT_BATCH_SIZE

25 in template (code default 1)

Numbers per Twilio run

SALES_SLACK_CHANNEL_ID

sales-tasks

Channel for reminders and escalation

LOG_LEVEL

INFO

PhoneAgentSecrets: DATABASE_URL, TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN (all validated non-empty), plus optional SLACK_BOT_TOKEN and BIGQUERY_CREDENTIALS.

Because the Twilio credentials are validated non-empty, this function will not cold-start without them — even for the two handlers that never call Twilio.

IAM

  • SQSSendMessagePolicy on CrmSyncQueue

  • SQSSendMessagePolicy on EmailAgentQueue

  • secretsmanager:GetSecretValue on AgentcoreSecret

No Bedrock access — this function runs no agents.

Tests

cd lambdas && uv run pytest tests/unit/test_phone_agent_twilio_lookup.py -v
cd lambdas && uv run pytest tests/unit/test_campaign_reengagement.py -v

Operating notes

  • AI_OWNER_ID here duplicates DIALER_FALLBACK_HUBSPOT_OWNER_ID in morpheus_hubspot/utils/constants.py. Both are "86817215" and both must change together.

  • Empty SLACK_BOT_TOKEN disables both reminder phases with a warning; nothing else breaks.

  • Empty BIGQUERY_CREDENTIALS disables campaign re-engagement only.

  • To re-run Twilio for a number, clear its syncTwilio flag:

    UPDATE target_phone_numbers SET "syncTwilio" = false WHERE "phoneNumber" = '+1302…';
  • All three schedules currently use rate(…​) values from development. See EventBridge schedules.