datadog 2.42.0 → 2.44.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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -2
  3. data/README.md +3 -0
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +46 -5
  5. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +6 -7
  6. data/ext/datadog_profiling_native_extension/collectors_stack.c +6 -7
  7. data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +80 -36
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
  10. data/ext/datadog_profiling_native_extension/extconf.rb +4 -2
  11. data/ext/datadog_profiling_native_extension/heap_recorder.c +13 -0
  12. data/ext/datadog_profiling_native_extension/heap_recorder.h +4 -0
  13. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +34 -2
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +1 -0
  15. data/ext/datadog_profiling_native_extension/profiling.c +1 -0
  16. data/ext/datadog_profiling_native_extension/stack_recorder.c +17 -4
  17. data/ext/datadog_profiling_native_extension/time_helpers.h +5 -4
  18. data/ext/libdatadog_api/di.c +8 -3
  19. data/ext/libdatadog_api/extconf.rb +1 -1
  20. data/ext/libdatadog_api/feature_flags.c +8 -0
  21. data/ext/libdatadog_api/init.c +3 -1
  22. data/ext/libdatadog_api/otel_thread_context.c +51 -15
  23. data/ext/libdatadog_api/trace_exporter.c +642 -22
  24. data/ext/libdatadog_extconf_helpers.rb +14 -9
  25. data/lib/datadog/ai_guard/component.rb +17 -8
  26. data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
  27. data/lib/datadog/ai_guard/configuration.rb +13 -0
  28. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
  29. data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
  30. data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
  31. data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
  32. data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
  33. data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
  34. data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
  35. data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
  36. data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
  37. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
  38. data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
  39. data/lib/datadog/ai_guard/evaluation/request.rb +4 -60
  40. data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
  41. data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
  42. data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
  43. data/lib/datadog/ai_guard/evaluation.rb +25 -11
  44. data/lib/datadog/ai_guard/ext.rb +1 -0
  45. data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +6 -4
  46. data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
  47. data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
  48. data/lib/datadog/ai_guard/redaction/result.rb +29 -0
  49. data/lib/datadog/ai_guard/redaction.rb +80 -0
  50. data/lib/datadog/ai_guard.rb +58 -57
  51. data/lib/datadog/core/configuration/components.rb +9 -1
  52. data/lib/datadog/core/configuration/settings.rb +5 -2
  53. data/lib/datadog/core/configuration/supported_configurations.rb +8 -0
  54. data/lib/datadog/core/remote/client/capabilities.rb +10 -4
  55. data/lib/datadog/core/remote/component.rb +9 -2
  56. data/lib/datadog/core/remote/worker.rb +8 -0
  57. data/lib/datadog/di/instrumenter.rb +1 -1
  58. data/lib/datadog/di/probe_manager.rb +10 -5
  59. data/lib/datadog/di/proc_responder.rb +1 -1
  60. data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
  61. data/lib/datadog/open_feature/agentless/parser.rb +43 -0
  62. data/lib/datadog/open_feature/agentless/response.rb +19 -0
  63. data/lib/datadog/open_feature/agentless/transport.rb +103 -0
  64. data/lib/datadog/open_feature/component.rb +57 -1
  65. data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
  66. data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
  67. data/lib/datadog/open_feature/configuration/source.rb +43 -0
  68. data/lib/datadog/open_feature/configuration.rb +118 -0
  69. data/lib/datadog/open_feature/native_evaluator.rb +1 -14
  70. data/lib/datadog/open_feature/remote.rb +5 -5
  71. data/lib/datadog/open_feature/transport.rb +5 -0
  72. data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
  73. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +2 -2
  74. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
  75. data/lib/datadog/profiling/collectors/info.rb +2 -6
  76. data/lib/datadog/symbol_database/extractor.rb +84 -102
  77. data/lib/datadog/tracing/component.rb +2 -0
  78. data/lib/datadog/tracing/configuration/ext.rb +1 -0
  79. data/lib/datadog/tracing/configuration/settings.rb +6 -0
  80. data/lib/datadog/tracing/context.rb +24 -2
  81. data/lib/datadog/tracing/context_provider.rb +9 -5
  82. data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
  83. data/lib/datadog/tracing/contrib/sequel/jdbc_connection_string.rb +140 -0
  84. data/lib/datadog/tracing/contrib/sequel/utils.rb +49 -62
  85. data/lib/datadog/tracing/metadata/ext.rb +3 -0
  86. data/lib/datadog/tracing/otel_thread_context.rb +78 -9
  87. data/lib/datadog/tracing/tracer.rb +15 -2
  88. data/lib/datadog/tracing/transport/native.rb +18 -26
  89. data/lib/datadog/tracing/transport/span_events_negotiation.rb +41 -0
  90. data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
  91. data/lib/datadog/tracing/transport/traces.rb +3 -23
  92. data/lib/datadog/version.rb +1 -1
  93. metadata +25 -7
  94. data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
@@ -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
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ module Evaluation
6
+ # Wrapper class for evaluation API response
7
+ #
8
+ # @api private
9
+ class Response
10
+ attr_reader :action, :reason, :tags, :sds_findings, :tag_probabilities,
11
+ :redaction_replacements
12
+
13
+ def initialize(raw_response)
14
+ attributes = raw_response.fetch("data").fetch("attributes")
15
+
16
+ @action = attributes.fetch("action")
17
+ @reason = attributes.fetch("reason")
18
+ @tags = attributes.fetch("tags")
19
+ @tag_probabilities = attributes.fetch("tag_probs")
20
+ @is_blocking_enabled = attributes.fetch("is_blocking_enabled")
21
+ @sds_findings = attributes.fetch("sds_findings", [])
22
+ @redaction_replacements = attributes.fetch("redaction_replacements", [])
23
+ rescue KeyError => e
24
+ raise AIGuardClientError, "Missing key: \"#{e.key}\""
25
+ end
26
+
27
+ def blocking_enabled?
28
+ !!@is_blocking_enabled
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -3,25 +3,22 @@
3
3
  module Datadog
4
4
  module AIGuard
5
5
  module Evaluation
6
- # Wrapper class for evaluation API response
7
6
  class Result
8
7
  ALLOW_ACTION = "ALLOW"
9
8
  DENY_ACTION = "DENY"
10
9
  ABORT_ACTION = "ABORT"
11
10
 
12
- attr_reader :action, :reason, :tags, :sds_findings, :tag_probabilities
13
-
14
- def initialize(raw_response)
15
- attributes = raw_response.fetch("data").fetch("attributes")
16
-
17
- @action = attributes.fetch("action")
18
- @reason = attributes.fetch("reason")
19
- @tags = attributes.fetch("tags")
20
- @tag_probabilities = attributes.fetch("tag_probs")
21
- @is_blocking_enabled = attributes.fetch("is_blocking_enabled")
22
- @sds_findings = attributes.fetch("sds_findings", [])
23
- rescue KeyError => e
24
- raise AIGuardClientError, "Missing key: \"#{e.key}\""
11
+ attr_reader :messages, :action, :reason, :tags, :sds_findings,
12
+ :tag_probabilities, :redaction_replacements
13
+
14
+ def initialize(messages, action:, reason:, tags:, sds_findings:, tag_probabilities:, redaction_replacements:)
15
+ @messages = messages
16
+ @action = action
17
+ @reason = reason
18
+ @tags = tags
19
+ @sds_findings = sds_findings
20
+ @tag_probabilities = tag_probabilities
21
+ @redaction_replacements = redaction_replacements
25
22
  end
26
23
 
27
24
  def allow?
@@ -35,10 +32,6 @@ module Datadog
35
32
  def abort?
36
33
  action == ABORT_ACTION
37
34
  end
38
-
39
- def blocking_enabled?
40
- !!@is_blocking_enabled
41
- end
42
35
  end
43
36
  end
44
37
  end
@@ -3,14 +3,27 @@
3
3
  module Datadog
4
4
  module AIGuard
5
5
  module Evaluation
6
- # Tool call class for AI Guard
6
+ # Represents a tool invocation requested by an assistant message
7
7
  class ToolCall
8
8
  attr_reader :tool_name, :id, :arguments
9
9
 
10
10
  def initialize(tool_name, id:, arguments:)
11
11
  @tool_name = tool_name
12
12
  @id = id
13
- @arguments = arguments
13
+ @arguments =
14
+ case arguments
15
+ when String then arguments
16
+ when Hash then JSON.generate(arguments)
17
+ else raise ArgumentError, "Tool call arguments must be a String or Hash"
18
+ end
19
+ end
20
+
21
+ def copy(arguments:)
22
+ ToolCall.new(tool_name, id: id, arguments: arguments)
23
+ end
24
+
25
+ def to_h
26
+ {id: id, function: {name: tool_name, arguments: arguments}}
14
27
  end
15
28
  end
16
29
  end
@@ -25,37 +25,51 @@ module Datadog
25
25
  end
26
26
 
27
27
  if (last_message = messages.last)
28
- if last_message.tool_call
28
+ if (tool_call = last_message.tool_calls.first)
29
29
  span.set_tag(Ext::TARGET_TAG, "tool")
30
- span.set_tag(Ext::TOOL_NAME_TAG, last_message.tool_call.tool_name)
30
+ span.set_tag(Ext::TOOL_NAME_TAG, tool_call.tool_name)
31
31
  elsif last_message.tool_call_id
32
32
  span.set_tag(Ext::TARGET_TAG, "tool")
33
33
 
34
- if (tool_call_message = messages.find { |m| m.tool_call&.id == last_message.tool_call_id })
35
- span.set_tag(Ext::TOOL_NAME_TAG, tool_call_message.tool_call.tool_name) # steep:ignore
34
+ messages.reverse_each do |message|
35
+ # @type var tool_call: ToolCall?
36
+ tool_call = message.tool_calls.find { |call| call.id == last_message.tool_call_id }
37
+ break span.set_tag(Ext::TOOL_NAME_TAG, tool_call.tool_name) if tool_call
36
38
  end
37
39
  else
38
40
  span.set_tag(Ext::TARGET_TAG, "prompt")
39
41
  end
40
42
  end
41
43
 
42
- request = Request.new(messages)
43
- result = request.perform
44
+ outcome =
45
+ begin
46
+ Client.evaluate(messages)
47
+ rescue
48
+ Metrics::Telemetry.report_error
49
+ raise
50
+ end
51
+
52
+ result = outcome.result
53
+ redaction = outcome.redaction
44
54
 
45
55
  span.set_tag(Ext::ACTION_TAG, result.action)
46
56
  span.set_tag(Ext::REASON_TAG, result.reason)
57
+ span.set_tag(Ext::REDACTED_TAG, redaction.redacted?) if redaction.performed?
47
58
 
48
59
  span.set_metastruct_tag(
49
60
  Ext::METASTRUCT_TAG,
50
61
  {
51
- messages: truncate_content(truncate_messages(request.serialized_messages)),
62
+ messages: truncate_content(truncate_messages(result.messages).map(&:to_h)),
52
63
  attack_categories: result.tags,
53
64
  sds: result.sds_findings,
54
65
  tag_probs: result.tag_probabilities,
55
66
  }
56
67
  )
57
68
 
58
- if allow_raise && (result.deny? || result.abort?) && result.blocking_enabled?
69
+ blocked = allow_raise && outcome.block?
70
+ Metrics::Telemetry.report_evaluation(outcome, blocked: blocked)
71
+
72
+ if blocked
59
73
  span.set_tag(Ext::BLOCKED_TAG, true)
60
74
  raise AIGuardAbortError.new(action: result.action, reason: result.reason, tags: result.tags)
61
75
  end
@@ -64,10 +78,10 @@ module Datadog
64
78
  end
65
79
  end
66
80
 
67
- def perform_no_op
81
+ def perform_no_op(messages)
68
82
  AIGuard.logger&.warn("AI Guard is disabled, messages were not evaluated")
69
83
 
70
- NoOpResult.new
84
+ NoOpResult.new(messages)
71
85
  end
72
86
 
73
87
  private
@@ -82,7 +96,7 @@ module Datadog
82
96
  def truncate_content(serialized_messages)
83
97
  max_bytes = Datadog.configuration.ai_guard.max_content_size_bytes
84
98
 
85
- serialized_messages.map do |message| # steep:ignore
99
+ serialized_messages.map do |message|
86
100
  next message unless message[:content]
87
101
 
88
102
  if message[:content].is_a?(::Array)
@@ -12,6 +12,7 @@ module Datadog
12
12
  ACTION_TAG = "ai_guard.action"
13
13
  REASON_TAG = "ai_guard.reason"
14
14
  BLOCKED_TAG = "ai_guard.blocked"
15
+ REDACTED_TAG = "ai_guard.redacted"
15
16
  EVENT_TAG = "ai_guard.event"
16
17
 
17
18
  METASTRUCT_TAG = "ai_guard"
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "uri"
4
- require "net/http"
5
4
  require "json"
5
+ require "net/http"
6
6
 
7
7
  module Datadog
8
8
  module AIGuard
9
- # API Client for AI Guard API.
10
- # Uses net/http to perform request. Raises on client and server errors.
11
- class APIClient
9
+ # HTTP client for the AI Guard API
10
+ # Uses net/http to perform requests, raises on client and server errors
11
+ #
12
+ # @api private
13
+ class HTTPClient
12
14
  DEFAULT_SITE = "app.datadoghq.com"
13
15
  DEFAULT_PATH = "/api/v2/ai-guard"
14
16
 
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ # Namespace for metrics
6
+ #
7
+ # @api private
8
+ module Metrics
9
+ # A module for telemetry metrics reporting
10
+ module Telemetry
11
+ NAMESPACE = "ai_guard"
12
+ CLIENT_ERROR = "client_error"
13
+ REDACTION_ERROR = "redaction_error"
14
+
15
+ module_function
16
+
17
+ def report_evaluation(outcome, blocked:)
18
+ telemetry = AIGuard.telemetry
19
+ return unless telemetry
20
+
21
+ result = outcome.result
22
+ redaction = outcome.redaction
23
+
24
+ tags = {action: result.action, block: blocked.to_s, error: "false"}
25
+ tags[:redacted] = redaction.redacted?.to_s if redaction.performed?
26
+
27
+ telemetry.inc(NAMESPACE, "requests", 1, tags: tags)
28
+ if redaction.failures_count.positive?
29
+ telemetry.inc(NAMESPACE, "error", redaction.failures_count, tags: {type: REDACTION_ERROR})
30
+ end
31
+ end
32
+
33
+ def report_error
34
+ telemetry = AIGuard.telemetry
35
+ return unless telemetry
36
+
37
+ telemetry.inc(NAMESPACE, "requests", 1, tags: {error: "true"})
38
+ telemetry.inc(NAMESPACE, "error", 1, tags: {type: CLIENT_ERROR})
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Datadog
6
+ module AIGuard
7
+ module Redaction
8
+ # Converts raw backend redaction replacements into a conflict-free
9
+ # path-to-replacement map
10
+ #
11
+ # @api private
12
+ class Replacements
13
+ extend Forwardable
14
+
15
+ TEXT_PATH_PATTERN = /\Amessages\[([0-9]+)\]\.content\[([0-9]+)\]\.text\z/
16
+ CONTENT_PATH_PATTERN = /\Amessages\[([0-9]+)\]\.content\z/
17
+ ARGUMENTS_PATH_PATTERN = /\Amessages\[([0-9]+)\]\.tool_calls\[([0-9]+)\]\.function\.arguments\z/
18
+
19
+ def_delegators :@replacements, :each, :empty?
20
+
21
+ attr_reader :failures_count
22
+
23
+ def initialize(raw_replacements)
24
+ @failures_count = 0
25
+ @replacements = build(raw_replacements)
26
+ end
27
+
28
+ private
29
+
30
+ # NOTE: Identical duplicates collapse into a single entry,
31
+ # conflicting replacements remove the path and record one failure,
32
+ # and malformed or unsupported entries are skipped and counted individually
33
+ #
34
+ # Examples
35
+ #
36
+ # translates this
37
+ #
38
+ # [
39
+ # {"path" => "messages[0].content", "replacement" => "<redacted>"},
40
+ # {"path" => "messages[1].content[2].text", "replacement" => "<redacted>"},
41
+ # {"path" => "messages[2].tool_calls[0].function.arguments", "replacement" => "{}"}
42
+ # ]
43
+ #
44
+ # into this
45
+ #
46
+ # {
47
+ # [0, :content] => "<redacted>",
48
+ # [1, :text, 2] => "<redacted>",
49
+ # [2, :arguments, 0] => "{}"
50
+ # }
51
+ def build(raw_replacements)
52
+ unless raw_replacements.is_a?(::Array)
53
+ @failures_count += 1
54
+ return {}
55
+ end
56
+
57
+ conflicting = {}
58
+ raw_replacements.each_with_object({}) do |entry, replacements|
59
+ next @failures_count += 1 unless entry.is_a?(::Hash)
60
+
61
+ raw_path = entry["path"]
62
+ replacement = entry["replacement"]
63
+
64
+ if !raw_path.is_a?(::String) || raw_path.empty? || !replacement.is_a?(::String)
65
+ next @failures_count += 1
66
+ end
67
+
68
+ # @type var path: Replacements::path?
69
+ path =
70
+ case raw_path
71
+ when CONTENT_PATH_PATTERN
72
+ [Regexp.last_match(1).to_i, :content]
73
+ when TEXT_PATH_PATTERN
74
+ [Regexp.last_match(1).to_i, :text, Regexp.last_match(2).to_i]
75
+ when ARGUMENTS_PATH_PATTERN
76
+ [Regexp.last_match(1).to_i, :arguments, Regexp.last_match(2).to_i]
77
+ end
78
+
79
+ next @failures_count += 1 unless path
80
+ next if conflicting.key?(path)
81
+
82
+ if replacements.key?(path)
83
+ next if replacements[path] == replacement
84
+
85
+ replacements.delete(path)
86
+ conflicting[path] = true
87
+
88
+ next @failures_count += 1
89
+ end
90
+
91
+ replacements[path] = replacement
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ module Redaction
6
+ # A result of applying redaction replacements
7
+ #
8
+ # @api private
9
+ class Result
10
+ attr_reader :messages, :applied_count, :failures_count
11
+
12
+ def initialize(messages, applied_count:, failures_count:, performed: true)
13
+ @messages = messages
14
+ @applied_count = applied_count
15
+ @failures_count = failures_count
16
+ @performed = performed
17
+ end
18
+
19
+ def performed?
20
+ @performed
21
+ end
22
+
23
+ def redacted?
24
+ applied_count.positive?
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ # Sensitive data redaction for evaluation messages
6
+ #
7
+ # @api private
8
+ module Redaction
9
+ class << self
10
+ def skip(messages)
11
+ Redaction::Result.new(messages, applied_count: 0, failures_count: 0, performed: false)
12
+ end
13
+
14
+ def perform(messages, replacements:)
15
+ redaction_replacements = Replacements.new(replacements)
16
+ failures_count = redaction_replacements.failures_count
17
+
18
+ if redaction_replacements.empty?
19
+ return Redaction::Result.new(messages, applied_count: 0, failures_count: failures_count)
20
+ end
21
+
22
+ applied_count = 0
23
+ redacted_messages = messages.dup
24
+
25
+ redaction_replacements.each do |path, replacement|
26
+ index = path[0]
27
+ redacted_message = redact(redacted_messages[index], path: path, replacement: replacement)
28
+
29
+ next failures_count += 1 unless redacted_message
30
+
31
+ redacted_messages[index] = redacted_message
32
+ applied_count += 1
33
+ rescue
34
+ failures_count += 1
35
+ end
36
+
37
+ Redaction::Result.new(
38
+ redacted_messages, applied_count: applied_count, failures_count: failures_count
39
+ )
40
+ end
41
+
42
+ private
43
+
44
+ def redact(message, path:, replacement:)
45
+ return unless message.is_a?(Evaluation::Message)
46
+
47
+ _, kind, index = path
48
+
49
+ case kind
50
+ when :content
51
+ return unless message.content.is_a?(::String)
52
+
53
+ message.copy(content: replacement)
54
+ when :text
55
+ # @type var index: Integer
56
+ content = message.content
57
+ return unless content.is_a?(::Array)
58
+
59
+ part = content[index]
60
+ return if !part.is_a?(Evaluation::ContentPart::Text) || !part.text.is_a?(::String)
61
+
62
+ redacted_content = ::Array.new(content)
63
+ redacted_content[index] = part.copy(text: replacement)
64
+
65
+ message.copy(content: redacted_content)
66
+ when :arguments
67
+ # @type var index: Integer
68
+ tool_call = message.tool_calls[index]
69
+ return unless tool_call && tool_call.arguments.is_a?(::String)
70
+
71
+ redacted_tool_calls = ::Array.new(message.tool_calls)
72
+ redacted_tool_calls[index] = tool_call.copy(arguments: replacement)
73
+
74
+ message.copy(tool_calls: redacted_tool_calls)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end