86 lines
2.7 KiB
Ruby
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
|