feat: include chatwoot metadata with each tool call (#12907)

This commit is contained in:
Shivam Mishra
2025-12-10 15:25:18 +05:30
committed by GitHub
parent 20fa5eeaa5
commit 0d8e249fe4
4 changed files with 257 additions and 3 deletions

View File

@@ -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

View File

@@ -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,