Compare commits

...
6 changed files with 34 additions and 17 deletions
@@ -78,7 +78,7 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
end
def handoff_requested?
@response['response'] == 'conversation_handoff'
@response['action'] == 'handoff'
end
def process_action(action)
@@ -97,9 +97,10 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
end
def create_handoff_message
create_outgoing_message(
@assistant.config['handoff_message'].presence || I18n.t('conversations.captain.handoff')
)
handoff_message = @response&.dig('response').presence ||
@assistant.config['handoff_message'].presence ||
I18n.t('conversations.captain.handoff')
create_outgoing_message(handoff_message)
end
def create_messages
@@ -94,8 +94,9 @@ class Captain::Assistant::AgentRunnerService
def error_response(error_message)
{
'response' => 'conversation_handoff',
'reasoning' => "Error occurred: #{error_message}"
'response' => 'Transferring to another agent for further assistance.',
'reasoning' => "Error occurred: #{error_message}",
'action' => 'handoff'
}
end
@@ -194,14 +194,25 @@ 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'] || ''}
#{config['instructions'].present? ? "\n [Custom Instructions]\n #{config['instructions']}\n" : ''}
```json
{
reasoning: '',
response: '',
"reasoning": "",
"response": "",
"action": "continue" | "offer_handoff" | "handoff"
}
```
- If the answer is not provided in context sections, Respond to the customer and ask whether they want to talk to another support agent . If they ask to Chat with another agent, return `conversation_handoff' as the response in JSON response
The action field MUST be one of:
- "continue": Default. Use for normal conversation flow.
- "offer_handoff": Use when a handoff scenario is triggered (by custom instructions, inability to help, user frustration, or complex situations). Your response MUST ask the user if they would like to be connected to a support agent. Do NOT transfer yet - wait for their confirmation.
- "handoff": Use ONLY when:
User has confirmed after you offered a handoff, OR
User explicitly requests human support unprompted (in any language)
Never use handoff for scenarios where the bot decides a handoff is needed - use offer_handoff instead.
When action is "handoff", your response field MUST contain a message letting the user know they'll be connected to support.
This message MUST be in the same language as the user's message.
#{'- You MUST provide numbered citations at the appropriate places in the text.' if config['feature_citation']}
SYSTEM_PROMPT_MESSAGE
end
@@ -57,11 +57,13 @@ module Enterprise::MessageTemplates::HookExecutionService
return unless conversation.pending?
Rails.logger.info("Captain limit exceeded, performing handoff mid-conversation for conversation: #{conversation.id}")
handoff_message = inbox.captain_assistant&.config&.dig('handoff_message').presence ||
'Transferring to another agent for further assistance.'
conversation.messages.create!(
message_type: :outgoing,
account_id: conversation.account.id,
inbox_id: conversation.inbox.id,
content: 'Transferring to another agent for further assistance.'
content: handoff_message
)
conversation.bot_handoff!
send_out_of_office_message_after_handoff
@@ -251,7 +251,7 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
closed_all_day: true,
open_all_day: false
)
allow(mock_llm_chat_service).to receive(:generate_response).and_return({ 'response' => 'conversation_handoff' })
allow(mock_llm_chat_service).to receive(:generate_response).and_return({ 'action' => 'handoff' })
end
it 'sends out of office message after handoff' do
@@ -275,7 +275,7 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
open_all_day: true,
closed_all_day: false
)
allow(mock_llm_chat_service).to receive(:generate_response).and_return({ 'response' => 'conversation_handoff' })
allow(mock_llm_chat_service).to receive(:generate_response).and_return({ 'action' => 'handoff' })
end
it 'does not send out of office message after handoff' do
@@ -321,7 +321,7 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
closed_all_day: true,
open_all_day: false
)
allow(mock_llm_chat_service).to receive(:generate_response).and_return({ 'response' => 'conversation_handoff' })
allow(mock_llm_chat_service).to receive(:generate_response).and_return({ 'action' => 'handoff' })
end
it 'does not send out of office message' do
@@ -148,8 +148,9 @@ RSpec.describe Captain::Assistant::AgentRunnerService do
result = service.generate_response(message_history: message_history)
expect(result).to eq({
'response' => 'conversation_handoff',
'reasoning' => 'Error occurred: Test error'
'response' => 'Transferring to another agent for further assistance.',
'reasoning' => 'Error occurred: Test error',
'action' => 'handoff'
})
end
@@ -169,7 +170,8 @@ RSpec.describe Captain::Assistant::AgentRunnerService do
result = service.generate_response(message_history: message_history)
expect(result).to eq({
'response' => 'conversation_handoff',
'response' => 'Transferring to another agent for further assistance.',
'action' => 'handoff',
'reasoning' => 'Error occurred: Test error'
})
end