feat: Add support for more tool, standardize copilot chat service (#11560)
This commit is contained in:
@@ -19,9 +19,9 @@ module Enterprise::Api::V1::Accounts::ConversationsController
|
||||
|
||||
response = Captain::Copilot::ChatService.new(
|
||||
assistant,
|
||||
previous_messages: copilot_params[:previous_messages],
|
||||
conversation_history: @conversation.to_llm_text,
|
||||
language: @conversation.account.locale_english_name
|
||||
previous_history: copilot_params[:previous_history],
|
||||
conversation_id: @conversation.display_id,
|
||||
user_id: Current.user.id
|
||||
).generate_response(copilot_params[:message])
|
||||
|
||||
render json: { message: response['response'] }
|
||||
@@ -44,6 +44,6 @@ module Enterprise::Api::V1::Accounts::ConversationsController
|
||||
private
|
||||
|
||||
def copilot_params
|
||||
params.permit(:previous_messages, :message, :assistant_id)
|
||||
params.permit(:previous_history, :message, :assistant_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module Captain::ChatHelper
|
||||
def request_chat_completion
|
||||
Rails.logger.debug { "[CAPTAIN][ChatCompletion] #{@messages}" }
|
||||
log_chat_completion_request
|
||||
|
||||
response = @client.chat(
|
||||
parameters: {
|
||||
@@ -15,13 +15,17 @@ module Captain::ChatHelper
|
||||
handle_response(response)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_response(response)
|
||||
Rails.logger.debug { "[CAPTAIN][ChatCompletion] #{response}" }
|
||||
Rails.logger.debug { "#{self.class.name} Assistant: #{@assistant.id}, Received response #{response}" }
|
||||
message = response.dig('choices', 0, 'message')
|
||||
if message['tool_calls']
|
||||
process_tool_calls(message['tool_calls'])
|
||||
else
|
||||
JSON.parse(message['content'].strip)
|
||||
message = JSON.parse(message['content'].strip)
|
||||
persist_message(message, 'assistant')
|
||||
message
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,12 +45,14 @@ module Captain::ChatHelper
|
||||
if @tool_registry.respond_to?(function_name)
|
||||
execute_tool(function_name, arguments, tool_call_id)
|
||||
else
|
||||
process_invalid_tool_call(tool_call_id)
|
||||
process_invalid_tool_call(function_name, tool_call_id)
|
||||
end
|
||||
end
|
||||
|
||||
def execute_tool(function_name, arguments, tool_call_id)
|
||||
persist_message({ content: "Using tool #{function_name}", function_name: function_name }, 'assistant_thinking')
|
||||
result = @tool_registry.send(function_name, arguments)
|
||||
persist_message({ content: "Completed #{function_name} tool call", function_name: function_name }, 'assistant_thinking')
|
||||
append_tool_response(result, tool_call_id)
|
||||
end
|
||||
|
||||
@@ -57,7 +63,8 @@ module Captain::ChatHelper
|
||||
}
|
||||
end
|
||||
|
||||
def process_invalid_tool_call(tool_call_id)
|
||||
def process_invalid_tool_call(function_name, tool_call_id)
|
||||
persist_message({ content: 'Invalid tool call', function_name: function_name }, 'assistant_thinking')
|
||||
append_tool_response('Tool not available', tool_call_id)
|
||||
end
|
||||
|
||||
@@ -68,4 +75,12 @@ module Captain::ChatHelper
|
||||
content: content
|
||||
}
|
||||
end
|
||||
|
||||
def log_chat_completion_request
|
||||
Rails.logger.info(
|
||||
"#{self.class.name} Assistant: #{@assistant.id}, Requesting chat completion
|
||||
for messages #{@messages} with #{@tool_registry&.registered_tools&.length || 0} tools
|
||||
"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,6 +25,7 @@ class CopilotMessage < ApplicationRecord
|
||||
|
||||
validates :message_type, presence: true, inclusion: { in: message_types.keys }
|
||||
validates :message, presence: true
|
||||
validate :validate_message_attributes
|
||||
|
||||
after_create_commit :broadcast_message
|
||||
|
||||
@@ -47,4 +48,13 @@ class CopilotMessage < ApplicationRecord
|
||||
def broadcast_message
|
||||
Rails.configuration.dispatcher.dispatch(COPILOT_MESSAGE_CREATED, Time.zone.now, copilot_message: self)
|
||||
end
|
||||
|
||||
def validate_message_attributes
|
||||
return if message.blank?
|
||||
|
||||
allowed_keys = %w[content reasoning function_name]
|
||||
invalid_keys = message.keys - allowed_keys
|
||||
|
||||
errors.add(:message, "contains invalid attributes: #{invalid_keys.join(', ')}") if invalid_keys.any?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,7 +40,7 @@ class CopilotThread < ApplicationRecord
|
||||
.order(created_at: :asc)
|
||||
.map do |copilot_message|
|
||||
{
|
||||
content: copilot_message.message,
|
||||
content: copilot_message.message['content'],
|
||||
role: copilot_message.message_type
|
||||
}
|
||||
end
|
||||
|
||||
@@ -6,7 +6,6 @@ module Enterprise::Concerns::User
|
||||
|
||||
has_many :captain_responses, class_name: 'Captain::AssistantResponse', dependent: :nullify, as: :documentable
|
||||
has_many :copilot_threads, dependent: :destroy_async
|
||||
has_many :copilot_messages, dependent: :destroy_async
|
||||
end
|
||||
|
||||
def ensure_installation_pricing_plan_quantity
|
||||
|
||||
@@ -3,49 +3,110 @@ require 'openai'
|
||||
class Captain::Copilot::ChatService < Llm::BaseOpenAiService
|
||||
include Captain::ChatHelper
|
||||
|
||||
attr_reader :assistant, :account, :user, :copilot_thread, :previous_history, :messages
|
||||
|
||||
def initialize(assistant, config)
|
||||
super()
|
||||
|
||||
@assistant = assistant
|
||||
@conversation_history = config[:conversation_history]
|
||||
@previous_messages = config[:previous_messages] || []
|
||||
@language = config[:language] || 'english'
|
||||
|
||||
@account = assistant.account
|
||||
@user = nil
|
||||
@copilot_thread = nil
|
||||
@previous_history = []
|
||||
setup_user(config)
|
||||
setup_message_history(config)
|
||||
register_tools
|
||||
@messages = [system_message, conversation_history_context] + @previous_messages
|
||||
@response = ''
|
||||
@messages = build_messages(config)
|
||||
end
|
||||
|
||||
def generate_response(input)
|
||||
@messages << { role: 'user', content: input } if input.present?
|
||||
response = request_chat_completion
|
||||
Rails.logger.info("[CAPTAIN][CopilotChatService] Incrementing response usage for #{@assistant.account.id}")
|
||||
@assistant.account.increment_response_usage
|
||||
|
||||
Rails.logger.debug { "#{self.class.name} Assistant: #{@assistant.id}, Received response #{response}" }
|
||||
Rails.logger.info(
|
||||
"#{self.class.name} Assistant: #{@assistant.id}, Incrementing response usage for account #{@account.id}"
|
||||
)
|
||||
@account.increment_response_usage
|
||||
|
||||
response
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def setup_user(config)
|
||||
@user = @account.users.find_by(id: config[:user_id]) if config[:user_id].present?
|
||||
end
|
||||
|
||||
def build_messages(config)
|
||||
messages= [system_message]
|
||||
messages << account_id_context
|
||||
messages += @previous_history if @previous_history.present?
|
||||
messages += current_viewing_history(config[:conversation_id]) if config[:conversation_id].present?
|
||||
messages
|
||||
end
|
||||
|
||||
def setup_message_history(config)
|
||||
Rails.logger.info(
|
||||
"#{self.class.name} Assistant: #{@assistant.id}, Previous History: #{config[:previous_history]&.length || 0}, Language: #{config[:language]}"
|
||||
)
|
||||
|
||||
@copilot_thread = @account.copilot_threads.find_by(id: config[:thread_id]) if config[:thread_id].present?
|
||||
@previous_history = if @copilot_thread.present?
|
||||
@copilot_thread.previous_history
|
||||
else
|
||||
config[:previous_history].presence || []
|
||||
end
|
||||
end
|
||||
|
||||
def register_tools
|
||||
@tool_registry = Captain::ToolRegistryService.new(@assistant)
|
||||
@tool_registry = Captain::ToolRegistryService.new(@assistant, user: @user)
|
||||
@tool_registry.register_tool(Captain::Tools::SearchDocumentationService)
|
||||
@tool_registry.register_tool(Captain::Tools::Copilot::GetArticleService)
|
||||
@tool_registry.register_tool(Captain::Tools::Copilot::GetContactService)
|
||||
@tool_registry.register_tool(Captain::Tools::Copilot::GetConversationService)
|
||||
@tool_registry.register_tool(Captain::Tools::Copilot::SearchArticlesService)
|
||||
@tool_registry.register_tool(Captain::Tools::Copilot::SearchContactsService)
|
||||
@tool_registry.register_tool(Captain::Tools::Copilot::SearchConversationsService)
|
||||
@tool_registry.register_tool(Captain::Tools::Copilot::SearchLinearIssuesService)
|
||||
end
|
||||
|
||||
def system_message
|
||||
{
|
||||
role: 'system',
|
||||
content: Captain::Llm::SystemPromptsService.copilot_response_generator(@assistant.config['product_name'], @language)
|
||||
content: Captain::Llm::SystemPromptsService.copilot_response_generator(@assistant.config['product_name'])
|
||||
}
|
||||
end
|
||||
|
||||
def conversation_history_context
|
||||
def account_id_context
|
||||
{
|
||||
role: 'system',
|
||||
content: "
|
||||
Message History with the user is below:
|
||||
#{@conversation_history}
|
||||
"
|
||||
content: "The current account id is #{@account.id}. The account is using #{@account.locale_english_name} as the language."
|
||||
}
|
||||
end
|
||||
|
||||
def current_viewing_history(conversation_id)
|
||||
conversation = @account.conversations.find_by(display_id: conversation_id)
|
||||
return [] unless conversation
|
||||
|
||||
Rails.logger.info("#{self.class.name} Assistant: #{@assistant.id}, Setting viewing history for conversation_id=#{conversation_id}")
|
||||
contact_id = conversation.contact_id
|
||||
[{
|
||||
role: 'system',
|
||||
content: <<~HISTORY.strip
|
||||
You are currently viewing the conversation with the following details:
|
||||
Conversation ID: #{conversation_id}
|
||||
Contact ID: #{contact_id}
|
||||
HISTORY
|
||||
}]
|
||||
end
|
||||
|
||||
def persist_message(message, message_type = 'assistant')
|
||||
return if @copilot_thread.blank?
|
||||
|
||||
@copilot_thread.copilot_messages.create!(
|
||||
message: message,
|
||||
message_type: message_type
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,7 +21,7 @@ class Captain::Llm::AssistantChatService < Llm::BaseOpenAiService
|
||||
private
|
||||
|
||||
def register_tools
|
||||
@tool_registry = Captain::ToolRegistryService.new(@assistant)
|
||||
@tool_registry = Captain::ToolRegistryService.new(@assistant, user: nil)
|
||||
@tool_registry.register_tool(Captain::Tools::SearchDocumentationService)
|
||||
end
|
||||
|
||||
@@ -31,4 +31,8 @@ class Captain::Llm::AssistantChatService < Llm::BaseOpenAiService
|
||||
content: Captain::Llm::SystemPromptsService.assistant_response_generator(@assistant.name, @assistant.config['product_name'], @assistant.config)
|
||||
}
|
||||
end
|
||||
|
||||
def persist_message(message, message_type = 'assistant')
|
||||
# No need to implement
|
||||
end
|
||||
end
|
||||
|
||||
@@ -56,18 +56,18 @@ class Captain::Llm::SystemPromptsService
|
||||
SYSTEM_PROMPT_MESSAGE
|
||||
end
|
||||
|
||||
def copilot_response_generator(product_name, language)
|
||||
def copilot_response_generator(product_name)
|
||||
<<~SYSTEM_PROMPT_MESSAGE
|
||||
[Identity]
|
||||
You are Captain, a helpful and friendly copilot assistant for support agents using the product #{product_name}. Your primary role is to assist support agents by retrieving information, compiling accurate responses, and guiding them through customer interactions.
|
||||
You should only provide information related to #{product_name} and must not address queries about other products or external events.
|
||||
|
||||
[Context]
|
||||
You will be provided with the message history between the support agent and the customer. Use this context to understand the conversation flow, identify unresolved queries, and ensure responses are relevant and consistent with previous interactions. Always maintain a coherent and professional tone throughout the conversation.
|
||||
Identify unresolved queries, and ensure responses are relevant and consistent with previous interactions. Always maintain a coherent and professional tone throughout the conversation.
|
||||
|
||||
[Response Guidelines]
|
||||
- Use natural, polite, and conversational language that is clear and easy to follow. Keep sentences short and use simple words.
|
||||
- Reply in the language the agent is using, if you're not able to detect the language, reply in #{language}.
|
||||
- Reply in the language the agent is using, if you're not able to detect the language.
|
||||
- Provide brief and relevant responses—typically one or two sentences unless a more detailed explanation is necessary.
|
||||
- Do not use your own training data or assumptions to answer queries. Base responses strictly on the provided information.
|
||||
- If the query is unclear, ask concise clarifying questions instead of making assumptions.
|
||||
|
||||
@@ -46,7 +46,7 @@ class Captain::Tools::Copilot::SearchLinearIssuesService < Captain::Tools::BaseS
|
||||
end
|
||||
|
||||
def active?
|
||||
@assistant.account.hooks.find_by(app_id: 'linear').present?
|
||||
@user.present? && @assistant.account.hooks.find_by(app_id: 'linear').present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
Reference in New Issue
Block a user