feat(lifecycle): inject concierge context into Captain orchestrator prompt
Adds concierge.* and reservation.* Liquid variables to agent_instructions so Sofia's orchestrator_prompt receives unit persona/knowledge/variables and reservation data resolved from conversation.custom_attributes.current_unit_id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d0d08ed662
commit
7b009cf47f
@ -21,7 +21,9 @@ module Concerns::Agentable
|
||||
contact_data = state[:contact] || {}
|
||||
enhanced_context = enhanced_context.merge(
|
||||
conversation: conversation_data,
|
||||
contact: contact_data
|
||||
contact: contact_data,
|
||||
concierge: resolve_concierge_context(conversation_data),
|
||||
reservation: resolve_reservation_context(conversation_data)
|
||||
)
|
||||
end
|
||||
|
||||
@ -68,4 +70,52 @@ module Concerns::Agentable
|
||||
def prompt_context
|
||||
raise NotImplementedError, "#{self.class} must implement prompt_context"
|
||||
end
|
||||
|
||||
def resolve_concierge_context(conversation_data)
|
||||
unit = resolve_current_unit(conversation_data)
|
||||
return default_concierge_context if unit.blank?
|
||||
|
||||
{
|
||||
'persona_name' => unit.concierge_persona_name,
|
||||
'unit_name' => unit.name.to_s,
|
||||
'knowledge' => unit.concierge_knowledge,
|
||||
'variables' => unit.concierge_variables
|
||||
}
|
||||
end
|
||||
|
||||
def resolve_reservation_context(conversation_data)
|
||||
conv = lookup_conversation(conversation_data)
|
||||
return {} if conv.blank?
|
||||
|
||||
reservation = Captain::Reservation
|
||||
.where(account_id: conv.account_id, contact_id: conv.contact_id)
|
||||
.order(created_at: :desc)
|
||||
.first
|
||||
return {} if reservation.blank?
|
||||
|
||||
Captain::Lifecycle::ContextBuilder.build(reservation).fetch('reservation', {})
|
||||
end
|
||||
|
||||
def resolve_current_unit(conversation_data)
|
||||
conv = lookup_conversation(conversation_data)
|
||||
return nil if conv.blank?
|
||||
|
||||
unit_id = conv.custom_attributes.to_h['current_unit_id']
|
||||
return nil if unit_id.blank?
|
||||
|
||||
Captain::Unit.find_by(id: unit_id)
|
||||
end
|
||||
|
||||
def lookup_conversation(conversation_data)
|
||||
return nil if conversation_data.blank?
|
||||
|
||||
id = conversation_data.is_a?(Hash) ? (conversation_data[:id] || conversation_data['id']) : nil
|
||||
return nil if id.blank?
|
||||
|
||||
::Conversation.find_by(id: id)
|
||||
end
|
||||
|
||||
def default_concierge_context
|
||||
{ 'persona_name' => 'Sofia', 'unit_name' => '', 'knowledge' => '', 'variables' => {} }
|
||||
end
|
||||
end
|
||||
|
||||
15
enterprise/lib/captain/prompts/concierge.liquid
Normal file
15
enterprise/lib/captain/prompts/concierge.liquid
Normal file
@ -0,0 +1,15 @@
|
||||
Você é {{ concierge.persona_name }}, assistente virtual do {{ concierge.unit_name }}.
|
||||
|
||||
{% render 'concierge' %}
|
||||
|
||||
## Dados da Estadia Atual
|
||||
- Suíte: {{ reservation.suite }}
|
||||
- Check-in: {{ reservation.check_in_at }}
|
||||
- Check-out: {{ reservation.check_out_at }}
|
||||
|
||||
## Como se comportar
|
||||
- Seja cordial, prestativo e profissional.
|
||||
- Identifique-se como IA quando questionado diretamente.
|
||||
- Transfira para um atendente humano se a solicitação exigir ação que não pode ser realizada aqui.
|
||||
- Não compartilhe informações confidenciais de outros hóspedes.
|
||||
- Limite respostas ao escopo do hotel e da estadia atual.
|
||||
9
enterprise/lib/captain/prompts/snippets/concierge.liquid
Normal file
9
enterprise/lib/captain/prompts/snippets/concierge.liquid
Normal file
@ -0,0 +1,9 @@
|
||||
# Concierge Context (unit: {{ concierge.unit_name }})
|
||||
Persona: {{ concierge.persona_name }}
|
||||
|
||||
## Base de Conhecimento
|
||||
{{ concierge.knowledge }}
|
||||
|
||||
## Variáveis Disponíveis
|
||||
{% for pair in concierge.variables %}- {{ pair[0] }}: {{ pair[1] }}
|
||||
{% endfor %}
|
||||
63
spec/enterprise/models/concerns/agentable_concierge_spec.rb
Normal file
63
spec/enterprise/models/concerns/agentable_concierge_spec.rb
Normal file
@ -0,0 +1,63 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Concerns::Agentable, type: :model do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:brand) { create(:captain_brand, account: account) }
|
||||
let(:unit) do
|
||||
Captain::Unit.create!(
|
||||
account: account,
|
||||
brand: brand,
|
||||
name: 'Prime Águas Lindas',
|
||||
concierge_config: {
|
||||
'persona_name' => 'Sofia',
|
||||
'knowledge' => 'Hotel com café às 7h',
|
||||
'variables' => { 'wifi_password' => 'abc123' }
|
||||
}
|
||||
)
|
||||
end
|
||||
let(:contact) { create(:contact, account: account, name: 'João Silva') }
|
||||
let(:conversation) do
|
||||
create(:conversation,
|
||||
account: account, inbox: inbox, contact: contact,
|
||||
custom_attributes: { 'current_unit_id' => unit.id })
|
||||
end
|
||||
let(:assistant) do
|
||||
create(:captain_assistant,
|
||||
account: account,
|
||||
orchestrator_prompt: '{{ concierge.persona_name }} in {{ concierge.unit_name }} knows: {{ concierge.knowledge }}')
|
||||
end
|
||||
|
||||
def mock_context_for(conv)
|
||||
state = { conversation: { id: conv.id } }.with_indifferent_access
|
||||
ctx = instance_double(Agents::RunContext)
|
||||
allow(ctx).to receive(:context).and_return({ state: state })
|
||||
ctx
|
||||
end
|
||||
|
||||
describe '#agent_instructions concierge injection' do
|
||||
it 'resolves concierge context from current_unit_id' do
|
||||
ctx = mock_context_for(conversation)
|
||||
rendered = assistant.agent_instructions(ctx)
|
||||
expect(rendered).to include('Sofia in Prime Águas Lindas')
|
||||
expect(rendered).to include('Hotel com café às 7h')
|
||||
end
|
||||
|
||||
it 'falls back to empty strings when current_unit_id missing' do
|
||||
conversation.update!(custom_attributes: {})
|
||||
ctx = mock_context_for(conversation)
|
||||
rendered = assistant.agent_instructions(ctx)
|
||||
expect(rendered).to be_a(String)
|
||||
end
|
||||
|
||||
it 'falls back gracefully when conversation id is nil' do
|
||||
state = { conversation: {} }.with_indifferent_access
|
||||
ctx = instance_double(Agents::RunContext)
|
||||
allow(ctx).to receive(:context).and_return({ state: state })
|
||||
rendered = assistant.agent_instructions(ctx)
|
||||
expect(rendered).to be_a(String)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue
Block a user