feat: Implementa gerenciamento de blocos de prompt do sistema e configurações de habilidades para assistentes, e ajusta permissões de estado vazio de ferramentas personalizadas.

This commit is contained in:
Rodrigo Borba 2026-01-06 12:59:56 -03:00
parent 4ee44fd953
commit 4141980f72
3 changed files with 415 additions and 13 deletions

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { reactive, computed, watch } from 'vue'; import { reactive, computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { minLength } from '@vuelidate/validators'; import { minLength } from '@vuelidate/validators';
@ -7,7 +7,10 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { useAccount } from 'dashboard/composables/useAccount'; import { useAccount } from 'dashboard/composables/useAccount';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import Editor from 'dashboard/components-next/Editor/Editor.vue'; import Editor from 'dashboard/components-next/Editor/Editor.vue';
import Input from 'dashboard/components-next/input/Input.vue';
import Draggable from 'vuedraggable';
const props = defineProps({ const props = defineProps({
assistant: { assistant: {
@ -36,6 +39,18 @@ const initialState = {
}; };
const state = reactive({ ...initialState }); const state = reactive({ ...initialState });
const systemPromptBlocks = ref([]);
const activeBlockIndex = ref(null);
const activeBlockDraft = reactive({ title: '', content: '' });
const blockEditorDialog = ref(null);
const promptPreviewDialog = ref(null);
const systemPromptVersions = computed(
() => props.assistant?.config?.system_prompt_versions || []
);
const hasSystemPromptVersions = computed(
() => systemPromptVersions.value.length > 0
);
const validationRules = { const validationRules = {
handoffMessage: { minLength: minLength(1) }, handoffMessage: { minLength: minLength(1) },
@ -57,6 +72,15 @@ const formErrors = computed(() => ({
playbook: getErrorMessage('playbook'), playbook: getErrorMessage('playbook'),
})); }));
const normalizeBlocks = blocks =>
blocks.map((block, index) => ({
uid: block.uid || `${Date.now()}-${index}`,
key: block.key || null,
title: block.title || '',
content: block.content || '',
order: block.order ?? index,
}));
const updateStateFromAssistant = assistant => { const updateStateFromAssistant = assistant => {
const { config = {} } = assistant; const { config = {} } = assistant;
state.handoffMessage = config.handoff_message || ''; state.handoffMessage = config.handoff_message || '';
@ -67,9 +91,94 @@ const updateStateFromAssistant = assistant => {
state.distanceThreshold = state.distanceThreshold =
config.distance_threshold !== undefined ? config.distance_threshold : 0.35; config.distance_threshold !== undefined ? config.distance_threshold : 0.35;
state.maxRagResults = config.max_rag_results || 3; state.maxRagResults = config.max_rag_results || 3;
const blocks =
config.system_prompt_blocks || assistant.system_prompt_blocks_preview || [];
systemPromptBlocks.value = normalizeBlocks(blocks);
};
const sanitizedBlocks = computed(() =>
systemPromptBlocks.value.map((block, index) => ({
key: block.key || null,
title: block.title,
content: block.content,
order: index,
}))
);
const fullPrompt = computed(() =>
systemPromptBlocks.value
.map(block => {
const title = block.title?.trim();
const content = block.content?.trim();
if (!title && !content) return null;
return `[${title}]\n${content}`;
})
.filter(Boolean)
.join('\n\n')
);
const promptCharLimit = computed(() => {
const provider = props.assistant?.llm_provider || '';
const model = props.assistant?.llm_model || '';
const isGemini =
provider.toLowerCase().includes('gemini') ||
model.toLowerCase().includes('gemini');
return isGemini ? 80000 : 40000;
});
const isPromptOverLimit = computed(
() => fullPrompt.value.length > promptCharLimit.value
);
const activeBlockTitle = computed({
get() {
return activeBlockDraft.title;
},
set(value) {
activeBlockDraft.title = value;
},
});
const activeBlockContent = computed({
get() {
return activeBlockDraft.content;
},
set(value) {
activeBlockDraft.content = value;
},
});
const playbookFromBlocks = computed(() => {
const block = systemPromptBlocks.value.find(b => b.key === 'playbook');
return block?.content || '';
});
const buildPayload = (extra = {}) => {
const config = {
...props.assistant.config,
handoff_message: state.handoffMessage,
resolution_message: state.resolutionMessage,
temperature: state.temperature !== undefined ? state.temperature : 1,
playbook: state.playbook,
distance_threshold: state.distanceThreshold,
max_rag_results: state.maxRagResults,
};
if (isCaptainV2Enabled.value) {
config.system_prompt_blocks = sanitizedBlocks.value;
config.playbook = playbookFromBlocks.value || state.playbook;
}
return {
config,
...extra,
};
}; };
const handleSystemMessagesUpdate = async () => { const handleSystemMessagesUpdate = async () => {
if (isCaptainV2Enabled.value && isPromptOverLimit.value) {
return;
}
const validations = [ const validations = [
v$.value.handoffMessage.$validate(), v$.value.handoffMessage.$validate(),
v$.value.resolutionMessage.$validate(), v$.value.resolutionMessage.$validate(),
@ -84,17 +193,7 @@ const handleSystemMessagesUpdate = async () => {
); );
if (!result) return; if (!result) return;
const payload = { const payload = buildPayload();
config: {
...props.assistant.config,
handoff_message: state.handoffMessage,
resolution_message: state.resolutionMessage,
temperature: state.temperature !== undefined ? state.temperature : 1,
playbook: state.playbook,
distance_threshold: state.distanceThreshold,
max_rag_results: state.maxRagResults,
},
};
if (!isCaptainV2Enabled.value) { if (!isCaptainV2Enabled.value) {
payload.config.instructions = state.instructions; payload.config.instructions = state.instructions;
@ -103,6 +202,71 @@ const handleSystemMessagesUpdate = async () => {
emit('submit', payload); emit('submit', payload);
}; };
const handleSaveSystemPromptVersion = () => {
if (isPromptOverLimit.value) return;
const payload = buildPayload({ system_prompt_action: 'save_version' });
emit('submit', payload);
};
const handleRevertSystemPromptVersion = () => {
const payload = buildPayload({ system_prompt_action: 'revert_last' });
emit('submit', payload);
};
const handleRestoreSystemPromptDefault = () => {
const payload = buildPayload({ system_prompt_action: 'restore_default' });
emit('submit', payload);
};
const handleAddBlock = () => {
systemPromptBlocks.value.push({
uid: `${Date.now()}-${systemPromptBlocks.value.length}`,
key: null,
title: '',
content: '',
order: systemPromptBlocks.value.length,
});
};
const handleRemoveBlock = index => {
systemPromptBlocks.value.splice(index, 1);
};
const handleEditBlock = index => {
activeBlockIndex.value = index;
const block = systemPromptBlocks.value[index];
activeBlockDraft.title = block?.title || '';
activeBlockDraft.content = block?.content || '';
blockEditorDialog.value?.open();
};
const handleBlockModalClosed = () => {
activeBlockIndex.value = null;
};
const handleCancelBlockModal = () => {
blockEditorDialog.value?.close();
};
const handleApplyBlockChanges = () => {
const block = systemPromptBlocks.value[activeBlockIndex.value];
if (block) {
block.title = activeBlockDraft.title;
block.content = activeBlockDraft.content;
}
handleCancelBlockModal();
};
const handleOpenPromptPreview = () => {
promptPreviewDialog.value?.open();
};
const handleClosePromptPreview = () => {
promptPreviewDialog.value?.close();
};
const handlePromptPreviewClosed = () => {};
watch( watch(
() => props.assistant, () => props.assistant,
newAssistant => { newAssistant => {
@ -143,7 +307,118 @@ watch(
class="z-0" class="z-0"
/> />
<div v-if="isCaptainV2Enabled" class="flex flex-col gap-4">
<div class="flex items-center justify-between gap-4">
<label class="text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.LABEL') }}
</label>
<div class="flex flex-wrap gap-2">
<Button
:label="t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT.SAVE_VERSION')"
variant="faded"
color="slate"
@click="handleSaveSystemPromptVersion"
/>
<Button
:label="t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT.REVERT_LAST')"
variant="faded"
color="slate"
:disabled="!hasSystemPromptVersions"
@click="handleRevertSystemPromptVersion"
/>
<Button
:label="t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT.RESTORE_DEFAULT')"
variant="faded"
color="slate"
@click="handleRestoreSystemPromptDefault"
/>
</div>
</div>
<div class="flex items-center justify-between text-xs text-n-slate-11">
<span>
{{
t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.CHAR_COUNT', {
count: fullPrompt.length,
limit: promptCharLimit,
})
}}
</span>
<Button
:label="t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.VIEW_FULL')"
variant="faded"
color="slate"
@click="handleOpenPromptPreview"
/>
</div>
<p v-if="isPromptOverLimit" class="text-xs text-ruby-9">
{{ t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.LIMIT_WARNING') }}
</p>
<Draggable
v-model="systemPromptBlocks"
item-key="uid"
handle=".drag-handle"
class="flex flex-col gap-3"
>
<template #item="{ element, index }">
<div class="rounded-lg border border-n-weak p-4 flex flex-col gap-3">
<div class="flex items-center gap-2">
<span class="drag-handle cursor-grab text-n-slate-11">
<i class="i-lucide-grip-vertical" aria-hidden="true" />
</span>
<Input
v-model="element.title"
:placeholder="
t(
'CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.TITLE_PLACEHOLDER'
)
"
/>
</div>
<div
class="flex items-center justify-between gap-3 text-xs text-n-slate-11"
>
<span class="truncate">
{{
element.content?.slice(0, 120) ||
t(
'CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.EMPTY_CONTENT'
)
}}
</span>
<div class="flex gap-2">
<Button
:label="
t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.EDIT')
"
variant="faded"
color="slate"
@click="handleEditBlock(index)"
/>
<Button
v-if="!element.key"
:label="
t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.REMOVE')
"
variant="faded"
color="ruby"
@click="handleRemoveBlock(index)"
/>
</div>
</div>
</div>
</template>
</Draggable>
<Button
:label="t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.ADD')"
variant="faded"
color="slate"
class="w-fit"
@click="handleAddBlock"
/>
</div>
<Editor <Editor
v-if="!isCaptainV2Enabled"
v-model="state.playbook" v-model="state.playbook"
:label="t('CAPTAIN.ASSISTANTS.FORM.PLAYBOOK.LABEL')" :label="t('CAPTAIN.ASSISTANTS.FORM.PLAYBOOK.LABEL')"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.PLAYBOOK.PLACEHOLDER')" :placeholder="t('CAPTAIN.ASSISTANTS.FORM.PLAYBOOK.PLACEHOLDER')"
@ -215,8 +490,89 @@ watch(
<div> <div>
<Button <Button
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')" :label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
:disabled="isCaptainV2Enabled && isPromptOverLimit"
@click="handleSystemMessagesUpdate" @click="handleSystemMessagesUpdate"
/> />
</div> </div>
<Dialog
ref="blockEditorDialog"
:title="t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.EDIT_TITLE')"
width="3xl"
overflow-y-auto
:show-confirm-button="false"
@close="handleBlockModalClosed"
>
<div class="flex flex-col gap-4 min-h-[75vh]">
<Input
v-model="activeBlockTitle"
:label="t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.TITLE_LABEL')"
:placeholder="
t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.TITLE_PLACEHOLDER')
"
/>
<div class="system-prompt-block-editor">
<Editor
v-model="activeBlockContent"
:label="
t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.CONTENT_LABEL')
"
:placeholder="
t(
'CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.CONTENT_PLACEHOLDER'
)
"
:max-length="promptCharLimit"
class="z-0 h-[70vh]"
/>
</div>
</div>
<template #footer>
<div class="flex items-center justify-end w-full gap-2">
<Button
:label="t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.CANCEL')"
variant="faded"
color="slate"
type="button"
@click.stop="handleCancelBlockModal"
/>
<Button
:label="t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.DONE')"
type="button"
@click.stop="handleApplyBlockChanges"
/>
</div>
</template>
</Dialog>
<Dialog
ref="promptPreviewDialog"
:title="t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.PREVIEW_TITLE')"
width="3xl"
:show-confirm-button="false"
@close="handlePromptPreviewClosed"
>
<pre
class="text-xs text-n-slate-11 whitespace-pre-wrap max-h-[60vh] overflow-y-auto"
>
{{ fullPrompt }}
</pre>
<template #footer>
<div class="flex items-center justify-end w-full">
<Button
:label="t('CAPTAIN.ASSISTANTS.FORM.SYSTEM_PROMPT_BLOCKS.CLOSE')"
type="button"
@click="handleClosePromptPreview"
/>
</div>
</template>
</Dialog>
</div> </div>
</template> </template>
<style scoped>
.system-prompt-block-editor :deep(.ProseMirror-woot-style) {
min-height: 60vh;
max-height: 60vh;
}
</style>

View File

@ -13,7 +13,7 @@ const onClick = () => {
<EmptyStateLayout <EmptyStateLayout
:title="$t('CAPTAIN.CUSTOM_TOOLS.EMPTY_STATE.TITLE')" :title="$t('CAPTAIN.CUSTOM_TOOLS.EMPTY_STATE.TITLE')"
:subtitle="$t('CAPTAIN.CUSTOM_TOOLS.EMPTY_STATE.SUBTITLE')" :subtitle="$t('CAPTAIN.CUSTOM_TOOLS.EMPTY_STATE.SUBTITLE')"
:action-perms="['administrator']" :action-perms="[]"
> >
<template #empty-state-item> <template #empty-state-item>
<div class="min-h-[600px]" /> <div class="min-h-[600px]" />

View File

@ -501,6 +501,32 @@
"LABEL": "Instructions", "LABEL": "Instructions",
"PLACEHOLDER": "Enter instructions for the assistant" "PLACEHOLDER": "Enter instructions for the assistant"
}, },
"SYSTEM_PROMPT": {
"LABEL": "System Prompt",
"PLACEHOLDER": "Edit the system prompt used by the assistant",
"SAVE_VERSION": "Save version",
"REVERT_LAST": "Revert last",
"RESTORE_DEFAULT": "Restore default"
},
"SYSTEM_PROMPT_BLOCKS": {
"LABEL": "System prompt blocks",
"ADD": "Add block",
"EDIT": "Edit",
"REMOVE": "Remove",
"CANCEL": "Cancel",
"DONE": "Done",
"VIEW_FULL": "View full prompt",
"PREVIEW_TITLE": "Full system prompt",
"EDIT_TITLE": "Edit block",
"TITLE_LABEL": "Block title",
"TITLE_PLACEHOLDER": "e.g., Identity",
"CONTENT_LABEL": "Block content",
"CONTENT_PLACEHOLDER": "Write the content for this block",
"EMPTY_CONTENT": "No content yet",
"CHAR_COUNT": "{{count}} / {{limit}} characters",
"LIMIT_WARNING": "Prompt length exceeds the limit for this model.",
"CLOSE": "Close"
},
"PLAYBOOK": { "PLAYBOOK": {
"LABEL": "SDR Playbook", "LABEL": "SDR Playbook",
"PLACEHOLDER": "Sales script and objection handling...", "PLACEHOLDER": "Sales script and objection handling...",
@ -735,6 +761,26 @@
} }
} }
} }
,
"SKILLS": {
"HEADER": "Assistant Skills",
"DESCRIPTION": "Configure the capabilities and tools available to this assistant.",
"SAVING": "Saving...",
"CONFIGURATION": "Configuration",
"EMPTY_STATE": "No skills available for this assistant.",
"WEBHOOK_URL": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "https://oxpi.com.br/api/..."
},
"PLUG_PLAY_ID": {
"LABEL": "Plug&Play Client ID",
"PLACEHOLDER": "Client ID"
},
"PLUG_PLAY_TOKEN": {
"LABEL": "Plug&Play Token",
"PLACEHOLDER": "Token"
}
}
}, },
"DOCUMENTS": { "DOCUMENTS": {
"HEADER": "Documents", "HEADER": "Documents",