From 41670bfae9668273c17d9d802ad4de18d7d6b517 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 22 May 2026 21:25:28 +0530 Subject: [PATCH] refactor: extract agent runner service concerns Split RunContextNormalizer and StateBuilder out of Captain::Assistant::AgentRunnerService to keep the service within the class length budget. --- .../captain/assistant/agent_runner_service.rb | 70 +------------------ .../assistant/run_context_normalizer.rb | 34 +++++++++ .../captain/assistant/state_builder.rb | 41 +++++++++++ 3 files changed, 77 insertions(+), 68 deletions(-) create mode 100644 enterprise/app/services/captain/assistant/run_context_normalizer.rb create mode 100644 enterprise/app/services/captain/assistant/state_builder.rb diff --git a/enterprise/app/services/captain/assistant/agent_runner_service.rb b/enterprise/app/services/captain/assistant/agent_runner_service.rb index e7d72dc4d..5f6089aa3 100644 --- a/enterprise/app/services/captain/assistant/agent_runner_service.rb +++ b/enterprise/app/services/captain/assistant/agent_runner_service.rb @@ -5,20 +5,9 @@ class Captain::Assistant::AgentRunnerService include Integrations::LlmInstrumentationConstants include Captain::Assistant::RunnerCallbacksHelper include Captain::Assistant::TracePayloadHelper + include Captain::Assistant::RunContextNormalizer + include Captain::Assistant::StateBuilder - 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 def initialize(assistant:, conversation: nil, callbacks: {}, source: nil) @assistant = assistant @conversation = conversation @@ -107,37 +96,6 @@ class Captain::Assistant::AgentRunnerService response end - def build_run_context(result, response) - { - 'messages' => normalized_run_messages(result.messages || []), - 'handoff_tool_called' => response['handoff_tool_called'] - } - end - - def normalized_run_messages(messages) - normalized_messages = messages.map { |message| normalize_run_message(message) } - final_assistant_message = normalized_messages.reverse.find { |message| message['role'] == 'assistant' } - strip_final_response(final_assistant_message) if final_assistant_message - normalized_messages - end - - def normalize_run_message(message) - normalized = message.deep_stringify_keys - normalized['role'] = normalized['role'].to_s if normalized['role'].present? - normalized['content'] = normalized['content'].deep_stringify_keys if normalized['content'].is_a?(Hash) - normalized - end - - def strip_final_response(message) - message['content'] = normalized_final_message_content(message['content']) - end - - def normalized_final_message_content(content) - return content.except('response') if content.is_a?(Hash) - - {} - end - def error_response(error_message) { 'response' => 'conversation_handoff', @@ -146,30 +104,6 @@ class Captain::Assistant::AgentRunnerService } end - def build_state - state = { - account_id: @assistant.account_id, - assistant_id: @assistant.id, - assistant_config: @assistant.config - } - 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 - def build_and_wire_agents assistant_agent = @assistant.agent scenario_agents = @assistant.scenarios.enabled.map(&:agent) diff --git a/enterprise/app/services/captain/assistant/run_context_normalizer.rb b/enterprise/app/services/captain/assistant/run_context_normalizer.rb new file mode 100644 index 000000000..b37c22367 --- /dev/null +++ b/enterprise/app/services/captain/assistant/run_context_normalizer.rb @@ -0,0 +1,34 @@ +module Captain::Assistant::RunContextNormalizer + private + + def build_run_context(result, response) + { + 'messages' => normalized_run_messages(result.messages || []), + 'handoff_tool_called' => response['handoff_tool_called'] + } + end + + def normalized_run_messages(messages) + normalized_messages = messages.map { |message| normalize_run_message(message) } + final_assistant_message = normalized_messages.reverse.find { |message| message['role'] == 'assistant' } + strip_final_response(final_assistant_message) if final_assistant_message + normalized_messages + end + + def normalize_run_message(message) + normalized = message.deep_stringify_keys + normalized['role'] = normalized['role'].to_s if normalized['role'].present? + normalized['content'] = normalized['content'].deep_stringify_keys if normalized['content'].is_a?(Hash) + normalized + end + + def strip_final_response(message) + message['content'] = normalized_final_message_content(message['content']) + end + + def normalized_final_message_content(content) + return content.except('response') if content.is_a?(Hash) + + {} + end +end diff --git a/enterprise/app/services/captain/assistant/state_builder.rb b/enterprise/app/services/captain/assistant/state_builder.rb new file mode 100644 index 000000000..99b2e0ddb --- /dev/null +++ b/enterprise/app/services/captain/assistant/state_builder.rb @@ -0,0 +1,41 @@ +module Captain::Assistant::StateBuilder + 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 + } + 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