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] || {}
|
contact_data = state[:contact] || {}
|
||||||
enhanced_context = enhanced_context.merge(
|
enhanced_context = enhanced_context.merge(
|
||||||
conversation: conversation_data,
|
conversation: conversation_data,
|
||||||
contact: contact_data
|
contact: contact_data,
|
||||||
|
concierge: resolve_concierge_context(conversation_data),
|
||||||
|
reservation: resolve_reservation_context(conversation_data)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -68,4 +70,52 @@ module Concerns::Agentable
|
|||||||
def prompt_context
|
def prompt_context
|
||||||
raise NotImplementedError, "#{self.class} must implement prompt_context"
|
raise NotImplementedError, "#{self.class} must implement prompt_context"
|
||||||
end
|
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
|
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