feat: include chatwoot metadata with each tool call (#12907)
This commit is contained in:
@@ -237,5 +237,135 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do
|
||||
expect(result).to eq('Created order #ORD-789 for Widget')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with metadata headers' do
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:contact) { conversation.contact }
|
||||
let(:tool_context_with_state) do
|
||||
Struct.new(:state).new({
|
||||
account_id: account.id,
|
||||
assistant_id: assistant.id,
|
||||
conversation: {
|
||||
id: conversation.id,
|
||||
display_id: conversation.display_id
|
||||
},
|
||||
contact: {
|
||||
id: contact.id,
|
||||
email: contact.email,
|
||||
phone_number: contact.phone_number
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
before do
|
||||
custom_tool.update!(
|
||||
endpoint_url: 'https://example.com/api/data',
|
||||
response_template: nil
|
||||
)
|
||||
end
|
||||
|
||||
it 'includes metadata headers in GET request' do
|
||||
stub_request(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Assistant-Id' => assistant.id.to_s,
|
||||
'X-Chatwoot-Tool-Slug' => custom_tool.slug,
|
||||
'X-Chatwoot-Conversation-Id' => conversation.id.to_s,
|
||||
'X-Chatwoot-Conversation-Display-Id' => conversation.display_id.to_s,
|
||||
'X-Chatwoot-Contact-Id' => contact.id.to_s,
|
||||
'X-Chatwoot-Contact-Email' => contact.email
|
||||
})
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_with_state)
|
||||
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Contact-Email' => contact.email
|
||||
})
|
||||
end
|
||||
|
||||
it 'includes metadata headers in POST request' do
|
||||
custom_tool.update!(http_method: 'POST', request_template: '{"data": "test"}')
|
||||
|
||||
stub_request(:post, 'https://example.com/api/data')
|
||||
.with(
|
||||
body: '{"data": "test"}',
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Tool-Slug' => custom_tool.slug,
|
||||
'X-Chatwoot-Contact-Email' => contact.email
|
||||
}
|
||||
)
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_with_state)
|
||||
|
||||
expect(WebMock).to have_requested(:post, 'https://example.com/api/data')
|
||||
end
|
||||
|
||||
it 'includes metadata headers along with authentication headers' do
|
||||
custom_tool.update!(
|
||||
auth_type: 'bearer',
|
||||
auth_config: { 'token' => 'test_token' }
|
||||
)
|
||||
|
||||
stub_request(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'Authorization' => 'Bearer test_token',
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Contact-Id' => contact.id.to_s
|
||||
})
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_with_state)
|
||||
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'Authorization' => 'Bearer test_token',
|
||||
'X-Chatwoot-Contact-Id' => contact.id.to_s
|
||||
})
|
||||
end
|
||||
|
||||
it 'handles missing contact in tool context' do
|
||||
tool_context_no_contact = Struct.new(:state).new({
|
||||
account_id: account.id,
|
||||
assistant_id: assistant.id,
|
||||
conversation: {
|
||||
id: conversation.id,
|
||||
display_id: conversation.display_id
|
||||
}
|
||||
})
|
||||
|
||||
stub_request(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Conversation-Id' => conversation.id.to_s
|
||||
})
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_no_contact)
|
||||
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/api/data')
|
||||
end
|
||||
|
||||
it 'includes contact phone when present' do
|
||||
contact.update!(phone_number: '+1234567890')
|
||||
tool_context_with_state.state[:contact][:phone_number] = '+1234567890'
|
||||
|
||||
stub_request(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'X-Chatwoot-Contact-Phone' => '+1234567890'
|
||||
})
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_with_state)
|
||||
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/api/data')
|
||||
.with(headers: { 'X-Chatwoot-Contact-Phone' => '+1234567890' })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -327,6 +327,98 @@ RSpec.describe Captain::CustomTool, type: :model do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build_metadata_headers' do
|
||||
let(:tool) { create(:captain_custom_tool, account: account, slug: 'custom_test_tool') }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:contact) { conversation.contact }
|
||||
|
||||
let(:state) do
|
||||
{
|
||||
account_id: account.id,
|
||||
assistant_id: 123,
|
||||
conversation: {
|
||||
id: conversation.id,
|
||||
display_id: conversation.display_id
|
||||
},
|
||||
contact: {
|
||||
id: contact.id,
|
||||
email: contact.email,
|
||||
phone_number: contact.phone_number
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'includes account and assistant metadata' do
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers['X-Chatwoot-Account-Id']).to eq(account.id.to_s)
|
||||
expect(headers['X-Chatwoot-Assistant-Id']).to eq('123')
|
||||
end
|
||||
|
||||
it 'includes tool slug' do
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers['X-Chatwoot-Tool-Slug']).to eq('custom_test_tool')
|
||||
end
|
||||
|
||||
it 'includes conversation metadata when present' do
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers['X-Chatwoot-Conversation-Id']).to eq(conversation.id.to_s)
|
||||
expect(headers['X-Chatwoot-Conversation-Display-Id']).to eq(conversation.display_id.to_s)
|
||||
end
|
||||
|
||||
it 'includes contact metadata when present' do
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers['X-Chatwoot-Contact-Id']).to eq(contact.id.to_s)
|
||||
expect(headers['X-Chatwoot-Contact-Email']).to eq(contact.email)
|
||||
end
|
||||
|
||||
it 'handles missing conversation gracefully' do
|
||||
state[:conversation] = nil
|
||||
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers['X-Chatwoot-Conversation-Id']).to be_nil
|
||||
expect(headers['X-Chatwoot-Conversation-Display-Id']).to be_nil
|
||||
expect(headers['X-Chatwoot-Account-Id']).to eq(account.id.to_s)
|
||||
end
|
||||
|
||||
it 'handles missing contact gracefully' do
|
||||
state[:contact] = nil
|
||||
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers['X-Chatwoot-Contact-Id']).to be_nil
|
||||
expect(headers['X-Chatwoot-Contact-Email']).to be_nil
|
||||
expect(headers['X-Chatwoot-Account-Id']).to eq(account.id.to_s)
|
||||
end
|
||||
|
||||
it 'handles empty state' do
|
||||
headers = tool.build_metadata_headers({})
|
||||
|
||||
expect(headers).to be_a(Hash)
|
||||
expect(headers['X-Chatwoot-Tool-Slug']).to eq('custom_test_tool')
|
||||
end
|
||||
|
||||
it 'omits contact email header when email is blank' do
|
||||
state[:contact][:email] = ''
|
||||
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers).not_to have_key('X-Chatwoot-Contact-Email')
|
||||
end
|
||||
|
||||
it 'omits contact phone header when phone number is blank' do
|
||||
state[:contact][:phone_number] = ''
|
||||
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers).not_to have_key('X-Chatwoot-Contact-Phone')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_tool_metadata' do
|
||||
it 'returns tool metadata hash with custom flag' do
|
||||
tool = create(:captain_custom_tool, account: account,
|
||||
|
||||
Reference in New Issue
Block a user