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:
Pranav
2025-04-29 15:42:15 -07:00
committed by GitHub
parent 970e76ace8
commit fb6409508b
21 changed files with 823 additions and 32 deletions

View File

@@ -2,7 +2,7 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
before_action :current_account
before_action -> { check_authorization(Captain::Assistant) }
before_action :set_assistant, only: [:show, :update, :destroy]
before_action :set_assistant, only: [:show, :update, :destroy, :playground]
def index
@assistants = account_assistants.ordered
@@ -23,6 +23,15 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
head :no_content
end
def playground
response = Captain::Llm::AssistantChatService.new(assistant: @assistant).generate_response(
params[:message_content],
message_history
)
render json: response
end
private
def set_assistant
@@ -34,6 +43,19 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
end
def assistant_params
params.require(:assistant).permit(:name, :description, config: [:product_name, :feature_faq, :feature_memory])
params.require(:assistant).permit(:name, :description,
config: [
:product_name, :feature_faq, :feature_memory,
:welcome_message, :handoff_message, :resolution_message,
:instructions
])
end
def playground_params
params.require(:assistant).permit(:message_content, message_history: [:role, :content])
end
def message_history
(playground_params[:message_history] || []).map { |message| { role: message[:role], content: message[:content] } }
end
end

View File

@@ -36,6 +36,7 @@ module Captain::ChatHelper
end
def handle_response(response)
Rails.logger.debug { "[CAPTAIN][ChatCompletion] #{response}" }
message = response.dig('choices', 0, 'message')
if message['tool_calls']
process_tool_calls(message['tool_calls'])
@@ -46,20 +47,26 @@ module Captain::ChatHelper
def process_tool_calls(tool_calls)
append_tool_calls(tool_calls)
process_tool_call(tool_calls.first)
end
def process_tool_call(tool_call)
return unless tool_call['function']['name'] == 'search_documentation'
tool_call_id = tool_call['id']
query = JSON.parse(tool_call['function']['arguments'])['search_query']
sections = fetch_documentation(query)
append_tool_response(sections, tool_call_id)
tool_calls.each do |tool_call|
process_tool_call(tool_call)
end
request_chat_completion
end
def process_tool_call(tool_call)
tool_call_id = tool_call['id']
if tool_call['function']['name'] == 'search_documentation'
query = JSON.parse(tool_call['function']['arguments'])['search_query']
sections = fetch_documentation(query)
append_tool_response(sections, tool_call_id)
else
append_tool_response('', tool_call_id)
end
end
def fetch_documentation(query)
Rails.logger.debug { "[CAPTAIN][DocumentationSearch] #{query}" }
@assistant
.responses
.approved

View File

@@ -60,7 +60,7 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
end
def create_handoff_message
create_outgoing_message('Transferring to another agent for further assistance.')
create_outgoing_message(@assistant.config['handoff_message'] || 'Transferring to another agent for further assistance.')
end
def create_messages

View File

@@ -5,12 +5,13 @@ class Captain::InboxPendingConversationsResolutionJob < ApplicationJob
# limiting the number of conversations to be resolved to avoid any performance issues
resolvable_conversations = inbox.conversations.pending.where('last_activity_at < ? ', Time.now.utc - 1.hour).limit(Limits::BULK_ACTIONS_LIMIT)
resolvable_conversations.each do |conversation|
resolution_message = conversation.inbox.captain_assistant.config['resolution_message']
conversation.messages.create!(
{
message_type: :outgoing,
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
content: I18n.t('conversations.activity.auto_resolution_message')
content: resolution_message || I18n.t('conversations.activity.auto_resolution_message')
}
)
conversation.resolved!

View File

@@ -18,4 +18,8 @@ class Captain::AssistantPolicy < ApplicationPolicy
def destroy?
@account_user.administrator?
end
def playground?
true
end
end

View File

@@ -22,7 +22,7 @@ class Captain::Llm::AssistantChatService < Llm::BaseOpenAiService
def system_message
{
role: 'system',
content: Captain::Llm::SystemPromptsService.assistant_response_generator(@assistant.config['product_name'])
content: Captain::Llm::SystemPromptsService.assistant_response_generator(@assistant.config['product_name'], @assistant.config)
}
end
end

View File

@@ -103,7 +103,7 @@ class Captain::Llm::SystemPromptsService
SYSTEM_PROMPT_MESSAGE
end
def assistant_response_generator(product_name)
def assistant_response_generator(product_name, config = {})
<<~SYSTEM_PROMPT_MESSAGE
[Identity]
You are Captain, a helpful, friendly, and knowledgeable assistant for the product #{product_name}. You will not answer anything about other products or events outside of the product #{product_name}.
@@ -111,6 +111,7 @@ class Captain::Llm::SystemPromptsService
[Response Guideline]
- Do not rush giving a response, always give step-by-step instructions to the customer. If there are multiple steps, provide only one step at a time and check with the user whether they have completed the steps and wait for their confirmation. If the user has said okay or yes, continue with the steps.
- Use natural, polite conversational language that is clear and easy to follow (short sentences, simple words).
- Always detect the language from input and reply in the same language. Do not use any other language.
- Be concise and relevant: Most of your responses should be a sentence or two, unless you're asked to go deeper. Don't monopolize the conversation.
- Use discourse markers to ease comprehension. Never use the list format.
- Do not generate a response more than three sentences.
@@ -136,6 +137,7 @@ class Captain::Llm::SystemPromptsService
- Do not share anything outside of the context provided.
- Add the reasoning why you arrived at the answer
- Your answers will always be formatted in a valid JSON hash, as shown below. Never respond in non-JSON format.
#{config['instructions'] || ''}
```json
{
reasoning: '',