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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -2
- data/README.md +3 -0
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +46 -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 +80 -36
- 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/heap_recorder.c +13 -0
- data/ext/datadog_profiling_native_extension/heap_recorder.h +4 -0
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +34 -2
- 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 +17 -4
- data/ext/datadog_profiling_native_extension/time_helpers.h +5 -4
- 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 +51 -15
- data/ext/libdatadog_api/trace_exporter.c +642 -22
- 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/open_feature/transport.rb +5 -0
- 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/profiling/collectors/info.rb +2 -6
- 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/contrib/sequel/jdbc_connection_string.rb +140 -0
- data/lib/datadog/tracing/contrib/sequel/utils.rb +49 -62
- data/lib/datadog/tracing/metadata/ext.rb +3 -0
- data/lib/datadog/tracing/otel_thread_context.rb +78 -9
- data/lib/datadog/tracing/tracer.rb +15 -2
- data/lib/datadog/tracing/transport/native.rb +18 -26
- data/lib/datadog/tracing/transport/span_events_negotiation.rb +41 -0
- data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
- data/lib/datadog/tracing/transport/traces.rb +3 -23
- data/lib/datadog/version.rb +1 -1
- metadata +25 -7
- data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "rubygems"
|
|
4
4
|
require "pathname"
|
|
5
|
+
require "shellwords"
|
|
5
6
|
|
|
6
7
|
module Datadog
|
|
7
8
|
# Contains a bunch of shared helpers that get used during building of extensions that link to libdatadog
|
|
@@ -10,7 +11,7 @@ module Datadog
|
|
|
10
11
|
module LibdatadogExtconfHelpers
|
|
11
12
|
# Used to make sure the correct gem version gets loaded, as extconf.rb does not get run with "bundle exec" and thus
|
|
12
13
|
# may see multiple libdatadog versions. See https://github.com/DataDog/dd-trace-rb/pull/2531 for the horror story.
|
|
13
|
-
LIBDATADOG_VERSION = "~>
|
|
14
|
+
LIBDATADOG_VERSION = "~> 44.0.1.1.0"
|
|
14
15
|
|
|
15
16
|
# Used as an workaround for a limitation with how dynamic linking works in environments where the datadog gem and
|
|
16
17
|
# libdatadog are moved after the extension gets compiled.
|
|
@@ -116,7 +117,10 @@ module Datadog
|
|
|
116
117
|
extconf_folder:,
|
|
117
118
|
libdatadog_pkgconfig_folder: Libdatadog.pkgconfig_folder,
|
|
118
119
|
gem_dir: Gem.dir,
|
|
119
|
-
logger: Logging
|
|
120
|
+
logger: Logging,
|
|
121
|
+
target_incflags: $INCFLAGS,
|
|
122
|
+
target_ldflags: $LDFLAGS,
|
|
123
|
+
target_libs: $libs
|
|
120
124
|
)
|
|
121
125
|
return unless libdatadog_pkgconfig_folder
|
|
122
126
|
|
|
@@ -124,10 +128,9 @@ module Datadog
|
|
|
124
128
|
libdir = "#{libdatadog_pkgconfig_folder}/../../lib"
|
|
125
129
|
includedir = "#{libdatadog_pkgconfig_folder}/../../include"
|
|
126
130
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
$libs << " -ldatadog_profiling"
|
|
131
|
+
target_incflags << " -I#{Shellwords.escape(includedir)}"
|
|
132
|
+
target_ldflags << " -L#{Shellwords.escape(libdir)} -Wl,-rpath,#{Shellwords.escape(libdir)}"
|
|
133
|
+
target_libs << " -ldatadog_profiling"
|
|
131
134
|
|
|
132
135
|
# Add extra relative rpaths using $ORIGIN to handle environments where gems are moved after installation.
|
|
133
136
|
# The excessive escaping is needed to get these special characters through Make and the shell untouched.
|
|
@@ -141,10 +144,12 @@ module Datadog
|
|
|
141
144
|
libdatadog_pkgconfig_folder: libdatadog_pkgconfig_folder,
|
|
142
145
|
),
|
|
143
146
|
]
|
|
144
|
-
extra_relative_rpaths.each { |folder|
|
|
147
|
+
extra_relative_rpaths.each { |folder| target_ldflags << " -Wl,-rpath,$$$\\\\{ORIGIN\\}/#{Shellwords.escape(folder)}" }
|
|
145
148
|
|
|
146
|
-
logger.message
|
|
147
|
-
|
|
149
|
+
# includedir/libdir/ldflags might have a `%` in there and `logger.message` bottoms out on `printf` so we need to
|
|
150
|
+
# use the `message("%s", ...)` format otherwise the logger will complain
|
|
151
|
+
logger.message("%s", "linking with libdatadog (include=#{includedir}, lib=#{libdir})\n")
|
|
152
|
+
logger.message("%s", "[datadog] $LDFLAGS were set to: #{target_ldflags.inspect}\n")
|
|
148
153
|
|
|
149
154
|
true
|
|
150
155
|
end
|
|
@@ -1,37 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "ext"
|
|
6
|
+
require_relative "http_client"
|
|
4
7
|
require_relative "evaluation"
|
|
5
8
|
require_relative "evaluation/request"
|
|
9
|
+
require_relative "evaluation/response"
|
|
10
|
+
require_relative "evaluation/client"
|
|
6
11
|
require_relative "evaluation/result"
|
|
12
|
+
require_relative "evaluation/outcome"
|
|
7
13
|
require_relative "evaluation/no_op_result"
|
|
8
14
|
require_relative "evaluation/message"
|
|
9
15
|
require_relative "evaluation/tool_call"
|
|
10
16
|
require_relative "evaluation/content_part"
|
|
11
|
-
require_relative "evaluation/
|
|
12
|
-
require_relative "
|
|
17
|
+
require_relative "evaluation/message_builder"
|
|
18
|
+
require_relative "redaction"
|
|
19
|
+
require_relative "redaction/result"
|
|
20
|
+
require_relative "redaction/replacements"
|
|
21
|
+
require_relative "metrics/telemetry"
|
|
13
22
|
|
|
14
23
|
module Datadog
|
|
15
24
|
module AIGuard
|
|
16
25
|
# Component for API Guard product
|
|
17
26
|
class Component
|
|
18
|
-
attr_reader :
|
|
27
|
+
attr_reader :http_client, :logger, :telemetry
|
|
19
28
|
|
|
20
29
|
def self.build(settings, logger:, telemetry:)
|
|
21
30
|
return unless settings.respond_to?(:ai_guard) && settings.ai_guard.enabled
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
http_client = HTTPClient.new(
|
|
24
33
|
endpoint: settings.ai_guard.endpoint,
|
|
25
34
|
api_key: settings.api_key,
|
|
26
35
|
application_key: settings.ai_guard.app_key,
|
|
27
36
|
timeout: settings.ai_guard.timeout_ms / 1_000
|
|
28
37
|
)
|
|
29
38
|
|
|
30
|
-
new(
|
|
39
|
+
new(http_client, logger: logger, telemetry: telemetry)
|
|
31
40
|
end
|
|
32
41
|
|
|
33
|
-
def initialize(
|
|
34
|
-
@
|
|
42
|
+
def initialize(http_client, logger:, telemetry:)
|
|
43
|
+
@http_client = http_client
|
|
35
44
|
@logger = logger
|
|
36
45
|
@telemetry = telemetry
|
|
37
46
|
end
|
|
@@ -6,6 +6,8 @@ module Datadog
|
|
|
6
6
|
# This module contains constants for AI Guard component
|
|
7
7
|
module Ext
|
|
8
8
|
ENV_AI_GUARD_ENABLED = "DD_AI_GUARD_ENABLED"
|
|
9
|
+
ENV_AI_GUARD_REDACTION_ENABLED = "DD_AI_GUARD_REDACTION_ENABLED"
|
|
10
|
+
|
|
9
11
|
ENV_AI_GUARD_ENDPOINT = "DD_AI_GUARD_ENDPOINT"
|
|
10
12
|
ENV_AI_GUARD_TIMEOUT = "DD_AI_GUARD_TIMEOUT"
|
|
11
13
|
ENV_AI_GUARD_MAX_CONTENT_SIZE = "DD_AI_GUARD_MAX_CONTENT_SIZE"
|
|
@@ -34,6 +34,19 @@ module Datadog
|
|
|
34
34
|
o.default false
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Redact sensitive data from evaluated messages.
|
|
38
|
+
#
|
|
39
|
+
# When enabled, values flagged by AI Guard are replaced before the messages are
|
|
40
|
+
# stored in the span or returned to the caller. Disable to skip redaction entirely.
|
|
41
|
+
#
|
|
42
|
+
# @default `DD_AI_GUARD_REDACTION_ENABLED`, otherwise `true`
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
option :redaction_enabled do |o|
|
|
45
|
+
o.type :bool
|
|
46
|
+
o.env Ext::ENV_AI_GUARD_REDACTION_ENABLED
|
|
47
|
+
o.default true
|
|
48
|
+
end
|
|
49
|
+
|
|
37
50
|
define_method(:instrument) do |integration_name|
|
|
38
51
|
return unless enabled # steep:ignore
|
|
39
52
|
|
|
@@ -4,74 +4,67 @@ module Datadog
|
|
|
4
4
|
module AIGuard
|
|
5
5
|
module Contrib
|
|
6
6
|
module RubyLLM
|
|
7
|
-
#
|
|
7
|
+
# Protects tool calls before RubyLLM executes them locally
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
8
10
|
module ChatInstrumentation
|
|
9
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
20
|
+
def preprocessed_messages(list = messages)
|
|
21
|
+
return super unless @_datadog_ai_guard_evaluate
|
|
30
22
|
|
|
31
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
66
|
-
|
|
47
|
+
def execute_pending_tool_calls(response)
|
|
48
|
+
response_index = messages.index { |message| message == response }
|
|
49
|
+
return super unless response_index
|
|
67
50
|
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
@@ -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
|
|
@@ -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
|