AI Origin & Member Attribution
Every outbound message tracks two attribution fields: ai_origin records which AI feature composed it, and sent_by_member_id records which team member triggered the send. Together they power audit trails, analytics, and the dashboard breakdown.
Member Attribution
Section titled “Member Attribution”The sent_by_member_id field is a nullable UUID foreign key to member. User-initiated sends automatically populate it from the session; system sends leave it null.
| Source | sent_by_member_id | ai_origin |
|---|---|---|
| User types and sends manually | Session member | None |
AI Assistant compose (ai_send_sms_tool) | Session member | Assistant |
Bulk SMS tool (ai_send_bulk_sms_tool) | Session member | Assistant |
| User accepts inline suggestion | Session member | Suggestion |
| Text Agent auto-reply | None | TextAgent |
| Plan send-SMS / send-email step | None | Plan |
| Twilio inbound / sync | None | None |
| Resend inbound webhook | None | None |
Database Column
Section titled “Database Column”ALTER TABLE messageADD COLUMN sent_by_member_id UUID REFERENCES member(id) ON DELETE SET NULL;
CREATE INDEX idx_message_sent_by_member_id ON message (sent_by_member_id);ON DELETE SET NULL preserves the message if a member is removed from the organization.
AiOrigin Enum
Section titled “AiOrigin Enum”pub enum AiOrigin { Assistant, // Sent via the in-app AI Assistant Suggestion, // User accepted an inline AI suggestion TextAgent, // Auto-replied by a Text Agent Plan, // Sent by an automated plan step}Each variant serializes to snake_case ("assistant", "suggestion", "text_agent", "plan"). The enum implements FromStr and Display for round-trip conversion.
display_label() returns the human-readable name used in UI tooltips and dashboard charts:
| Variant | as_str() | display_label() |
|---|---|---|
Assistant | "assistant" | "Assistant" |
Suggestion | "suggestion" | "Suggestion" |
TextAgent | "text_agent" | "Text Agent" |
Plan | "plan" | "Automated Plan" |
How Attribution Gets Set
Section titled “How Attribution Gets Set”For user-initiated sends, the server reads session.member_id and sets sent_by_member_id automatically — the client never sends it. For AI origin, client-driven paths like inline suggestions pass ai_origin: "suggestion" in the request body, while server-driven paths (assistant tools, text agent, plans) set it internally.
For suggestions, the client tracks origin in a signal. When the user clicks Use on a suggestion, the compose bar stores "suggestion" and passes it to POST /api/messages/send.
Sending with AI Origin
Section titled “Sending with AI Origin”Pass ai_origin as an optional string in the send-message request:
POST /api/messages/send{ "contact_id": "a1b2c3d4-...", "channel": "Sms", "body": "Thanks for reaching out! I can help with that.", "phone_number_id": "e5f6a7b8-...", "ai_origin": "suggestion"}The server parses the string to AiOrigin and returns 400 for invalid values. Omit the field or pass null for human-composed messages.
Message Bubble Indicator
Section titled “Message Bubble Indicator”Outbound messages with a non-null ai_origin display a sparkle icon (✨) next to the timestamp. Hovering shows a tooltip with the feature name:
- Assistant → “Composed by Loquent AI (Assistant)”
- Suggestion → “Composed from Suggestion”
- Text Agent → “Auto-replied by Text Agent”
- Plan → “Sent by Automated Plan”
The indicator renders in MessageEntry after the delivery status icon:
if let Some(ref origin) = message.ai_origin { Tooltip { text: ai_origin_tooltip(origin), side: TooltipSide::TopRight, span { class: "inline-flex items-center", Sparkles { class: "w-3 h-3 text-primary/50" } } }}Dashboard Analytics
Section titled “Dashboard Analytics”A new dashboard card shows AI-composed message volume and breakdown by feature.
Endpoint
Section titled “Endpoint”GET /api/dashboard/ai-message-origin-stats?time_range=Last7DaysResponse:
{ "total_ai_messages": 42, "total_messages": 100, "by_origin": [ { "origin": "Assistant", "count": 25 }, { "origin": "Suggestion", "count": 10 }, { "origin": "Text Agent", "count": 5 }, { "origin": "Automated Plan", "count": 2 } ]}The query filters outbound messages by organization_id, time range, and dashboard scope (org-wide or scoped to member’s assigned phones). Results are grouped by ai_origin with a B-tree index for fast aggregation.
AiMessageOriginStats
Section titled “AiMessageOriginStats”pub struct AiMessageOriginStats { pub total_ai_messages: u64, pub total_messages: u64, pub by_origin: Vec<AiOriginCount>,}
pub struct AiOriginCount { pub origin: String, pub count: u64,}Dashboard Card
Section titled “Dashboard Card”The AiMessageOrigin component renders:
- Total AI messages count and AI share percentage
- Horizontal bar chart showing each feature’s contribution, sorted by count descending
Empty states: “No messages yet” when total_messages is zero, “No AI-composed messages yet” when only total_ai_messages is zero.
Source Column Normalization
Section titled “Source Column Normalization”Before this feature, source mixed transport-level and feature-level values ("text_agent", "plan"). Now:
sourcetracks transport only:"loquent","external","import"ai_origintracks the AI feature:"assistant","suggestion","text_agent","plan"
Text Agent and Plan messages now use source: "loquent" with the appropriate ai_origin value.
Database
Section titled “Database”Two columns track attribution:
message.ai_origin—VARCHAR, nullable. Index:idx_message_ai_origin(B-tree) forGROUP BYperformance. Migration:m20260403_120000_message_add_ai_origin.message.sent_by_member_id—UUID, nullable FK tomember(id)withON DELETE SET NULL. Index:idx_message_sent_by_member_id. Migration:m20260405_120000_message_add_sent_by_member_id.
Existing messages have both fields as NULL. Parse errors on ai_origin silently fall back to None.
Related
Section titled “Related”- Messaging Overview —
Messagetype and send API - Dashboard — analytics endpoints and UI cards
- Text Agents — auto-reply pipeline