From 0bbcb488b695f6fd3b3a291cac67cb28f8c6983d Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Mon, 28 Sep 2026 11:46:06 +0200 Subject: [PATCH 1/3] Enable AI Guard sensitive data redaction --- manifests/ruby.yml | 100 +++++++++++++++--- .../app/controllers/ai_guard_controller.rb | 46 ++++---- .../app/controllers/ai_guard_controller.rb | 46 ++++---- .../app/controllers/ai_guard_controller.rb | 46 ++++---- .../app/controllers/ai_guard_controller.rb | 46 ++++---- 5 files changed, 197 insertions(+), 87 deletions(-) diff --git a/manifests/ruby.yml b/manifests/ruby.yml index ed9a0d853af..e5ecca924eb 100644 --- a/manifests/ruby.yml +++ b/manifests/ruby.yml @@ -33,7 +33,14 @@ manifest: rails72: '>=2.40.0' rails61: '>=2.40.0' rails80: '>=2.40.0' - tests/ai_guard/test_ai_guard_sdk.py::Test_AIGuardTelemetryRedacted: missing_feature + tests/ai_guard/test_ai_guard_sdk.py::Test_AIGuardTelemetryRedacted: + - weblog_declaration: + '*': irrelevant + uds-rails: v2.44.0-dev + rails52: v2.44.0-dev + rails72: v2.44.0-dev + rails80: v2.44.0-dev + rails61: v2.44.0-dev tests/ai_guard/test_ai_guard_sdk.py::Test_AIGuardTelemetryRequests: missing_feature tests/ai_guard/test_ai_guard_sdk.py::Test_AIGuardTelemetryTruncated: missing_feature tests/ai_guard/test_ai_guard_sdk.py::Test_AnomalyDetectionTags: @@ -91,9 +98,30 @@ manifest: sinatra32: irrelevant sinatra41: irrelevant uds-sinatra: irrelevant - tests/ai_guard/test_ai_guard_sdk.py::Test_NoRedaction: missing_feature - tests/ai_guard/test_ai_guard_sdk.py::Test_RedactedMessagesInSDKResponse: missing_feature - tests/ai_guard/test_ai_guard_sdk.py::Test_Redaction: missing_feature + tests/ai_guard/test_ai_guard_sdk.py::Test_NoRedaction: + - weblog_declaration: + '*': irrelevant + uds-rails: v2.44.0-dev + rails52: v2.44.0-dev + rails72: v2.44.0-dev + rails80: v2.44.0-dev + rails61: v2.44.0-dev + tests/ai_guard/test_ai_guard_sdk.py::Test_RedactedMessagesInSDKResponse: + - weblog_declaration: + '*': irrelevant + uds-rails: v2.44.0-dev + rails52: v2.44.0-dev + rails72: v2.44.0-dev + rails80: v2.44.0-dev + rails61: v2.44.0-dev + tests/ai_guard/test_ai_guard_sdk.py::Test_Redaction: + - weblog_declaration: + '*': irrelevant + uds-rails: v2.44.0-dev + rails52: v2.44.0-dev + rails72: v2.44.0-dev + rails80: v2.44.0-dev + rails61: v2.44.0-dev tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionDisabled: # TODO: a lower version might be supported - weblog_declaration: '*': missing_feature @@ -104,17 +132,59 @@ manifest: rails61: '>=2.41.0' tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionDisabled::test_findings_still_reported: - weblog_declaration: - rails80: missing_feature - rails61: missing_feature - uds-rails: missing_feature - rails52: missing_feature - rails72: missing_feature - tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionDisabledTelemetry: missing_feature - tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionFailSafe: missing_feature - tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionInSDKResponse: missing_feature - tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionMultiTurnContext: missing_feature - tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionOnBlock: missing_feature - tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionSkipsStructuralFields: missing_feature + rails80: v2.44.0-dev + rails61: v2.44.0-dev + uds-rails: v2.44.0-dev + rails52: v2.44.0-dev + rails72: v2.44.0-dev + tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionDisabledTelemetry: + - weblog_declaration: + '*': missing_feature + uds-rails: v2.44.0-dev + rails52: v2.44.0-dev + rails72: v2.44.0-dev + rails80: v2.44.0-dev + rails61: v2.44.0-dev + tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionFailSafe: + - weblog_declaration: + '*': irrelevant + uds-rails: v2.44.0-dev + rails52: v2.44.0-dev + rails72: v2.44.0-dev + rails80: v2.44.0-dev + rails61: v2.44.0-dev + tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionInSDKResponse: + - weblog_declaration: + '*': irrelevant + uds-rails: v2.44.0-dev + rails52: v2.44.0-dev + rails72: v2.44.0-dev + rails80: v2.44.0-dev + rails61: v2.44.0-dev + tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionMultiTurnContext: + - weblog_declaration: + '*': irrelevant + uds-rails: v2.44.0-dev + rails52: v2.44.0-dev + rails72: v2.44.0-dev + rails80: v2.44.0-dev + rails61: v2.44.0-dev + tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionOnBlock: + - weblog_declaration: + '*': irrelevant + uds-rails: v2.44.0-dev + rails52: v2.44.0-dev + rails72: v2.44.0-dev + rails80: v2.44.0-dev + rails61: v2.44.0-dev + tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionSkipsStructuralFields: + - weblog_declaration: + '*': irrelevant + uds-rails: v2.44.0-dev + rails52: v2.44.0-dev + rails72: v2.44.0-dev + rails80: v2.44.0-dev + rails61: v2.44.0-dev tests/ai_guard/test_ai_guard_sdk.py::Test_RootSpanUserKeep: - weblog_declaration: "*": v2.31.0-dev diff --git a/utils/build/docker/ruby/rails52/app/controllers/ai_guard_controller.rb b/utils/build/docker/ruby/rails52/app/controllers/ai_guard_controller.rb index 97ab47644cb..da39b6b8714 100644 --- a/utils/build/docker/ruby/rails52/app/controllers/ai_guard_controller.rb +++ b/utils/build/docker/ruby/rails52/app/controllers/ai_guard_controller.rb @@ -4,30 +4,37 @@ class AiGuardController < ApplicationController skip_before_action :verify_authenticity_token def evaluate - messages = params.to_unsafe_h.fetch(:_json).flat_map do |message_data| - if message_data[:tool_calls] - message_data[:tool_calls].map do |tool_call_data| - Datadog::AIGuard.assistant( - id: tool_call_data[:id], - tool_name: tool_call_data.dig(:function, :name), - arguments: tool_call_data.dig(:function, :arguments) - ) - end - elsif message_data[:tool_call_id] - Datadog::AIGuard.tool(tool_call_id: message_data[:tool_call_id], content: message_data[:content]) - elsif message_data[:content].is_a?(Array) - Datadog::AIGuard.message(role: message_data[:role]) do |m| + messages = params.to_unsafe_h.fetch(:_json).map do |message_data| + tool_calls = message_data.fetch(:tool_calls, []).map do |tool_call_data| + Datadog::AIGuard.tool_call( + id: tool_call_data[:id], + name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + + if message_data[:content].is_a?(Array) + Datadog::AIGuard.message( + role: message_data[:role], + tool_calls: tool_calls, + tool_call_id: message_data[:tool_call_id] + ) do |message| message_data[:content].each do |part| case part[:type] when 'text' - m.text(part[:text]) + message.text(part[:text]) when 'image_url' - m.image_url(part.dig(:image_url, :url)) + message.image_url(part.dig(:image_url, :url)) end end end else - Datadog::AIGuard.message(role: message_data[:role], content: message_data[:content]) + Datadog::AIGuard.message( + role: message_data[:role], + content: message_data[:content], + tool_calls: tool_calls, + tool_call_id: message_data[:tool_call_id] + ) end end @@ -41,11 +48,14 @@ def evaluate response_data = { action: result.action, reason: result.reason, - tags: result.tags, - is_blocking_enabled: result.blocking_enabled? + tags: result.tags } response_data[:tag_probs] = result.tag_probabilities if result.respond_to?(:tag_probabilities) response_data[:sds] = result.sds_findings if result.respond_to?(:sds_findings) + response_data[:messages] = result.messages.map(&:to_h) if result.respond_to?(:messages) + if result.respond_to?(:redaction_replacements) + response_data[:redaction_replacements] = result.redaction_replacements + end render json: response_data rescue Datadog::AIGuard::AIGuardAbortError => e error_data = { action: e.action, reason: e.reason, tags: e.tags } diff --git a/utils/build/docker/ruby/rails61/app/controllers/ai_guard_controller.rb b/utils/build/docker/ruby/rails61/app/controllers/ai_guard_controller.rb index 97ab47644cb..da39b6b8714 100644 --- a/utils/build/docker/ruby/rails61/app/controllers/ai_guard_controller.rb +++ b/utils/build/docker/ruby/rails61/app/controllers/ai_guard_controller.rb @@ -4,30 +4,37 @@ class AiGuardController < ApplicationController skip_before_action :verify_authenticity_token def evaluate - messages = params.to_unsafe_h.fetch(:_json).flat_map do |message_data| - if message_data[:tool_calls] - message_data[:tool_calls].map do |tool_call_data| - Datadog::AIGuard.assistant( - id: tool_call_data[:id], - tool_name: tool_call_data.dig(:function, :name), - arguments: tool_call_data.dig(:function, :arguments) - ) - end - elsif message_data[:tool_call_id] - Datadog::AIGuard.tool(tool_call_id: message_data[:tool_call_id], content: message_data[:content]) - elsif message_data[:content].is_a?(Array) - Datadog::AIGuard.message(role: message_data[:role]) do |m| + messages = params.to_unsafe_h.fetch(:_json).map do |message_data| + tool_calls = message_data.fetch(:tool_calls, []).map do |tool_call_data| + Datadog::AIGuard.tool_call( + id: tool_call_data[:id], + name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + + if message_data[:content].is_a?(Array) + Datadog::AIGuard.message( + role: message_data[:role], + tool_calls: tool_calls, + tool_call_id: message_data[:tool_call_id] + ) do |message| message_data[:content].each do |part| case part[:type] when 'text' - m.text(part[:text]) + message.text(part[:text]) when 'image_url' - m.image_url(part.dig(:image_url, :url)) + message.image_url(part.dig(:image_url, :url)) end end end else - Datadog::AIGuard.message(role: message_data[:role], content: message_data[:content]) + Datadog::AIGuard.message( + role: message_data[:role], + content: message_data[:content], + tool_calls: tool_calls, + tool_call_id: message_data[:tool_call_id] + ) end end @@ -41,11 +48,14 @@ def evaluate response_data = { action: result.action, reason: result.reason, - tags: result.tags, - is_blocking_enabled: result.blocking_enabled? + tags: result.tags } response_data[:tag_probs] = result.tag_probabilities if result.respond_to?(:tag_probabilities) response_data[:sds] = result.sds_findings if result.respond_to?(:sds_findings) + response_data[:messages] = result.messages.map(&:to_h) if result.respond_to?(:messages) + if result.respond_to?(:redaction_replacements) + response_data[:redaction_replacements] = result.redaction_replacements + end render json: response_data rescue Datadog::AIGuard::AIGuardAbortError => e error_data = { action: e.action, reason: e.reason, tags: e.tags } diff --git a/utils/build/docker/ruby/rails72/app/controllers/ai_guard_controller.rb b/utils/build/docker/ruby/rails72/app/controllers/ai_guard_controller.rb index 97ab47644cb..da39b6b8714 100644 --- a/utils/build/docker/ruby/rails72/app/controllers/ai_guard_controller.rb +++ b/utils/build/docker/ruby/rails72/app/controllers/ai_guard_controller.rb @@ -4,30 +4,37 @@ class AiGuardController < ApplicationController skip_before_action :verify_authenticity_token def evaluate - messages = params.to_unsafe_h.fetch(:_json).flat_map do |message_data| - if message_data[:tool_calls] - message_data[:tool_calls].map do |tool_call_data| - Datadog::AIGuard.assistant( - id: tool_call_data[:id], - tool_name: tool_call_data.dig(:function, :name), - arguments: tool_call_data.dig(:function, :arguments) - ) - end - elsif message_data[:tool_call_id] - Datadog::AIGuard.tool(tool_call_id: message_data[:tool_call_id], content: message_data[:content]) - elsif message_data[:content].is_a?(Array) - Datadog::AIGuard.message(role: message_data[:role]) do |m| + messages = params.to_unsafe_h.fetch(:_json).map do |message_data| + tool_calls = message_data.fetch(:tool_calls, []).map do |tool_call_data| + Datadog::AIGuard.tool_call( + id: tool_call_data[:id], + name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + + if message_data[:content].is_a?(Array) + Datadog::AIGuard.message( + role: message_data[:role], + tool_calls: tool_calls, + tool_call_id: message_data[:tool_call_id] + ) do |message| message_data[:content].each do |part| case part[:type] when 'text' - m.text(part[:text]) + message.text(part[:text]) when 'image_url' - m.image_url(part.dig(:image_url, :url)) + message.image_url(part.dig(:image_url, :url)) end end end else - Datadog::AIGuard.message(role: message_data[:role], content: message_data[:content]) + Datadog::AIGuard.message( + role: message_data[:role], + content: message_data[:content], + tool_calls: tool_calls, + tool_call_id: message_data[:tool_call_id] + ) end end @@ -41,11 +48,14 @@ def evaluate response_data = { action: result.action, reason: result.reason, - tags: result.tags, - is_blocking_enabled: result.blocking_enabled? + tags: result.tags } response_data[:tag_probs] = result.tag_probabilities if result.respond_to?(:tag_probabilities) response_data[:sds] = result.sds_findings if result.respond_to?(:sds_findings) + response_data[:messages] = result.messages.map(&:to_h) if result.respond_to?(:messages) + if result.respond_to?(:redaction_replacements) + response_data[:redaction_replacements] = result.redaction_replacements + end render json: response_data rescue Datadog::AIGuard::AIGuardAbortError => e error_data = { action: e.action, reason: e.reason, tags: e.tags } diff --git a/utils/build/docker/ruby/rails80/app/controllers/ai_guard_controller.rb b/utils/build/docker/ruby/rails80/app/controllers/ai_guard_controller.rb index 97ab47644cb..da39b6b8714 100644 --- a/utils/build/docker/ruby/rails80/app/controllers/ai_guard_controller.rb +++ b/utils/build/docker/ruby/rails80/app/controllers/ai_guard_controller.rb @@ -4,30 +4,37 @@ class AiGuardController < ApplicationController skip_before_action :verify_authenticity_token def evaluate - messages = params.to_unsafe_h.fetch(:_json).flat_map do |message_data| - if message_data[:tool_calls] - message_data[:tool_calls].map do |tool_call_data| - Datadog::AIGuard.assistant( - id: tool_call_data[:id], - tool_name: tool_call_data.dig(:function, :name), - arguments: tool_call_data.dig(:function, :arguments) - ) - end - elsif message_data[:tool_call_id] - Datadog::AIGuard.tool(tool_call_id: message_data[:tool_call_id], content: message_data[:content]) - elsif message_data[:content].is_a?(Array) - Datadog::AIGuard.message(role: message_data[:role]) do |m| + messages = params.to_unsafe_h.fetch(:_json).map do |message_data| + tool_calls = message_data.fetch(:tool_calls, []).map do |tool_call_data| + Datadog::AIGuard.tool_call( + id: tool_call_data[:id], + name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + + if message_data[:content].is_a?(Array) + Datadog::AIGuard.message( + role: message_data[:role], + tool_calls: tool_calls, + tool_call_id: message_data[:tool_call_id] + ) do |message| message_data[:content].each do |part| case part[:type] when 'text' - m.text(part[:text]) + message.text(part[:text]) when 'image_url' - m.image_url(part.dig(:image_url, :url)) + message.image_url(part.dig(:image_url, :url)) end end end else - Datadog::AIGuard.message(role: message_data[:role], content: message_data[:content]) + Datadog::AIGuard.message( + role: message_data[:role], + content: message_data[:content], + tool_calls: tool_calls, + tool_call_id: message_data[:tool_call_id] + ) end end @@ -41,11 +48,14 @@ def evaluate response_data = { action: result.action, reason: result.reason, - tags: result.tags, - is_blocking_enabled: result.blocking_enabled? + tags: result.tags } response_data[:tag_probs] = result.tag_probabilities if result.respond_to?(:tag_probabilities) response_data[:sds] = result.sds_findings if result.respond_to?(:sds_findings) + response_data[:messages] = result.messages.map(&:to_h) if result.respond_to?(:messages) + if result.respond_to?(:redaction_replacements) + response_data[:redaction_replacements] = result.redaction_replacements + end render json: response_data rescue Datadog::AIGuard::AIGuardAbortError => e error_data = { action: e.action, reason: e.reason, tags: e.tags } From 7624ea2919a72738a2e923adbdf9c2d538bc4ae2 Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Tue, 29 Sep 2026 14:57:50 +0200 Subject: [PATCH 2/3] Update SDK usage --- .../app/controllers/ai_guard_controller.rb | 36 +++++++++---------- .../app/controllers/ai_guard_controller.rb | 36 +++++++++---------- .../app/controllers/ai_guard_controller.rb | 36 +++++++++---------- .../app/controllers/ai_guard_controller.rb | 36 +++++++++---------- 4 files changed, 68 insertions(+), 76 deletions(-) diff --git a/utils/build/docker/ruby/rails52/app/controllers/ai_guard_controller.rb b/utils/build/docker/ruby/rails52/app/controllers/ai_guard_controller.rb index da39b6b8714..9a19e9d9f13 100644 --- a/utils/build/docker/ruby/rails52/app/controllers/ai_guard_controller.rb +++ b/utils/build/docker/ruby/rails52/app/controllers/ai_guard_controller.rb @@ -5,21 +5,26 @@ class AiGuardController < ApplicationController def evaluate messages = params.to_unsafe_h.fetch(:_json).map do |message_data| - tool_calls = message_data.fetch(:tool_calls, []).map do |tool_call_data| - Datadog::AIGuard.tool_call( - id: tool_call_data[:id], - name: tool_call_data.dig(:function, :name), - arguments: tool_call_data.dig(:function, :arguments) + if message_data[:role] == 'tool' + next Datadog::AIGuard.tool( + tool_call_id: message_data[:tool_call_id], content: message_data[:content] ) end - if message_data[:content].is_a?(Array) - Datadog::AIGuard.message( - role: message_data[:role], - tool_calls: tool_calls, - tool_call_id: message_data[:tool_call_id] - ) do |message| - message_data[:content].each do |part| + message_content = message_data[:content] + content = message_content unless message_content.is_a?(Array) + + Datadog::AIGuard.message(role: message_data[:role], content: content) do |message| + message_data.fetch(:tool_calls, []).each do |tool_call_data| + message.tool_call( + id: tool_call_data[:id], + name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + + if message_content.is_a?(Array) + message_content.each do |part| case part[:type] when 'text' message.text(part[:text]) @@ -28,13 +33,6 @@ def evaluate end end end - else - Datadog::AIGuard.message( - role: message_data[:role], - content: message_data[:content], - tool_calls: tool_calls, - tool_call_id: message_data[:tool_call_id] - ) end end diff --git a/utils/build/docker/ruby/rails61/app/controllers/ai_guard_controller.rb b/utils/build/docker/ruby/rails61/app/controllers/ai_guard_controller.rb index da39b6b8714..9a19e9d9f13 100644 --- a/utils/build/docker/ruby/rails61/app/controllers/ai_guard_controller.rb +++ b/utils/build/docker/ruby/rails61/app/controllers/ai_guard_controller.rb @@ -5,21 +5,26 @@ class AiGuardController < ApplicationController def evaluate messages = params.to_unsafe_h.fetch(:_json).map do |message_data| - tool_calls = message_data.fetch(:tool_calls, []).map do |tool_call_data| - Datadog::AIGuard.tool_call( - id: tool_call_data[:id], - name: tool_call_data.dig(:function, :name), - arguments: tool_call_data.dig(:function, :arguments) + if message_data[:role] == 'tool' + next Datadog::AIGuard.tool( + tool_call_id: message_data[:tool_call_id], content: message_data[:content] ) end - if message_data[:content].is_a?(Array) - Datadog::AIGuard.message( - role: message_data[:role], - tool_calls: tool_calls, - tool_call_id: message_data[:tool_call_id] - ) do |message| - message_data[:content].each do |part| + message_content = message_data[:content] + content = message_content unless message_content.is_a?(Array) + + Datadog::AIGuard.message(role: message_data[:role], content: content) do |message| + message_data.fetch(:tool_calls, []).each do |tool_call_data| + message.tool_call( + id: tool_call_data[:id], + name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + + if message_content.is_a?(Array) + message_content.each do |part| case part[:type] when 'text' message.text(part[:text]) @@ -28,13 +33,6 @@ def evaluate end end end - else - Datadog::AIGuard.message( - role: message_data[:role], - content: message_data[:content], - tool_calls: tool_calls, - tool_call_id: message_data[:tool_call_id] - ) end end diff --git a/utils/build/docker/ruby/rails72/app/controllers/ai_guard_controller.rb b/utils/build/docker/ruby/rails72/app/controllers/ai_guard_controller.rb index da39b6b8714..9a19e9d9f13 100644 --- a/utils/build/docker/ruby/rails72/app/controllers/ai_guard_controller.rb +++ b/utils/build/docker/ruby/rails72/app/controllers/ai_guard_controller.rb @@ -5,21 +5,26 @@ class AiGuardController < ApplicationController def evaluate messages = params.to_unsafe_h.fetch(:_json).map do |message_data| - tool_calls = message_data.fetch(:tool_calls, []).map do |tool_call_data| - Datadog::AIGuard.tool_call( - id: tool_call_data[:id], - name: tool_call_data.dig(:function, :name), - arguments: tool_call_data.dig(:function, :arguments) + if message_data[:role] == 'tool' + next Datadog::AIGuard.tool( + tool_call_id: message_data[:tool_call_id], content: message_data[:content] ) end - if message_data[:content].is_a?(Array) - Datadog::AIGuard.message( - role: message_data[:role], - tool_calls: tool_calls, - tool_call_id: message_data[:tool_call_id] - ) do |message| - message_data[:content].each do |part| + message_content = message_data[:content] + content = message_content unless message_content.is_a?(Array) + + Datadog::AIGuard.message(role: message_data[:role], content: content) do |message| + message_data.fetch(:tool_calls, []).each do |tool_call_data| + message.tool_call( + id: tool_call_data[:id], + name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + + if message_content.is_a?(Array) + message_content.each do |part| case part[:type] when 'text' message.text(part[:text]) @@ -28,13 +33,6 @@ def evaluate end end end - else - Datadog::AIGuard.message( - role: message_data[:role], - content: message_data[:content], - tool_calls: tool_calls, - tool_call_id: message_data[:tool_call_id] - ) end end diff --git a/utils/build/docker/ruby/rails80/app/controllers/ai_guard_controller.rb b/utils/build/docker/ruby/rails80/app/controllers/ai_guard_controller.rb index da39b6b8714..9a19e9d9f13 100644 --- a/utils/build/docker/ruby/rails80/app/controllers/ai_guard_controller.rb +++ b/utils/build/docker/ruby/rails80/app/controllers/ai_guard_controller.rb @@ -5,21 +5,26 @@ class AiGuardController < ApplicationController def evaluate messages = params.to_unsafe_h.fetch(:_json).map do |message_data| - tool_calls = message_data.fetch(:tool_calls, []).map do |tool_call_data| - Datadog::AIGuard.tool_call( - id: tool_call_data[:id], - name: tool_call_data.dig(:function, :name), - arguments: tool_call_data.dig(:function, :arguments) + if message_data[:role] == 'tool' + next Datadog::AIGuard.tool( + tool_call_id: message_data[:tool_call_id], content: message_data[:content] ) end - if message_data[:content].is_a?(Array) - Datadog::AIGuard.message( - role: message_data[:role], - tool_calls: tool_calls, - tool_call_id: message_data[:tool_call_id] - ) do |message| - message_data[:content].each do |part| + message_content = message_data[:content] + content = message_content unless message_content.is_a?(Array) + + Datadog::AIGuard.message(role: message_data[:role], content: content) do |message| + message_data.fetch(:tool_calls, []).each do |tool_call_data| + message.tool_call( + id: tool_call_data[:id], + name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + + if message_content.is_a?(Array) + message_content.each do |part| case part[:type] when 'text' message.text(part[:text]) @@ -28,13 +33,6 @@ def evaluate end end end - else - Datadog::AIGuard.message( - role: message_data[:role], - content: message_data[:content], - tool_calls: tool_calls, - tool_call_id: message_data[:tool_call_id] - ) end end From 02931327be3faa99689b392f09bdfd309c306a0f Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Tue, 29 Sep 2026 14:57:50 +0200 Subject: [PATCH 3/3] Update SDK usage --- .../app/controllers/ai_guard_controller.rb | 102 ++++++++++++------ .../app/controllers/ai_guard_controller.rb | 102 ++++++++++++------ .../app/controllers/ai_guard_controller.rb | 102 ++++++++++++------ .../app/controllers/ai_guard_controller.rb | 102 ++++++++++++------ 4 files changed, 284 insertions(+), 124 deletions(-) diff --git a/utils/build/docker/ruby/rails52/app/controllers/ai_guard_controller.rb b/utils/build/docker/ruby/rails52/app/controllers/ai_guard_controller.rb index 9a19e9d9f13..dfa58c1864d 100644 --- a/utils/build/docker/ruby/rails52/app/controllers/ai_guard_controller.rb +++ b/utils/build/docker/ruby/rails52/app/controllers/ai_guard_controller.rb @@ -4,37 +4,7 @@ class AiGuardController < ApplicationController skip_before_action :verify_authenticity_token def evaluate - messages = params.to_unsafe_h.fetch(:_json).map do |message_data| - if message_data[:role] == 'tool' - next Datadog::AIGuard.tool( - tool_call_id: message_data[:tool_call_id], content: message_data[:content] - ) - end - - message_content = message_data[:content] - content = message_content unless message_content.is_a?(Array) - - Datadog::AIGuard.message(role: message_data[:role], content: content) do |message| - message_data.fetch(:tool_calls, []).each do |tool_call_data| - message.tool_call( - id: tool_call_data[:id], - name: tool_call_data.dig(:function, :name), - arguments: tool_call_data.dig(:function, :arguments) - ) - end - - if message_content.is_a?(Array) - message_content.each do |part| - case part[:type] - when 'text' - message.text(part[:text]) - when 'image_url' - message.image_url(part.dig(:image_url, :url)) - end - end - end - end - end + messages = params.to_unsafe_h.fetch(:_json).flat_map { |message_data| build_messages(message_data) } user_id = request.headers['X-User-Id'] session_id = request.headers['X-Session-Id'] @@ -63,4 +33,74 @@ def evaluate rescue StandardError => e render json: { error: e.to_s, type: e.class.name }, status: 500 end + + private + + def build_messages(message_data) + # TODO: Remove ContentBuilder compatibility after dd-trace-rb 2.44.0 is released + if Datadog::AIGuard::Evaluation.const_defined?(:MessageBuilder, false) + build_message(message_data) + else + build_content_builder_messages(message_data) + end + end + + def build_message(message_data) + if message_data[:role] == 'tool' + return Datadog::AIGuard.tool( + tool_call_id: message_data[:tool_call_id], content: message_data[:content] + ) + end + + message_content = message_data[:content] + content = message_content unless message_content.is_a?(Array) + + Datadog::AIGuard.message(role: message_data[:role], content: content) do |message| + message_data.fetch(:tool_calls, []).each do |tool_call_data| + message.tool_call( + id: tool_call_data[:id], + name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + + if message_content.is_a?(Array) + message_content.each do |part| + case part[:type] + when 'text' + message.text(part[:text]) + when 'image_url' + message.image_url(part.dig(:image_url, :url)) + end + end + end + end + end + + def build_content_builder_messages(message_data) + if message_data[:tool_calls] + message_data[:tool_calls].map do |tool_call_data| + Datadog::AIGuard.assistant( + id: tool_call_data[:id], + tool_name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + elsif message_data[:tool_call_id] + Datadog::AIGuard.tool(tool_call_id: message_data[:tool_call_id], content: message_data[:content]) + elsif message_data[:content].is_a?(Array) + Datadog::AIGuard.message(role: message_data[:role]) do |message| + message_data[:content].each do |part| + case part[:type] + when 'text' + message.text(part[:text]) + when 'image_url' + message.image_url(part.dig(:image_url, :url)) + end + end + end + else + Datadog::AIGuard.message(role: message_data[:role], content: message_data[:content]) + end + end end diff --git a/utils/build/docker/ruby/rails61/app/controllers/ai_guard_controller.rb b/utils/build/docker/ruby/rails61/app/controllers/ai_guard_controller.rb index 9a19e9d9f13..dfa58c1864d 100644 --- a/utils/build/docker/ruby/rails61/app/controllers/ai_guard_controller.rb +++ b/utils/build/docker/ruby/rails61/app/controllers/ai_guard_controller.rb @@ -4,37 +4,7 @@ class AiGuardController < ApplicationController skip_before_action :verify_authenticity_token def evaluate - messages = params.to_unsafe_h.fetch(:_json).map do |message_data| - if message_data[:role] == 'tool' - next Datadog::AIGuard.tool( - tool_call_id: message_data[:tool_call_id], content: message_data[:content] - ) - end - - message_content = message_data[:content] - content = message_content unless message_content.is_a?(Array) - - Datadog::AIGuard.message(role: message_data[:role], content: content) do |message| - message_data.fetch(:tool_calls, []).each do |tool_call_data| - message.tool_call( - id: tool_call_data[:id], - name: tool_call_data.dig(:function, :name), - arguments: tool_call_data.dig(:function, :arguments) - ) - end - - if message_content.is_a?(Array) - message_content.each do |part| - case part[:type] - when 'text' - message.text(part[:text]) - when 'image_url' - message.image_url(part.dig(:image_url, :url)) - end - end - end - end - end + messages = params.to_unsafe_h.fetch(:_json).flat_map { |message_data| build_messages(message_data) } user_id = request.headers['X-User-Id'] session_id = request.headers['X-Session-Id'] @@ -63,4 +33,74 @@ def evaluate rescue StandardError => e render json: { error: e.to_s, type: e.class.name }, status: 500 end + + private + + def build_messages(message_data) + # TODO: Remove ContentBuilder compatibility after dd-trace-rb 2.44.0 is released + if Datadog::AIGuard::Evaluation.const_defined?(:MessageBuilder, false) + build_message(message_data) + else + build_content_builder_messages(message_data) + end + end + + def build_message(message_data) + if message_data[:role] == 'tool' + return Datadog::AIGuard.tool( + tool_call_id: message_data[:tool_call_id], content: message_data[:content] + ) + end + + message_content = message_data[:content] + content = message_content unless message_content.is_a?(Array) + + Datadog::AIGuard.message(role: message_data[:role], content: content) do |message| + message_data.fetch(:tool_calls, []).each do |tool_call_data| + message.tool_call( + id: tool_call_data[:id], + name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + + if message_content.is_a?(Array) + message_content.each do |part| + case part[:type] + when 'text' + message.text(part[:text]) + when 'image_url' + message.image_url(part.dig(:image_url, :url)) + end + end + end + end + end + + def build_content_builder_messages(message_data) + if message_data[:tool_calls] + message_data[:tool_calls].map do |tool_call_data| + Datadog::AIGuard.assistant( + id: tool_call_data[:id], + tool_name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + elsif message_data[:tool_call_id] + Datadog::AIGuard.tool(tool_call_id: message_data[:tool_call_id], content: message_data[:content]) + elsif message_data[:content].is_a?(Array) + Datadog::AIGuard.message(role: message_data[:role]) do |message| + message_data[:content].each do |part| + case part[:type] + when 'text' + message.text(part[:text]) + when 'image_url' + message.image_url(part.dig(:image_url, :url)) + end + end + end + else + Datadog::AIGuard.message(role: message_data[:role], content: message_data[:content]) + end + end end diff --git a/utils/build/docker/ruby/rails72/app/controllers/ai_guard_controller.rb b/utils/build/docker/ruby/rails72/app/controllers/ai_guard_controller.rb index 9a19e9d9f13..dfa58c1864d 100644 --- a/utils/build/docker/ruby/rails72/app/controllers/ai_guard_controller.rb +++ b/utils/build/docker/ruby/rails72/app/controllers/ai_guard_controller.rb @@ -4,37 +4,7 @@ class AiGuardController < ApplicationController skip_before_action :verify_authenticity_token def evaluate - messages = params.to_unsafe_h.fetch(:_json).map do |message_data| - if message_data[:role] == 'tool' - next Datadog::AIGuard.tool( - tool_call_id: message_data[:tool_call_id], content: message_data[:content] - ) - end - - message_content = message_data[:content] - content = message_content unless message_content.is_a?(Array) - - Datadog::AIGuard.message(role: message_data[:role], content: content) do |message| - message_data.fetch(:tool_calls, []).each do |tool_call_data| - message.tool_call( - id: tool_call_data[:id], - name: tool_call_data.dig(:function, :name), - arguments: tool_call_data.dig(:function, :arguments) - ) - end - - if message_content.is_a?(Array) - message_content.each do |part| - case part[:type] - when 'text' - message.text(part[:text]) - when 'image_url' - message.image_url(part.dig(:image_url, :url)) - end - end - end - end - end + messages = params.to_unsafe_h.fetch(:_json).flat_map { |message_data| build_messages(message_data) } user_id = request.headers['X-User-Id'] session_id = request.headers['X-Session-Id'] @@ -63,4 +33,74 @@ def evaluate rescue StandardError => e render json: { error: e.to_s, type: e.class.name }, status: 500 end + + private + + def build_messages(message_data) + # TODO: Remove ContentBuilder compatibility after dd-trace-rb 2.44.0 is released + if Datadog::AIGuard::Evaluation.const_defined?(:MessageBuilder, false) + build_message(message_data) + else + build_content_builder_messages(message_data) + end + end + + def build_message(message_data) + if message_data[:role] == 'tool' + return Datadog::AIGuard.tool( + tool_call_id: message_data[:tool_call_id], content: message_data[:content] + ) + end + + message_content = message_data[:content] + content = message_content unless message_content.is_a?(Array) + + Datadog::AIGuard.message(role: message_data[:role], content: content) do |message| + message_data.fetch(:tool_calls, []).each do |tool_call_data| + message.tool_call( + id: tool_call_data[:id], + name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + + if message_content.is_a?(Array) + message_content.each do |part| + case part[:type] + when 'text' + message.text(part[:text]) + when 'image_url' + message.image_url(part.dig(:image_url, :url)) + end + end + end + end + end + + def build_content_builder_messages(message_data) + if message_data[:tool_calls] + message_data[:tool_calls].map do |tool_call_data| + Datadog::AIGuard.assistant( + id: tool_call_data[:id], + tool_name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + elsif message_data[:tool_call_id] + Datadog::AIGuard.tool(tool_call_id: message_data[:tool_call_id], content: message_data[:content]) + elsif message_data[:content].is_a?(Array) + Datadog::AIGuard.message(role: message_data[:role]) do |message| + message_data[:content].each do |part| + case part[:type] + when 'text' + message.text(part[:text]) + when 'image_url' + message.image_url(part.dig(:image_url, :url)) + end + end + end + else + Datadog::AIGuard.message(role: message_data[:role], content: message_data[:content]) + end + end end diff --git a/utils/build/docker/ruby/rails80/app/controllers/ai_guard_controller.rb b/utils/build/docker/ruby/rails80/app/controllers/ai_guard_controller.rb index 9a19e9d9f13..dfa58c1864d 100644 --- a/utils/build/docker/ruby/rails80/app/controllers/ai_guard_controller.rb +++ b/utils/build/docker/ruby/rails80/app/controllers/ai_guard_controller.rb @@ -4,37 +4,7 @@ class AiGuardController < ApplicationController skip_before_action :verify_authenticity_token def evaluate - messages = params.to_unsafe_h.fetch(:_json).map do |message_data| - if message_data[:role] == 'tool' - next Datadog::AIGuard.tool( - tool_call_id: message_data[:tool_call_id], content: message_data[:content] - ) - end - - message_content = message_data[:content] - content = message_content unless message_content.is_a?(Array) - - Datadog::AIGuard.message(role: message_data[:role], content: content) do |message| - message_data.fetch(:tool_calls, []).each do |tool_call_data| - message.tool_call( - id: tool_call_data[:id], - name: tool_call_data.dig(:function, :name), - arguments: tool_call_data.dig(:function, :arguments) - ) - end - - if message_content.is_a?(Array) - message_content.each do |part| - case part[:type] - when 'text' - message.text(part[:text]) - when 'image_url' - message.image_url(part.dig(:image_url, :url)) - end - end - end - end - end + messages = params.to_unsafe_h.fetch(:_json).flat_map { |message_data| build_messages(message_data) } user_id = request.headers['X-User-Id'] session_id = request.headers['X-Session-Id'] @@ -63,4 +33,74 @@ def evaluate rescue StandardError => e render json: { error: e.to_s, type: e.class.name }, status: 500 end + + private + + def build_messages(message_data) + # TODO: Remove ContentBuilder compatibility after dd-trace-rb 2.44.0 is released + if Datadog::AIGuard::Evaluation.const_defined?(:MessageBuilder, false) + build_message(message_data) + else + build_content_builder_messages(message_data) + end + end + + def build_message(message_data) + if message_data[:role] == 'tool' + return Datadog::AIGuard.tool( + tool_call_id: message_data[:tool_call_id], content: message_data[:content] + ) + end + + message_content = message_data[:content] + content = message_content unless message_content.is_a?(Array) + + Datadog::AIGuard.message(role: message_data[:role], content: content) do |message| + message_data.fetch(:tool_calls, []).each do |tool_call_data| + message.tool_call( + id: tool_call_data[:id], + name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + + if message_content.is_a?(Array) + message_content.each do |part| + case part[:type] + when 'text' + message.text(part[:text]) + when 'image_url' + message.image_url(part.dig(:image_url, :url)) + end + end + end + end + end + + def build_content_builder_messages(message_data) + if message_data[:tool_calls] + message_data[:tool_calls].map do |tool_call_data| + Datadog::AIGuard.assistant( + id: tool_call_data[:id], + tool_name: tool_call_data.dig(:function, :name), + arguments: tool_call_data.dig(:function, :arguments) + ) + end + elsif message_data[:tool_call_id] + Datadog::AIGuard.tool(tool_call_id: message_data[:tool_call_id], content: message_data[:content]) + elsif message_data[:content].is_a?(Array) + Datadog::AIGuard.message(role: message_data[:role]) do |message| + message_data[:content].each do |part| + case part[:type] + when 'text' + message.text(part[:text]) + when 'image_url' + message.image_url(part.dig(:image_url, :url)) + end + end + end + else + Datadog::AIGuard.message(role: message_data[:role], content: message_data[:content]) + end + end end