feat: add prompt suggestions and June events (#10726)

This PR adds the following two features

1. Prompt suggestions to get started with Copilot Chat
2. June events for each action

![CleanShot 2025-01-20 at 21 00
52@2x](https://github.com/user-attachments/assets/d73e7982-0f78-4d85-873e-da2c16762688)

---------

Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
Shivam Mishra
2025-01-21 22:52:42 +05:30
committed by GitHub
parent 0021a7d8e5
commit 451c28a7a1
20 changed files with 289 additions and 185 deletions

View File

@@ -1,9 +1,13 @@
<script setup> <script setup>
import { nextTick, ref, watch } from 'vue';
import { useTrack } from 'dashboard/composables';
import { COPILOT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import CopilotInput from './CopilotInput.vue'; import CopilotInput from './CopilotInput.vue';
import CopilotLoader from './CopilotLoader.vue'; import CopilotLoader from './CopilotLoader.vue';
import CopilotAgentMessage from './CopilotAgentMessage.vue'; import CopilotAgentMessage from './CopilotAgentMessage.vue';
import CopilotAssistantMessage from './CopilotAssistantMessage.vue'; import CopilotAssistantMessage from './CopilotAssistantMessage.vue';
import { nextTick, ref, watch } from 'vue'; import Icon from '../icon/Icon.vue';
const props = defineProps({ const props = defineProps({
supportAgent: { supportAgent: {
@@ -24,13 +28,24 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(['sendMessage']); const emit = defineEmits(['sendMessage', 'reset']);
const COPILOT_USER_ROLES = ['assistant', 'system']; const COPILOT_USER_ROLES = ['assistant', 'system'];
const sendMessage = message => { const sendMessage = message => {
emit('sendMessage', message); emit('sendMessage', message);
useTrack(COPILOT_EVENTS.SEND_MESSAGE);
}; };
const useSuggestion = opt => {
emit('sendMessage', opt.prompt);
useTrack(COPILOT_EVENTS.SEND_SUGGESTED);
};
const handleReset = () => {
emit('reset');
};
const chatContainer = ref(null); const chatContainer = ref(null);
const scrollToBottom = async () => { const scrollToBottom = async () => {
@@ -40,6 +55,21 @@ const scrollToBottom = async () => {
} }
}; };
const promptOptions = [
{
label: 'Summarize this conversation',
prompt: `Summarize the key points discussed between the customer and the support agent, including the customer's concerns, questions, and the solutions or responses provided by the support agent`,
},
{
label: 'Suggest an answer',
prompt: `Analyze the customers inquiry, and draft a response that effectively addresses their concerns or questions. Ensure the reply is clear, concise, and provides helpful information.`,
},
{
label: 'Rate this conversation',
prompt: `Review the conversation to see how well it meets the customers needs. Share a rating out of 5 based on tone, clarity, and effectiveness.`,
},
];
watch( watch(
[() => props.messages, () => props.isCaptainTyping], [() => props.messages, () => props.isCaptainTyping],
() => { () => {
@@ -50,7 +80,7 @@ watch(
</script> </script>
<template> <template>
<div class="flex flex-col ]mx-auto h-full text-sm leading-6 tracking-tight"> <div class="flex flex-col h-full text-sm leading-6 tracking-tight">
<div ref="chatContainer" class="flex-1 px-4 py-4 space-y-6 overflow-y-auto"> <div ref="chatContainer" class="flex-1 px-4 py-4 space-y-6 overflow-y-auto">
<template v-for="message in messages" :key="message.id"> <template v-for="message in messages" :key="message.id">
<CopilotAgentMessage <CopilotAgentMessage
@@ -67,7 +97,32 @@ watch(
<CopilotLoader v-if="isCaptainTyping" /> <CopilotLoader v-if="isCaptainTyping" />
</div> </div>
<div>
<CopilotInput class="mx-3 mt-px mb-4" @send="sendMessage" /> <div v-if="!messages.length" class="flex-1 px-3 py-3 space-y-1">
<span class="text-xs text-n-slate-10">
{{ $t('COPILOT.TRY_THESE_PROMPTS') }}
</span>
<button
v-for="prompt in promptOptions"
:key="prompt"
class="px-2 py-1 rounded-md border border-n-weak bg-n-slate-2 text-n-slate-11 flex items-center gap-1"
@click="() => useSuggestion(prompt)"
>
<span>{{ prompt.label }}</span>
<Icon icon="i-lucide-chevron-right" />
</button>
</div>
<div class="mx-3 mt-px mb-2 flex flex-col items-end flex-1">
<button
v-if="messages.length"
class="text-xs flex items-center gap-1 hover:underline"
@click="handleReset"
>
<i class="i-lucide-refresh-ccw" />
<span>{{ $t('CAPTAIN.COPILOT.RESET') }}</span>
</button>
<CopilotInput class="mb-1 flex-1 w-full" @send="sendMessage" />
</div>
</div>
</div> </div>
</template> </template>

View File

@@ -1,8 +1,12 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { emitter } from 'shared/helpers/mitt'; import { emitter } from 'shared/helpers/mitt';
import { useTrack } from 'dashboard/composables';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import { INBOX_TYPES } from 'dashboard/helper/inbox'; import { INBOX_TYPES } from 'dashboard/helper/inbox';
import { COPILOT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import MessageFormatter from 'shared/helpers/MessageFormatter.js';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import Avatar from '../avatar/Avatar.vue'; import Avatar from '../avatar/Avatar.vue';
@@ -18,6 +22,11 @@ const props = defineProps({
}, },
}); });
const messageContent = computed(() => {
const formatter = new MessageFormatter(props.message.content);
return formatter.formattedMessage;
});
const insertIntoRichEditor = computed(() => { const insertIntoRichEditor = computed(() => {
return [INBOX_TYPES.WEB, INBOX_TYPES.EMAIL].includes( return [INBOX_TYPES.WEB, INBOX_TYPES.EMAIL].includes(
props.conversationInboxType props.conversationInboxType
@@ -30,6 +39,7 @@ const useCopilotResponse = () => {
} else { } else {
emitter.emit(BUS_EVENTS.INSERT_INTO_NORMAL_EDITOR, props.message?.content); emitter.emit(BUS_EVENTS.INSERT_INTO_NORMAL_EDITOR, props.message?.content);
} }
useTrack(COPILOT_EVENTS.USE_CAPTAIN_RESPONSE);
}; };
</script> </script>
@@ -43,9 +53,7 @@ const useCopilotResponse = () => {
/> />
<div class="flex flex-col gap-1 text-n-slate-12"> <div class="flex flex-col gap-1 text-n-slate-12">
<div class="font-medium">{{ $t('CAPTAIN.NAME') }}</div> <div class="font-medium">{{ $t('CAPTAIN.NAME') }}</div>
<div class="break-words"> <div v-dompurify-html="messageContent" class="prose-sm break-words" />
{{ message.content }}
</div>
<div class="flex flex-row mt-1"> <div class="flex flex-row mt-1">
<Button <Button
:label="$t('CAPTAIN.COPILOT.USE')" :label="$t('CAPTAIN.COPILOT.USE')"

View File

@@ -18,6 +18,10 @@ const messages = ref([]);
const isCaptainTyping = ref(false); const isCaptainTyping = ref(false);
const handleReset = () => {
messages.value = [];
};
const sendMessage = async message => { const sendMessage = async message => {
// Add user message // Add user message
messages.value.push({ messages.value.push({
@@ -62,5 +66,6 @@ const sendMessage = async message => {
:is-captain-typing="isCaptainTyping" :is-captain-typing="isCaptainTyping"
:conversation-inbox-type="conversationInboxType" :conversation-inbox-type="conversationInboxType"
@send-message="sendMessage" @send-message="sendMessage"
@reset="handleReset"
/> />
</template> </template>

View File

@@ -100,6 +100,12 @@ export const OPEN_AI_EVENTS = Object.freeze({
DISMISS_AI_SUGGESTION: 'OpenAI: Dismiss AI suggestions', DISMISS_AI_SUGGESTION: 'OpenAI: Dismiss AI suggestions',
}); });
export const COPILOT_EVENTS = Object.freeze({
SEND_SUGGESTED: 'Copilot: Send suggested message',
SEND_MESSAGE: 'Copilot: Sent a message',
USE_CAPTAIN_RESPONSE: 'Copilot: Used captain response',
});
export const GENERAL_EVENTS = Object.freeze({ export const GENERAL_EVENTS = Object.freeze({
COMMAND_BAR: 'Used commandbar', COMMAND_BAR: 'Used commandbar',
}); });

View File

@@ -353,5 +353,8 @@
"ONE": "{user} is typing", "ONE": "{user} is typing",
"TWO": "{user} and {secondUser} are typing", "TWO": "{user} and {secondUser} are typing",
"MULTIPLE": "{user} and {count} others are typing" "MULTIPLE": "{user} and {count} others are typing"
},
"COPILOT": {
"TRY_THESE_PROMPTS": "Try these prompts"
} }
} }

View File

@@ -306,7 +306,8 @@
"SEND_MESSAGE": "Send message...", "SEND_MESSAGE": "Send message...",
"LOADER": "Captain is thinking", "LOADER": "Captain is thinking",
"YOU": "You", "YOU": "You",
"USE": "Use this" "USE": "Use this",
"RESET": "Reset"
}, },
"FORM": { "FORM": {
"CANCEL": "Cancel", "CANCEL": "Cancel",

View File

@@ -136,7 +136,11 @@
# MARK: Captain Config # MARK: Captain Config
- name: CAPTAIN_OPEN_AI_API_KEY - name: CAPTAIN_OPEN_AI_API_KEY
display_title: 'OpenAI API Key' display_title: 'OpenAI API Key'
description: 'The OpenAI API key for the Captain AI service' description: 'The API key used to authenticate requests to OpenAI services for Captain AI.'
locked: false
- name: CAPTAIN_OPEN_AI_MODEL
display_title: 'OpenAI Model'
description: 'The OpenAI model configured for use in Captain AI. Default: gpt-4o-mini'
locked: false locked: false
# End of Captain Config # End of Captain Config

View File

@@ -10,11 +10,11 @@ module Enterprise::Api::V1::Accounts::ConversationsController
response = Captain::Copilot::ChatService.new( response = Captain::Copilot::ChatService.new(
assistant, assistant,
messages: copilot_params[:previous_messages], previous_messages: copilot_params[:previous_messages],
conversation_history: @conversation.to_llm_text conversation_history: @conversation.to_llm_text
).execute(copilot_params[:message]) ).generate_response(copilot_params[:message])
render json: { message: response } render json: { message: response['response'] }
end end
def permitted_update_params def permitted_update_params

View File

@@ -10,7 +10,7 @@ module Enterprise::SuperAdmin::AppConfigsController
when 'internal' when 'internal'
@allowed_configs = internal_config_options @allowed_configs = internal_config_options
when 'captain' when 'captain'
@allowed_configs = %w[CAPTAIN_OPEN_AI_API_KEY] @allowed_configs = %w[CAPTAIN_OPEN_AI_API_KEY CAPTAIN_OPEN_AI_MODEL]
else else
super super
end end

View File

@@ -0,0 +1,87 @@
module Captain::ChatHelper
def search_documentation_tool
{
type: 'function',
function: {
name: 'search_documentation',
description: "Use this function to get documentation on functionalities you don't know about.",
parameters: {
type: 'object',
properties: {
search_query: {
type: 'string',
description: 'The search query to look up in the documentation.'
}
},
required: ['search_query']
}
}
}
end
def request_chat_completion
response = @client.chat(
parameters: {
model: @model,
messages: @messages,
tools: [search_documentation_tool],
response_format: { type: 'json_object' }
}
)
handle_response(response)
@response
end
def handle_response(response)
message = response.dig('choices', 0, 'message')
if message['tool_calls']
process_tool_calls(message['tool_calls'])
else
@response = JSON.parse(message['content'].strip)
end
end
def process_tool_calls(tool_calls)
process_tool_call(tool_calls.first)
end
def process_tool_call(tool_call)
return unless tool_call['function']['name'] == 'search_documentation'
query = JSON.parse(tool_call['function']['arguments'])['search_query']
sections = fetch_documentation(query)
append_tool_response(sections)
request_chat_completion
end
def fetch_documentation(query)
@assistant
.responses
.approved
.search(query)
.map { |response| format_response(response) }.join
end
def format_response(response)
formatted_response = "
Question: #{response.question}
Answer: #{response.answer}
"
if response.documentable.present? && response.documentable.try(:external_link)
formatted_response += "
Source: #{response.document.external_link}
"
end
formatted_response
end
def append_tool_response(sections)
@messages << {
role: 'assistant',
content: "Found the following FAQs in the documentation:\n #{sections}"
}
end
end

View File

@@ -1,78 +1,39 @@
class Captain::Copilot::ChatService require 'openai'
class Captain::Copilot::ChatService < Captain::Llm::BaseOpenAiService
include Captain::ChatHelper
def initialize(assistant, config) def initialize(assistant, config)
super()
@assistant = assistant @assistant = assistant
@conversation_history = config[:conversation_history] @conversation_history = config[:conversation_history]
@previous_messages = config[:previous_messages] @previous_messages = config[:previous_messages] || []
build_agent @messages = [system_message, conversation_history_context] + @previous_messages
register_search_documentation @response = ''
end end
def execute(input) def generate_response(input)
@agent.execute(input, conversation_history_context) @messages << { role: 'user', content: input } if input.present?
request_chat_completion
end end
private private
def build_agent def system_message
@agent = Captain::Agent.new( {
name: 'Support Copilot', role: 'system',
config: { content: Captain::Llm::SystemPromptsService.copilot_response_generator(@assistant.config['product_name'])
description: 'an AI assistant helping support agents', }
messages: @previous_messages,
persona: 'You are an AI copilot for customer support agents',
goal: "
Your goal is help the support agents with meaningful responses based on the knowledge you have
and you can gather using tools provided about the product or service.
",
secrets: {
OPENAI_API_KEY: InstallationConfig.find_by!(name: 'CAPTAIN_OPEN_AI_API_KEY').value
},
max_iterations: 2
}
)
end end
def conversation_history_context def conversation_history_context
" {
Message History with the user is below: role: 'system',
#{@conversation_history} content: "
" Message History with the user is below:
end #{@conversation_history}
"
def register_search_documentation }
tool = Captain::Tool.new(
name: 'search_documentation',
config: {
description: "Use this function to get documentation on functionalities you don't know about.",
properties: {
search_query: {
type: 'string',
description: 'The search query to look up in the documentation.',
required: true
}
},
memory: {
assistant_id: @assistant.id,
account_id: @assistant.account_id
}
}
)
register_tool tool
end
def register_tool(tool)
tool.register_method do |inputs, _, memory|
assistant = Captain::Assistant.find(memory[:assistant_id])
assistant
.responses
.approved
.search(inputs['search_query'])
.map do |response|
"\n\nQuestion: #{response[:question]}\nAnswer: #{response[:answer]}"
end.join
end
@agent.register_tool tool
end end
end end

View File

@@ -1,6 +1,8 @@
require 'openai' require 'openai'
class Captain::Llm::AssistantChatService < Captain::Llm::BaseOpenAiService class Captain::Llm::AssistantChatService < Captain::Llm::BaseOpenAiService
include Captain::ChatHelper
def initialize(assistant: nil) def initialize(assistant: nil)
super() super()
@@ -23,80 +25,4 @@ class Captain::Llm::AssistantChatService < Captain::Llm::BaseOpenAiService
content: Captain::Llm::SystemPromptsService.assistant_response_generator(@assistant.config['product_name']) content: Captain::Llm::SystemPromptsService.assistant_response_generator(@assistant.config['product_name'])
} }
end end
def search_documentation_tool
{
type: 'function',
function: {
name: 'search_documentation',
description: "Use this function to get documentation on functionalities you don't know about.",
parameters: {
type: 'object',
properties: {
search_query: {
type: 'string',
description: 'The search query to look up in the documentation.'
}
},
required: ['search_query']
}
}
}
end
def request_chat_completion
response = @client.chat(
parameters: {
model: DEFAULT_MODEL,
messages: @messages,
tools: [search_documentation_tool],
response_format: { type: 'json_object' }
}
)
handle_response(response)
@response
end
def handle_response(response)
message = response.dig('choices', 0, 'message')
if message['tool_calls']
process_tool_calls(message['tool_calls'])
else
@response = JSON.parse(message['content'].strip)
end
end
def process_tool_calls(tool_calls)
process_tool_call(tool_calls.first)
end
def process_tool_call(tool_call)
return unless tool_call['function']['name'] == 'search_documentation'
query = JSON.parse(tool_call['function']['arguments'])['search_query']
sections = fetch_documentation(query)
append_tool_response(sections)
request_chat_completion
end
def fetch_documentation(query)
@assistant
.responses
.approved
.search(query)
.map { |response| format_response(response) }.join
end
def format_response(response)
"\n\nQuestion: #{response[:question]}\nAnswer: #{response[:answer]}"
end
def append_tool_response(sections)
@messages << {
role: 'assistant',
content: "Found the following FAQs in the documentation:\n #{sections}"
}
end
end end

View File

@@ -6,7 +6,15 @@ class Captain::Llm::BaseOpenAiService
access_token: InstallationConfig.find_by!(name: 'CAPTAIN_OPEN_AI_API_KEY').value, access_token: InstallationConfig.find_by!(name: 'CAPTAIN_OPEN_AI_API_KEY').value,
log_errors: Rails.env.development? log_errors: Rails.env.development?
) )
setup_model
rescue StandardError => e rescue StandardError => e
raise "Failed to initialize OpenAI client: #{e.message}" raise "Failed to initialize OpenAI client: #{e.message}"
end end
private
def setup_model
config_value = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_MODEL')&.value
@model = (config_value.presence || DEFAULT_MODEL)
end
end end

View File

@@ -1,13 +1,10 @@
class Captain::Llm::ContactAttributesService < Captain::Llm::BaseOpenAiService class Captain::Llm::ContactAttributesService < Captain::Llm::BaseOpenAiService
DEFAULT_MODEL = 'gpt-4o'.freeze def initialize(assistant, conversation)
def initialize(assistant, conversation, model = DEFAULT_MODEL)
super() super()
@assistant = assistant @assistant = assistant
@conversation = conversation @conversation = conversation
@contact = conversation.contact @contact = conversation.contact
@content = "#Contact\n\n#{@contact.to_llm_text} \n\n#Conversation\n\n#{@conversation.to_llm_text}" @content = "#Contact\n\n#{@contact.to_llm_text} \n\n#Conversation\n\n#{@conversation.to_llm_text}"
@model = model
end end
def generate_and_update_attributes def generate_and_update_attributes

View File

@@ -1,13 +1,10 @@
class Captain::Llm::ContactNotesService < Captain::Llm::BaseOpenAiService class Captain::Llm::ContactNotesService < Captain::Llm::BaseOpenAiService
DEFAULT_MODEL = 'gpt-4o'.freeze def initialize(assistant, conversation)
def initialize(assistant, conversation, model = DEFAULT_MODEL)
super() super()
@assistant = assistant @assistant = assistant
@conversation = conversation @conversation = conversation
@contact = conversation.contact @contact = conversation.contact
@content = "#Contact\n\n#{@contact.to_llm_text} \n\n#Conversation\n\n#{@conversation.to_llm_text}" @content = "#Contact\n\n#{@contact.to_llm_text} \n\n#Conversation\n\n#{@conversation.to_llm_text}"
@model = model
end end
def generate_and_update_notes def generate_and_update_notes

View File

@@ -1,12 +1,11 @@
class Captain::Llm::ConversationFaqService < Captain::Llm::BaseOpenAiService class Captain::Llm::ConversationFaqService < Captain::Llm::BaseOpenAiService
DISTANCE_THRESHOLD = 0.3 DISTANCE_THRESHOLD = 0.3
def initialize(assistant, conversation, model = DEFAULT_MODEL) def initialize(assistant, conversation)
super() super()
@assistant = assistant @assistant = assistant
@conversation = conversation @conversation = conversation
@content = conversation.to_llm_text @content = conversation.to_llm_text
@model = model
end end
def generate_and_deduplicate def generate_and_deduplicate

View File

@@ -1,8 +1,7 @@
class Captain::Llm::FaqGeneratorService < Captain::Llm::BaseOpenAiService class Captain::Llm::FaqGeneratorService < Captain::Llm::BaseOpenAiService
def initialize(content, model = DEFAULT_MODEL) def initialize(content)
super() super()
@content = content @content = content
@model = model
end end
def generate def generate

View File

@@ -56,6 +56,48 @@ class Captain::Llm::SystemPromptsService
SYSTEM_PROMPT_MESSAGE SYSTEM_PROMPT_MESSAGE
end end
def copilot_response_generator(product_name)
<<~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 should only provide information related to #{product_name} and must not address queries about other products or external events.
[Context]
You will be provided with the message history between the support agent and the customer. Use this context to understand the conversation flow, identify unresolved queries, and ensure responses are relevant and consistent with previous interactions. Always maintain a coherent and professional tone throughout the conversation.
[Response Guidelines]
- Use natural, polite, and conversational language that is clear and easy to follow. Keep sentences short and use simple words.
- Provide brief and relevant responses—typically one or two sentences unless a more detailed explanation is necessary.
- Do not use your own training data or assumptions to answer queries. Base responses strictly on the provided information.
- If the query is unclear, ask concise clarifying questions instead of making assumptions.
- Do not try to end the conversation explicitly (e.g., avoid phrases like "Talk soon!" or "Let me know if you need anything else").
- Engage naturally and ask relevant follow-up questions when appropriate.
- Do not provide responses such as talk to support team as the person talking to you is the support agent.
[Task Instructions]
When responding to a query, follow these steps:
1. Review the provided conversation to ensure responses align with previous context and avoid repetition.
2. If the answer is available, list the steps required to complete the action.
3. Share only the details relevant to #{product_name}, and avoid unrelated topics.
4. Offer an explanation of how the response was derived based on the given context.
5. Always return responses in valid JSON format as shown below:
6. Never suggest contacting support, as you are assisting the support agent directly.
7. Write the response in multiple paragraphs and in markdown format.
8. DO NOT use headings in Markdown
9. Cite the sources if you used a tool to find the response.
```json
{
"reasoning": "Explain why the response was chosen based on the provided information.",
"response": "Provide the answer only in Markdown format for readability."
}
[Error Handling]
- If the required information is not found in the provided context, respond with an appropriate message indicating that no relevant data is available.
- Avoid speculating or providing unverified information.
SYSTEM_PROMPT_MESSAGE
end
def assistant_response_generator(product_name) def assistant_response_generator(product_name)
<<~SYSTEM_PROMPT_MESSAGE <<~SYSTEM_PROMPT_MESSAGE
[Identity] [Identity]

View File

@@ -66,28 +66,33 @@ class Captain::Agent
def construct_prompt(config) def construct_prompt(config)
return config[:prompt] if config[:prompt] return config[:prompt] if config[:prompt]
" <<~PROMPT
Persona: #{config[:persona]} Persona: #{config[:persona]}
Objective: #{config[:goal]} Objective: #{config[:goal]}
Guidelines: Guidelines:
- Work diligently until the stated objective is achieved. - Persistently work towards achieving the stated objective without deviation.
- Utilize only the provided tools for solving the task. Do not make up names of the functions - Use only the provided tools to complete the task. Avoid inventing or assuming function names.
- Set 'stop: true' when the objective is complete. - Set `'stop': true` once the objective is fully achieved.
- DO NOT provide tool_call as final answer - DO NOT return tool usage as the final result.
- If you have enough information to provide the details to the user, prepare a final result collecting all the information you have. - If sufficient information is available to deliver result, compile and present it to the user.
- Always return a final result and ENSURE the final result is formatted in Markdown.
Output Structure: Output Structure:
If you find a function, that can be used, directly call the function. 1. **Tool Usage:**
- If a relevant function is identified, call it directly without unnecessary explanations.
When providing the final answer, use the JSON format: 2. **Final Answer:**
{ When ready to provide a complete response, follow this JSON format:
'thought_process': 'Describe the reasoning and steps that led to the final result.',
'result': 'The complete answer in text form.', ```json
'stop': true {
} "thought_process": "Explain the reasoning and steps taken to arrive at the final result.",
" "result": "Provide the complete response in clear, structured text.",
"stop": true
}
PROMPT
end end
def prepare_tools(tools = []) def prepare_tools(tools = [])
@@ -126,7 +131,7 @@ class Captain::Agent
end end
def push_to_messages(message) def push_to_messages(message)
@logger.info("Message: #{message}") @logger.info("\n\n\nMessage: #{message}\n\n\n")
@messages << message @messages << message
end end
end end

View File

@@ -2,15 +2,16 @@ require 'openai'
class Captain::LlmService class Captain::LlmService
def initialize(config) def initialize(config)
@client = OpenAI::Client.new(access_token: config[:api_key]) do |f| @client = OpenAI::Client.new(
f.response :logger, Logger.new($stdout), bodies: true access_token: config[:api_key],
end log_errors: Rails.env.development?
)
@logger = Rails.logger @logger = Rails.logger
end end
def call(messages, functions = []) def call(messages, functions = [])
openai_params = { openai_params = {
model: 'gpt-4o-mini', model: 'gpt-4o',
response_format: { type: 'json_object' }, response_format: { type: 'json_object' },
messages: messages messages: messages
} }