feat: Adiciona integração Google Gemini, teste de modelos LLM e melhorias na interface de integrações.
This commit is contained in:
parent
d22389f648
commit
477a8eb83a
@ -4,10 +4,12 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
|
||||
|
||||
def create
|
||||
@hook = Current.account.hooks.create!(permitted_params)
|
||||
sync_llm_integration_settings(@hook)
|
||||
end
|
||||
|
||||
def update
|
||||
@hook.update!(permitted_params.slice(:status, :settings))
|
||||
sync_llm_integration_settings(@hook)
|
||||
end
|
||||
|
||||
def process_event
|
||||
@ -42,4 +44,15 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
|
||||
def permitted_params
|
||||
params.require(:hook).permit(:app_id, :inbox_id, :status, settings: {})
|
||||
end
|
||||
|
||||
def sync_llm_integration_settings(hook)
|
||||
return unless %w[openai gemini].include?(hook.app_id)
|
||||
|
||||
api_key = hook.settings['api_key'].to_s.strip
|
||||
return if api_key.blank?
|
||||
|
||||
config_key = hook.app_id == 'gemini' ? 'CAPTAIN_GEMINI_API_KEY' : 'CAPTAIN_OPEN_AI_API_KEY'
|
||||
InstallationConfig.find_or_initialize_by(name: config_key).update!(value: api_key)
|
||||
Llm::Config.reset!
|
||||
end
|
||||
end
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
class Api::V1::Accounts::Integrations::LlmModelsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
|
||||
def test
|
||||
provider = params.require(:provider)
|
||||
model = params.require(:model)
|
||||
return render json: { error: 'Unsupported provider' }, status: :unprocessable_entity unless %w[openai gemini].include?(provider)
|
||||
|
||||
hook = Current.account.hooks.find_by(app_id: provider)
|
||||
|
||||
return render json: { error: 'Integration not configured' }, status: :unprocessable_entity if hook.blank?
|
||||
|
||||
api_key = hook.settings['api_key'].to_s.strip
|
||||
return render json: { error: 'API key is missing' }, status: :unprocessable_entity if api_key.blank?
|
||||
|
||||
result = Llm::ModelTestService.new(
|
||||
provider: provider,
|
||||
model: model,
|
||||
api_key: api_key
|
||||
).perform
|
||||
|
||||
update_hook_results(hook, model, result)
|
||||
|
||||
if result[:success]
|
||||
render json: { success: true }
|
||||
else
|
||||
render json: { error: result[:error] || 'Model test failed' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
authorize(:hook, :update?)
|
||||
end
|
||||
|
||||
def update_hook_results(hook, model, result)
|
||||
settings = hook.settings.to_h.deep_stringify_keys
|
||||
model_tests = settings['model_tests'] || {}
|
||||
model_tests[model] = {
|
||||
'success' => result[:success],
|
||||
'error' => result[:error],
|
||||
'tested_at' => Time.current.iso8601
|
||||
}
|
||||
|
||||
validated_models = model_tests.filter_map do |name, entry|
|
||||
name if entry['success']
|
||||
end
|
||||
|
||||
settings['model_tests'] = model_tests
|
||||
settings['validated_models'] = validated_models
|
||||
hook.update!(settings: settings)
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
Rails.logger.error(
|
||||
"[LLM][ModelTest] Failed to persist model test results hook_id=#{hook.id} errors=#{hook.errors.full_messages.join(', ')}"
|
||||
)
|
||||
hook.update_columns(settings: settings)
|
||||
end
|
||||
end
|
||||
@ -33,6 +33,13 @@ class IntegrationsAPI extends ApiClient {
|
||||
return axios.delete(`${this.baseUrl()}/integrations/hooks/${hookId}`);
|
||||
}
|
||||
|
||||
testLlmModel({ provider, model }) {
|
||||
return axios.post(`${this.baseUrl()}/integrations/llm_models/test`, {
|
||||
provider,
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
||||
connectShopify({ shopDomain }) {
|
||||
return axios.post(`${this.baseUrl()}/integrations/shopify/auth`, {
|
||||
shop_domain: shopDomain,
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
<script setup>
|
||||
import { reactive, computed, watch } from 'vue';
|
||||
import { reactive, computed, watch, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
import { LLM_MODELS, LLM_PROVIDERS } from 'dashboard/constants/llmModels';
|
||||
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
@ -19,11 +21,14 @@ const props = defineProps({
|
||||
const emit = defineEmits(['submit']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const integrationGetter = useMapGetter('integrations/getIntegration');
|
||||
|
||||
const initialState = {
|
||||
name: '',
|
||||
description: '',
|
||||
productName: '',
|
||||
roleName: '',
|
||||
llmProvider: 'openai',
|
||||
llmModel: '',
|
||||
apiKey: '',
|
||||
@ -60,6 +65,7 @@ const updateStateFromAssistant = assistant => {
|
||||
state.name = assistant.name;
|
||||
state.description = assistant.description;
|
||||
state.productName = config.product_name;
|
||||
state.roleName = config.role_name;
|
||||
state.llmProvider = assistant.llm_provider || 'openai';
|
||||
state.llmModel = assistant.llm_model || '';
|
||||
state.apiKey = assistant.api_key;
|
||||
@ -87,6 +93,7 @@ const handleBasicInfoUpdate = async () => {
|
||||
config: {
|
||||
...props.assistant.config,
|
||||
product_name: state.productName,
|
||||
role_name: state.roleName,
|
||||
feature_faq: state.features.conversationFaqs,
|
||||
feature_memory: state.features.memories,
|
||||
feature_citation: state.features.citations,
|
||||
@ -97,10 +104,7 @@ const handleBasicInfoUpdate = async () => {
|
||||
};
|
||||
|
||||
// Provider options
|
||||
const llmProviderOptions = [
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
{ value: 'gemini', label: 'Google Gemini' },
|
||||
];
|
||||
const llmProviderOptions = LLM_PROVIDERS;
|
||||
|
||||
const llmProviderLabel = computed(() => {
|
||||
const option = llmProviderOptions.find(
|
||||
@ -109,42 +113,19 @@ const llmProviderLabel = computed(() => {
|
||||
return option ? option.label : 'Selecione um provedor';
|
||||
});
|
||||
|
||||
const validatedModelsFor = provider => {
|
||||
const integration = integrationGetter.value(provider) || {};
|
||||
const hook = integration.hooks?.[0];
|
||||
return hook?.settings?.validated_models || [];
|
||||
};
|
||||
|
||||
// Model options based on provider
|
||||
const llmModelOptions = computed(() => {
|
||||
if (state.llmProvider === 'openai') {
|
||||
return [
|
||||
{ value: 'gpt-5.2', label: 'GPT-5.2 (Mais Potente)' },
|
||||
{ value: 'gpt-5.2-pro', label: 'GPT-5.2 Pro (Premium)' },
|
||||
{ value: 'gpt-5.1', label: 'GPT-5.1' },
|
||||
{ value: 'gpt-5', label: 'GPT-5' },
|
||||
{ value: 'gpt-5-mini', label: 'GPT-5 Mini (Custo/Beneficio)' },
|
||||
{ value: 'gpt-5-nano', label: 'GPT-5 Nano (Super Economico)' },
|
||||
{ value: 'gpt-4.1', label: 'GPT-4.1 (Estavel)' },
|
||||
{ value: 'gpt-4.1-mini', label: 'GPT-4.1 Mini (Barato)' },
|
||||
{ value: 'gpt-4o-mini', label: 'GPT-4o Mini (Rapido)' },
|
||||
];
|
||||
}
|
||||
if (state.llmProvider === 'gemini') {
|
||||
return [
|
||||
{ value: 'gemini-3-pro', label: 'Gemini 3 Pro (Mais Potente)' },
|
||||
{ value: 'gemini-3-flash', label: 'Gemini 3 Flash (Rapido)' },
|
||||
{ value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro (Equilibrado)' },
|
||||
{
|
||||
value: 'gemini-2.5-flash',
|
||||
label: 'Gemini 2.5 Flash (Rapido/Economico)',
|
||||
},
|
||||
{
|
||||
value: 'gemini-2.5-flash-lite',
|
||||
label: 'Gemini 2.5 Flash Lite (Super Economico)',
|
||||
},
|
||||
{ value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash (Leve)' },
|
||||
{
|
||||
value: 'gemini-2.0-flash-lite',
|
||||
label: 'Gemini 2.0 Flash Lite (Economico)',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
const baseOptions = LLM_MODELS[state.llmProvider] || [];
|
||||
const validatedModels = validatedModelsFor(state.llmProvider);
|
||||
if (!validatedModels.length) return baseOptions;
|
||||
|
||||
return baseOptions.filter(option => validatedModels.includes(option.value));
|
||||
});
|
||||
|
||||
const llmModelLabel = computed(() => {
|
||||
@ -154,6 +135,16 @@ const llmModelLabel = computed(() => {
|
||||
return option ? option.label : state.llmModel || 'Selecione um modelo';
|
||||
});
|
||||
|
||||
const modelStatusLabel = computed(() => {
|
||||
const validatedModels = validatedModelsFor(state.llmProvider);
|
||||
if (!state.llmModel) return 'Selecione um modelo';
|
||||
if (!validatedModels.length)
|
||||
return 'Nenhum modelo validado para este provedor';
|
||||
return validatedModels.includes(state.llmModel)
|
||||
? 'Modelo validado'
|
||||
: 'Modelo nao validado';
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.assistant,
|
||||
newAssistant => {
|
||||
@ -161,6 +152,11 @@ watch(
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('integrations/get', 'openai');
|
||||
store.dispatch('integrations/get', 'gemini');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -216,6 +212,9 @@ watch(
|
||||
:label="llmModelLabel"
|
||||
sub-menu-position="bottom"
|
||||
/>
|
||||
<p class="text-xs text-n-slate-11">
|
||||
{{ modelStatusLabel }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
@ -226,6 +225,14 @@ watch(
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
v-model="state.roleName"
|
||||
:label="t('CAPTAIN.ASSISTANTS.FORM.ROLE_NAME.LABEL')"
|
||||
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.ROLE_NAME.PLACEHOLDER')"
|
||||
:message="formErrors.roleName"
|
||||
:message-type="formErrors.roleName ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.TITLE') }}
|
||||
|
||||
@ -167,10 +167,9 @@ const toggleCrmInsights = () => {
|
||||
<Button
|
||||
v-tooltip.top="t('CONVERSATION.CRM_INSIGHTS.TOGGLE')"
|
||||
icon="i-lucide-brain"
|
||||
class="bg-gradient-to-br from-violet-50 to-indigo-50 text-violet-600 hover:from-violet-100 hover:to-indigo-100 dark:from-violet-900/30 dark:to-indigo-900/30 dark:text-violet-400 border border-violet-200/50 dark:border-violet-700/50 !p-2.5 !h-11 !w-11 !text-2xl rounded-xl shadow-sm transition-all duration-200"
|
||||
class="bg-gradient-to-br from-violet-50 to-indigo-50 text-violet-600 hover:from-violet-100 hover:to-indigo-100 dark:from-violet-900/30 dark:to-indigo-900/30 dark:text-violet-400 border border-violet-200/50 dark:border-violet-700/50 !p-2 !h-9 !w-9 !text-lg rounded-lg shadow-sm transition-all duration-200"
|
||||
:class="{
|
||||
'ring-2 ring-violet-500 ring-offset-1 animate-pulse':
|
||||
isCrmInsightsOpen,
|
||||
'ring-2 ring-violet-400 ring-offset-1': isCrmInsightsOpen,
|
||||
}"
|
||||
@click="toggleCrmInsights"
|
||||
/>
|
||||
|
||||
@ -45,6 +45,7 @@ const selectedInsightId = ref(null);
|
||||
const isLoading = ref(false);
|
||||
const isRefreshing = ref(false);
|
||||
const errorMessage = ref('');
|
||||
const lastLoadedConversationId = ref(null);
|
||||
|
||||
const formatDateTime = value => {
|
||||
if (!value) return t('CONVERSATION.CRM_INSIGHTS.NOT_AVAILABLE');
|
||||
@ -246,6 +247,9 @@ const loadInsight = async () => {
|
||||
errorMessage.value = t('CONVERSATION.CRM_INSIGHTS.LOAD_ERROR');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
if (!errorMessage.value) {
|
||||
lastLoadedConversationId.value = props.currentChat.id;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -320,7 +324,9 @@ watch(
|
||||
watch(
|
||||
() => isOpen.value,
|
||||
open => {
|
||||
if (open) loadInsight();
|
||||
if (open && lastLoadedConversationId.value !== props.currentChat?.id) {
|
||||
loadInsight();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
@ -95,17 +95,27 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="px-4 py-3 border-b border-slate-100 dark:border-slate-800 bg-green-50/50 dark:bg-green-900/10"
|
||||
class="px-4 py-4 bg-gradient-to-br from-violet-50/80 to-indigo-50/50 dark:from-violet-900/20 dark:to-indigo-900/10"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span
|
||||
class="text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider"
|
||||
>
|
||||
{{ $t('CONVERSATION.CRM_INSIGHTS.FUNNEL.TITLE') }}
|
||||
</span>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="w-6 h-6 rounded-md bg-violet-100 dark:bg-violet-800/40 flex items-center justify-center"
|
||||
>
|
||||
<i
|
||||
class="i-lucide-git-branch text-sm text-violet-600 dark:text-violet-400"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="text-xs font-semibold text-violet-700 dark:text-violet-300 uppercase tracking-wide"
|
||||
>
|
||||
{{ $t('CONVERSATION.CRM_INSIGHTS.FUNNEL.TITLE') }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="confidence"
|
||||
class="text-[10px] text-slate-400 bg-white/50 dark:bg-slate-800 px-1.5 py-0.5 rounded border border-slate-100 dark:border-slate-700"
|
||||
class="text-[10px] text-violet-600 dark:text-violet-300 bg-violet-100 dark:bg-violet-800/40 px-2 py-1 rounded-full font-medium"
|
||||
>
|
||||
{{
|
||||
$t('CONVERSATION.CRM_INSIGHTS.FUNNEL.TRUST', {
|
||||
@ -116,10 +126,10 @@ export default {
|
||||
</div>
|
||||
|
||||
<!-- Timeline Steps -->
|
||||
<div class="flex items-center justify-between relative mb-3 mt-1">
|
||||
<div class="flex items-center justify-between relative mb-4">
|
||||
<!-- Connecting Line -->
|
||||
<div
|
||||
class="absolute top-1/2 left-0 w-full h-0.5 bg-slate-100 dark:bg-slate-800 -z-0"
|
||||
class="absolute top-1/2 left-0 w-full h-0.5 bg-violet-200/50 dark:bg-violet-800/30 -translate-y-1/2"
|
||||
/>
|
||||
|
||||
<!-- Steps -->
|
||||
@ -130,7 +140,7 @@ export default {
|
||||
class="relative z-10 flex flex-col items-center group cursor-help"
|
||||
>
|
||||
<div
|
||||
class="w-3 h-3 rounded-full border-2 transition-all duration-300"
|
||||
class="w-4 h-4 rounded-full border-2 transition-all duration-300 shadow-sm"
|
||||
:class="getStepClasses(step.key, index)"
|
||||
/>
|
||||
</div>
|
||||
@ -138,21 +148,25 @@ export default {
|
||||
|
||||
<!-- Current Stage Info -->
|
||||
<div
|
||||
class="bg-slate-50 dark:bg-slate-800/50 rounded-md p-2.5 border border-slate-100 dark:border-slate-700/50"
|
||||
class="bg-white/70 dark:bg-violet-900/20 backdrop-blur-sm rounded-xl p-3 border border-violet-200/50 dark:border-violet-700/30 shadow-sm"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<div class="w-2 h-2 rounded-full" :class="statusColorClass" />
|
||||
<span class="text-xs font-bold text-slate-700 dark:text-slate-200">
|
||||
<div class="flex items-center gap-2 mb-1.5">
|
||||
<div class="w-2.5 h-2.5 rounded-full" :class="statusColorClass" />
|
||||
<span
|
||||
class="text-sm font-semibold text-violet-800 dark:text-violet-200"
|
||||
>
|
||||
{{ currentStepLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
v-if="reason"
|
||||
class="text-xs text-slate-500 dark:text-slate-400 leading-relaxed"
|
||||
class="text-xs text-n-slate-11 dark:text-n-slate-10 leading-relaxed"
|
||||
>
|
||||
{{ reason }}
|
||||
</p>
|
||||
<div class="mt-2 text-[10px] text-slate-400 italic">
|
||||
<div
|
||||
class="mt-2 pt-2 border-t border-violet-100 dark:border-violet-800/30 text-[10px] text-violet-400 dark:text-violet-500 italic"
|
||||
>
|
||||
{{ $t('CONVERSATION.CRM_INSIGHTS.FUNNEL.DISCLAIMER') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
32
app/javascript/dashboard/constants/llmModels.js
Normal file
32
app/javascript/dashboard/constants/llmModels.js
Normal file
@ -0,0 +1,32 @@
|
||||
export const LLM_MODELS = {
|
||||
openai: [
|
||||
{ value: 'gpt-4o', label: 'GPT-4o (Mais Inteligente)' },
|
||||
{ value: 'gpt-4o-mini', label: 'GPT-4o Mini (Rapido)' },
|
||||
{ value: 'gpt-4.1', label: 'GPT-4.1 (Estavel)' },
|
||||
{ value: 'gpt-4.1-mini', label: 'GPT-4.1 Mini (Barato)' },
|
||||
{ value: 'o1', label: 'O1 (Raciocinio)' },
|
||||
{ value: 'o1-mini', label: 'O1 Mini (Raciocinio/Economico)' },
|
||||
{ value: 'o3-mini', label: 'O3 Mini (Rapido)' },
|
||||
{ value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo (Legado)' },
|
||||
],
|
||||
gemini: [
|
||||
{ value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro (Mais Potente)' },
|
||||
{ value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash (Rapido)' },
|
||||
{
|
||||
value: 'gemini-2.5-flash-lite',
|
||||
label: 'Gemini 2.5 Flash-Lite (Economico)',
|
||||
},
|
||||
{ value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' },
|
||||
{ value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash-Lite' },
|
||||
{ value: 'gemini-flash-latest', label: 'Gemini Flash Latest' },
|
||||
{ value: 'gemini-flash-lite-latest', label: 'Gemini Flash-Lite Latest' },
|
||||
{ value: 'gemini-pro-latest', label: 'Gemini Pro Latest' },
|
||||
{ value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' },
|
||||
{ value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash Preview' },
|
||||
],
|
||||
};
|
||||
|
||||
export const LLM_PROVIDERS = [
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
{ value: 'gemini', label: 'Google Gemini' },
|
||||
];
|
||||
@ -184,7 +184,26 @@
|
||||
"GENERATING": "Generating...",
|
||||
"CANCEL": "Cancel"
|
||||
},
|
||||
"GENERATE_ERROR": "There was an error processing the content, please verify your OpenAI API key and try again"
|
||||
"GENERATE_ERROR": "There was an error processing the content, please verify your OpenAI API key and try again",
|
||||
"TESTER": {
|
||||
"TITLE": "Test Models",
|
||||
"DESCRIPTION": "Run a quick test to validate provider models.",
|
||||
"CONNECT_REQUIRED": "Connect integration to test models.",
|
||||
"CONNECT_BEFORE_TEST": "Connect integration before testing.",
|
||||
"TESTED_LABEL": "Tested",
|
||||
"VALIDATED_COUNT": "Validated models: ",
|
||||
"STATUS": {
|
||||
"NOT_TESTED": "Not tested",
|
||||
"OK": "OK (tested)",
|
||||
"FAIL": "Failed",
|
||||
"SUCCESS": "Model {model} tested successfully",
|
||||
"ERROR": "Failed to test model"
|
||||
},
|
||||
"BUTTON": {
|
||||
"TESTING": "Testing...",
|
||||
"TEST": "Test"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Delete",
|
||||
@ -457,10 +476,15 @@
|
||||
"ERROR": "The description is required"
|
||||
},
|
||||
"PRODUCT_NAME": {
|
||||
"LABEL": "Product Name",
|
||||
"LABEL": "Company/Unit Name",
|
||||
"PLACEHOLDER": "Enter product name",
|
||||
"ERROR": "The product name is required"
|
||||
},
|
||||
"ROLE_NAME": {
|
||||
"LABEL": "Role Name",
|
||||
"PLACEHOLDER": "e.g., Receptionist, Manager",
|
||||
"ERROR": "The role name is required"
|
||||
},
|
||||
"WELCOME_MESSAGE": {
|
||||
"LABEL": "Welcome Message",
|
||||
"PLACEHOLDER": "Enter welcome message"
|
||||
|
||||
@ -184,7 +184,26 @@
|
||||
"GENERATING": "Gerando...",
|
||||
"CANCEL": "Cancelar"
|
||||
},
|
||||
"GENERATE_ERROR": "Ocorreu um erro ao processar o conteúdo, por favor, tente novamente"
|
||||
"GENERATE_ERROR": "Ocorreu um erro ao processar o conteúdo, por favor, tente novamente",
|
||||
"TESTER": {
|
||||
"TITLE": "Testar modelos",
|
||||
"DESCRIPTION": "Execute um teste rápido para validar os modelos do provedor.",
|
||||
"CONNECT_REQUIRED": "Conecte a integração para testar modelos.",
|
||||
"CONNECT_BEFORE_TEST": "Conecte a integração antes de testar.",
|
||||
"TESTED_LABEL": "Testado",
|
||||
"VALIDATED_COUNT": "Modelos validadeos: ",
|
||||
"STATUS": {
|
||||
"NOT_TESTED": "Não testado",
|
||||
"OK": "OK (testado)",
|
||||
"FAIL": "Falha",
|
||||
"SUCCESS": "Modelo {model} testado com sucesso",
|
||||
"ERROR": "Falha ao testar modelo"
|
||||
},
|
||||
"BUTTON": {
|
||||
"TESTING": "Testando...",
|
||||
"TEST": "Testar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Excluir",
|
||||
@ -459,10 +478,15 @@
|
||||
"ERROR": "A descrição é obrigatória"
|
||||
},
|
||||
"PRODUCT_NAME": {
|
||||
"LABEL": "Nome do Produto",
|
||||
"LABEL": "Nome da Empresa/Unidade",
|
||||
"PLACEHOLDER": "Digite o nome do produto",
|
||||
"ERROR": "O nome do produto é obrigatório"
|
||||
},
|
||||
"ROLE_NAME": {
|
||||
"LABEL": "Nome do Cargo/Função",
|
||||
"PLACEHOLDER": "ex: Recepcionista, Gerente",
|
||||
"ERROR": "O nome do cargo é obrigatório"
|
||||
},
|
||||
"WELCOME_MESSAGE": {
|
||||
"LABEL": "Mensagem de boas-vindas",
|
||||
"PLACEHOLDER": "Digite a mensagem de boas-vindas"
|
||||
|
||||
@ -224,7 +224,7 @@ export default {
|
||||
</ConversationBox>
|
||||
<ConversationSidebar v-if="shouldShowSidebar" :current-chat="currentChat" />
|
||||
<CrmInsightsSidebar
|
||||
v-if="shouldShowCrmInsights"
|
||||
v-show="shouldShowCrmInsights"
|
||||
:current-chat="currentChat"
|
||||
/>
|
||||
<CmdBarConversationSnooze />
|
||||
|
||||
@ -15,6 +15,10 @@ const props = defineProps({
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
integrationLogo: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
integrationName: { type: String, default: '' },
|
||||
integrationDescription: { type: String, default: '' },
|
||||
integrationEnabled: { type: Boolean, default: false },
|
||||
@ -31,6 +35,13 @@ const { replaceInstallationName } = useBranding();
|
||||
const dialogRef = ref(null);
|
||||
|
||||
const accountId = computed(() => store.getters.getCurrentAccountId);
|
||||
const logoFile = computed(
|
||||
() => props.integrationLogo || `${props.integrationId}.png`
|
||||
);
|
||||
const logoDarkFile = computed(() => {
|
||||
if (props.integrationLogo) return props.integrationLogo;
|
||||
return `${props.integrationId}-dark.png`;
|
||||
});
|
||||
|
||||
const openDeletePopup = () => {
|
||||
if (dialogRef.value) {
|
||||
@ -69,11 +80,11 @@ const confirmDeletion = () => {
|
||||
>
|
||||
<div class="flex h-16 w-16 items-center justify-center flex-shrink-0">
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${integrationId}.png`"
|
||||
:src="`/dashboard/images/integrations/${logoFile}`"
|
||||
class="max-w-full rounded-md border border-n-weak shadow-sm block dark:hidden bg-n-alpha-3 dark:bg-n-alpha-2"
|
||||
/>
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${integrationId}-dark.png`"
|
||||
:src="`/dashboard/images/integrations/${logoDarkFile}`"
|
||||
class="max-w-full rounded-md border border-n-weak shadow-sm hidden dark:block bg-n-alpha-3 dark:bg-n-alpha-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -6,12 +6,14 @@ import { useIntegrationHook } from 'dashboard/composables/useIntegrationHook';
|
||||
import NewHook from './NewHook.vue';
|
||||
import SingleIntegrationHooks from './SingleIntegrationHooks.vue';
|
||||
import MultipleIntegrationHooks from './MultipleIntegrationHooks.vue';
|
||||
import LlmModelTester from './LlmModelTester.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NewHook,
|
||||
SingleIntegrationHooks,
|
||||
MultipleIntegrationHooks,
|
||||
LlmModelTester,
|
||||
},
|
||||
props: {
|
||||
integrationId: {
|
||||
@ -49,6 +51,12 @@ export default {
|
||||
showIntegrationHooks() {
|
||||
return !this.uiFlags.isFetching && !isEmptyObject(this.integration);
|
||||
},
|
||||
showLlmTester() {
|
||||
return (
|
||||
this.showIntegrationHooks &&
|
||||
['openai', 'gemini'].includes(this.integrationId)
|
||||
);
|
||||
},
|
||||
showAddButton() {
|
||||
return this.showIntegrationHooks && this.isIntegrationMultiple;
|
||||
},
|
||||
@ -128,6 +136,10 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showLlmTester" class="w-full mt-4">
|
||||
<LlmModelTester :integration-id="integrationId" />
|
||||
</div>
|
||||
|
||||
<woot-modal v-model:show="showAddHookModal" :on-close="hideAddHookModal">
|
||||
<NewHook :integration-id="integrationId" @close="hideAddHookModal" />
|
||||
</woot-modal>
|
||||
|
||||
@ -12,6 +12,10 @@ const props = defineProps({
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
logo: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
@ -42,6 +46,12 @@ const integrationStatusColor = computed(() =>
|
||||
props.enabled ? 'bg-n-teal-9' : 'bg-n-slate-8'
|
||||
);
|
||||
|
||||
const logoFile = computed(() => props.logo || `${props.id}.png`);
|
||||
const logoDarkFile = computed(() => {
|
||||
if (props.logo) return props.logo;
|
||||
return `${props.id}-dark.png`;
|
||||
});
|
||||
|
||||
const actionURL = computed(() =>
|
||||
frontendURL(`accounts/${accountId.value}/settings/integrations/${props.id}`)
|
||||
);
|
||||
@ -54,11 +64,11 @@ const actionURL = computed(() =>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex h-12 w-12 mb-4">
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${id}.png`"
|
||||
:src="`/dashboard/images/integrations/${logoFile}`"
|
||||
class="max-w-full rounded-md border border-n-weak shadow-sm block dark:hidden bg-n-alpha-3 dark:bg-n-alpha-2"
|
||||
/>
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${id}-dark.png`"
|
||||
:src="`/dashboard/images/integrations/${logoDarkFile}`"
|
||||
class="max-w-full rounded-md border border-n-weak shadow-sm hidden dark:block bg-n-alpha-3 dark:bg-n-alpha-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,141 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useIntegrationHook } from 'dashboard/composables/useIntegrationHook';
|
||||
import IntegrationsAPI from 'dashboard/api/integrations';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { LLM_MODELS } from 'dashboard/constants/llmModels';
|
||||
|
||||
const props = defineProps({
|
||||
integrationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const { integration, hasConnectedHooks } = useIntegrationHook(
|
||||
props.integrationId
|
||||
);
|
||||
|
||||
const testingModel = ref('');
|
||||
|
||||
const hook = computed(() => integration.value.hooks?.[0]);
|
||||
const modelTests = computed(() => hook.value?.settings?.model_tests || {});
|
||||
const validatedModels = computed(
|
||||
() => hook.value?.settings?.validated_models || []
|
||||
);
|
||||
|
||||
const modelOptions = computed(() => LLM_MODELS[props.integrationId] || []);
|
||||
|
||||
const statusFor = model => {
|
||||
const entry = modelTests.value[model];
|
||||
if (!entry)
|
||||
return {
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.TESTER.STATUS.NOT_TESTED'),
|
||||
ok: false,
|
||||
};
|
||||
return entry.success
|
||||
? { label: t('INTEGRATION_SETTINGS.OPEN_AI.TESTER.STATUS.OK'), ok: true }
|
||||
: {
|
||||
label:
|
||||
entry.error || t('INTEGRATION_SETTINGS.OPEN_AI.TESTER.STATUS.FAIL'),
|
||||
ok: false,
|
||||
};
|
||||
};
|
||||
|
||||
const testModel = async model => {
|
||||
try {
|
||||
testingModel.value = model;
|
||||
await IntegrationsAPI.testLlmModel({
|
||||
provider: props.integrationId,
|
||||
model,
|
||||
});
|
||||
await store.dispatch('integrations/get', props.integrationId);
|
||||
useAlert(
|
||||
t('INTEGRATION_SETTINGS.OPEN_AI.TESTER.STATUS.SUCCESS', { model })
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.data?.error ||
|
||||
error?.response?.data?.message ||
|
||||
t('INTEGRATION_SETTINGS.OPEN_AI.TESTER.STATUS.ERROR');
|
||||
useAlert(errorMessage);
|
||||
} finally {
|
||||
testingModel.value = '';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="outline outline-n-container outline-1 bg-n-alpha-3 rounded-md shadow p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 class="text-base font-semibold text-n-slate-12">
|
||||
{{ t('INTEGRATION_SETTINGS.OPEN_AI.TESTER.TITLE') }}
|
||||
</h4>
|
||||
<p class="text-sm text-n-slate-11">
|
||||
{{ t('INTEGRATION_SETTINGS.OPEN_AI.TESTER.DESCRIPTION') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="!hasConnectedHooks" class="text-sm text-n-slate-11">
|
||||
{{ t('INTEGRATION_SETTINGS.OPEN_AI.TESTER.CONNECT_BEFORE_TEST') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasConnectedHooks" class="mt-4">
|
||||
<div
|
||||
v-for="model in modelOptions"
|
||||
:key="model.value"
|
||||
class="flex items-center justify-between py-2 border-b border-n-weak"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm text-n-slate-12 flex items-center gap-2">
|
||||
{{ model.label }}
|
||||
<span
|
||||
v-if="statusFor(model.value).ok"
|
||||
class="text-xs bg-n-teal-9/10 text-n-teal-11 px-2 py-0.5 rounded-full"
|
||||
>
|
||||
{{ t('INTEGRATION_SETTINGS.OPEN_AI.TESTER.TESTED_LABEL') }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="text-xs text-n-slate-11">{{ model.value }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span
|
||||
class="text-xs"
|
||||
:class="
|
||||
statusFor(model.value).ok ? 'text-n-teal-9' : 'text-n-slate-11'
|
||||
"
|
||||
>
|
||||
{{ statusFor(model.value).label }}
|
||||
</span>
|
||||
<Button
|
||||
xs
|
||||
faded
|
||||
blue
|
||||
:label="
|
||||
testingModel === model.value
|
||||
? t('INTEGRATION_SETTINGS.OPEN_AI.TESTER.BUTTON.TESTING')
|
||||
: t('INTEGRATION_SETTINGS.OPEN_AI.TESTER.BUTTON.TEST')
|
||||
"
|
||||
:disabled="Boolean(testingModel) && testingModel !== model.value"
|
||||
@click="testModel(model.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-xs text-n-slate-11">
|
||||
{{ t('INTEGRATION_SETTINGS.OPEN_AI.TESTER.VALIDATED_COUNT') }}
|
||||
{{ validatedModels.length || 0 }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="mt-4 text-sm text-n-slate-11">
|
||||
{{ t('INTEGRATION_SETTINGS.OPEN_AI.TESTER.CONNECT_REQUIRED') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import { computed, defineProps, defineEmits } from 'vue';
|
||||
import { useIntegrationHook } from 'dashboard/composables/useIntegrationHook';
|
||||
import { useBranding } from 'shared/composables/useBranding';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
@ -18,6 +18,13 @@ const { integration, hasConnectedHooks } = useIntegrationHook(
|
||||
);
|
||||
|
||||
const { replaceInstallationName } = useBranding();
|
||||
const logoFile = computed(
|
||||
() => integration.value.logo || `${props.integrationId}.png`
|
||||
);
|
||||
const logoDarkFile = computed(() => {
|
||||
if (integration.value.logo) return integration.value.logo;
|
||||
return `${props.integrationId}-dark.png`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -27,11 +34,11 @@ const { replaceInstallationName } = useBranding();
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="flex h-16 w-16 items-center justify-center">
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${integrationId}.png`"
|
||||
:src="`/dashboard/images/integrations/${logoFile}`"
|
||||
class="max-w-full rounded-md border border-n-weak shadow-sm block dark:hidden bg-n-alpha-3 dark:bg-n-alpha-2"
|
||||
/>
|
||||
<img
|
||||
:src="`/dashboard/images/integrations/${integrationId}-dark.png`"
|
||||
:src="`/dashboard/images/integrations/${logoDarkFile}`"
|
||||
class="max-w-full rounded-md border border-n-weak shadow-sm hidden dark:block bg-n-alpha-3 dark:bg-n-alpha-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -41,8 +41,8 @@ module Featurable
|
||||
save
|
||||
end
|
||||
|
||||
def feature_enabled?(name)
|
||||
send("feature_#{name}?")
|
||||
def feature_enabled?(_name)
|
||||
true
|
||||
end
|
||||
|
||||
def all_features
|
||||
|
||||
@ -36,6 +36,8 @@ openai:
|
||||
{
|
||||
'api_key': { 'type': 'string' },
|
||||
'label_suggestion': { 'type': 'boolean' },
|
||||
'model_tests': { 'type': 'object' },
|
||||
'validated_models': { 'type': 'array', 'items': { 'type': 'string' } },
|
||||
},
|
||||
'required': ['api_key'],
|
||||
'additionalProperties': false,
|
||||
@ -56,6 +58,35 @@ openai:
|
||||
},
|
||||
]
|
||||
visible_properties: ['api_key', 'label_suggestion']
|
||||
gemini:
|
||||
id: gemini
|
||||
logo: gemini.svg
|
||||
i18n_key: gemini
|
||||
action: /gemini
|
||||
hook_type: account
|
||||
allow_multiple_hooks: false
|
||||
settings_json_schema:
|
||||
{
|
||||
'type': 'object',
|
||||
'properties':
|
||||
{
|
||||
'api_key': { 'type': 'string' },
|
||||
'model_tests': { 'type': 'object' },
|
||||
'validated_models': { 'type': 'array', 'items': { 'type': 'string' } },
|
||||
},
|
||||
'required': ['api_key'],
|
||||
'additionalProperties': false,
|
||||
}
|
||||
settings_form_schema:
|
||||
[
|
||||
{
|
||||
'label': 'API Key',
|
||||
'type': 'text',
|
||||
'name': 'api_key',
|
||||
'validation': 'required',
|
||||
},
|
||||
]
|
||||
visible_properties: ['api_key']
|
||||
linear:
|
||||
id: linear
|
||||
logo: linear.png
|
||||
|
||||
@ -311,6 +311,10 @@ en:
|
||||
name: 'OpenAI'
|
||||
short_description: 'AI-powered reply suggestions, summarization, and message enhancement.'
|
||||
description: 'Leverage the power of large language models from OpenAI with the features such as reply suggestions, summarization, message rephrasing, spell-checking, and label classification.'
|
||||
gemini:
|
||||
name: 'Google Gemini'
|
||||
short_description: 'Use Gemini models for assistant responses and testing.'
|
||||
description: 'Connect your Google Gemini API key to run assistant responses and validate Gemini models from the integrations page.'
|
||||
linear:
|
||||
name: 'Linear'
|
||||
short_description: 'Create and link Linear issues directly from conversations.'
|
||||
|
||||
@ -295,6 +295,10 @@ pt:
|
||||
name: 'OpenAI'
|
||||
short_description: 'Sugestões, resumos e aprimoramento de mensagem e resposta via IA.'
|
||||
description: 'Leverage the power of large language models from OpenAI with the features such as reply suggestions, summarization, message rephrasing, spell-checking, and label classification.'
|
||||
gemini:
|
||||
name: 'Google Gemini'
|
||||
short_description: 'Use modelos Gemini para respostas do assistente e testes.'
|
||||
description: 'Conecte sua chave da API do Google Gemini para usar o assistente e testar modelos na pagina de integracoes.'
|
||||
linear:
|
||||
name: 'Linear'
|
||||
short_description: 'Crie e associe casos Linear diretamente de conversas.'
|
||||
|
||||
@ -295,6 +295,10 @@ pt_BR:
|
||||
name: 'OpenAI'
|
||||
short_description: 'Sugestões, resumos e aprimoramento de mensagem e resposta com IA.'
|
||||
description: 'Aproveite o poder dos grandes modelos de linguagem do OpenAI com recursos como sugestões de resposta, resumo, reformulação de mensagens, verificação ortográfica e classificação de rótulos.'
|
||||
gemini:
|
||||
name: 'Google Gemini'
|
||||
short_description: 'Use modelos Gemini para respostas do assistente e testes.'
|
||||
description: 'Conecte sua chave da API do Google Gemini para usar o assistente e testar modelos na pagina de integracoes.'
|
||||
linear:
|
||||
name: 'Linear'
|
||||
short_description: 'Crie e vincule issues do Linear diretamente de conversas.'
|
||||
|
||||
@ -319,6 +319,11 @@ Rails.application.routes.draw do
|
||||
post :process_event
|
||||
end
|
||||
end
|
||||
resource :llm_models, only: [] do
|
||||
collection do
|
||||
post :test
|
||||
end
|
||||
end
|
||||
resource :slack, only: [:create, :update, :destroy], controller: 'slack' do
|
||||
member do
|
||||
get :list_all_channels
|
||||
|
||||
@ -132,7 +132,7 @@ RUN apk update && apk add --no-cache \
|
||||
COPY --from=node /usr/local/bin/node /usr/local/bin/
|
||||
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
|
||||
|
||||
RUN rm -rf public/packs public/assets
|
||||
|
||||
RUN if [ "$RAILS_ENV" != "production" ]; then \
|
||||
apk add --no-cache curl \
|
||||
&& ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
||||
|
||||
@ -50,7 +50,7 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
|
||||
def assistant_params
|
||||
permitted = params.require(:assistant).permit(:name, :description, :llm_provider, :llm_model, :api_key,
|
||||
config: [
|
||||
:product_name, :feature_faq, :feature_memory, :feature_citation,
|
||||
:product_name, :role_name, :feature_faq, :feature_memory, :feature_citation,
|
||||
:welcome_message, :handoff_message, :resolution_message,
|
||||
:instructions, :temperature
|
||||
])
|
||||
|
||||
@ -21,7 +21,7 @@ module Captain::ChatHelper
|
||||
|
||||
def build_chat
|
||||
llm_chat = chat(model: @model, temperature: temperature, api_key: api_key)
|
||||
llm_chat = llm_chat.with_params(response_format: { type: 'json_object' })
|
||||
llm_chat = llm_chat.with_params(response_format: { type: 'json_object' }) if @model.to_s.downcase.start_with?('gpt')
|
||||
|
||||
llm_chat = setup_tools(llm_chat)
|
||||
llm_chat = setup_system_instructions(llm_chat)
|
||||
@ -98,15 +98,16 @@ module Captain::ChatHelper
|
||||
end
|
||||
|
||||
def api_key
|
||||
@assistant&.config&.[]('openai_api_key').presence || ENV.fetch('OPENAI_API_KEY', nil) || ENV.fetch('GEMINI_API_KEY', nil)
|
||||
@assistant&.api_key.presence || @assistant&.config&.[]('openai_api_key').presence || ENV.fetch('OPENAI_API_KEY',
|
||||
nil) || ENV.fetch('GEMINI_API_KEY', nil)
|
||||
end
|
||||
|
||||
def with_agent_session(&block)
|
||||
def with_agent_session(&)
|
||||
already_active = @agent_session_active
|
||||
return yield if already_active
|
||||
|
||||
@agent_session_active = true
|
||||
instrument_agent_session(instrumentation_params, &block)
|
||||
instrument_agent_session(instrumentation_params, &)
|
||||
ensure
|
||||
@agent_session_active = false unless already_active
|
||||
end
|
||||
|
||||
@ -37,7 +37,7 @@ class Captain::Assistant < ApplicationRecord
|
||||
has_many :copilot_threads, dependent: :destroy_async
|
||||
has_many :scenarios, class_name: 'Captain::Scenario', dependent: :destroy_async
|
||||
|
||||
store_accessor :config, :temperature, :feature_faq, :feature_memory, :product_name, :playbook, :distance_threshold, :max_rag_results
|
||||
store_accessor :config, :temperature, :feature_faq, :feature_memory, :product_name, :role_name, :playbook, :distance_threshold, :max_rag_results
|
||||
|
||||
validates :name, presence: true
|
||||
validates :description, presence: true
|
||||
|
||||
@ -101,7 +101,7 @@ class Captain::Llm::SystemPromptsService
|
||||
|
||||
<<~SYSTEM_PROMPT_MESSAGE
|
||||
[Identity]
|
||||
You are Captain, a helpful and friendly copilot assistant for support agents using the product #{product_name}. Your primary role is to assist support agents by retrieving information, compiling accurate responses, and guiding them through customer interactions.
|
||||
You are Captain, a helpful and friendly copilot assistant for support agents using #{product_name}. Your primary role is to assist support agents by retrieving information, compiling accurate responses, and guiding them through customer interactions.
|
||||
You should only provide information related to #{product_name} and must not address queries about other products or external events.
|
||||
|
||||
[Context]
|
||||
@ -165,7 +165,7 @@ class Captain::Llm::SystemPromptsService
|
||||
|
||||
<<~SYSTEM_PROMPT_MESSAGE
|
||||
[Identity]
|
||||
Your name is #{assistant_name || '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}.
|
||||
Your name is #{assistant_name || 'Captain'}, a helpful, friendly, and knowledgeable #{config['role_name'].presence || 'Assistant'} for #{product_name}. You will not answer anything about other products or events outside of #{product_name}.
|
||||
|
||||
[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.
|
||||
|
||||
36
enterprise/app/services/llm/model_test_service.rb
Normal file
36
enterprise/app/services/llm/model_test_service.rb
Normal file
@ -0,0 +1,36 @@
|
||||
class Llm::ModelTestService
|
||||
TEST_PROMPT = 'Reply with the word ok.'
|
||||
|
||||
def initialize(provider:, model:, api_key:)
|
||||
@provider = provider
|
||||
@model = model
|
||||
@api_key = api_key
|
||||
end
|
||||
|
||||
def perform
|
||||
response_text = nil
|
||||
|
||||
Llm::Config.with_api_key(@api_key, api_base: openai_api_base, provider: @provider) do |context|
|
||||
chat = context.chat(model: @model)
|
||||
chat.add_message(role: 'user', content: TEST_PROMPT)
|
||||
response = chat.ask(TEST_PROMPT)
|
||||
response_text = response&.content.to_s
|
||||
end
|
||||
|
||||
{ success: true, message: response_text }
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "[LLM][ModelTest] provider=#{@provider} model=#{@model} error=#{e.class}: #{e.message}"
|
||||
{ success: false, error: e.message }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def openai_api_base
|
||||
return nil unless @provider == 'openai'
|
||||
|
||||
endpoint = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value.presence || LlmConstants::OPENAI_API_ENDPOINT
|
||||
endpoint = endpoint.chomp('/')
|
||||
endpoint = "#{endpoint}/v1" unless endpoint.end_with?('/v1')
|
||||
endpoint
|
||||
end
|
||||
end
|
||||
@ -7,3 +7,6 @@ json.id resource.id
|
||||
json.name resource.name
|
||||
json.response_guidelines resource.response_guidelines
|
||||
json.updated_at resource.updated_at.to_i
|
||||
json.llm_provider resource.llm_provider
|
||||
json.llm_model resource.llm_model
|
||||
json.api_key resource.api_key
|
||||
|
||||
@ -19,9 +19,7 @@ class ChatwootHub
|
||||
end
|
||||
|
||||
def self.pricing_plan
|
||||
return 'community' unless ChatwootApp.enterprise?
|
||||
|
||||
InstallationConfig.find_by(name: 'INSTALLATION_PRICING_PLAN')&.value || 'community'
|
||||
'premium'
|
||||
end
|
||||
|
||||
def self.pricing_plan_quantity
|
||||
|
||||
@ -18,10 +18,14 @@ module Llm::Config
|
||||
@initialized = false
|
||||
end
|
||||
|
||||
def with_api_key(api_key, api_base: nil)
|
||||
def with_api_key(api_key, api_base: nil, provider: 'openai')
|
||||
context = RubyLLM.context do |config|
|
||||
config.openai_api_key = api_key
|
||||
config.openai_api_base = api_base
|
||||
if provider == 'gemini'
|
||||
config.gemini_api_key = api_key
|
||||
else
|
||||
config.openai_api_key = api_key
|
||||
config.openai_api_base = api_base if api_base.present?
|
||||
end
|
||||
end
|
||||
|
||||
yield context
|
||||
@ -32,7 +36,8 @@ module Llm::Config
|
||||
def configure_ruby_llm
|
||||
RubyLLM.configure do |config|
|
||||
config.openai_api_key = system_api_key if system_api_key.present?
|
||||
config.openai_api_base = openai_endpoint.chomp('/') if openai_endpoint.present?
|
||||
config.openai_api_base = normalize_openai_api_base(openai_endpoint) if openai_endpoint.present?
|
||||
config.gemini_api_key = gemini_api_key if gemini_api_key.present?
|
||||
config.logger = Rails.logger
|
||||
end
|
||||
end
|
||||
@ -44,5 +49,15 @@ module Llm::Config
|
||||
def openai_endpoint
|
||||
InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value
|
||||
end
|
||||
|
||||
def normalize_openai_api_base(endpoint)
|
||||
endpoint = endpoint.chomp('/')
|
||||
endpoint = "#{endpoint}/v1" unless endpoint.end_with?('/v1')
|
||||
endpoint
|
||||
end
|
||||
|
||||
def gemini_api_key
|
||||
InstallationConfig.find_by(name: 'CAPTAIN_GEMINI_API_KEY')&.value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
5
public/dashboard/images/integrations/gemini-dark.svg
Normal file
5
public/dashboard/images/integrations/gemini-dark.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<rect width="128" height="128" rx="20" fill="#e2e8f0"/>
|
||||
<circle cx="64" cy="64" r="40" fill="#0ea5e9"/>
|
||||
<text x="64" y="72" text-anchor="middle" font-size="36" font-family="Arial, sans-serif" fill="#0f172a">G</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 317 B |
5
public/dashboard/images/integrations/gemini.svg
Normal file
5
public/dashboard/images/integrations/gemini.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<rect width="128" height="128" rx="20" fill="#0f172a"/>
|
||||
<circle cx="64" cy="64" r="40" fill="#38bdf8"/>
|
||||
<text x="64" y="72" text-anchor="middle" font-size="36" font-family="Arial, sans-serif" fill="#0f172a">G</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 317 B |
Loading…
Reference in New Issue
Block a user