+import { ref } from 'vue';
+import { useI18n } from 'vue-i18n';
+import NextButton from 'dashboard/components-next/button/Button.vue';
+import MessageList from './MessageList.vue';
+import CaptainAssistant from 'dashboard/api/captain/assistant';
+
+const { assistantId } = defineProps({
+ assistantId: {
+ type: Number,
+ required: true,
+ },
+});
+
+const { t } = useI18n();
+const messages = ref([]);
+const newMessage = ref('');
+const isLoading = ref(false);
+
+const formatMessagesForApi = () => {
+ return messages.value.map(message => ({
+ role: message.sender,
+ content: message.content,
+ }));
+};
+
+const resetConversation = () => {
+ messages.value = [];
+ newMessage.value = '';
+};
+
+const sendMessage = async () => {
+ if (!newMessage.value.trim() || isLoading.value) return;
+
+ const userMessage = {
+ content: newMessage.value,
+ sender: 'user',
+ timestamp: new Date().toISOString(),
+ };
+ messages.value.push(userMessage);
+ const currentMessage = newMessage.value;
+ newMessage.value = '';
+
+ try {
+ isLoading.value = true;
+ const { data } = await CaptainAssistant.playground({
+ assistantId,
+ messageContent: currentMessage,
+ messageHistory: formatMessagesForApi(),
+ });
+
+ messages.value.push({
+ content: data.response,
+ sender: 'assistant',
+ timestamp: new Date().toISOString(),
+ });
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error('Error getting assistant response:', error);
+ } finally {
+ isLoading.value = false;
+ }
+};
+
+
+
+
+
+
+
+ {{ t('CAPTAIN.PLAYGROUND.HEADER') }}
+
+
+
+
+ {{ t('CAPTAIN.PLAYGROUND.DESCRIPTION') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('CAPTAIN.PLAYGROUND.CREDIT_NOTE') }}
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/MessageList.vue b/app/javascript/dashboard/components-next/captain/assistant/MessageList.vue
new file mode 100644
index 000000000..1d6529a45
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/MessageList.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue
new file mode 100644
index 000000000..3a668a757
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue
@@ -0,0 +1,306 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json
index 8af57975c..ed56b1669 100644
--- a/app/javascript/dashboard/i18n/locale/en/integrations.json
+++ b/app/javascript/dashboard/i18n/locale/en/integrations.json
@@ -333,6 +333,14 @@
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
},
+ "PLAYGROUND": {
+ "USER": "You",
+ "ASSISTANT": "Assistant",
+ "MESSAGE_PLACEHOLDER": "Type your message...",
+ "HEADER": "Playground",
+ "DESCRIPTION": "Use this playground to send messages to your assistant and check if it responds accurately, quickly, and in the tone you expect.",
+ "CREDIT_NOTE": "Messages sent here will count toward your Captain credits."
+ },
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
"AVAILABLE_ON": "Captain is not available on the free plan.",
@@ -371,20 +379,41 @@
"ERROR_MESSAGE": "There was an error creating the assistant, please try again."
},
"FORM": {
+ "UPDATE": "Update",
+ "SECTIONS": {
+ "BASIC_INFO": "Basic Information",
+ "SYSTEM_MESSAGES": "System Messages",
+ "INSTRUCTIONS": "Instructions",
+ "FEATURES": "Features",
+ "TOOLS": "Tools "
+ },
"NAME": {
- "LABEL": "Assistant Name",
- "PLACEHOLDER": "Enter a name for the assistant",
- "ERROR": "Please provide a name for the assistant"
+ "LABEL": "Name",
+ "PLACEHOLDER": "Enter assistant name"
},
"DESCRIPTION": {
- "LABEL": "Assistant Description",
- "PLACEHOLDER": "Describe how and where this assistant will be used",
- "ERROR": "A description is required"
+ "LABEL": "Description",
+ "PLACEHOLDER": "Enter assistant description"
},
"PRODUCT_NAME": {
"LABEL": "Product Name",
- "PLACEHOLDER": "Enter the name of the product this assistant is designed for",
- "ERROR": "The product name is required"
+ "PLACEHOLDER": "Enter product name"
+ },
+ "WELCOME_MESSAGE": {
+ "LABEL": "Welcome Message",
+ "PLACEHOLDER": "Enter welcome message"
+ },
+ "HANDOFF_MESSAGE": {
+ "LABEL": "Handoff Message",
+ "PLACEHOLDER": "Enter handoff message"
+ },
+ "RESOLUTION_MESSAGE": {
+ "LABEL": "Resolution Message",
+ "PLACEHOLDER": "Enter resolution message"
+ },
+ "INSTRUCTIONS": {
+ "LABEL": "Instructions",
+ "PLACEHOLDER": "Enter instructions for the assistant"
},
"FEATURES": {
"TITLE": "Features",
@@ -395,7 +424,8 @@
"EDIT": {
"TITLE": "Update the assistant",
"SUCCESS_MESSAGE": "The assistant has been successfully updated",
- "ERROR_MESSAGE": "There was an error updating the assistant, please try again."
+ "ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
+ "NOT_FOUND": "Could not find the assistant. Please try again."
},
"OPTIONS": {
"EDIT_ASSISTANT": "Edit Assistant",
diff --git a/app/javascript/dashboard/routes/dashboard/captain/assistants/Edit.vue b/app/javascript/dashboard/routes/dashboard/captain/assistants/Edit.vue
new file mode 100644
index 000000000..8b64e2663
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/captain/assistants/Edit.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+ {{ t('CAPTAIN.ASSISTANTS.EDIT.NOT_FOUND') }}
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue b/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue
index 242e6f086..800783a7b 100644
--- a/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue
@@ -36,8 +36,10 @@ const handleCreate = () => {
};
const handleEdit = () => {
- dialogType.value = 'edit';
- nextTick(() => createAssistantDialog.value.dialogRef.open());
+ router.push({
+ name: 'captain_assistants_edit',
+ params: { assistantId: selectedAssistant.value.id },
+ });
};
const handleViewConnectedInboxes = () => {
diff --git a/app/javascript/dashboard/routes/dashboard/captain/captain.routes.js b/app/javascript/dashboard/routes/dashboard/captain/captain.routes.js
index cf7751751..17afeca14 100644
--- a/app/javascript/dashboard/routes/dashboard/captain/captain.routes.js
+++ b/app/javascript/dashboard/routes/dashboard/captain/captain.routes.js
@@ -2,6 +2,7 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
import { frontendURL } from '../../../helper/URLHelper';
import AssistantIndex from './assistants/Index.vue';
+import AssistantEdit from './assistants/Edit.vue';
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
import DocumentsIndex from './documents/Index.vue';
import ResponsesIndex from './responses/Index.vue';
@@ -20,6 +21,19 @@ export const routes = [
],
},
},
+ {
+ path: frontendURL('accounts/:accountId/captain/assistants/:assistantId'),
+ component: AssistantEdit,
+ name: 'captain_assistants_edit',
+ meta: {
+ permissions: ['administrator', 'agent'],
+ featureFlag: FEATURE_FLAGS.CAPTAIN,
+ installationTypes: [
+ INSTALLATION_TYPES.CLOUD,
+ INSTALLATION_TYPES.ENTERPRISE,
+ ],
+ },
+ },
{
path: frontendURL(
'accounts/:accountId/captain/assistants/:assistantId/inboxes'
diff --git a/config/routes.rb b/config/routes.rb
index 6c71a8cc6..c623ff053 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -53,6 +53,9 @@ Rails.application.routes.draw do
end
namespace :captain do
resources :assistants do
+ member do
+ post :playground
+ end
resources :inboxes, only: [:index, :create, :destroy], param: :inbox_id
end
resources :documents, only: [:index, :show, :create, :destroy]
diff --git a/enterprise/app/controllers/api/v1/accounts/captain/assistants_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/assistants_controller.rb
index e424a8a62..35b6ffe1d 100644
--- a/enterprise/app/controllers/api/v1/accounts/captain/assistants_controller.rb
+++ b/enterprise/app/controllers/api/v1/accounts/captain/assistants_controller.rb
@@ -2,7 +2,7 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
before_action :current_account
before_action -> { check_authorization(Captain::Assistant) }
- before_action :set_assistant, only: [:show, :update, :destroy]
+ before_action :set_assistant, only: [:show, :update, :destroy, :playground]
def index
@assistants = account_assistants.ordered
@@ -23,6 +23,15 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
head :no_content
end
+ def playground
+ response = Captain::Llm::AssistantChatService.new(assistant: @assistant).generate_response(
+ params[:message_content],
+ message_history
+ )
+
+ render json: response
+ end
+
private
def set_assistant
@@ -34,6 +43,19 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
end
def assistant_params
- params.require(:assistant).permit(:name, :description, config: [:product_name, :feature_faq, :feature_memory])
+ params.require(:assistant).permit(:name, :description,
+ config: [
+ :product_name, :feature_faq, :feature_memory,
+ :welcome_message, :handoff_message, :resolution_message,
+ :instructions
+ ])
+ end
+
+ def playground_params
+ params.require(:assistant).permit(:message_content, message_history: [:role, :content])
+ end
+
+ def message_history
+ (playground_params[:message_history] || []).map { |message| { role: message[:role], content: message[:content] } }
end
end
diff --git a/enterprise/app/helpers/captain/chat_helper.rb b/enterprise/app/helpers/captain/chat_helper.rb
index 3cb5bf8fe..146e8f813 100644
--- a/enterprise/app/helpers/captain/chat_helper.rb
+++ b/enterprise/app/helpers/captain/chat_helper.rb
@@ -36,6 +36,7 @@ module Captain::ChatHelper
end
def handle_response(response)
+ Rails.logger.debug { "[CAPTAIN][ChatCompletion] #{response}" }
message = response.dig('choices', 0, 'message')
if message['tool_calls']
process_tool_calls(message['tool_calls'])
@@ -46,20 +47,26 @@ module Captain::ChatHelper
def process_tool_calls(tool_calls)
append_tool_calls(tool_calls)
- process_tool_call(tool_calls.first)
- end
-
- def process_tool_call(tool_call)
- return unless tool_call['function']['name'] == 'search_documentation'
-
- tool_call_id = tool_call['id']
- query = JSON.parse(tool_call['function']['arguments'])['search_query']
- sections = fetch_documentation(query)
- append_tool_response(sections, tool_call_id)
+ tool_calls.each do |tool_call|
+ process_tool_call(tool_call)
+ end
request_chat_completion
end
+ def process_tool_call(tool_call)
+ tool_call_id = tool_call['id']
+
+ if tool_call['function']['name'] == 'search_documentation'
+ query = JSON.parse(tool_call['function']['arguments'])['search_query']
+ sections = fetch_documentation(query)
+ append_tool_response(sections, tool_call_id)
+ else
+ append_tool_response('', tool_call_id)
+ end
+ end
+
def fetch_documentation(query)
+ Rails.logger.debug { "[CAPTAIN][DocumentationSearch] #{query}" }
@assistant
.responses
.approved
diff --git a/enterprise/app/jobs/captain/conversation/response_builder_job.rb b/enterprise/app/jobs/captain/conversation/response_builder_job.rb
index c8c511a57..5bc9defa4 100644
--- a/enterprise/app/jobs/captain/conversation/response_builder_job.rb
+++ b/enterprise/app/jobs/captain/conversation/response_builder_job.rb
@@ -60,7 +60,7 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
end
def create_handoff_message
- create_outgoing_message('Transferring to another agent for further assistance.')
+ create_outgoing_message(@assistant.config['handoff_message'] || 'Transferring to another agent for further assistance.')
end
def create_messages
diff --git a/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb b/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb
index 657d31bd3..37a2729ec 100644
--- a/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb
+++ b/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb
@@ -5,12 +5,13 @@ class Captain::InboxPendingConversationsResolutionJob < ApplicationJob
# limiting the number of conversations to be resolved to avoid any performance issues
resolvable_conversations = inbox.conversations.pending.where('last_activity_at < ? ', Time.now.utc - 1.hour).limit(Limits::BULK_ACTIONS_LIMIT)
resolvable_conversations.each do |conversation|
+ resolution_message = conversation.inbox.captain_assistant.config['resolution_message']
conversation.messages.create!(
{
message_type: :outgoing,
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
- content: I18n.t('conversations.activity.auto_resolution_message')
+ content: resolution_message || I18n.t('conversations.activity.auto_resolution_message')
}
)
conversation.resolved!
diff --git a/enterprise/app/policies/captain/assistant_policy.rb b/enterprise/app/policies/captain/assistant_policy.rb
index e70af4ba4..2a03fc0ea 100644
--- a/enterprise/app/policies/captain/assistant_policy.rb
+++ b/enterprise/app/policies/captain/assistant_policy.rb
@@ -18,4 +18,8 @@ class Captain::AssistantPolicy < ApplicationPolicy
def destroy?
@account_user.administrator?
end
+
+ def playground?
+ true
+ end
end
diff --git a/enterprise/app/services/captain/llm/assistant_chat_service.rb b/enterprise/app/services/captain/llm/assistant_chat_service.rb
index 3d2b0da44..1e45cb1d8 100644
--- a/enterprise/app/services/captain/llm/assistant_chat_service.rb
+++ b/enterprise/app/services/captain/llm/assistant_chat_service.rb
@@ -22,7 +22,7 @@ class Captain::Llm::AssistantChatService < Llm::BaseOpenAiService
def system_message
{
role: 'system',
- content: Captain::Llm::SystemPromptsService.assistant_response_generator(@assistant.config['product_name'])
+ content: Captain::Llm::SystemPromptsService.assistant_response_generator(@assistant.config['product_name'], @assistant.config)
}
end
end
diff --git a/enterprise/app/services/captain/llm/system_prompts_service.rb b/enterprise/app/services/captain/llm/system_prompts_service.rb
index 8a8753f60..b1e627275 100644
--- a/enterprise/app/services/captain/llm/system_prompts_service.rb
+++ b/enterprise/app/services/captain/llm/system_prompts_service.rb
@@ -103,7 +103,7 @@ class Captain::Llm::SystemPromptsService
SYSTEM_PROMPT_MESSAGE
end
- def assistant_response_generator(product_name)
+ def assistant_response_generator(product_name, config = {})
<<~SYSTEM_PROMPT_MESSAGE
[Identity]
You are Captain, a helpful, friendly, and knowledgeable assistant for the product #{product_name}. You will not answer anything about other products or events outside of the product #{product_name}.
@@ -111,6 +111,7 @@ class Captain::Llm::SystemPromptsService
[Response Guideline]
- Do not rush giving a response, always give step-by-step instructions to the customer. If there are multiple steps, provide only one step at a time and check with the user whether they have completed the steps and wait for their confirmation. If the user has said okay or yes, continue with the steps.
- Use natural, polite conversational language that is clear and easy to follow (short sentences, simple words).
+ - Always detect the language from input and reply in the same language. Do not use any other language.
- Be concise and relevant: Most of your responses should be a sentence or two, unless you're asked to go deeper. Don't monopolize the conversation.
- Use discourse markers to ease comprehension. Never use the list format.
- Do not generate a response more than three sentences.
@@ -136,6 +137,7 @@ class Captain::Llm::SystemPromptsService
- Do not share anything outside of the context provided.
- Add the reasoning why you arrived at the answer
- Your answers will always be formatted in a valid JSON hash, as shown below. Never respond in non-JSON format.
+ #{config['instructions'] || ''}
```json
{
reasoning: '',
diff --git a/spec/enterprise/controllers/api/v1/accounts/captain/assistants_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/captain/assistants_controller_spec.rb
index c3c83e457..1f6d83d80 100644
--- a/spec/enterprise/controllers/api/v1/accounts/captain/assistants_controller_spec.rb
+++ b/spec/enterprise/controllers/api/v1/accounts/captain/assistants_controller_spec.rb
@@ -175,4 +175,67 @@ RSpec.describe 'Api::V1::Accounts::Captain::Assistants', type: :request do
end
end
end
+
+ describe 'POST /api/v1/accounts/{account.id}/captain/assistants/{id}/playground' do
+ let(:assistant) { create(:captain_assistant, account: account) }
+ let(:valid_params) do
+ {
+ message_content: 'Hello assistant',
+ message_history: [
+ { role: 'user', content: 'Previous message' },
+ { role: 'assistant', content: 'Previous response' }
+ ]
+ }
+ end
+
+ context 'when it is an un-authenticated user' do
+ it 'returns unauthorized' do
+ post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/playground",
+ params: valid_params,
+ as: :json
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an agent' do
+ it 'generates a response' do
+ chat_service = instance_double(Captain::Llm::AssistantChatService)
+ allow(Captain::Llm::AssistantChatService).to receive(:new).with(assistant: assistant).and_return(chat_service)
+ allow(chat_service).to receive(:generate_response).and_return({ content: 'Assistant response' })
+
+ post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/playground",
+ params: valid_params,
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(chat_service).to have_received(:generate_response).with(
+ valid_params[:message_content],
+ valid_params[:message_history]
+ )
+ expect(json_response[:content]).to eq('Assistant response')
+ end
+ end
+
+ context 'when message_history is not provided' do
+ it 'uses empty array as default' do
+ params_without_history = { message_content: 'Hello assistant' }
+ chat_service = instance_double(Captain::Llm::AssistantChatService)
+ allow(Captain::Llm::AssistantChatService).to receive(:new).with(assistant: assistant).and_return(chat_service)
+ allow(chat_service).to receive(:generate_response).and_return({ content: 'Assistant response' })
+
+ post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/playground",
+ params: params_without_history,
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(chat_service).to have_received(:generate_response).with(
+ params_without_history[:message_content],
+ []
+ )
+ end
+ end
+ end
end
diff --git a/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb b/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb
index 90e9360d3..09f62272d 100644
--- a/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb
+++ b/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb
@@ -4,11 +4,13 @@ RSpec.describe Captain::InboxPendingConversationsResolutionJob, type: :job do
include ActiveJob::TestHelper
let!(:inbox) { create(:inbox) }
+
let!(:resolvable_pending_conversation) { create(:conversation, inbox: inbox, last_activity_at: 2.hours.ago, status: :pending) }
let!(:recent_pending_conversation) { create(:conversation, inbox: inbox, last_activity_at: 10.minutes.ago, status: :pending) }
let!(:open_conversation) { create(:conversation, inbox: inbox, last_activity_at: 1.hour.ago, status: :open) }
before do
+ create(:captain_inbox, inbox: inbox, captain_assistant: create(:captain_assistant, account: inbox.account))
stub_const('Limits::BULK_ACTIONS_LIMIT', 2)
end