datadog 2.43.0 → 2.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +136 -132
  4. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -5
  5. data/ext/datadog_profiling_native_extension/collectors_gc_profiling_helper.c +0 -2
  6. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +19 -20
  7. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.h +5 -1
  8. data/ext/datadog_profiling_native_extension/collectors_stack.c +7 -7
  9. data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
  10. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +146 -109
  11. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
  12. data/ext/datadog_profiling_native_extension/extconf.rb +6 -2
  13. data/ext/datadog_profiling_native_extension/heap_recorder.c +2 -2
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +122 -53
  15. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +6 -2
  16. data/ext/datadog_profiling_native_extension/profiling.c +22 -23
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.c +3 -1
  18. data/ext/datadog_profiling_native_extension/ruby_helpers.h +4 -0
  19. data/ext/datadog_profiling_native_extension/setup_signal_handler.c +0 -2
  20. data/ext/datadog_profiling_native_extension/stack_recorder.c +10 -28
  21. data/ext/datadog_profiling_native_extension/stack_recorder.h +0 -1
  22. data/ext/datadog_profiling_native_extension/time_helpers.c +2 -4
  23. data/ext/datadog_profiling_native_extension/time_helpers.h +13 -14
  24. data/ext/libdatadog_api/di.c +8 -3
  25. data/ext/libdatadog_api/extconf.rb +1 -1
  26. data/ext/libdatadog_api/feature_flags.c +8 -0
  27. data/ext/libdatadog_api/init.c +3 -1
  28. data/ext/libdatadog_api/otel_thread_context.c +38 -17
  29. data/ext/libdatadog_api/trace_exporter.c +77 -11
  30. data/ext/libdatadog_extconf_helpers.rb +14 -9
  31. data/lib/datadog/ai_guard/component.rb +17 -8
  32. data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
  33. data/lib/datadog/ai_guard/configuration.rb +13 -0
  34. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
  35. data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
  36. data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
  37. data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
  38. data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
  39. data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
  40. data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
  41. data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
  42. data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
  43. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
  44. data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
  45. data/lib/datadog/ai_guard/evaluation/request.rb +4 -60
  46. data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
  47. data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
  48. data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
  49. data/lib/datadog/ai_guard/evaluation.rb +25 -11
  50. data/lib/datadog/ai_guard/ext.rb +1 -0
  51. data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +6 -4
  52. data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
  53. data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
  54. data/lib/datadog/ai_guard/redaction/result.rb +29 -0
  55. data/lib/datadog/ai_guard/redaction.rb +80 -0
  56. data/lib/datadog/ai_guard.rb +58 -57
  57. data/lib/datadog/core/configuration/components.rb +9 -1
  58. data/lib/datadog/core/configuration/settings.rb +5 -2
  59. data/lib/datadog/core/configuration/supported_configurations.rb +8 -0
  60. data/lib/datadog/core/remote/client/capabilities.rb +10 -4
  61. data/lib/datadog/core/remote/component.rb +9 -2
  62. data/lib/datadog/core/remote/worker.rb +8 -0
  63. data/lib/datadog/di/instrumenter.rb +1 -1
  64. data/lib/datadog/di/probe_manager.rb +10 -5
  65. data/lib/datadog/di/proc_responder.rb +1 -1
  66. data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
  67. data/lib/datadog/open_feature/agentless/parser.rb +43 -0
  68. data/lib/datadog/open_feature/agentless/response.rb +19 -0
  69. data/lib/datadog/open_feature/agentless/transport.rb +103 -0
  70. data/lib/datadog/open_feature/component.rb +57 -1
  71. data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
  72. data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
  73. data/lib/datadog/open_feature/configuration/source.rb +43 -0
  74. data/lib/datadog/open_feature/configuration.rb +118 -0
  75. data/lib/datadog/open_feature/native_evaluator.rb +1 -14
  76. data/lib/datadog/open_feature/remote.rb +5 -5
  77. data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
  78. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +7 -2
  79. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
  80. data/lib/datadog/profiling/component.rb +2 -3
  81. data/lib/datadog/profiling/exporter.rb +7 -5
  82. data/lib/datadog/symbol_database/extractor.rb +84 -102
  83. data/lib/datadog/tracing/component.rb +3 -1
  84. data/lib/datadog/tracing/configuration/ext.rb +1 -0
  85. data/lib/datadog/tracing/configuration/settings.rb +9 -2
  86. data/lib/datadog/tracing/context.rb +24 -2
  87. data/lib/datadog/tracing/context_provider.rb +9 -5
  88. data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
  89. data/lib/datadog/tracing/contrib/utils/quantization/http.rb +6 -3
  90. data/lib/datadog/tracing/diagnostics/environment_logger.rb +1 -0
  91. data/lib/datadog/tracing/metadata/ext.rb +3 -0
  92. data/lib/datadog/tracing/otel_thread_context.rb +76 -11
  93. data/lib/datadog/tracing/tracer.rb +15 -2
  94. data/lib/datadog/tracing/transport/native.rb +23 -22
  95. data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
  96. data/lib/datadog/tracing/transport/traces.rb +1 -1
  97. data/lib/datadog/version.rb +1 -1
  98. metadata +23 -7
  99. data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
@@ -4,74 +4,67 @@ module Datadog
4
4
  module AIGuard
5
5
  module Contrib
6
6
  module RubyLLM
7
- # module that gets prepended to RubyLLM::Chat
7
+ # Protects tool calls before RubyLLM executes them locally
8
+ #
9
+ # @api private
8
10
  module ChatInstrumentation
9
- class << self
10
- def evaluate!(messages)
11
- ai_guard_messages = messages.flat_map do |message|
12
- if message.tool_call?
13
- message.tool_calls.map do |tool_call_id, tool_call|
14
- AIGuard.assistant(id: tool_call_id, tool_name: tool_call.name, arguments: tool_call.arguments.to_s)
15
- end
16
- elsif message.tool_result?
17
- build_ai_guard_tool(message)
18
- else
19
- build_ai_guard_message(message)
20
- end
21
- end
11
+ private
22
12
 
23
- AIGuard.evaluate(*ai_guard_messages)
24
- end
25
-
26
- private
13
+ def provider_completion(usage_recorder:, stream_tracker: nil, &block)
14
+ @_datadog_ai_guard_evaluate = true
15
+ super
16
+ ensure
17
+ @_datadog_ai_guard_evaluate = false
18
+ end
27
19
 
28
- def build_ai_guard_message(message)
29
- content = message.content
20
+ def preprocessed_messages(list = messages)
21
+ return super unless @_datadog_ai_guard_evaluate
30
22
 
31
- case content
32
- when ::RubyLLM::Content
33
- AIGuard.message(role: message.role) do |m|
34
- m.text(content.text.to_s) if content.text
23
+ adapter = MessageAdapter.new(list)
35
24
 
36
- # Calling attachment.for_llm triggers lazy loading of file contents.
37
- # The result is memoized, so providers won't re-read.
38
- content.attachments.each do |attachment|
39
- case attachment.type
40
- when :image
41
- m.image_url(attachment.for_llm)
42
- when :text
43
- m.text(attachment.for_llm)
44
- end
45
- # Skip :pdf, :audio, :video, :unknown — not supported by AIGuard
46
- end
47
- end
48
- else
49
- AIGuard.message(role: message.role, content: content)
50
- end
25
+ begin
26
+ converted_messages = adapter.to_ai_guard
27
+ rescue JSON::JSONError
28
+ Metrics::Telemetry.report_error
29
+ return super
30
+ rescue => e
31
+ AIGuard.telemetry&.report(e, description: "AI Guard: Failed to convert RubyLLM messages")
32
+ return super
51
33
  end
52
34
 
53
- def build_ai_guard_tool(message)
54
- content = message.content
55
- # Tools can return Content or Content::Raw objects (e.g. with attachments),
56
- # but AIGuard.tool expects a String. Extract text when content is a Content object.
57
- case content
58
- when ::RubyLLM::Content
59
- content = content.text.to_s
60
- end
61
- AIGuard.tool(tool_call_id: message.tool_call_id, content: content)
35
+ evaluation = AIGuard.evaluate(*converted_messages)
36
+
37
+ begin
38
+ redacted_messages = adapter.apply_redactions(evaluation.messages)
39
+ rescue JSON::JSONError => e
40
+ AIGuard.telemetry&.report(e, description: "AI Guard: Failed to apply RubyLLM redaction")
41
+ return super
62
42
  end
43
+
44
+ super(redacted_messages)
63
45
  end
64
46
 
65
- def complete(&block)
66
- Datadog::AIGuard::Contrib::RubyLLM::ChatInstrumentation.evaluate!(messages)
47
+ def execute_pending_tool_calls(response)
48
+ response_index = messages.index { |message| message == response }
49
+ return super unless response_index
67
50
 
68
- super
69
- end
51
+ adapter = MessageAdapter.new(messages)
52
+ begin
53
+ converted_messages = adapter.to_ai_guard
54
+ rescue JSON::JSONError
55
+ Metrics::Telemetry.report_error
56
+ return super
57
+ end
70
58
 
71
- def handle_tool_calls(response, &block)
72
- Datadog::AIGuard::Contrib::RubyLLM::ChatInstrumentation.evaluate!(messages)
59
+ evaluation = AIGuard.evaluate(*converted_messages)
60
+ begin
61
+ redacted_messages = adapter.apply_redactions(evaluation.messages)
62
+ rescue JSON::JSONError => e
63
+ AIGuard.telemetry&.report(e, description: "AI Guard: Failed to apply RubyLLM redaction")
64
+ return super
65
+ end
73
66
 
74
- super
67
+ super(redacted_messages[response_index])
75
68
  end
76
69
  end
77
70
  end
@@ -11,7 +11,7 @@ module Datadog
11
11
  class Integration
12
12
  include Datadog::AIGuard::Contrib::Integration
13
13
 
14
- MINIMUM_VERSION = Gem::Version.new("1.0.0")
14
+ MINIMUM_VERSION = Gem::Version.new("2.0.0")
15
15
 
16
16
  register_as :ruby_llm, auto_patch: false
17
17
 
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "redaction_change_set"
4
+
5
+ module Datadog
6
+ module AIGuard
7
+ module Contrib
8
+ module RubyLLM
9
+ # RubyLLM messages paired with their AI Guard representation
10
+ #
11
+ # @api private
12
+ class MessageAdapter
13
+ def initialize(messages)
14
+ @ruby_llm_messages = messages
15
+ end
16
+
17
+ def to_ai_guard
18
+ @ai_guard_messages ||= @ruby_llm_messages.map { |message| build_ai_guard_message(message) }
19
+ end
20
+
21
+ def apply_redactions(redacted_messages)
22
+ ai_guard_messages = to_ai_guard
23
+ return @ruby_llm_messages if redacted_messages == ai_guard_messages
24
+
25
+ @ruby_llm_messages.each_with_index.map do |message, index|
26
+ original_message = ai_guard_messages[index]
27
+ redacted_message = redacted_messages[index]
28
+ next message if redacted_message == original_message
29
+
30
+ RedactionChangeSet.new(original_message, redacted_message).apply_to(message)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def build_ai_guard_message(message)
37
+ builder = Evaluation::MessageBuilder.new
38
+
39
+ # NOTE: Hash block arguments are misinterpreted by Steep after `nil.to_h`
40
+ # steep:ignore:start
41
+ message.tool_calls.to_h.each do |id, tool_call|
42
+ builder.tool_call(name: tool_call.name, id: id, arguments: tool_call.arguments)
43
+ end
44
+ # steep:ignore:end
45
+
46
+ if message.attachments.empty?
47
+ content = message.content
48
+ else
49
+ builder.text(message.content) if message.content && !message.content.empty?
50
+
51
+ message.attachments.each do |attachment|
52
+ next if attachment.provider_file?
53
+
54
+ case attachment.type
55
+ when :text
56
+ builder.text(attachment.content)
57
+ when :image
58
+ # NOTE: Local images are Base64-encoded here by `#for_llm`
59
+ # and again when RubyLLM builds the provider request
60
+ url = attachment.url? ? attachment.source.to_s : attachment.for_llm
61
+ builder.image_url(url)
62
+ end
63
+ end
64
+
65
+ content = builder.content
66
+ end
67
+
68
+ Evaluation::Message.new(
69
+ role: message.role, content: content, tool_calls: builder.tool_calls, tool_call_id: message.tool_call_id
70
+ )
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "message_adapter"
3
4
  require_relative "chat_instrumentation"
4
5
 
5
6
  module Datadog
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "stringio"
5
+
6
+ module Datadog
7
+ module AIGuard
8
+ module Contrib
9
+ module RubyLLM
10
+ # Changes required to redact a RubyLLM message
11
+ #
12
+ # @api private
13
+ class RedactionChangeSet
14
+ def initialize(original_message, redacted_message)
15
+ @content_replacement = content_replacement(original_message.content, redacted_message.content)
16
+ @content_part_replacements = content_part_replacements(original_message.content, redacted_message.content)
17
+ @tool_call_replacements = tool_call_replacements(original_message.tool_calls, redacted_message.tool_calls)
18
+ end
19
+
20
+ def apply_to(message)
21
+ return message unless changed?
22
+
23
+ # NOTE: Preserve `raw_content` after provider-specific redaction is implemented.
24
+ # Unchanged raw content could bypass redaction
25
+ if message.raw_content
26
+ Datadog.logger.warn("AI Guard omitted RubyLLM raw content because it may contain sensitive data")
27
+ end
28
+
29
+ content = build_content(message)
30
+ attachments = build_attachments(message)
31
+ tool_calls = build_tool_calls(message)
32
+
33
+ changed_message = ::RubyLLM::Message.new(
34
+ content: content, attachments: attachments, tool_calls: tool_calls,
35
+ role: message.role, model: message.model, tool_call_id: message.tool_call_id,
36
+ raw: message.raw, thinking: message.thinking, citations: message.citations,
37
+ finish_reason: message.finish_reason, server_tool_calls: message.server_tool_calls,
38
+ raw_reasoning: message.raw_reasoning, cache_until_here: message.cache_until_here?
39
+ )
40
+ changed_message.conversation = message.conversation
41
+ changed_message
42
+ end
43
+
44
+ private
45
+
46
+ # NOTE: Steep treats original and redacted content shapes as independent
47
+ def content_replacement(original_content, redacted_content) # steep:ignore MethodBodyTypeMismatch
48
+ return if original_content.is_a?(Array) || redacted_content == original_content
49
+
50
+ redacted_content
51
+ end
52
+
53
+ def content_part_replacements(original_content, redacted_content)
54
+ return {} unless original_content.is_a?(Array)
55
+
56
+ replacements = {}
57
+ original_content.each_with_index do |content_part, index|
58
+ next if content_part == redacted_content[index] # steep:ignore NoMethod
59
+
60
+ replacements[index] = redacted_content[index] # steep:ignore NoMethod
61
+ end
62
+
63
+ replacements
64
+ end
65
+
66
+ def tool_call_replacements(original_tool_calls, redacted_tool_calls)
67
+ replacements = {}
68
+ original_tool_calls.each_with_index do |tool_call, index|
69
+ next if redacted_tool_calls[index] == tool_call
70
+
71
+ replacements[tool_call.id] = redacted_tool_calls[index]
72
+ end
73
+
74
+ replacements
75
+ end
76
+
77
+ def changed?
78
+ !!@content_replacement || !@content_part_replacements.empty? ||
79
+ !@tool_call_replacements.empty?
80
+ end
81
+
82
+ def build_content(message)
83
+ return @content_replacement if @content_replacement
84
+ return message.content if message.content.to_s.empty?
85
+
86
+ replacement = @content_part_replacements[0]
87
+ replacement ? replacement.text : message.content
88
+ end
89
+
90
+ def build_attachments(message)
91
+ return message.attachments if @content_part_replacements.empty?
92
+
93
+ content_part_index = message.content.to_s.empty? ? 0 : 1
94
+ # @type var attachments: Array[::RubyLLM::Attachment]?
95
+ attachments = nil
96
+
97
+ message.attachments.each_with_index do |attachment, attachment_index|
98
+ next if attachment.provider_file?
99
+ next unless attachment.type == :text || attachment.type == :image
100
+
101
+ replacement = @content_part_replacements[content_part_index]
102
+ content_part_index += 1
103
+
104
+ next unless replacement && attachment.type == :text
105
+
106
+ attachments ||= message.attachments.dup
107
+ attachments[attachment_index] = ::RubyLLM::Attachment.new( # steep:ignore NoMethod
108
+ StringIO.new(replacement.text), filename: attachment.filename, config: attachment.config
109
+ )
110
+ end
111
+
112
+ attachments || message.attachments
113
+ end
114
+
115
+ def build_tool_calls(message)
116
+ return message.tool_calls if @tool_call_replacements.empty?
117
+
118
+ # @type var tool_calls: Hash[String, ::RubyLLM::ToolCall]
119
+ tool_calls = message.tool_calls.dup # steep:ignore IncompatibleAssignment
120
+ @tool_call_replacements.each do |id, replacement|
121
+ tool_call = tool_calls.fetch(id)
122
+ tool_calls[id] = ::RubyLLM::ToolCall.new(
123
+ id: tool_call.id, name: tool_call.name, remote: tool_call.remote?,
124
+ thought_signature: tool_call.thought_signature,
125
+ arguments: JSON.parse(replacement.arguments)
126
+ )
127
+ end
128
+
129
+ tool_calls
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ module Evaluation
6
+ # Client for evaluation API requests
7
+ #
8
+ # @api private
9
+ module Client
10
+ def self.evaluate(messages)
11
+ request = Request.new(messages)
12
+ http_client = AIGuard.http_client
13
+
14
+ # This should never happen, as we are only calling this method when AI Guard is enabled,
15
+ # and this means the HTTP client was not initialized properly.
16
+ #
17
+ # Please report this at https://github.com/datadog/dd-trace-rb/blob/master/CONTRIBUTING.md#found-a-bug
18
+ raise "AI Guard HTTP client not initialized" unless http_client
19
+
20
+ response = Response.new(
21
+ http_client.post(Request::REQUEST_PATH, body: request.body)
22
+ )
23
+
24
+ redaction =
25
+ if Datadog.configuration.ai_guard.redaction_enabled
26
+ Redaction.perform(messages, replacements: response.redaction_replacements)
27
+ else
28
+ Redaction.skip(messages)
29
+ end
30
+
31
+ result = Result.new(
32
+ redaction.messages,
33
+ action: response.action,
34
+ reason: response.reason,
35
+ tags: response.tags,
36
+ sds_findings: response.sds_findings,
37
+ tag_probabilities: response.tag_probabilities,
38
+ redaction_replacements: response.redaction_replacements
39
+ )
40
+
41
+ Outcome.new(
42
+ result: result,
43
+ redaction: redaction,
44
+ blocking_enabled: response.blocking_enabled?
45
+ )
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -16,6 +16,14 @@ module Datadog
16
16
  def type
17
17
  :text
18
18
  end
19
+
20
+ def copy(text:)
21
+ Text.new(text)
22
+ end
23
+
24
+ def to_h
25
+ {type: type.to_s, text: text}
26
+ end
19
27
  end
20
28
 
21
29
  # An image URL content part. Accepts an absolute URL or a base64 data URI.
@@ -29,6 +37,10 @@ module Datadog
29
37
  def type
30
38
  :image_url
31
39
  end
40
+
41
+ def to_h
42
+ {type: type.to_s, image_url: {url: url}}
43
+ end
32
44
  end
33
45
  end
34
46
  end
@@ -3,21 +3,39 @@
3
3
  module Datadog
4
4
  module AIGuard
5
5
  module Evaluation
6
- # Message class for AI Guard
6
+ # Represents a message submitted for evaluation
7
7
  class Message
8
- attr_reader :role, :content, :tool_call, :tool_call_id
8
+ attr_reader :role, :content, :tool_calls, :tool_call_id
9
9
 
10
- def initialize(role:, content: nil, tool_call: nil, tool_call_id: nil)
10
+ def initialize(role:, content: nil, tool_calls: [], tool_call_id: nil)
11
11
  raise ArgumentError, "Role must be set to a non-empty value" if role.to_s.empty?
12
+ raise ArgumentError, "Tool calls must be an Array" unless tool_calls.is_a?(::Array)
13
+
14
+ if tool_calls.any? { |tool_call| !tool_call.is_a?(ToolCall) }
15
+ raise ArgumentError, "Tool calls must contain only #{ToolCall.name} instances"
16
+ end
12
17
 
13
18
  @role = role.to_sym
14
19
  @content = content
15
- @tool_call = tool_call
20
+ @tool_calls = tool_calls
16
21
  @tool_call_id = tool_call_id
22
+ end
17
23
 
18
- if @tool_call && !@tool_call.is_a?(ToolCall)
19
- raise ArgumentError, "Expected an instance of #{ToolCall.name} for :tool_call argument"
20
- end
24
+ def copy(content: self.content, tool_calls: self.tool_calls)
25
+ Message.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
26
+ end
27
+
28
+ def to_h
29
+ serialized = {
30
+ role: role,
31
+ content: content.is_a?(::Array) ? content.map(&:to_h) : content,
32
+ tool_call_id: tool_call_id,
33
+ }
34
+
35
+ serialized[:tool_calls] = tool_calls.map(&:to_h) unless tool_calls.empty?
36
+ serialized.compact!
37
+
38
+ serialized
21
39
  end
22
40
  end
23
41
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ module Evaluation
6
+ # Builds a message from content parts and tool calls
7
+ #
8
+ # @public_api
9
+ class MessageBuilder
10
+ attr_reader :content, :tool_calls
11
+
12
+ def initialize
13
+ @content = []
14
+ @tool_calls = []
15
+ end
16
+
17
+ def text(text)
18
+ @content << ContentPart::Text.new(text)
19
+ end
20
+
21
+ def image_url(url)
22
+ @content << ContentPart::ImageURL.new(url)
23
+ end
24
+
25
+ def tool_call(name:, id:, arguments:)
26
+ @tool_calls << ToolCall.new(name, id: id.to_s, arguments: arguments)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -5,14 +5,18 @@ module Datadog
5
5
  module Evaluation
6
6
  # Class for emulating AI Guard evaluation result when AI Guard is disabled.
7
7
  class NoOpResult
8
- attr_reader :action, :reason, :tags, :sds_findings, :tag_probabilities
8
+ attr_reader :action, :reason, :tags, :sds_findings, :redaction_replacements,
9
+ :tag_probabilities, :messages
9
10
 
10
- def initialize
11
+ def initialize(messages)
11
12
  @action = Result::ALLOW_ACTION
12
13
  @reason = "AI Guard is disabled"
14
+
13
15
  @tags = []
16
+ @messages = messages
14
17
  @sds_findings = []
15
18
  @tag_probabilities = {}
19
+ @redaction_replacements = []
16
20
  end
17
21
 
18
22
  def allow?
@@ -26,10 +30,6 @@ module Datadog
26
30
  def abort?
27
31
  false
28
32
  end
29
-
30
- def blocking_enabled?
31
- false
32
- end
33
33
  end
34
34
  end
35
35
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ module Evaluation
6
+ # Internal outcome of an evaluation request
7
+ #
8
+ # @api private
9
+ class Outcome
10
+ attr_reader :result, :redaction
11
+
12
+ def initialize(result:, redaction:, blocking_enabled:)
13
+ @result = result
14
+ @redaction = redaction
15
+ @blocking_enabled = blocking_enabled
16
+ end
17
+
18
+ def block?
19
+ @blocking_enabled && (result.deny? || result.abort?)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -3,37 +3,19 @@
3
3
  module Datadog
4
4
  module AIGuard
5
5
  module Evaluation
6
- # Request builds the request body from an array of messages and processes the response
6
+ # Request builds the request body for the AI Guard /evaluate endpoint
7
7
  class Request
8
8
  REQUEST_PATH = "/evaluate"
9
9
 
10
- attr_reader :serialized_messages
11
-
12
10
  def initialize(messages)
13
- @serialized_messages = serialize_messages(messages)
11
+ @messages = messages
14
12
  end
15
13
 
16
- def perform
17
- api_client = AIGuard.api_client
18
-
19
- # This should never happen, as we are only calling this method when AI Guard is enabled,
20
- # and this means the API Client was not initialized properly.
21
- #
22
- # Please report this at https://github.com/datadog/dd-trace-rb/blob/master/CONTRIBUTING.md#found-a-bug
23
- raise "AI Guard API Client not initialized" unless api_client
24
-
25
- raw_response = api_client.post(REQUEST_PATH, body: build_request_body)
26
-
27
- Result.new(raw_response)
28
- end
29
-
30
- private
31
-
32
- def build_request_body
14
+ def body
33
15
  {
34
16
  data: {
35
17
  attributes: {
36
- messages: @serialized_messages,
18
+ messages: @messages.map(&:to_h),
37
19
  meta: {
38
20
  service: Datadog.configuration.service,
39
21
  env: Datadog.configuration.env,
@@ -42,44 +24,6 @@ module Datadog
42
24
  },
43
25
  }
44
26
  end
45
-
46
- def serialize_messages(messages)
47
- messages.map { |message| serialize_message(message) }
48
- end
49
-
50
- def serialize_message(message)
51
- if message.tool_call
52
- {
53
- role: message.role,
54
- tool_calls: [
55
- {
56
- id: message.tool_call.id,
57
- function: {
58
- name: message.tool_call.tool_name,
59
- arguments: message.tool_call.arguments,
60
- },
61
- },
62
- ],
63
- }
64
- elsif message.content.is_a?(::Array)
65
- {role: message.role, content: serialize_content_parts(message.content)}
66
- elsif message.tool_call_id
67
- {role: message.role, tool_call_id: message.tool_call_id, content: message.content}
68
- else
69
- {role: message.role, content: message.content}
70
- end
71
- end
72
-
73
- def serialize_content_parts(parts)
74
- parts.map do |part|
75
- case part
76
- when ContentPart::Text
77
- {type: "text", text: part.text}
78
- when ContentPart::ImageURL
79
- {type: "image_url", image_url: {url: part.url}}
80
- end
81
- end
82
- end
83
27
  end
84
28
  end
85
29
  end