mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
feat: Allow customizing the responses, flows in Captain (#11385)
- Ability to provide custom instructions to captain <img width="1107" alt="Screenshot 2025-04-28 at 6 11 43 PM" src="https://github.com/user-attachments/assets/f94cbccc-b4d8-48fd-b6b9-55524129bc50" />
This commit is contained in:
@@ -14,6 +14,13 @@ class CaptainAssistant extends ApiClient {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playground({ assistantId, messageContent, messageHistory }) {
|
||||||
|
return axios.post(`${this.url}/${assistantId}/playground`, {
|
||||||
|
message_content: messageContent,
|
||||||
|
message_history: messageHistory,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new CaptainAssistant();
|
export default new CaptainAssistant();
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
isOpen: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const isExpanded = ref(props.isOpen);
|
||||||
|
|
||||||
|
const toggleAccordion = () => {
|
||||||
|
isExpanded.value = !isExpanded.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.isOpen,
|
||||||
|
newValue => {
|
||||||
|
isExpanded.value = newValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="border rounded-lg border-n-slate-4">
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-between w-full p-4 text-left"
|
||||||
|
@click="toggleAccordion"
|
||||||
|
>
|
||||||
|
<span class="text-sm font-medium text-n-slate-12">{{ title }}</span>
|
||||||
|
<span
|
||||||
|
class="w-5 h-5 transition-transform duration-200 i-lucide-chevron-down"
|
||||||
|
:class="{ 'rotate-180': isExpanded }"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div v-if="isExpanded" class="p-4 pt-0">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { usePolicy } from 'dashboard/composables/usePolicy';
|
import { usePolicy } from 'dashboard/composables/usePolicy';
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import BackButton from 'dashboard/components/widgets/BackButton.vue';
|
||||||
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
||||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
import Policy from 'dashboard/components/policy.vue';
|
import Policy from 'dashboard/components/policy.vue';
|
||||||
@@ -23,6 +24,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
backUrl: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
buttonPolicy: {
|
buttonPolicy: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
@@ -39,6 +44,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
showKnowMore: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
isEmpty: {
|
isEmpty: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@@ -73,19 +82,23 @@ const handlePageChange = event => {
|
|||||||
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
|
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
|
||||||
>
|
>
|
||||||
<div class="flex gap-4 items-center">
|
<div class="flex gap-4 items-center">
|
||||||
|
<BackButton v-if="backUrl" :to="backUrl" />
|
||||||
<slot name="headerTitle">
|
<slot name="headerTitle">
|
||||||
<span class="text-xl font-medium text-n-slate-12">
|
<span class="text-xl font-medium text-n-slate-12">
|
||||||
{{ headerTitle }}
|
{{ headerTitle }}
|
||||||
</span>
|
</span>
|
||||||
</slot>
|
</slot>
|
||||||
<div v-if="!isEmpty" class="flex items-center gap-2">
|
<div
|
||||||
|
v-if="!isEmpty && showKnowMore"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
>
|
||||||
<div class="w-0.5 h-4 rounded-2xl bg-n-weak" />
|
<div class="w-0.5 h-4 rounded-2xl bg-n-weak" />
|
||||||
<slot name="knowMore" />
|
<slot name="knowMore" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!showPaywall"
|
v-if="!showPaywall && buttonLabel"
|
||||||
v-on-clickaway="() => emit('close')"
|
v-on-clickaway="() => emit('close')"
|
||||||
class="relative group/campaign-button"
|
class="relative group/campaign-button"
|
||||||
>
|
>
|
||||||
@@ -104,7 +117,7 @@ const handlePageChange = event => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="flex-1 px-6 overflow-y-auto xl:px-0">
|
<main class="flex-1 px-6 overflow-y-auto xl:px-0">
|
||||||
<div class="w-full max-w-[60rem] mx-auto py-4">
|
<div class="w-full max-w-[60rem] h-full mx-auto py-4">
|
||||||
<slot v-if="!showPaywall" name="controls" />
|
<slot v-if="!showPaywall" name="controls" />
|
||||||
<div
|
<div
|
||||||
v-if="isFetching"
|
v-if="isFetching"
|
||||||
|
|||||||
@@ -76,9 +76,12 @@ const handleAction = ({ action, value }) => {
|
|||||||
<template>
|
<template>
|
||||||
<CardLayout>
|
<CardLayout>
|
||||||
<div class="flex justify-between w-full gap-1">
|
<div class="flex justify-between w-full gap-1">
|
||||||
<span class="text-base text-n-slate-12 line-clamp-1">
|
<router-link
|
||||||
|
:to="{ name: 'captain_assistants_edit', params: { assistantId: id } }"
|
||||||
|
class="text-base text-n-slate-12 line-clamp-1 hover:underline transition-colors"
|
||||||
|
>
|
||||||
{{ name }}
|
{{ name }}
|
||||||
</span>
|
</router-link>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
v-on-clickaway="() => toggleDropdown(false)"
|
v-on-clickaway="() => toggleDropdown(false)"
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<script setup>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col h-full rounded-lg p-4 border border-n-slate-4 text-n-slate-11"
|
||||||
|
>
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="flex justify-between items-center mb-1">
|
||||||
|
<h3 class="text-lg font-medium">
|
||||||
|
{{ t('CAPTAIN.PLAYGROUND.HEADER') }}
|
||||||
|
</h3>
|
||||||
|
<NextButton
|
||||||
|
ghost
|
||||||
|
size="small"
|
||||||
|
icon="i-lucide-rotate-ccw"
|
||||||
|
@click="resetConversation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-n-slate-11">
|
||||||
|
{{ t('CAPTAIN.PLAYGROUND.DESCRIPTION') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MessageList :messages="messages" :is-loading="isLoading" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-center bg-n-solid-1 outline outline-n-container rounded-lg p-3"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="newMessage"
|
||||||
|
class="flex-1 bg-transparent border-none focus:outline-none text-sm mb-0"
|
||||||
|
:placeholder="t('CAPTAIN.PLAYGROUND.MESSAGE_PLACEHOLDER')"
|
||||||
|
@keyup.enter="sendMessage"
|
||||||
|
/>
|
||||||
|
<NextButton
|
||||||
|
ghost
|
||||||
|
size="small"
|
||||||
|
:disabled="!newMessage.trim()"
|
||||||
|
icon="i-lucide-send"
|
||||||
|
@click="sendMessage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-n-slate-11 pt-2 text-center">
|
||||||
|
{{ t('CAPTAIN.PLAYGROUND.CREDIT_NOTE') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { ref, watch, nextTick } from 'vue';
|
||||||
|
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||||
|
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
messages: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageContainer = ref(null);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { formatMessage } = useMessageFormatter();
|
||||||
|
|
||||||
|
const isUserMessage = sender => sender === 'user';
|
||||||
|
|
||||||
|
const getMessageAlignment = sender =>
|
||||||
|
isUserMessage(sender) ? 'justify-end' : 'justify-start';
|
||||||
|
|
||||||
|
const getMessageDirection = sender =>
|
||||||
|
isUserMessage(sender) ? 'flex-row-reverse' : 'flex-row';
|
||||||
|
|
||||||
|
const getAvatarName = sender =>
|
||||||
|
isUserMessage(sender)
|
||||||
|
? t('CAPTAIN.PLAYGROUND.USER')
|
||||||
|
: t('CAPTAIN.PLAYGROUND.ASSISTANT');
|
||||||
|
|
||||||
|
const getMessageStyle = sender =>
|
||||||
|
isUserMessage(sender)
|
||||||
|
? 'bg-n-strong text-n-white'
|
||||||
|
: 'bg-n-solid-iris text-n-slate-12';
|
||||||
|
|
||||||
|
const scrollToBottom = async () => {
|
||||||
|
await nextTick();
|
||||||
|
if (messageContainer.value) {
|
||||||
|
messageContainer.value.scrollTop = messageContainer.value.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => props.messages.length, scrollToBottom);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="messageContainer" class="flex-1 overflow-y-auto mb-4 space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="(message, index) in messages"
|
||||||
|
:key="index"
|
||||||
|
class="flex"
|
||||||
|
:class="getMessageAlignment(message.sender)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-start gap-1.5"
|
||||||
|
:class="getMessageDirection(message.sender)"
|
||||||
|
>
|
||||||
|
<Avatar :name="getAvatarName(message.sender)" rounded-full :size="24" />
|
||||||
|
<div
|
||||||
|
class="max-w-[80%] rounded-lg p-3 text-sm"
|
||||||
|
:class="getMessageStyle(message.sender)"
|
||||||
|
>
|
||||||
|
<div v-html="formatMessage(message.content)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isLoading" class="flex justify-start">
|
||||||
|
<div class="flex items-start gap-1.5">
|
||||||
|
<Avatar :name="getAvatarName('assistant')" rounded-full :size="24" />
|
||||||
|
<div
|
||||||
|
class="max-w-sm rounded-lg p-3 text-sm bg-n-solid-iris text-n-slate-12"
|
||||||
|
>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-n-iris-10 animate-bounce" />
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 rounded-full bg-n-iris-10 animate-bounce [animation-delay:0.2s]"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 rounded-full bg-n-iris-10 animate-bounce [animation-delay:0.4s]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactive, computed, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { required, minLength } from '@vuelidate/validators';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
|
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||||
|
import Accordion from 'dashboard/components-next/Accordion/Accordion.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
validator: value => ['edit', 'create'].includes(value),
|
||||||
|
},
|
||||||
|
assistant: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['submit']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const formState = {
|
||||||
|
uiFlags: useMapGetter('captainAssistants/getUIFlags'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
productName: '',
|
||||||
|
welcomeMessage: '',
|
||||||
|
handoffMessage: '',
|
||||||
|
resolutionMessage: '',
|
||||||
|
instructions: '',
|
||||||
|
features: {
|
||||||
|
conversationFaqs: false,
|
||||||
|
memories: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive({ ...initialState });
|
||||||
|
|
||||||
|
const validationRules = {
|
||||||
|
name: { required, minLength: minLength(1) },
|
||||||
|
description: { required, minLength: minLength(1) },
|
||||||
|
productName: { required, minLength: minLength(1) },
|
||||||
|
welcomeMessage: { minLength: minLength(1) },
|
||||||
|
handoffMessage: { minLength: minLength(1) },
|
||||||
|
resolutionMessage: { minLength: minLength(1) },
|
||||||
|
instructions: { minLength: minLength(1) },
|
||||||
|
};
|
||||||
|
|
||||||
|
const v$ = useVuelidate(validationRules, state);
|
||||||
|
|
||||||
|
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
|
||||||
|
|
||||||
|
const getErrorMessage = field => {
|
||||||
|
return v$.value[field].$error ? v$.value[field].$errors[0].$message : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formErrors = computed(() => ({
|
||||||
|
name: getErrorMessage('name'),
|
||||||
|
description: getErrorMessage('description'),
|
||||||
|
productName: getErrorMessage('productName'),
|
||||||
|
welcomeMessage: getErrorMessage('welcomeMessage'),
|
||||||
|
handoffMessage: getErrorMessage('handoffMessage'),
|
||||||
|
resolutionMessage: getErrorMessage('resolutionMessage'),
|
||||||
|
instructions: getErrorMessage('instructions'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updateStateFromAssistant = assistant => {
|
||||||
|
const { config = {} } = assistant;
|
||||||
|
state.name = assistant.name;
|
||||||
|
state.description = assistant.description;
|
||||||
|
state.productName = config.product_name;
|
||||||
|
state.welcomeMessage = config.welcome_message;
|
||||||
|
state.handoffMessage = config.handoff_message;
|
||||||
|
state.resolutionMessage = config.resolution_message;
|
||||||
|
state.instructions = config.instructions;
|
||||||
|
state.features = {
|
||||||
|
conversationFaqs: config.feature_faq || false,
|
||||||
|
memories: config.feature_memory || false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBasicInfoUpdate = async () => {
|
||||||
|
const result = await Promise.all([
|
||||||
|
v$.value.name.$validate(),
|
||||||
|
v$.value.description.$validate(),
|
||||||
|
v$.value.productName.$validate(),
|
||||||
|
]).then(results => results.every(Boolean));
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: state.name,
|
||||||
|
description: state.description,
|
||||||
|
product_name: state.productName,
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('submit', payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSystemMessagesUpdate = async () => {
|
||||||
|
const result = await Promise.all([
|
||||||
|
v$.value.welcomeMessage.$validate(),
|
||||||
|
v$.value.handoffMessage.$validate(),
|
||||||
|
v$.value.resolutionMessage.$validate(),
|
||||||
|
]).then(results => results.every(Boolean));
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
config: {
|
||||||
|
...props.assistant.config,
|
||||||
|
welcome_message: state.welcomeMessage,
|
||||||
|
handoff_message: state.handoffMessage,
|
||||||
|
resolution_message: state.resolutionMessage,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('submit', payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstructionsUpdate = async () => {
|
||||||
|
const result = await v$.value.instructions.$validate();
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
config: {
|
||||||
|
...props.assistant.config,
|
||||||
|
instructions: state.instructions,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('submit', payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFeaturesUpdate = () => {
|
||||||
|
const payload = {
|
||||||
|
config: {
|
||||||
|
...props.assistant.config,
|
||||||
|
feature_faq: state.features.conversationFaqs,
|
||||||
|
feature_memory: state.features.memories,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('submit', payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.assistant,
|
||||||
|
newAssistant => {
|
||||||
|
if (props.mode === 'edit' && newAssistant) {
|
||||||
|
updateStateFromAssistant(newAssistant);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
||||||
|
<!-- Basic Information Section -->
|
||||||
|
<Accordion
|
||||||
|
:title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.BASIC_INFO')"
|
||||||
|
is-open
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4 pt-4">
|
||||||
|
<Input
|
||||||
|
v-model="state.name"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.NAME.LABEL')"
|
||||||
|
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.NAME.PLACEHOLDER')"
|
||||||
|
:message="formErrors.name"
|
||||||
|
:message-type="formErrors.name ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Editor
|
||||||
|
v-model="state.description"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.DESCRIPTION.LABEL')"
|
||||||
|
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.DESCRIPTION.PLACEHOLDER')"
|
||||||
|
:message="formErrors.description"
|
||||||
|
:message-type="formErrors.description ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
v-model="state.productName"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.PRODUCT_NAME.LABEL')"
|
||||||
|
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.PRODUCT_NAME.PLACEHOLDER')"
|
||||||
|
:message="formErrors.productName"
|
||||||
|
:message-type="formErrors.productName ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
:loading="isLoading"
|
||||||
|
@click="handleBasicInfoUpdate"
|
||||||
|
>
|
||||||
|
{{ t('CAPTAIN.ASSISTANTS.FORM.UPDATE') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<!-- Instructions Section -->
|
||||||
|
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.INSTRUCTIONS')">
|
||||||
|
<div class="flex flex-col gap-4 pt-4">
|
||||||
|
<Editor
|
||||||
|
v-model="state.instructions"
|
||||||
|
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.PLACEHOLDER')"
|
||||||
|
:message="formErrors.instructions"
|
||||||
|
:max-length="2000"
|
||||||
|
:message-type="formErrors.instructions ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
:loading="isLoading"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
|
||||||
|
@click="handleInstructionsUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<!-- Greeting Messages Section -->
|
||||||
|
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.SYSTEM_MESSAGES')">
|
||||||
|
<div class="flex flex-col gap-4 pt-4">
|
||||||
|
<Editor
|
||||||
|
v-model="state.handoffMessage"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.HANDOFF_MESSAGE.LABEL')"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN.ASSISTANTS.FORM.HANDOFF_MESSAGE.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
:message="formErrors.handoffMessage"
|
||||||
|
:message-type="formErrors.handoffMessage ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Editor
|
||||||
|
v-model="state.resolutionMessage"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.RESOLUTION_MESSAGE.LABEL')"
|
||||||
|
:placeholder="
|
||||||
|
t('CAPTAIN.ASSISTANTS.FORM.RESOLUTION_MESSAGE.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
:message="formErrors.resolutionMessage"
|
||||||
|
:message-type="formErrors.resolutionMessage ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
:loading="isLoading"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
|
||||||
|
@click="handleSystemMessagesUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<!-- Features Section -->
|
||||||
|
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.FEATURES')">
|
||||||
|
<div class="flex flex-col gap-4 pt-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.TITLE') }}
|
||||||
|
</label>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
v-model="state.features.conversationFaqs"
|
||||||
|
type="checkbox"
|
||||||
|
class="form-checkbox"
|
||||||
|
/>
|
||||||
|
{{
|
||||||
|
t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS')
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
v-model="state.features.memories"
|
||||||
|
type="checkbox"
|
||||||
|
class="form-checkbox"
|
||||||
|
/>
|
||||||
|
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
:loading="isLoading"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
|
||||||
|
@click="handleFeaturesUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Accordion>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -333,6 +333,14 @@
|
|||||||
"RESET": "Reset",
|
"RESET": "Reset",
|
||||||
"SELECT_ASSISTANT": "Select Assistant"
|
"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": {
|
"PAYWALL": {
|
||||||
"TITLE": "Upgrade to use Captain AI",
|
"TITLE": "Upgrade to use Captain AI",
|
||||||
"AVAILABLE_ON": "Captain is not available on the free plan.",
|
"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."
|
"ERROR_MESSAGE": "There was an error creating the assistant, please try again."
|
||||||
},
|
},
|
||||||
"FORM": {
|
"FORM": {
|
||||||
|
"UPDATE": "Update",
|
||||||
|
"SECTIONS": {
|
||||||
|
"BASIC_INFO": "Basic Information",
|
||||||
|
"SYSTEM_MESSAGES": "System Messages",
|
||||||
|
"INSTRUCTIONS": "Instructions",
|
||||||
|
"FEATURES": "Features",
|
||||||
|
"TOOLS": "Tools "
|
||||||
|
},
|
||||||
"NAME": {
|
"NAME": {
|
||||||
"LABEL": "Assistant Name",
|
"LABEL": "Name",
|
||||||
"PLACEHOLDER": "Enter a name for the assistant",
|
"PLACEHOLDER": "Enter assistant name"
|
||||||
"ERROR": "Please provide a name for the assistant"
|
|
||||||
},
|
},
|
||||||
"DESCRIPTION": {
|
"DESCRIPTION": {
|
||||||
"LABEL": "Assistant Description",
|
"LABEL": "Description",
|
||||||
"PLACEHOLDER": "Describe how and where this assistant will be used",
|
"PLACEHOLDER": "Enter assistant description"
|
||||||
"ERROR": "A description is required"
|
|
||||||
},
|
},
|
||||||
"PRODUCT_NAME": {
|
"PRODUCT_NAME": {
|
||||||
"LABEL": "Product Name",
|
"LABEL": "Product Name",
|
||||||
"PLACEHOLDER": "Enter the name of the product this assistant is designed for",
|
"PLACEHOLDER": "Enter product name"
|
||||||
"ERROR": "The product name is required"
|
},
|
||||||
|
"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": {
|
"FEATURES": {
|
||||||
"TITLE": "Features",
|
"TITLE": "Features",
|
||||||
@@ -395,7 +424,8 @@
|
|||||||
"EDIT": {
|
"EDIT": {
|
||||||
"TITLE": "Update the assistant",
|
"TITLE": "Update the assistant",
|
||||||
"SUCCESS_MESSAGE": "The assistant has been successfully updated",
|
"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": {
|
"OPTIONS": {
|
||||||
"EDIT_ASSISTANT": "Edit Assistant",
|
"EDIT_ASSISTANT": "Edit Assistant",
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, onMounted } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useStore } from 'dashboard/composables/store';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||||
|
import EditAssistantForm from '../../../../components-next/captain/pageComponents/assistant/EditAssistantForm.vue';
|
||||||
|
import AssistantPlayground from 'dashboard/components-next/captain/assistant/AssistantPlayground.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const store = useStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const assistantId = route.params.assistantId;
|
||||||
|
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
||||||
|
const isFetching = computed(() => uiFlags.value.fetchingItem);
|
||||||
|
const assistant = computed(() =>
|
||||||
|
store.getters['captainAssistants/getRecord'](Number(assistantId))
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAssistantAvailable = computed(() => !!assistant.value?.id);
|
||||||
|
|
||||||
|
const handleSubmit = async updatedAssistant => {
|
||||||
|
try {
|
||||||
|
await store.dispatch('captainAssistants/update', {
|
||||||
|
id: assistantId,
|
||||||
|
...updatedAssistant,
|
||||||
|
});
|
||||||
|
useAlert(t('CAPTAIN.ASSISTANTS.EDIT.SUCCESS_MESSAGE'));
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error?.message || t('CAPTAIN.ASSISTANTS.EDIT.ERROR_MESSAGE');
|
||||||
|
useAlert(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isAssistantAvailable.value) {
|
||||||
|
store.dispatch('captainAssistants/show', assistantId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageLayout
|
||||||
|
:header-title="assistant?.name"
|
||||||
|
:show-pagination-footer="false"
|
||||||
|
:is-fetching="isFetching"
|
||||||
|
:show-know-more="false"
|
||||||
|
:back-url="{ name: 'captain_assistants_index' }"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div v-if="!isAssistantAvailable">
|
||||||
|
{{ t('CAPTAIN.ASSISTANTS.EDIT.NOT_FOUND') }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex gap-4 h-full">
|
||||||
|
<div class="flex-1 lg:overflow-auto pr-4 h-full md:h-auto">
|
||||||
|
<EditAssistantForm
|
||||||
|
:assistant="assistant"
|
||||||
|
mode="edit"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-[400px] hidden lg:block h-full">
|
||||||
|
<AssistantPlayground :assistant-id="Number(assistantId)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</PageLayout>
|
||||||
|
</template>
|
||||||
@@ -36,8 +36,10 @@ const handleCreate = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
dialogType.value = 'edit';
|
router.push({
|
||||||
nextTick(() => createAssistantDialog.value.dialogRef.open());
|
name: 'captain_assistants_edit',
|
||||||
|
params: { assistantId: selectedAssistant.value.id },
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewConnectedInboxes = () => {
|
const handleViewConnectedInboxes = () => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
|||||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||||
import { frontendURL } from '../../../helper/URLHelper';
|
import { frontendURL } from '../../../helper/URLHelper';
|
||||||
import AssistantIndex from './assistants/Index.vue';
|
import AssistantIndex from './assistants/Index.vue';
|
||||||
|
import AssistantEdit from './assistants/Edit.vue';
|
||||||
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
|
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
|
||||||
import DocumentsIndex from './documents/Index.vue';
|
import DocumentsIndex from './documents/Index.vue';
|
||||||
import ResponsesIndex from './responses/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(
|
path: frontendURL(
|
||||||
'accounts/:accountId/captain/assistants/:assistantId/inboxes'
|
'accounts/:accountId/captain/assistants/:assistantId/inboxes'
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
namespace :captain do
|
namespace :captain do
|
||||||
resources :assistants do
|
resources :assistants do
|
||||||
|
member do
|
||||||
|
post :playground
|
||||||
|
end
|
||||||
resources :inboxes, only: [:index, :create, :destroy], param: :inbox_id
|
resources :inboxes, only: [:index, :create, :destroy], param: :inbox_id
|
||||||
end
|
end
|
||||||
resources :documents, only: [:index, :show, :create, :destroy]
|
resources :documents, only: [:index, :show, :create, :destroy]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
|
|||||||
before_action :current_account
|
before_action :current_account
|
||||||
before_action -> { check_authorization(Captain::Assistant) }
|
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
|
def index
|
||||||
@assistants = account_assistants.ordered
|
@assistants = account_assistants.ordered
|
||||||
@@ -23,6 +23,15 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
|
|||||||
head :no_content
|
head :no_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def playground
|
||||||
|
response = Captain::Llm::AssistantChatService.new(assistant: @assistant).generate_response(
|
||||||
|
params[:message_content],
|
||||||
|
message_history
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: response
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_assistant
|
def set_assistant
|
||||||
@@ -34,6 +43,19 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def assistant_params
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ module Captain::ChatHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_response(response)
|
def handle_response(response)
|
||||||
|
Rails.logger.debug { "[CAPTAIN][ChatCompletion] #{response}" }
|
||||||
message = response.dig('choices', 0, 'message')
|
message = response.dig('choices', 0, 'message')
|
||||||
if message['tool_calls']
|
if message['tool_calls']
|
||||||
process_tool_calls(message['tool_calls'])
|
process_tool_calls(message['tool_calls'])
|
||||||
@@ -46,20 +47,26 @@ module Captain::ChatHelper
|
|||||||
|
|
||||||
def process_tool_calls(tool_calls)
|
def process_tool_calls(tool_calls)
|
||||||
append_tool_calls(tool_calls)
|
append_tool_calls(tool_calls)
|
||||||
process_tool_call(tool_calls.first)
|
tool_calls.each do |tool_call|
|
||||||
end
|
process_tool_call(tool_call)
|
||||||
|
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)
|
|
||||||
request_chat_completion
|
request_chat_completion
|
||||||
end
|
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)
|
def fetch_documentation(query)
|
||||||
|
Rails.logger.debug { "[CAPTAIN][DocumentationSearch] #{query}" }
|
||||||
@assistant
|
@assistant
|
||||||
.responses
|
.responses
|
||||||
.approved
|
.approved
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_handoff_message
|
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
|
end
|
||||||
|
|
||||||
def create_messages
|
def create_messages
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ class Captain::InboxPendingConversationsResolutionJob < ApplicationJob
|
|||||||
# limiting the number of conversations to be resolved to avoid any performance issues
|
# 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 = inbox.conversations.pending.where('last_activity_at < ? ', Time.now.utc - 1.hour).limit(Limits::BULK_ACTIONS_LIMIT)
|
||||||
resolvable_conversations.each do |conversation|
|
resolvable_conversations.each do |conversation|
|
||||||
|
resolution_message = conversation.inbox.captain_assistant.config['resolution_message']
|
||||||
conversation.messages.create!(
|
conversation.messages.create!(
|
||||||
{
|
{
|
||||||
message_type: :outgoing,
|
message_type: :outgoing,
|
||||||
account_id: conversation.account_id,
|
account_id: conversation.account_id,
|
||||||
inbox_id: conversation.inbox_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!
|
conversation.resolved!
|
||||||
|
|||||||
@@ -18,4 +18,8 @@ class Captain::AssistantPolicy < ApplicationPolicy
|
|||||||
def destroy?
|
def destroy?
|
||||||
@account_user.administrator?
|
@account_user.administrator?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def playground?
|
||||||
|
true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class Captain::Llm::AssistantChatService < Llm::BaseOpenAiService
|
|||||||
def system_message
|
def system_message
|
||||||
{
|
{
|
||||||
role: 'system',
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class Captain::Llm::SystemPromptsService
|
|||||||
SYSTEM_PROMPT_MESSAGE
|
SYSTEM_PROMPT_MESSAGE
|
||||||
end
|
end
|
||||||
|
|
||||||
def assistant_response_generator(product_name)
|
def assistant_response_generator(product_name, config = {})
|
||||||
<<~SYSTEM_PROMPT_MESSAGE
|
<<~SYSTEM_PROMPT_MESSAGE
|
||||||
[Identity]
|
[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}.
|
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]
|
[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.
|
- 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).
|
- 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.
|
- 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.
|
- Use discourse markers to ease comprehension. Never use the list format.
|
||||||
- Do not generate a response more than three sentences.
|
- 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.
|
- Do not share anything outside of the context provided.
|
||||||
- Add the reasoning why you arrived at the answer
|
- 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.
|
- Your answers will always be formatted in a valid JSON hash, as shown below. Never respond in non-JSON format.
|
||||||
|
#{config['instructions'] || ''}
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
reasoning: '',
|
reasoning: '',
|
||||||
|
|||||||
@@ -175,4 +175,67 @@ RSpec.describe 'Api::V1::Accounts::Captain::Assistants', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ RSpec.describe Captain::InboxPendingConversationsResolutionJob, type: :job do
|
|||||||
include ActiveJob::TestHelper
|
include ActiveJob::TestHelper
|
||||||
|
|
||||||
let!(:inbox) { create(:inbox) }
|
let!(:inbox) { create(:inbox) }
|
||||||
|
|
||||||
let!(:resolvable_pending_conversation) { create(:conversation, inbox: inbox, last_activity_at: 2.hours.ago, status: :pending) }
|
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!(: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) }
|
let!(:open_conversation) { create(:conversation, inbox: inbox, last_activity_at: 1.hour.ago, status: :open) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
create(:captain_inbox, inbox: inbox, captain_assistant: create(:captain_assistant, account: inbox.account))
|
||||||
stub_const('Limits::BULK_ACTIONS_LIMIT', 2)
|
stub_const('Limits::BULK_ACTIONS_LIMIT', 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user