diff --git a/app/javascript/dashboard/store/captain/copilotMessages.js b/app/javascript/dashboard/store/captain/copilotMessages.js
index 83b7fddce..2b296cdc1 100644
--- a/app/javascript/dashboard/store/captain/copilotMessages.js
+++ b/app/javascript/dashboard/store/captain/copilotMessages.js
@@ -6,9 +6,9 @@ export default createStore({
API: CopilotMessagesAPI,
getters: {
getMessagesByThreadId: state => copilotThreadId => {
- return state.records.filter(
- record => record.copilot_thread?.id === Number(copilotThreadId)
- );
+ return state.records
+ .filter(record => record.copilot_thread?.id === Number(copilotThreadId))
+ .sort((a, b) => a.id - b.id);
},
},
actions: mutationTypes => ({
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 9a35197ad..74177f7dd 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -256,6 +256,11 @@ en:
captain:
copilot_error: 'Please connect an assistant to this inbox to use Copilot'
copilot_limit: 'You are out of Copilot credits. You can buy more credits from the billing section.'
+ copilot:
+ using_tool: 'Using tool %{function_name}'
+ completed_tool_call: 'Completed %{function_name} tool call'
+ invalid_tool_call: 'Invalid tool call'
+ tool_not_available: 'Tool not available'
public_portal:
search:
search_placeholder: Search for article by title or body...
diff --git a/config/routes.rb b/config/routes.rb
index ea65a02c9..fbecd2d50 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -128,7 +128,6 @@ Rails.application.routes.draw do
post :unread
post :custom_attributes
get :attachments
- post :copilot
get :inbox_assistant
end
end
diff --git a/enterprise/app/controllers/enterprise/api/v1/accounts/conversations_controller.rb b/enterprise/app/controllers/enterprise/api/v1/accounts/conversations_controller.rb
index cdea79064..c0ecc2a45 100644
--- a/enterprise/app/controllers/enterprise/api/v1/accounts/conversations_controller.rb
+++ b/enterprise/app/controllers/enterprise/api/v1/accounts/conversations_controller.rb
@@ -1,31 +1,5 @@
module Enterprise::Api::V1::Accounts::ConversationsController
extend ActiveSupport::Concern
- included do
- before_action :set_assistant, only: [:copilot]
- end
-
- def copilot
- # First try to get the user's preferred assistant from UI settings or from the request
- assistant_id = copilot_params[:assistant_id] || current_user.ui_settings&.dig('preferred_captain_assistant_id')
-
- # Find the assistant either by ID or from inbox
- assistant = if assistant_id.present?
- Captain::Assistant.find_by(id: assistant_id, account_id: Current.account.id)
- else
- @conversation.inbox.captain_assistant
- end
-
- return render json: { message: I18n.t('captain.copilot_error') } unless assistant
-
- response = Captain::Copilot::ChatService.new(
- assistant,
- 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'] }
- end
def inbox_assistant
assistant = @conversation.inbox.captain_assistant
diff --git a/enterprise/app/helpers/captain/chat_helper.rb b/enterprise/app/helpers/captain/chat_helper.rb
index 924f11269..f90b8d07e 100644
--- a/enterprise/app/helpers/captain/chat_helper.rb
+++ b/enterprise/app/helpers/captain/chat_helper.rb
@@ -53,9 +53,21 @@ module Captain::ChatHelper
end
def execute_tool(function_name, arguments, tool_call_id)
- persist_message({ content: "Using tool #{function_name}", function_name: function_name }, 'assistant_thinking')
+ persist_message(
+ {
+ content: I18n.t('captain.copilot.using_tool', function_name: 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')
+ persist_message(
+ {
+ content: I18n.t('captain.copilot.completed_tool_call', function_name: function_name),
+ function_name: function_name
+ },
+ 'assistant_thinking'
+ )
append_tool_response(result, tool_call_id)
end
@@ -67,8 +79,8 @@ module Captain::ChatHelper
end
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)
+ persist_message({ content: I18n.t('captain.copilot.invalid_tool_call'), function_name: function_name }, 'assistant_thinking')
+ append_tool_response(I18n.t('captain.copilot.tool_not_available'), tool_call_id)
end
def append_tool_response(content, tool_call_id)
diff --git a/enterprise/app/models/copilot_message.rb b/enterprise/app/models/copilot_message.rb
index 5941656c3..d19cb69c6 100644
--- a/enterprise/app/models/copilot_message.rb
+++ b/enterprise/app/models/copilot_message.rb
@@ -60,7 +60,7 @@ class CopilotMessage < ApplicationRecord
def validate_message_attributes
return if message.blank?
- allowed_keys = %w[content reasoning function_name]
+ allowed_keys = %w[content reasoning function_name reply_suggestion]
invalid_keys = message.keys - allowed_keys
errors.add(:message, "contains invalid attributes: #{invalid_keys.join(', ')}") if invalid_keys.any?
diff --git a/enterprise/app/services/captain/copilot/chat_service.rb b/enterprise/app/services/captain/copilot/chat_service.rb
index 701837f55..f6e3a1105 100644
--- a/enterprise/app/services/captain/copilot/chat_service.rb
+++ b/enterprise/app/services/captain/copilot/chat_service.rb
@@ -74,7 +74,10 @@ class Captain::Copilot::ChatService < Llm::BaseOpenAiService
def system_message
{
role: 'system',
- content: Captain::Llm::SystemPromptsService.copilot_response_generator(@assistant.config['product_name'])
+ content: Captain::Llm::SystemPromptsService.copilot_response_generator(
+ @assistant.config['product_name'],
+ @tool_registry.tools_summary
+ )
}
end
diff --git a/enterprise/app/services/captain/llm/system_prompts_service.rb b/enterprise/app/services/captain/llm/system_prompts_service.rb
index f50ad0c6c..3a699b445 100644
--- a/enterprise/app/services/captain/llm/system_prompts_service.rb
+++ b/enterprise/app/services/captain/llm/system_prompts_service.rb
@@ -56,7 +56,7 @@ class Captain::Llm::SystemPromptsService
SYSTEM_PROMPT_MESSAGE
end
- def copilot_response_generator(product_name)
+ def copilot_response_generator(product_name, available_tools)
<<~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.
@@ -94,12 +94,20 @@ class Captain::Llm::SystemPromptsService
```json
{
"reasoning": "Explain why the response was chosen based on the provided information.",
- "response": "Provide the answer only in Markdown format for readability."
+ "content": "Provide the answer only in Markdown format for readability.",
+ "reply_suggestion": "A boolean value that is true only if the support agent has explicitly asked to draft a response to the customer, and the response fulfills that request. Otherwise, it should be false."
}
[Error Handling]
- If the required information is not found in the provided context, respond with an appropriate message indicating that no relevant data is available.
- Avoid speculating or providing unverified information.
+
+ [Available Actions]
+ You have the following actions available to assist support agents:
+ - summarize_conversation: Summarize the conversation
+ - draft_response: Draft a response for the support agent
+ - rate_conversation: Rate the conversation
+ #{available_tools}
SYSTEM_PROMPT_MESSAGE
end
diff --git a/enterprise/app/services/captain/tool_registry_service.rb b/enterprise/app/services/captain/tool_registry_service.rb
index 8d6632e57..c788566d6 100644
--- a/enterprise/app/services/captain/tool_registry_service.rb
+++ b/enterprise/app/services/captain/tool_registry_service.rb
@@ -27,4 +27,10 @@ class Captain::ToolRegistryService
def respond_to_missing?(method_name, include_private = false)
@tools.key?(method_name.to_s) || super
end
+
+ def tools_summary
+ @tools.map do |name, tool|
+ "- #{name}: #{tool.description}"
+ end.join("\n")
+ end
end
diff --git a/enterprise/app/services/captain/tools/copilot/search_conversations_service.rb b/enterprise/app/services/captain/tools/copilot/search_conversations_service.rb
index f97604793..6dfdf3897 100644
--- a/enterprise/app/services/captain/tools/copilot/search_conversations_service.rb
+++ b/enterprise/app/services/captain/tools/copilot/search_conversations_service.rb
@@ -19,8 +19,9 @@ class Captain::Tools::Copilot::SearchConversationsService < Captain::Tools::Base
status = arguments['status']
contact_id = arguments['contact_id']
priority = arguments['priority']
+ labels = arguments['labels']
- conversations = get_conversations(status, contact_id, priority)
+ conversations = get_conversations(status, contact_id, priority, labels)
return 'No conversations found' unless conversations.exists?
@@ -41,11 +42,12 @@ class Captain::Tools::Copilot::SearchConversationsService < Captain::Tools::Base
private
- def get_conversations(status, contact_id, priority)
+ def get_conversations(status, contact_id, priority, labels)
conversations = permissible_conversations
conversations = conversations.where(contact_id: contact_id) if contact_id.present?
conversations = conversations.where(status: status) if status.present?
conversations = conversations.where(priority: priority) if priority.present?
+ conversations = conversations.tagged_with(labels, any: true) if labels.present?
conversations
end
@@ -59,20 +61,10 @@ class Captain::Tools::Copilot::SearchConversationsService < Captain::Tools::Base
def properties
{
- contact_id: {
- type: 'number',
- description: 'Filter conversations by contact ID'
- },
- status: {
- type: 'string',
- enum: %w[open resolved pending snoozed],
- description: 'Filter conversations by status'
- },
- priority: {
- type: 'string',
- enum: %w[low medium high urgent],
- description: 'Filter conversations by priority'
- }
+ contact_id: { type: 'number', description: 'Filter conversations by contact ID' },
+ status: { type: 'string', enum: %w[open resolved pending snoozed], description: 'Filter conversations by status' },
+ priority: { type: 'string', enum: %w[low medium high urgent], description: 'Filter conversations by priority' },
+ labels: { type: 'array', items: { type: 'string' }, description: 'Filter conversations by labels' }
}
end
end
diff --git a/spec/enterprise/services/captain/tool_registry_service_spec.rb b/spec/enterprise/services/captain/tool_registry_service_spec.rb
index c8d97fe3a..9f4cde046 100644
--- a/spec/enterprise/services/captain/tool_registry_service_spec.rb
+++ b/spec/enterprise/services/captain/tool_registry_service_spec.rb
@@ -109,4 +109,46 @@ RSpec.describe Captain::ToolRegistryService do
end
end
end
+
+ describe '#tools_summary' do
+ let(:tool_class) { TestTool }
+
+ before do
+ service.register_tool(tool_class)
+ end
+
+ it 'returns formatted summary of registered tools' do
+ expect(service.tools_summary).to eq('- test_tool: A test tool for specs')
+ end
+
+ context 'when multiple tools are registered' do
+ let(:another_tool_class) do
+ Class.new(Captain::Tools::BaseService) do
+ def name
+ 'another_tool'
+ end
+
+ def description
+ 'Another test tool'
+ end
+
+ def parameters
+ {
+ type: 'object',
+ properties: {}
+ }
+ end
+
+ def active?
+ true
+ end
+ end
+ end
+
+ it 'includes all tools in the summary' do
+ service.register_tool(another_tool_class)
+ expect(service.tools_summary).to eq("- test_tool: A test tool for specs\n- another_tool: Another test tool")
+ end
+ end
+ end
end