Records a `Captain::Session` row for every Captain V2 assistant response delivered in a conversation, so we can show how a response was generated and report on credit, FAQ, and document usage. Stacked on #14970 (the `captain_sessions` model). ## What changed - `FaqLookupTool` now records the retrieved FAQ ids (and their backing document ids) into the shared run state, accumulated across tool calls. - `AgentRunnerService` exposes the raw ai-agents run result via `last_run_result`; the `generate_response` return shape is unchanged, so the playground path is unaffected. - New `Captain::Assistant::SessionCaptureService` builds the session: scenario resolved from the answering agent name, model from `assistant.agent_model`, token usage plus the trimmed current-turn conversation history stored in `run_context`. - `ResponseBuilderJob` captures after delivery: `credits_consumed` mirrors the actual charge (1.0 for a billed response, 0.0 for handoffs, where the session points at the customer-facing handoff message). Capture runs outside the delivery transaction and swallows its own failures, so a logging bug can never block or roll back a customer reply. V1 responses and copilot are out of scope; copilot capture comes next. ## How to test On an account with `captain_integration_v2` enabled and an inbox connected to an assistant with approved FAQs, send a customer message on a pending conversation. After the assistant replies, a `Captain::Session` row should exist with the conversation as subject, the reply message as result, the FAQs/documents used, and the run context for that turn. Asking for a human agent should produce a zero-credit session pointing at the handoff message. <img width="2428" height="1058" alt="CleanShot 2026-07-15 at 17 25 40@2x" src="https://github.com/user-attachments/assets/d8e44923-c17b-494f-8c33-c8fa4219438c" />
43 lines
1.5 KiB
Ruby
43 lines
1.5 KiB
Ruby
module Captain::Assistant::RunnerStateHelper
|
|
CONVERSATION_STATE_ATTRIBUTES = %i[
|
|
id display_id inbox_id contact_id status priority
|
|
label_list custom_attributes additional_attributes
|
|
].freeze
|
|
|
|
CONTACT_STATE_ATTRIBUTES = %i[
|
|
id name email phone_number identifier contact_type
|
|
custom_attributes additional_attributes
|
|
].freeze
|
|
|
|
CONTACT_INBOX_STATE_ATTRIBUTES = %i[id hmac_verified].freeze
|
|
|
|
CAMPAIGN_STATE_ATTRIBUTES = %i[id title message campaign_type description].freeze
|
|
|
|
private
|
|
|
|
def build_state
|
|
state = {
|
|
account_id: @assistant.account_id,
|
|
assistant_id: @assistant.id,
|
|
assistant_config: @assistant.config,
|
|
timezone: @conversation&.inbox&.timezone.presence || 'UTC'
|
|
}
|
|
state[:source] = @source if @source.present?
|
|
|
|
build_conversation_state(state) if @conversation
|
|
state
|
|
end
|
|
|
|
def build_conversation_state(state)
|
|
state[:conversation] = slice_attrs(@conversation, CONVERSATION_STATE_ATTRIBUTES)
|
|
state[:channel_type] = @conversation.inbox&.channel_type
|
|
state[:contact] = slice_attrs(@conversation.contact, CONTACT_STATE_ATTRIBUTES) if @conversation.contact
|
|
state[:campaign] = slice_attrs(@conversation.campaign, CAMPAIGN_STATE_ATTRIBUTES) if @conversation.campaign
|
|
state[:contact_inbox] = slice_attrs(@conversation.contact_inbox, CONTACT_INBOX_STATE_ATTRIBUTES) if @conversation.contact_inbox
|
|
end
|
|
|
|
def slice_attrs(record, keys)
|
|
record.attributes.symbolize_keys.slice(*keys)
|
|
end
|
|
end
|