Files

86 lines
2.7 KiB
Ruby

class Captain::Assistant::SessionCaptureService
SCENARIO_AGENT_REGEX = /\A#{Captain::Scenario::HANDOFF_KEY_PREFIX}_(\d+)_/
def initialize(assistant:, conversation:, run_result:, result_message:, credits_consumed:)
@assistant = assistant
@conversation = conversation
@run_result = run_result
@result_message = result_message
@credits_consumed = credits_consumed
end
def capture
# TODO: Capture failed runs once error-session semantics are defined. For now,
# only successful runs that produce a customer-facing reply or handoff are recorded.
return unless @run_result&.success?
capture!
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: @assistant.account).capture_exception
Rails.logger.error("[CAPTAIN][SessionCaptureService] Capture failed for conversation=#{@conversation.display_id}: #{e.message}")
end
def capture!
model = @assistant.agent_model
Captain::AgentSession.create!(
assistant: @assistant,
session_type: :assistant,
subject: @conversation,
result: result_message,
llm_model: "#{Llm::Models.provider_for(model)}-#{model}",
credits_consumed: @credits_consumed,
faq_ids: metadata[:faq_ids] || [],
document_ids: metadata[:document_ids] || [],
scenario_ids: scenario_ids,
run_context: current_turn_history
)
end
private
def context
@run_result.context || {}
end
def metadata
@metadata ||= context.dig(:state, :cw_metadata) || {}
end
# On handoff, HandoffTool records the private reason note it created; the session
# attaches there so agents can inspect the generation path on the note itself.
def result_message
handoff_note || @result_message
end
def handoff_note
note_id = metadata[:handoff_note_id]
return if note_id.blank?
@conversation.messages.find_by(id: note_id)
end
def scenario_ids
ids = current_turn_history.filter_map do |message|
next unless message[:role].to_s == 'assistant'
message[:agent_name].to_s.match(SCENARIO_AGENT_REGEX)&.[](1)&.to_i
end.uniq
ids & @assistant.scenarios.where(id: ids).pluck(:id)
end
# Trim to the current turn: the last user message and everything after it
# (assistant replies, tool calls/results, handoff hops).
def current_turn_history
history = Array(context[:conversation_history])
last_user_index = history.rindex { |message| message[:role].to_s == 'user' }
current_turn = last_user_index ? history[last_user_index..] : history
current_turn.map do |message|
content = message[:content]
content.is_a?(RubyLLM::Content) ? message.merge(content: content.to_h) : message
end
end
end