Compare commits

...
6 changed files with 90 additions and 15 deletions
+2 -1
View File
@@ -87,6 +87,7 @@ Metrics/ModuleLength:
Rails/HelperInstanceVariable: Rails/HelperInstanceVariable:
Exclude: Exclude:
- enterprise/app/helpers/captain/chat_helper.rb - enterprise/app/helpers/captain/chat_helper.rb
- enterprise/app/helpers/captain/loggable.rb
Rails/ApplicationController: Rails/ApplicationController:
Exclude: Exclude:
@@ -336,4 +337,4 @@ FactoryBot/RedundantFactoryOption:
Enabled: false Enabled: false
FactoryBot/FactoryAssociationWithStrategy: FactoryBot/FactoryAssociationWithStrategy:
Enabled: false Enabled: false
+23 -11
View File
@@ -1,19 +1,21 @@
module Captain::ChatHelper module Captain::ChatHelper
include Captain::Loggable
def request_chat_completion def request_chat_completion
log_chat_completion_request log_chat_completion_request
response = @client.chat( tools = @tool_registry&.registered_tools || []
parameters: { temperature = @assistant&.config&.[]('temperature').to_f || 1
model: @model,
messages: @messages, log_captain_request(tools, temperature)
tools: @tool_registry&.registered_tools || [], start_time = Time.current
response_format: { type: 'json_object' }, response = @client.chat(parameters: chat_parameters(tools, temperature))
temperature: @assistant&.config&.[]('temperature').to_f || 1 duration_ms = ((Time.current - start_time) * 1000).round
} log_captain_response(response, duration_ms)
)
handle_response(response) handle_response(response)
rescue StandardError => e rescue StandardError => e
log_captain_error(e)
Rails.logger.error "#{self.class.name} Assistant: #{@assistant.id}, Error in chat completion: #{e}" Rails.logger.error "#{self.class.name} Assistant: #{@assistant.id}, Error in chat completion: #{e}"
raise e raise e
end end
@@ -61,6 +63,7 @@ module Captain::ChatHelper
'assistant_thinking' 'assistant_thinking'
) )
result = @tool_registry.send(function_name, arguments) result = @tool_registry.send(function_name, arguments)
log_captain_tool_call(function_name, arguments, result)
persist_message( persist_message(
{ {
content: I18n.t('captain.copilot.completed_tool_call', function_name: function_name), content: I18n.t('captain.copilot.completed_tool_call', function_name: function_name),
@@ -94,8 +97,17 @@ module Captain::ChatHelper
def log_chat_completion_request def log_chat_completion_request
Rails.logger.info( Rails.logger.info(
"#{self.class.name} Assistant: #{@assistant.id}, Requesting chat completion "#{self.class.name} Assistant: #{@assistant.id}, Requesting chat completion
for messages #{@messages} with #{@tool_registry&.registered_tools&.length || 0} tools for messages #{@messages} with #{@tool_registry&.registered_tools&.length || 0} tools"
"
) )
end end
def chat_parameters(tools, temperature)
{
model: @model,
messages: @messages,
tools: tools,
response_format: { type: 'json_object' },
temperature: temperature
}
end
end end
@@ -0,0 +1,54 @@
module Captain::Loggable
private
def log_captain_activity(event_type, log_data = {})
return if ENV['CAPTAIN_ENHANCED_LOGGING'].blank?
conversation_id = log_data[:conversation_id]
prefix = conversation_id ? "[##{conversation_id}]" : ''
# Add timestamp to all log data
log_data[:timestamp] = Time.current.iso8601
Rails.logger.info "[CAPTAIN_DEBUG] #{prefix} [#{event_type}] #{JSON.generate(log_data)}"
end
def log_captain_request(tools, temperature)
log_captain_activity(Llm::BaseOpenAiService::REQUEST, {
conversation_id: @conversation&.id,
assistant_id: @assistant&.id,
model: @model,
temperature: temperature,
messages_count: @messages.size,
tools_count: tools.size,
last_message: @messages.last
})
end
def log_captain_response(response, duration_ms)
log_captain_activity(Llm::BaseOpenAiService::RESPONSE, {
conversation_id: @conversation&.id,
duration_ms: duration_ms,
response: response.dig('choices', 0, 'message'),
usage: response['usage']
})
end
def log_captain_error(error)
log_captain_activity(Llm::BaseOpenAiService::ERROR, {
conversation_id: @conversation&.id,
error_class: error.class.name,
error_message: error.message,
context: { model: @model, assistant_id: @assistant&.id }
})
end
def log_captain_tool_call(function_name, arguments, result)
log_captain_activity(Llm::BaseOpenAiService::TOOL_CALL, {
conversation_id: @conversation&.id,
tool_name: function_name,
arguments: arguments,
result: result.to_s.truncate(1000)
})
end
end
@@ -26,7 +26,7 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
delegate :account, :inbox, to: :@conversation delegate :account, :inbox, to: :@conversation
def generate_and_process_response def generate_and_process_response
@response = Captain::Llm::AssistantChatService.new(assistant: @assistant).generate_response( @response = Captain::Llm::AssistantChatService.new(assistant: @assistant, conversation: @conversation).generate_response(
message_history: collect_previous_messages message_history: collect_previous_messages
) )
@@ -3,10 +3,11 @@ require 'openai'
class Captain::Llm::AssistantChatService < Llm::BaseOpenAiService class Captain::Llm::AssistantChatService < Llm::BaseOpenAiService
include Captain::ChatHelper include Captain::ChatHelper
def initialize(assistant: nil) def initialize(assistant: nil, conversation: nil)
super() super()
@assistant = assistant @assistant = assistant
@conversation = conversation
@messages = [system_message] @messages = [system_message]
@response = '' @response = ''
register_tools register_tools
@@ -1,6 +1,12 @@
class Llm::BaseOpenAiService class Llm::BaseOpenAiService
DEFAULT_MODEL = 'gpt-4o-mini'.freeze DEFAULT_MODEL = 'gpt-4o-mini'.freeze
# Captain logging event types
REQUEST = 'REQUEST'.freeze
RESPONSE = 'RESPONSE'.freeze
TOOL_CALL = 'TOOL_CALL'.freeze
ERROR = 'ERROR'.freeze
def initialize def initialize
@client = OpenAI::Client.new( @client = OpenAI::Client.new(
access_token: InstallationConfig.find_by!(name: 'CAPTAIN_OPEN_AI_API_KEY').value, access_token: InstallationConfig.find_by!(name: 'CAPTAIN_OPEN_AI_API_KEY').value,
@@ -15,7 +21,8 @@ class Llm::BaseOpenAiService
private private
def uri_base def uri_base
InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value || 'https://api.openai.com/' endpoint = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value
endpoint.presence || 'https://api.openai.com/'
end end
def setup_model def setup_model