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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -1
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +36 -5
- data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +6 -7
- data/ext/datadog_profiling_native_extension/collectors_stack.c +6 -7
- data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +65 -31
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
- data/ext/datadog_profiling_native_extension/extconf.rb +4 -2
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +25 -0
- data/ext/datadog_profiling_native_extension/private_vm_api_access.h +1 -0
- data/ext/datadog_profiling_native_extension/profiling.c +1 -0
- data/ext/datadog_profiling_native_extension/stack_recorder.c +4 -4
- data/ext/datadog_profiling_native_extension/time_helpers.h +3 -3
- data/ext/libdatadog_api/di.c +8 -3
- data/ext/libdatadog_api/extconf.rb +1 -1
- data/ext/libdatadog_api/feature_flags.c +8 -0
- data/ext/libdatadog_api/init.c +3 -1
- data/ext/libdatadog_api/otel_thread_context.c +38 -17
- data/ext/libdatadog_extconf_helpers.rb +14 -9
- data/lib/datadog/ai_guard/component.rb +17 -8
- data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
- data/lib/datadog/ai_guard/configuration.rb +13 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
- data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
- data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
- data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
- data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
- data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
- data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
- data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
- data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
- data/lib/datadog/ai_guard/evaluation/request.rb +4 -60
- data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
- data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
- data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
- data/lib/datadog/ai_guard/evaluation.rb +25 -11
- data/lib/datadog/ai_guard/ext.rb +1 -0
- data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +6 -4
- data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
- data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
- data/lib/datadog/ai_guard/redaction/result.rb +29 -0
- data/lib/datadog/ai_guard/redaction.rb +80 -0
- data/lib/datadog/ai_guard.rb +58 -57
- data/lib/datadog/core/configuration/components.rb +9 -1
- data/lib/datadog/core/configuration/settings.rb +5 -2
- data/lib/datadog/core/configuration/supported_configurations.rb +8 -0
- data/lib/datadog/core/remote/client/capabilities.rb +10 -4
- data/lib/datadog/core/remote/component.rb +9 -2
- data/lib/datadog/core/remote/worker.rb +8 -0
- data/lib/datadog/di/instrumenter.rb +1 -1
- data/lib/datadog/di/probe_manager.rb +10 -5
- data/lib/datadog/di/proc_responder.rb +1 -1
- data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
- data/lib/datadog/open_feature/agentless/parser.rb +43 -0
- data/lib/datadog/open_feature/agentless/response.rb +19 -0
- data/lib/datadog/open_feature/agentless/transport.rb +103 -0
- data/lib/datadog/open_feature/component.rb +57 -1
- data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
- data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
- data/lib/datadog/open_feature/configuration/source.rb +43 -0
- data/lib/datadog/open_feature/configuration.rb +118 -0
- data/lib/datadog/open_feature/native_evaluator.rb +1 -14
- data/lib/datadog/open_feature/remote.rb +5 -5
- data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +2 -2
- data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
- data/lib/datadog/symbol_database/extractor.rb +84 -102
- data/lib/datadog/tracing/component.rb +2 -0
- data/lib/datadog/tracing/configuration/ext.rb +1 -0
- data/lib/datadog/tracing/configuration/settings.rb +6 -0
- data/lib/datadog/tracing/context.rb +24 -2
- data/lib/datadog/tracing/context_provider.rb +9 -5
- data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
- data/lib/datadog/tracing/metadata/ext.rb +3 -0
- data/lib/datadog/tracing/otel_thread_context.rb +76 -11
- data/lib/datadog/tracing/tracer.rb +15 -2
- data/lib/datadog/tracing/transport/native.rb +1 -1
- data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
- data/lib/datadog/tracing/transport/traces.rb +1 -1
- data/lib/datadog/version.rb +1 -1
- metadata +23 -7
- 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
|
-
#
|
|
6
|
+
# Represents a message submitted for evaluation
|
|
7
7
|
class Message
|
|
8
|
-
attr_reader :role, :content, :
|
|
8
|
+
attr_reader :role, :content, :tool_calls, :tool_call_id
|
|
9
9
|
|
|
10
|
-
def initialize(role:, content: 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
|
-
@
|
|
20
|
+
@tool_calls = tool_calls
|
|
16
21
|
@tool_call_id = tool_call_id
|
|
22
|
+
end
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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, :
|
|
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
|
|
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
|
-
@
|
|
11
|
+
@messages = messages
|
|
14
12
|
end
|
|
15
13
|
|
|
16
|
-
def
|
|
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: @
|
|
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,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@action =
|
|
18
|
-
@reason =
|
|
19
|
-
@tags =
|
|
20
|
-
@
|
|
21
|
-
@
|
|
22
|
-
@
|
|
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
|
-
#
|
|
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 =
|
|
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.
|
|
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,
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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(
|
|
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
|
-
|
|
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|
|
|
99
|
+
serialized_messages.map do |message|
|
|
86
100
|
next message unless message[:content]
|
|
87
101
|
|
|
88
102
|
if message[:content].is_a?(::Array)
|
data/lib/datadog/ai_guard/ext.rb
CHANGED
|
@@ -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
|
-
#
|
|
10
|
-
# Uses net/http to perform
|
|
11
|
-
|
|
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
|
|