datadog 2.24.0 → 2.25.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -2
  3. data/lib/datadog/ai_guard/api_client.rb +82 -0
  4. data/lib/datadog/ai_guard/component.rb +42 -0
  5. data/lib/datadog/ai_guard/configuration/ext.rb +17 -0
  6. data/lib/datadog/ai_guard/configuration/settings.rb +98 -0
  7. data/lib/datadog/ai_guard/configuration.rb +11 -0
  8. data/lib/datadog/ai_guard/evaluation/message.rb +25 -0
  9. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +34 -0
  10. data/lib/datadog/ai_guard/evaluation/request.rb +81 -0
  11. data/lib/datadog/ai_guard/evaluation/result.rb +43 -0
  12. data/lib/datadog/ai_guard/evaluation/tool_call.rb +18 -0
  13. data/lib/datadog/ai_guard/evaluation.rb +72 -0
  14. data/lib/datadog/ai_guard/ext.rb +16 -0
  15. data/lib/datadog/ai_guard.rb +153 -0
  16. data/lib/datadog/appsec/remote.rb +4 -3
  17. data/lib/datadog/appsec/security_engine/engine.rb +3 -3
  18. data/lib/datadog/appsec/security_engine/runner.rb +2 -2
  19. data/lib/datadog/core/configuration/components.rb +6 -0
  20. data/lib/datadog/core/configuration/supported_configurations.rb +6 -0
  21. data/lib/datadog/core/error.rb +6 -6
  22. data/lib/datadog/core/pin.rb +4 -0
  23. data/lib/datadog/core/rate_limiter.rb +1 -1
  24. data/lib/datadog/core/semaphore.rb +1 -4
  25. data/lib/datadog/core/telemetry/event/app_started.rb +2 -1
  26. data/lib/datadog/core/transport/response.rb +3 -1
  27. data/lib/datadog/core/utils/safe_dup.rb +2 -2
  28. data/lib/datadog/core/utils/sequence.rb +2 -0
  29. data/lib/datadog/di/boot.rb +4 -2
  30. data/lib/datadog/di/contrib/active_record.rb +4 -5
  31. data/lib/datadog/di/instrumenter.rb +9 -3
  32. data/lib/datadog/di/logger.rb +2 -2
  33. data/lib/datadog/di/probe_file_loader/railtie.rb +1 -1
  34. data/lib/datadog/di/probe_notifier_worker.rb +5 -5
  35. data/lib/datadog/error_tracking/filters.rb +2 -2
  36. data/lib/datadog/kit/appsec/events/v2.rb +2 -3
  37. data/lib/datadog/profiling/collectors/code_provenance.rb +1 -1
  38. data/lib/datadog/profiling/collectors/info.rb +3 -3
  39. data/lib/datadog/profiling/ext/dir_monkey_patches.rb +18 -0
  40. data/lib/datadog/tracing/contrib/waterdrop.rb +4 -0
  41. data/lib/datadog/tracing/distributed/baggage.rb +3 -2
  42. data/lib/datadog/tracing/sampling/priority_sampler.rb +3 -1
  43. data/lib/datadog/tracing/span.rb +1 -1
  44. data/lib/datadog/tracing/span_operation.rb +15 -9
  45. data/lib/datadog/version.rb +1 -1
  46. data/lib/datadog.rb +1 -0
  47. metadata +21 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1e3df0d4845537939ee39a49f98f9645cf0b5f3cd8f62cd9b7c8dd7f7cf112f
4
- data.tar.gz: e84c39b5b0d3b464aa5d9c1d050a215374520575832bee48671549bbf9696973
3
+ metadata.gz: b31bd418a350c8999b821462c0699a19af1b02242a7a22f4fee10859cd520d4c
4
+ data.tar.gz: 4a77d7cb714bfb82312d642b583d23f7b9407435c6577b864f1ca98751826ec6
5
5
  SHA512:
6
- metadata.gz: f00d5556de98278dc5f80d1146374b378e6d21e32c041239e0e6a6237f4169d79061f1e94df1f8221255a2a807ed6a3ff6f80668ff3421030737c6ec7bf35f76
7
- data.tar.gz: 94a76fd5432c238ee71c261419cb99e943143e802092ba0ec4aa9391e988f5ed6e9c307295211f0c16951d47e060633b62b40f982c83834aa179b6e66b03af91
6
+ metadata.gz: f51a7d85ab5e057e5a485f7dffe7d329b4aacde56d784239a15dbb8a48508d082ba69dbecb992336d132965b642ccdf39cd4f4e2378d749174cab64496b1fe2f
7
+ data.tar.gz: 83504124b5fad578d31002d5ab4d19450c14c8a857b7b553925bbff2cb2fdb53ee2dbeb2e7c774128c92203e69b3c484896591435ec4df01740e2028b70ff7fe
data/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.25.0] - 2026-01-13
6
+
7
+ ### Added
8
+
9
+ AI Guard: Add SDK for evaluating the safety of user messages and assistant commands for LLM session ([#5144][])
10
+
11
+ ### Changed
12
+
13
+ Core: Bump minimum version of `datadog-ruby_core_source` dependency ([#5215][])
14
+
15
+ ### Fixed
16
+
17
+ AppSec: Fix processing of numeric data for WAF and RASP checks ([#5222][])
18
+
5
19
  ## [2.24.0] - 2026-01-08
6
20
 
7
21
  ### Added
@@ -9,6 +23,11 @@
9
23
  * Core: Add support for installing the gem on Ruby 4.0.x stable ([#5157][])
10
24
  * Tracing: Add origin detection using extra headers and the `DD_EXTERNAL_ENV` variable. ([#5028][])
11
25
  * Dynamic Instrumentation: Add one-click enablement support ([#5150][])
26
+ * SSI: Add support for Bundler deployment mode ([#5053][])
27
+ * SSI: Report UI-oriented injection results ([#5053][])
28
+ * SSI: Guard against Bundler global force_ruby_platform ([#5053][])
29
+ * SSI: Guard against Bundler 4.0 and Bundler 2.7 in 4.0 mode ([#5053][])
30
+ * SSI: Guard against Ruby 3.5+ ([#5053][])
12
31
 
13
32
  ### Changed
14
33
 
@@ -3418,7 +3437,8 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
3418
3437
  Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
3419
3438
 
3420
3439
 
3421
- [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.24.0...master
3440
+ [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.25.0...master
3441
+ [2.25.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.24.0...v2.25.0
3422
3442
  [2.24.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.23.0...v2.24.0
3423
3443
  [2.23.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.22.0...v2.23.0
3424
3444
  [2.22.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.21.0...v2.22.0
@@ -5059,12 +5079,14 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5059
5079
  [#5044]: https://github.com/DataDog/dd-trace-rb/issues/5044
5060
5080
  [#5045]: https://github.com/DataDog/dd-trace-rb/issues/5045
5061
5081
  [#5049]: https://github.com/DataDog/dd-trace-rb/issues/5049
5082
+ [#5053]: https://github.com/DataDog/dd-trace-rb/issues/5053
5062
5083
  [#5054]: https://github.com/DataDog/dd-trace-rb/issues/5054
5063
5084
  [#5058]: https://github.com/DataDog/dd-trace-rb/issues/5058
5064
5085
  [#5073]: https://github.com/DataDog/dd-trace-rb/issues/5073
5065
5086
  [#5086]: https://github.com/DataDog/dd-trace-rb/issues/5086
5066
5087
  [#5091]: https://github.com/DataDog/dd-trace-rb/issues/5091
5067
5088
  [#5122]: https://github.com/DataDog/dd-trace-rb/issues/5122
5089
+ [#5144]: https://github.com/DataDog/dd-trace-rb/issues/5144
5068
5090
  [#5145]: https://github.com/DataDog/dd-trace-rb/issues/5145
5069
5091
  [#5146]: https://github.com/DataDog/dd-trace-rb/issues/5146
5070
5092
  [#5148]: https://github.com/DataDog/dd-trace-rb/issues/5148
@@ -5078,6 +5100,8 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5078
5100
  [#5176]: https://github.com/DataDog/dd-trace-rb/issues/5176
5079
5101
  [#5194]: https://github.com/DataDog/dd-trace-rb/issues/5194
5080
5102
  [#5197]: https://github.com/DataDog/dd-trace-rb/issues/5197
5103
+ [#5215]: https://github.com/DataDog/dd-trace-rb/issues/5215
5104
+ [#5222]: https://github.com/DataDog/dd-trace-rb/issues/5222
5081
5105
  [@AdrianLC]: https://github.com/AdrianLC
5082
5106
  [@Azure7111]: https://github.com/Azure7111
5083
5107
  [@BabyGroot]: https://github.com/BabyGroot
@@ -5232,4 +5256,4 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5232
5256
  [@y-yagi]: https://github.com/y-yagi
5233
5257
  [@yujideveloper]: https://github.com/yujideveloper
5234
5258
  [@yukimurasawa]: https://github.com/yukimurasawa
5235
- [@zachmccormick]: https://github.com/zachmccormick
5259
+ [@zachmccormick]: https://github.com/zachmccormick
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "net/http"
5
+ require "json"
6
+
7
+ module Datadog
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
12
+ DEFAULT_SITE = "app.datadoghq.com"
13
+ DEFAULT_PATH = "/api/v2/ai-guard"
14
+
15
+ def initialize(endpoint:, api_key:, application_key:, timeout:)
16
+ @timeout = timeout
17
+
18
+ @endpoint_uri = if endpoint
19
+ URI(endpoint) #: URI::HTTP
20
+ else
21
+ URI::HTTPS.build(
22
+ host: Datadog.configuration.site || DEFAULT_SITE,
23
+ path: DEFAULT_PATH
24
+ )
25
+ end
26
+
27
+ @headers = {
28
+ "DD-API-KEY": api_key.to_s,
29
+ "DD-APPLICATION-KEY": application_key.to_s,
30
+ "DD-AI-GUARD-VERSION": Datadog::VERSION::STRING,
31
+ "DD-AI-GUARD-SOURCE": "SDK",
32
+ "DD-AI-GUARD-LANGUAGE": "ruby",
33
+ "content-type": "application/json"
34
+ }.freeze
35
+ end
36
+
37
+ def post(path, body:)
38
+ Net::HTTP.start(@endpoint_uri.host.to_s, @endpoint_uri.port, use_ssl: use_ssl?, read_timeout: @timeout) do |http|
39
+ request = Net::HTTP::Post.new(@endpoint_uri.request_uri + path, @headers)
40
+ request.body = body.to_json
41
+
42
+ response = http.request(request)
43
+ raise_on_http_error!(response)
44
+
45
+ parse_response_body(response.body)
46
+ end
47
+ rescue Net::ReadTimeout
48
+ raise AIGuardClientError, "Request to AI Guard timed out"
49
+ end
50
+
51
+ private
52
+
53
+ def raise_on_http_error!(response)
54
+ case response
55
+ when Net::HTTPSuccess
56
+ # do nothing
57
+ when Net::HTTPRedirection
58
+ raise AIGuardClientError, "Redirects for AI Guard API are not supported"
59
+ else
60
+ error_message = begin
61
+ parsed_body = JSON.parse(response.body)
62
+ Array(parsed_body.fetch('errors')).join(', ')
63
+ rescue JSON::ParserError, KeyError
64
+ response.body
65
+ end
66
+
67
+ raise AIGuardClientError, error_message
68
+ end
69
+ end
70
+
71
+ def parse_response_body(body)
72
+ JSON.parse(body)
73
+ rescue JSON::ParserError
74
+ raise AIGuardClientError, "Could not parse response body"
75
+ end
76
+
77
+ def use_ssl?
78
+ @endpoint_uri.scheme == 'https'
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'api_client'
4
+ require_relative 'evaluation'
5
+ require_relative 'evaluation/request'
6
+ require_relative 'evaluation/result'
7
+ require_relative 'evaluation/no_op_result'
8
+ require_relative 'evaluation/message'
9
+ require_relative 'evaluation/tool_call'
10
+ require_relative 'ext'
11
+
12
+ module Datadog
13
+ module AIGuard
14
+ # Component for API Guard product
15
+ class Component
16
+ attr_reader :api_client, :logger
17
+
18
+ def self.build(settings, logger:, telemetry:)
19
+ return unless settings.respond_to?(:ai_guard) && settings.ai_guard.enabled
20
+
21
+ api_client = APIClient.new(
22
+ endpoint: settings.ai_guard.endpoint,
23
+ api_key: settings.api_key,
24
+ application_key: settings.ai_guard.app_key,
25
+ timeout: settings.ai_guard.timeout_ms / 1_000
26
+ )
27
+
28
+ new(api_client, logger: logger, telemetry: telemetry)
29
+ end
30
+
31
+ def initialize(api_client, logger:, telemetry:)
32
+ @api_client = api_client
33
+ @logger = logger
34
+ @telemetry = telemetry
35
+ end
36
+
37
+ def shutdown!
38
+ # no-op
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ module Configuration
6
+ # This module contains constants for AI Guard component
7
+ module Ext
8
+ ENV_AI_GUARD_ENABLED = "DD_AI_GUARD_ENABLED"
9
+ ENV_AI_GUARD_ENDPOINT = "DD_AI_GUARD_ENDPOINT"
10
+ ENV_AI_GUARD_TIMEOUT = "DD_AI_GUARD_TIMEOUT"
11
+ ENV_AI_GUARD_MAX_CONTENT_SIZE = "DD_AI_GUARD_MAX_CONTENT_SIZE"
12
+ ENV_AI_GUARD_MAX_MESSAGES_LENGTH = "DD_AI_GUARD_MAX_MESSAGES_LENGTH"
13
+ ENV_APP_KEY = "DD_APP_KEY"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require_relative "ext"
5
+
6
+ module Datadog
7
+ module AIGuard
8
+ module Configuration
9
+ # AI Guard specific settings
10
+ module Settings
11
+ def self.extended(base)
12
+ base = base.singleton_class unless base.is_a?(Class)
13
+ add_settings!(base)
14
+ end
15
+
16
+ def self.add_settings!(base)
17
+ base.class_eval do
18
+ # AI Guard specific configurations.
19
+ # @public_api
20
+ settings :ai_guard do
21
+ # Enable AI Guard.
22
+ #
23
+ # You can use this option to skip calls to AI Guard API without having to remove library as a whole.
24
+ #
25
+ # @default `DD_AI_GUARD_ENABLED`, otherwise `false`
26
+ # @return [Boolean]
27
+ option :enabled do |o|
28
+ o.type :bool
29
+ o.env Ext::ENV_AI_GUARD_ENABLED
30
+ o.default false
31
+ end
32
+
33
+ # AI Guard API endpoint path.
34
+ #
35
+ # @default `DD_AI_GUARD_ENDPOINT`, otherwise `nil`
36
+ # @return [String, nil]
37
+ option :endpoint do |o|
38
+ o.type :string, nilable: true
39
+ o.env Ext::ENV_AI_GUARD_ENDPOINT
40
+
41
+ o.setter do |value|
42
+ next unless value
43
+
44
+ uri = URI(value.to_s)
45
+ raise ArgumentError, "Please provide an absolute URI that includes a protocol" unless uri.absolute?
46
+
47
+ uri.to_s.delete_suffix("/")
48
+ end
49
+ end
50
+
51
+ # Datadog Application key.
52
+ #
53
+ # @default `DD_APP_KEY` environment variable, otherwise `nil`
54
+ # @return [String, nil]
55
+ option :app_key do |o|
56
+ o.type :string, nilable: true
57
+ o.env Ext::ENV_APP_KEY
58
+ end
59
+
60
+ # Request timeout in milliseconds.
61
+ #
62
+ # @default `DD_AI_GUARD_TIMEOUT`, otherwise 10 000 ms
63
+ # @return [Integer]
64
+ option :timeout_ms do |o|
65
+ o.type :int
66
+ o.env Ext::ENV_AI_GUARD_TIMEOUT
67
+ o.default 10_000
68
+ end
69
+
70
+ # Maximum content size in bytes.
71
+ # Content that exceeds the maximum allowed size is truncated before
72
+ # being stored in the current span context.
73
+ #
74
+ # @default `DD_AI_GUARD_MAX_CONTENT_SIZE`, otherwise 524 228 bytes
75
+ # @return [Integer]
76
+ option :max_content_size_bytes do |o|
77
+ o.type :int
78
+ o.env Ext::ENV_AI_GUARD_MAX_CONTENT_SIZE
79
+ o.default 512 * 1024
80
+ end
81
+
82
+ # Maximum number of messages.
83
+ # Older messages are omitted once the message limit is reached.
84
+ #
85
+ # @default `DD_AI_GUARD_MAX_MESSAGES_LENGTH`, otherwise 16 messages
86
+ # @return [Integer]
87
+ option :max_messages_length do |o|
88
+ o.type :int
89
+ o.env Ext::ENV_AI_GUARD_MAX_MESSAGES_LENGTH
90
+ o.default 16
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ # Configuration module for AI Guard
6
+ module Configuration
7
+ end
8
+ end
9
+ end
10
+
11
+ require_relative "configuration/settings"
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ module Evaluation
6
+ # Message class for AI Guard
7
+ class Message
8
+ attr_reader :role, :content, :tool_call, :tool_call_id
9
+
10
+ def initialize(role:, content: nil, tool_call: nil, tool_call_id: nil)
11
+ raise ArgumentError, "Role must be set to a non-empty value" if role.to_s.empty?
12
+
13
+ @role = role.to_sym
14
+ @content = content
15
+ @tool_call = tool_call
16
+ @tool_call_id = tool_call_id
17
+
18
+ if @tool_call && !@tool_call.is_a?(ToolCall)
19
+ raise ArgumentError, "Expected an instance of #{ToolCall.name} for :tool_call argument"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ module Evaluation
6
+ # Class for emulating AI Guard evaluation result when AI Guard is disabled.
7
+ class NoOpResult
8
+ attr_reader :action, :reason, :tags
9
+
10
+ def initialize
11
+ @action = Result::ALLOW_ACTION
12
+ @reason = "AI Guard is disabled"
13
+ @tags = []
14
+ end
15
+
16
+ def allow?
17
+ true
18
+ end
19
+
20
+ def deny?
21
+ false
22
+ end
23
+
24
+ def abort?
25
+ false
26
+ end
27
+
28
+ def blocking_enabled?
29
+ false
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ module Evaluation
6
+ # Request builds the request body from an array of messages and processes the response
7
+ class Request
8
+ REQUEST_PATH = "/evaluate"
9
+
10
+ attr_reader :serialized_messages
11
+
12
+ def initialize(messages)
13
+ @serialized_messages = serialize_messages(messages)
14
+ end
15
+
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
33
+ {
34
+ data: {
35
+ attributes: {
36
+ messages: @serialized_messages,
37
+ meta: {
38
+ service: Datadog.configuration.service,
39
+ env: Datadog.configuration.env
40
+ }
41
+ }
42
+ }
43
+ }
44
+ end
45
+
46
+ def serialize_messages(messages)
47
+ serialized_messages = []
48
+
49
+ messages.each do |message|
50
+ serialized_messages << serialize_message(message)
51
+
52
+ break if serialized_messages.count == Datadog.configuration.ai_guard.max_messages_length
53
+ end
54
+
55
+ serialized_messages
56
+ end
57
+
58
+ def serialize_message(message)
59
+ if message.tool_call
60
+ {
61
+ role: message.role,
62
+ tool_calls: [
63
+ {
64
+ id: message.tool_call.id,
65
+ function: {
66
+ name: message.tool_call.tool_name,
67
+ arguments: message.tool_call.arguments
68
+ }
69
+ }
70
+ ]
71
+ }
72
+ elsif message.tool_call_id
73
+ {role: message.role, tool_call_id: message.tool_call_id, content: message.content}
74
+ else
75
+ {role: message.role, content: message.content}
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ module Evaluation
6
+ # Wrapper class for evaluation API response
7
+ class Result
8
+ ALLOW_ACTION = "ALLOW"
9
+ DENY_ACTION = "DENY"
10
+ ABORT_ACTION = "ABORT"
11
+
12
+ attr_reader :action, :reason, :tags
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
+ @is_blocking_enabled = attributes.fetch("is_blocking_enabled")
21
+ rescue KeyError => e
22
+ raise AIGuardClientError, "Missing key: \"#{e.key}\""
23
+ end
24
+
25
+ def allow?
26
+ action == ALLOW_ACTION
27
+ end
28
+
29
+ def deny?
30
+ action == DENY_ACTION
31
+ end
32
+
33
+ def abort?
34
+ action == ABORT_ACTION
35
+ end
36
+
37
+ def blocking_enabled?
38
+ !!@is_blocking_enabled
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ module Evaluation
6
+ # Tool call class for AI Guard
7
+ class ToolCall
8
+ attr_reader :tool_name, :id, :arguments
9
+
10
+ def initialize(tool_name, id:, arguments:)
11
+ @tool_name = tool_name
12
+ @id = id
13
+ @arguments = arguments
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ # module that contains a function for performing AI Guard Evaluation request
6
+ # and creating `ai_guard` span with required tags
7
+ module Evaluation
8
+ class << self
9
+ def perform(messages, allow_raise: false)
10
+ raise ArgumentError, "Messages must not be empty" if messages&.empty?
11
+
12
+ Tracing.trace(Ext::SPAN_NAME) do |span, trace|
13
+ if (last_message = messages.last)
14
+ if last_message.tool_call
15
+ span.set_tag(Ext::TARGET_TAG, "tool")
16
+ span.set_tag(Ext::TOOL_NAME_TAG, last_message.tool_call.tool_name)
17
+ elsif last_message.tool_call_id
18
+ span.set_tag(Ext::TARGET_TAG, "tool")
19
+
20
+ if (tool_call_message = messages.find { |m| m.tool_call&.id == last_message.tool_call_id })
21
+ span.set_tag(Ext::TOOL_NAME_TAG, tool_call_message.tool_call.tool_name) # steep:ignore
22
+ end
23
+ else
24
+ span.set_tag(Ext::TARGET_TAG, "prompt")
25
+ end
26
+ end
27
+
28
+ request = Request.new(messages)
29
+ result = request.perform
30
+
31
+ span.set_tag(Ext::ACTION_TAG, result.action)
32
+ span.set_tag(Ext::REASON_TAG, result.reason)
33
+
34
+ span.set_metastruct_tag(
35
+ Ext::METASTRUCT_TAG,
36
+ {
37
+ messages: truncate_content(request.serialized_messages),
38
+ attack_categories: result.tags
39
+ }
40
+ )
41
+
42
+ if allow_raise && (result.deny? || result.abort?) && result.blocking_enabled?
43
+ span.set_tag(Ext::BLOCKED_TAG, true)
44
+ raise AIGuardAbortError.new(action: result.action, reason: result.reason, tags: result.tags)
45
+ end
46
+
47
+ result
48
+ end
49
+ end
50
+
51
+ def perform_no_op
52
+ AIGuard.logger&.warn("AI Guard is disabled, messages were not evaluated")
53
+
54
+ NoOpResult.new
55
+ end
56
+
57
+ private
58
+
59
+ def truncate_content(serialized_messages)
60
+ serialized_messages.map do |message| # steep:ignore
61
+ next message unless message[:content]
62
+
63
+ {
64
+ **message,
65
+ content: message[:content].byteslice(0, Datadog.configuration.ai_guard.max_content_size_bytes)
66
+ }
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module AIGuard
5
+ # AI Guard specific constants
6
+ module Ext
7
+ SPAN_NAME = "ai_guard"
8
+ TARGET_TAG = "ai_guard.target"
9
+ TOOL_NAME_TAG = "ai_guard.tool_name"
10
+ ACTION_TAG = "ai_guard.action"
11
+ REASON_TAG = "ai_guard.reason"
12
+ BLOCKED_TAG = "ai_guard.blocked"
13
+ METASTRUCT_TAG = "ai_guard"
14
+ end
15
+ end
16
+ end