datadog 2.43.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +36 -5
  4. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +6 -7
  5. data/ext/datadog_profiling_native_extension/collectors_stack.c +6 -7
  6. data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +65 -31
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
  9. data/ext/datadog_profiling_native_extension/extconf.rb +4 -2
  10. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +25 -0
  11. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +1 -0
  12. data/ext/datadog_profiling_native_extension/profiling.c +1 -0
  13. data/ext/datadog_profiling_native_extension/stack_recorder.c +4 -4
  14. data/ext/datadog_profiling_native_extension/time_helpers.h +3 -3
  15. data/ext/libdatadog_api/di.c +8 -3
  16. data/ext/libdatadog_api/extconf.rb +1 -1
  17. data/ext/libdatadog_api/feature_flags.c +8 -0
  18. data/ext/libdatadog_api/init.c +3 -1
  19. data/ext/libdatadog_api/otel_thread_context.c +38 -17
  20. data/ext/libdatadog_extconf_helpers.rb +14 -9
  21. data/lib/datadog/ai_guard/component.rb +17 -8
  22. data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
  23. data/lib/datadog/ai_guard/configuration.rb +13 -0
  24. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
  25. data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
  26. data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
  27. data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
  28. data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
  29. data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
  30. data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
  31. data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
  32. data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
  33. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
  34. data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
  35. data/lib/datadog/ai_guard/evaluation/request.rb +4 -60
  36. data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
  37. data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
  38. data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
  39. data/lib/datadog/ai_guard/evaluation.rb +25 -11
  40. data/lib/datadog/ai_guard/ext.rb +1 -0
  41. data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +6 -4
  42. data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
  43. data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
  44. data/lib/datadog/ai_guard/redaction/result.rb +29 -0
  45. data/lib/datadog/ai_guard/redaction.rb +80 -0
  46. data/lib/datadog/ai_guard.rb +58 -57
  47. data/lib/datadog/core/configuration/components.rb +9 -1
  48. data/lib/datadog/core/configuration/settings.rb +5 -2
  49. data/lib/datadog/core/configuration/supported_configurations.rb +8 -0
  50. data/lib/datadog/core/remote/client/capabilities.rb +10 -4
  51. data/lib/datadog/core/remote/component.rb +9 -2
  52. data/lib/datadog/core/remote/worker.rb +8 -0
  53. data/lib/datadog/di/instrumenter.rb +1 -1
  54. data/lib/datadog/di/probe_manager.rb +10 -5
  55. data/lib/datadog/di/proc_responder.rb +1 -1
  56. data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
  57. data/lib/datadog/open_feature/agentless/parser.rb +43 -0
  58. data/lib/datadog/open_feature/agentless/response.rb +19 -0
  59. data/lib/datadog/open_feature/agentless/transport.rb +103 -0
  60. data/lib/datadog/open_feature/component.rb +57 -1
  61. data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
  62. data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
  63. data/lib/datadog/open_feature/configuration/source.rb +43 -0
  64. data/lib/datadog/open_feature/configuration.rb +118 -0
  65. data/lib/datadog/open_feature/native_evaluator.rb +1 -14
  66. data/lib/datadog/open_feature/remote.rb +5 -5
  67. data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
  68. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +2 -2
  69. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
  70. data/lib/datadog/symbol_database/extractor.rb +84 -102
  71. data/lib/datadog/tracing/component.rb +2 -0
  72. data/lib/datadog/tracing/configuration/ext.rb +1 -0
  73. data/lib/datadog/tracing/configuration/settings.rb +6 -0
  74. data/lib/datadog/tracing/context.rb +24 -2
  75. data/lib/datadog/tracing/context_provider.rb +9 -5
  76. data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
  77. data/lib/datadog/tracing/metadata/ext.rb +3 -0
  78. data/lib/datadog/tracing/otel_thread_context.rb +76 -11
  79. data/lib/datadog/tracing/tracer.rb +15 -2
  80. data/lib/datadog/tracing/transport/native.rb +1 -1
  81. data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
  82. data/lib/datadog/tracing/transport/traces.rb +1 -1
  83. data/lib/datadog/version.rb +1 -1
  84. metadata +23 -7
  85. data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
@@ -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
@@ -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