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" />
54 lines
1.6 KiB
Ruby
54 lines
1.6 KiB
Ruby
module Captain::Conversation::MessageBuilder
|
|
private
|
|
|
|
def collect_previous_messages
|
|
@conversation
|
|
.messages
|
|
.where(message_type: [:incoming, :outgoing])
|
|
.where(private: false)
|
|
.map do |message|
|
|
message_hash = {
|
|
content: prepare_multimodal_message_content(message),
|
|
role: determine_role(message)
|
|
}
|
|
|
|
# Include agent_name if present in additional_attributes
|
|
message_hash[:agent_name] = message.additional_attributes['agent_name'] if message.additional_attributes&.dig('agent_name').present?
|
|
|
|
message_hash
|
|
end
|
|
end
|
|
|
|
def determine_role(message)
|
|
message.message_type == 'incoming' ? 'user' : 'assistant'
|
|
end
|
|
|
|
def prepare_multimodal_message_content(message)
|
|
Captain::OpenAiMessageBuilderService.new(message: message).generate_content
|
|
end
|
|
|
|
def create_messages
|
|
validate_message_content!(@response['response'])
|
|
create_outgoing_message(@response['response'], agent_name: @response['agent_name'])
|
|
end
|
|
|
|
def validate_message_content!(content)
|
|
raise ArgumentError, 'Message content cannot be blank' if content.blank?
|
|
end
|
|
|
|
def create_outgoing_message(message_content, agent_name: nil, preserve_waiting_since: false)
|
|
additional_attrs = {}
|
|
additional_attrs[:agent_name] = agent_name if agent_name.present?
|
|
|
|
@conversation.messages.create!(
|
|
message_type: :outgoing,
|
|
account_id: account.id,
|
|
inbox_id: inbox.id,
|
|
sender: @assistant,
|
|
content: message_content,
|
|
additional_attributes: additional_attrs,
|
|
preserve_waiting_since: preserve_waiting_since
|
|
)
|
|
end
|
|
end
|