datadog 2.43.0 → 2.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -1
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +136 -132
- data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -5
- data/ext/datadog_profiling_native_extension/collectors_gc_profiling_helper.c +0 -2
- data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +19 -20
- data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.h +5 -1
- data/ext/datadog_profiling_native_extension/collectors_stack.c +7 -7
- data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +146 -109
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
- data/ext/datadog_profiling_native_extension/extconf.rb +6 -2
- data/ext/datadog_profiling_native_extension/heap_recorder.c +2 -2
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +122 -53
- data/ext/datadog_profiling_native_extension/private_vm_api_access.h +6 -2
- data/ext/datadog_profiling_native_extension/profiling.c +22 -23
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +3 -1
- data/ext/datadog_profiling_native_extension/ruby_helpers.h +4 -0
- data/ext/datadog_profiling_native_extension/setup_signal_handler.c +0 -2
- data/ext/datadog_profiling_native_extension/stack_recorder.c +10 -28
- data/ext/datadog_profiling_native_extension/stack_recorder.h +0 -1
- data/ext/datadog_profiling_native_extension/time_helpers.c +2 -4
- data/ext/datadog_profiling_native_extension/time_helpers.h +13 -14
- 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_api/trace_exporter.c +77 -11
- 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 +7 -2
- data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
- data/lib/datadog/profiling/component.rb +2 -3
- data/lib/datadog/profiling/exporter.rb +7 -5
- data/lib/datadog/symbol_database/extractor.rb +84 -102
- data/lib/datadog/tracing/component.rb +3 -1
- data/lib/datadog/tracing/configuration/ext.rb +1 -0
- data/lib/datadog/tracing/configuration/settings.rb +9 -2
- 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/utils/quantization/http.rb +6 -3
- data/lib/datadog/tracing/diagnostics/environment_logger.rb +1 -0
- 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 +23 -22
- 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
|
@@ -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
|
|
@@ -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
|