Contact Filter & Bulk SMS Tools
The assistant uses the active page context — filters, inbox tab, search query — to scope contact operations. Two tools enable filter-aware workflows: get_contacts_by_filter resolves the visible contact set, and send_bulk_sms broadcasts messages to multiple contacts with a preview-confirm flow.
Tools at a Glance
Section titled “Tools at a Glance”| Tool | Permission Required | Purpose |
|---|---|---|
get_contacts_by_filter | Contact:Collection:List or ListAssigned | Resolve UI filters into a contact list with message counts |
send_bulk_sms | Message:Collection:Create | Send identical SMS to up to 50 contacts per call |
How Page Context Drives Scope
Section titled “How Page Context Drives Scope”When a user opens the assistant from a contact-aware page (Contacts list, Messaging inbox), the router extracts an AssistantActiveContactScope from the URL query parameters:
pub struct AssistantActiveContactScope { pub kind: AssistantActiveContactScopeKind, pub filter: ContactFilter,}
pub enum AssistantActiveContactScopeKind { FilteredContacts, Unanswered { direction: UnansweredDirection },}The kind field maps to the page type:
| Page / Tab | kind | direction |
|---|---|---|
| Contacts list (with filters) | FilteredContacts | — |
| Messaging → Needs Reply | Unanswered | NeedsReply |
| Messaging → Needs Follow-up | Unanswered | NeedsFollowUp |
The system prompt injects the scope summary and the serialized ContactFilter JSON, so the assistant knows exactly which contacts “these contacts” refers to.
Context-Aware Contact Deep Links
Section titled “Context-Aware Contact Deep Links”When the user is on a messaging page, contact deep links in tool results route to /messaging/{id} instead of /contacts/{id}. If the page has active filters (tab, search, filter state), those are appended as query parameters so clicking a link preserves the user’s view.
PageContext carries a filter_query_string field populated from the ContactFilterQuery on messaging routes:
pub struct PageContext { pub route: String, pub entity_type: Option<String>, pub entity_id: Option<String>, pub active_contact_scope: Option<AssistantActiveContactScope>, /// Dioxus-router-encoded filter query string from the current page URL. pub filter_query_string: Option<String>,}The contact_deep_link() helper in page_context_mapper.rs handles the routing:
pub fn contact_deep_link(contact_id: &str, page_context: Option<&PageContext>) -> String { if let Some(ctx) = page_context { if ctx.route.starts_with("/messaging") { return match &ctx.filter_query_string { Some(qs) if !qs.is_empty() => format!("/messaging/{contact_id}?{qs}"), _ => format!("/messaging/{contact_id}"), }; } } entity_deep_link("contact", contact_id)}Routing behavior:
| User’s current page | Generated deep link |
|---|---|
/messaging?tab~needs_reply&search~vip | /messaging/{id}?tab~needs_reply&search~vip |
/messaging (no filters) | /messaging/{id} |
/contacts or any other page | /contacts/{id} |
Eight tools use contact_deep_link(): get_contacts_by_filter, search_contacts, get_contact_details, find_contact_by_phone, get_fresh_contacts, get_unanswered_contacts, get_contact_messages, and send_sms. Each receives page_context via collect_assistant_tools(), which threads it from the router.
File: src/mods/assistant/page_context_mapper.rs
get_contacts_by_filter
Section titled “get_contacts_by_filter”Resolves a ContactFilter into matching contacts with message count metadata.
Input:
| Parameter | Type | Default | Description |
|---|---|---|---|
filter_json | String | — | JSON-serialized ContactFilter from page context |
limit | Option<u32> | 100 | Results per page (max 500) |
offset | Option<u32> | 0 | Pagination offset |
Response shape:
{ "count": 25, "total_count": 150, "has_more": true, "contacts": [ { "id": "uuid", "deep_link": "/messaging/{id}?tab~needs_reply&search~vip", "first_name": "John", "last_name": "Doe", "status": "active", "phones": [ { "phone_number": "+15551234567", "label": "mobile", "is_preferred": true } ], "tags": [{ "name": "vip" }], "total_messages": 42, "inbound_messages_count": 18, "outbound_messages_count": 24 } ]}The message counts (total_messages, inbound_messages_count, outbound_messages_count) come from get_contact_message_counts — a batched query that groups by contact_id and direction in a single SQL call.
File: src/mods/assistant/tools/contacts/ai_get_contacts_by_filter_tool.rs
send_bulk_sms
Section titled “send_bulk_sms”Sends an identical message to multiple contacts. Uses a two-phase preview-confirm flow.
Input:
| Parameter | Type | Default | Description |
|---|---|---|---|
contact_ids | Vec<String> | — | Contact UUIDs (max 50 per call) |
message_body | String | — | The SMS text to send |
confirmed | bool | — | false = preview, true = send |
Phase 1 — Preview (confirmed: false):
{ "status": "preview", "contact_count": 3, "contacts": [ { "contact_id": "uuid", "deep_link": "/messaging/{id}?tab~needs_reply", "name": "John Doe", "phone_number": "+15551234567" } ], "message_body": "Your appointment is tomorrow at 10 AM.", "estimated_cost": "$0.03"}Phase 2 — Send (confirmed: true):
{ "status": "sent", "contact_count": 3, "message_count": 3}Phone resolution uses is_preferred numbers first, then falls back to resolve_phone_number_for_contact for org phone selection.
File: src/mods/assistant/tools/messages/ai_send_bulk_sms_tool.rs
Send Strategy: Bulk vs. Individualized
Section titled “Send Strategy: Bulk vs. Individualized”The system prompt guides the assistant to choose between send_bulk_sms and iterative send_sms calls:
Use send_bulk_sms | Use send_sms (per contact) |
|---|---|
| Identical message for all contacts | Contact has a known name for personalization |
| No inbound history to reference | Contact has inbound message history |
| Generic outreach or announcements | User requests personalized drafts |
The assistant checks inbound_messages_count from the filter results: if it’s 0, there’s no conversation history to personalize against, so bulk is appropriate. If it’s > 0, the assistant loads the thread with get_contact_messages before drafting.
Shared Contact Collection Scope
Section titled “Shared Contact Collection Scope”Both tools and the existing contact/messaging APIs use resolve_contact_collection_scope for ABAC enforcement:
pub struct ContactCollectionScope { pub can_list_all: bool, pub visible_member_id: Option<Uuid>, pub member_scope: ContactMemberScope,}This resolver centralizes the permission check — users with List see all contacts, users with ListAssigned see only their assigned contacts. The scope applies identically whether the query comes from the UI API or an assistant tool.
File: src/mods/contact/services/resolve_contact_collection_scope_service.rs