diff --git a/.generator/schemas/v2/openapi.yaml b/.generator/schemas/v2/openapi.yaml index ce0ea5b844..d582c8e138 100644 --- a/.generator/schemas/v2/openapi.yaml +++ b/.generator/schemas/v2/openapi.yaml @@ -2066,6 +2066,256 @@ components: $ref: "#/components/schemas/JSONAPIErrorResponse" description: The server cannot process the request because it contains invalid data. schemas: + AIGuardAction: + description: The action recommendation from the AI Guard evaluation. + enum: + - ALLOW + - DENY + - ABORT + example: ALLOW + type: string + x-enum-varnames: + - ALLOW + - DENY + - ABORT + AIGuardContentPart: + description: A single part of a multipart message content. + properties: + image_url: + $ref: "#/components/schemas/AIGuardImageURL" + text: + description: The text content of this part, required when type is text. + example: "How do I delete all files?" + type: string + type: + description: The type of content part, either text or image_url. + example: text + type: string + required: + - type + type: object + AIGuardContentPartList: + description: A list of content parts forming a multipart message. + items: + $ref: "#/components/schemas/AIGuardContentPart" + type: array + AIGuardEvaluateRequest: + description: The evaluation request payload containing conversation messages and optional metadata. + example: + messages: + - content: How do I delete all files on the system? + role: user + meta: + env: production + service: my-llm-service + properties: + messages: + description: The list of conversation messages to evaluate. Must contain at least one message. + example: + - content: How do I delete all files on the system? + role: user + items: + $ref: "#/components/schemas/AIGuardMessage" + type: array + meta: + $ref: "#/components/schemas/AIGuardMeta" + required: + - messages + type: object + AIGuardEvaluateResponse: + description: The result of the AI Guard evaluation. + properties: + action: + $ref: "#/components/schemas/AIGuardAction" + global_prob: + description: The overall threat probability score across all evaluated tags. + example: 0.02 + format: double + type: number + is_blocking_enabled: + description: Whether blocking mode is enabled for this organization. + example: false + type: boolean + reason: + description: A human-readable explanation of the action recommendation. + example: No threats detected. + type: string + sds_findings: + description: Sensitive data findings detected in the evaluated conversation. + items: + $ref: "#/components/schemas/AIGuardSdsFinding" + type: array + tag_probs: + additionalProperties: + format: double + type: number + description: Probability scores for each evaluated threat tag. + example: + indirect-prompt-injection: 0.01 + jailbreak: 0.02 + type: object + tags: + description: Security threat tags detected in the evaluated conversation. + example: [] + items: + type: string + type: array + required: + - action + - reason + - tags + - tag_probs + - is_blocking_enabled + type: object + AIGuardImageURL: + description: An image URL reference for multimodal content. + properties: + url: + description: The URL pointing to the image. + example: "https://example.com/image.png" + type: string + required: + - url + type: object + AIGuardMessage: + description: A single message in the conversation to evaluate. + properties: + content: + $ref: "#/components/schemas/AIGuardMessageContent" + role: + $ref: "#/components/schemas/AIGuardMessageRole" + tool_call_id: + description: The ID of the tool call this message is responding to, required for tool messages. + example: call_abc123 + type: string + tool_calls: + description: Tool calls issued by the assistant in this message. + items: + $ref: "#/components/schemas/AIGuardToolCall" + type: array + required: + - role + type: object + AIGuardMessageContent: + description: The message content, either a plain string or an array of content parts. + oneOf: + - example: "How do I delete all files on the system?" + type: string + - $ref: "#/components/schemas/AIGuardContentPartList" + AIGuardMessageRole: + description: The role of the message author in the conversation. + enum: + - user + - assistant + - system + - tool + - developer + example: user + type: string + x-enum-varnames: + - USER + - ASSISTANT + - SYSTEM + - TOOL + - DEVELOPER + AIGuardMeta: + description: Optional metadata providing context about the originating service and request. + properties: + coding_agent: + description: Identifier of the coding agent sending the request, if applicable. + example: claude-code + type: string + confidence_threshold: + description: Override for the default threat detection confidence threshold, between 0.0 and 1.0. + example: 0.7 + format: double + type: number + env: + description: The deployment environment of the originating service. + example: production + type: string + is_sds_enabled_override: + description: Override whether sensitive data scanning is applied to this request. + example: false + type: boolean + service: + description: The name of the service sending the evaluation request. + example: my-llm-service + type: string + type: object + AIGuardSdsFinding: + description: A sensitive data finding detected by the SDS scanner. + properties: + category: + description: The category of sensitive data detected. + example: payment_card_number + type: string + location: + $ref: "#/components/schemas/AIGuardSdsFindingLocation" + rule_display_name: + description: The human-readable name of the SDS rule that triggered. + example: Credit Card Number + type: string + rule_tag: + description: The tag identifier of the SDS rule that triggered. + example: credit_card + type: string + required: + - rule_display_name + - rule_tag + - category + - location + type: object + AIGuardSdsFindingLocation: + description: The location of a sensitive data match within the evaluated request. + properties: + end_index_exclusive: + description: The end character index (exclusive) of the sensitive data match. + example: 42 + format: int64 + type: integer + path: + description: The JSON path to the field containing the sensitive data. + example: "messages[0].content" + type: string + start_index: + description: The start character index of the sensitive data match. + example: 0 + format: int64 + type: integer + required: + - path + - start_index + - end_index_exclusive + type: object + AIGuardToolCall: + description: A tool call issued by the assistant. + properties: + function: + $ref: "#/components/schemas/AIGuardToolCallFunction" + id: + description: The unique identifier of the tool call. + example: call_abc123 + type: string + required: + - id + - function + type: object + AIGuardToolCallFunction: + description: The function definition within a tool call. + properties: + arguments: + description: The JSON-encoded arguments passed to the function. + example: '{"location": "San Francisco"}' + type: string + name: + description: The name of the function being called. + example: get_weather + type: string + required: + - name + - arguments + type: object APIErrorResponse: description: API error response. properties: @@ -108469,6 +108719,88 @@ paths: operator: OR permissions: - security_monitoring_findings_read + /api/v2/ai-guard/evaluate: + post: + description: |- + Analyzes a conversation for security threats such as prompt injection, jailbreak + attempts, and other AI-specific attacks. Returns an action recommendation (ALLOW, + DENY, or ABORT) along with the detected threat tags. + operationId: EvaluateAIGuardRequest + requestBody: + content: + application/json: + examples: + default: + value: + messages: + - content: How do I delete all files on the system? + role: user + meta: + env: production + service: my-llm-service + schema: + $ref: "#/components/schemas/AIGuardEvaluateRequest" + required: true + responses: + "200": + content: + application/json: + examples: + default: + value: + action: ALLOW + global_prob: 0.02 + is_blocking_enabled: false + reason: No threats detected. + sds_findings: [] + tag_probs: + authority-override: 0.01 + data-exfiltration: 0.01 + denial-of-service-tool-call: 0.01 + destructive-tool-call: 0.01 + indirect-prompt-injection: 0.01 + instruction-override: 0.01 + jailbreak: 0.02 + obfuscation: 0.01 + role-play: 0.01 + security-exploit: 0.01 + system-prompt-extraction: 0.01 + tags: [] + schema: + $ref: "#/components/schemas/AIGuardEvaluateResponse" + description: Evaluation result with action recommendation + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/JSONAPIErrorResponse" + description: Bad Request + "401": + content: + application/json: + schema: + $ref: "#/components/schemas/JSONAPIErrorResponse" + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: "#/components/schemas/JSONAPIErrorResponse" + description: Forbidden + "429": + $ref: "#/components/responses/TooManyRequestsResponse" + security: + - apiKeyAuth: [] + appKeyAuth: + - ai_guard_evaluate + summary: Evaluate an AI Guard request + tags: + - AI Guard + x-codegen-request-body-name: body + x-permission: + operator: AND + permissions: + - ai_guard_evaluate /api/v2/annotation: get: description: Returns a flat list of annotations matching the given page, time window, and optional widget filter. @@ -186414,6 +186746,12 @@ servers: default: api description: The subdomain where the API is deployed. tags: + - description: |- + Analyze AI conversations for security threats including prompt injection, + jailbreak attempts, and other AI-specific attacks. + externalDocs: + url: https://docs.datadoghq.com/security/ai_security/ + name: AI Guard - description: |- Configure your API endpoints through the Datadog API. name: API Management diff --git a/examples/v2_ai-guard_EvaluateAIGuardRequest.rs b/examples/v2_ai-guard_EvaluateAIGuardRequest.rs new file mode 100644 index 0000000000..916478191f --- /dev/null +++ b/examples/v2_ai-guard_EvaluateAIGuardRequest.rs @@ -0,0 +1,30 @@ +// Evaluate an AI Guard request returns "Evaluation result with action +// recommendation" response +use datadog_api_client::datadog; +use datadog_api_client::datadogV2::api_ai_guard::AIGuardAPI; +use datadog_api_client::datadogV2::model::AIGuardEvaluateRequest; +use datadog_api_client::datadogV2::model::AIGuardMessage; +use datadog_api_client::datadogV2::model::AIGuardMessageContent; +use datadog_api_client::datadogV2::model::AIGuardMessageRole; +use datadog_api_client::datadogV2::model::AIGuardMeta; + +#[tokio::main] +async fn main() { + let body = AIGuardEvaluateRequest::new(vec![AIGuardMessage::new(AIGuardMessageRole::USER) + .content(AIGuardMessageContent::String( + "How do I delete all files on the system?".to_string(), + ))]) + .meta( + AIGuardMeta::new() + .env("production".to_string()) + .service("my-llm-service".to_string()), + ); + let configuration = datadog::Configuration::new(); + let api = AIGuardAPI::with_config(configuration); + let resp = api.evaluate_ai_guard_request(body).await; + if let Ok(value) = resp { + println!("{:#?}", value); + } else { + println!("{:#?}", resp.unwrap_err()); + } +} diff --git a/src/datadogV2/api/api_ai_guard.rs b/src/datadogV2/api/api_ai_guard.rs new file mode 100644 index 0000000000..b6da30fab5 --- /dev/null +++ b/src/datadogV2/api/api_ai_guard.rs @@ -0,0 +1,255 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. +use crate::datadog; +use flate2::{ + write::{GzEncoder, ZlibEncoder}, + Compression, +}; +use reqwest::header::{HeaderMap, HeaderValue}; +use serde::{Deserialize, Serialize}; +use std::io::Write; + +/// EvaluateAIGuardRequestError is a struct for typed errors of method [`AIGuardAPI::evaluate_ai_guard_request`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum EvaluateAIGuardRequestError { + JSONAPIErrorResponse(crate::datadogV2::model::JSONAPIErrorResponse), + APIErrorResponse(crate::datadogV2::model::APIErrorResponse), + UnknownValue(serde_json::Value), +} + +/// Analyze AI conversations for security threats including prompt injection, +/// jailbreak attempts, and other AI-specific attacks. +#[derive(Debug, Clone)] +pub struct AIGuardAPI { + config: datadog::Configuration, + client: reqwest_middleware::ClientWithMiddleware, +} + +impl Default for AIGuardAPI { + fn default() -> Self { + Self::with_config(datadog::Configuration::default()) + } +} + +impl AIGuardAPI { + pub fn new() -> Self { + Self::default() + } + pub fn with_config(config: datadog::Configuration) -> Self { + let reqwest_client_builder = { + let builder = reqwest::Client::builder(); + #[cfg(not(target_arch = "wasm32"))] + let builder = if let Some(proxy_url) = &config.proxy_url { + builder.proxy(reqwest::Proxy::all(proxy_url).expect("Failed to parse proxy URL")) + } else { + builder + }; + builder + }; + + let middleware_client_builder = { + let builder = + reqwest_middleware::ClientBuilder::new(reqwest_client_builder.build().unwrap()); + #[cfg(feature = "retry")] + let builder = if config.enable_retry { + struct RetryableStatus; + impl reqwest_retry::RetryableStrategy for RetryableStatus { + fn handle( + &self, + res: &Result, + ) -> Option { + match res { + Ok(success) => reqwest_retry::default_on_request_success(success), + Err(_) => None, + } + } + } + let backoff_policy = reqwest_retry::policies::ExponentialBackoff::builder() + .build_with_max_retries(config.max_retries); + + let retry_middleware = + reqwest_retry::RetryTransientMiddleware::new_with_policy_and_strategy( + backoff_policy, + RetryableStatus, + ); + + builder.with(retry_middleware) + } else { + builder + }; + builder + }; + + let client = middleware_client_builder.build(); + + Self { config, client } + } + + pub fn with_client_and_config( + config: datadog::Configuration, + client: reqwest_middleware::ClientWithMiddleware, + ) -> Self { + Self { config, client } + } + + /// Analyzes a conversation for security threats such as prompt injection, jailbreak + /// attempts, and other AI-specific attacks. Returns an action recommendation (ALLOW, + /// DENY, or ABORT) along with the detected threat tags. + pub async fn evaluate_ai_guard_request( + &self, + body: crate::datadogV2::model::AIGuardEvaluateRequest, + ) -> Result< + crate::datadogV2::model::AIGuardEvaluateResponse, + datadog::Error, + > { + match self.evaluate_ai_guard_request_with_http_info(body).await { + Ok(response_content) => { + if let Some(e) = response_content.entity { + Ok(e) + } else { + Err(datadog::Error::Serde(serde::de::Error::custom( + "response content was None", + ))) + } + } + Err(err) => Err(err), + } + } + + /// Analyzes a conversation for security threats such as prompt injection, jailbreak + /// attempts, and other AI-specific attacks. Returns an action recommendation (ALLOW, + /// DENY, or ABORT) along with the detected threat tags. + pub async fn evaluate_ai_guard_request_with_http_info( + &self, + body: crate::datadogV2::model::AIGuardEvaluateRequest, + ) -> Result< + datadog::ResponseContent, + datadog::Error, + > { + let local_configuration = &self.config; + let operation_id = "v2.evaluate_ai_guard_request"; + + let local_client = &self.client; + + let local_uri_str = format!( + "{}/api/v2/ai-guard/evaluate", + local_configuration.get_operation_host(operation_id) + ); + let mut local_req_builder = + local_client.request(reqwest::Method::POST, local_uri_str.as_str()); + + // build headers + let mut headers = HeaderMap::new(); + headers.insert("Content-Type", HeaderValue::from_static("application/json")); + headers.insert("Accept", HeaderValue::from_static("application/json")); + + // build user agent + match HeaderValue::from_str(local_configuration.user_agent.as_str()) { + Ok(user_agent) => headers.insert(reqwest::header::USER_AGENT, user_agent), + Err(e) => { + log::warn!("Failed to parse user agent header: {e}, falling back to default"); + headers.insert( + reqwest::header::USER_AGENT, + HeaderValue::from_static(datadog::DEFAULT_USER_AGENT.as_str()), + ) + } + }; + + // build auth + if let Some(local_key) = local_configuration.auth_keys.get("apiKeyAuth") { + headers.insert( + "DD-API-KEY", + HeaderValue::from_str(local_key.key.as_str()) + .expect("failed to parse DD-API-KEY header"), + ); + }; + if let Some(local_key) = local_configuration.auth_keys.get("appKeyAuth") { + headers.insert( + "DD-APPLICATION-KEY", + HeaderValue::from_str(local_key.key.as_str()) + .expect("failed to parse DD-APPLICATION-KEY header"), + ); + }; + + // build body parameters + let output = Vec::new(); + let mut ser = serde_json::Serializer::with_formatter(output, datadog::DDFormatter); + if body.serialize(&mut ser).is_ok() { + if let Some(content_encoding) = headers.get("Content-Encoding") { + match content_encoding.to_str().unwrap_or_default() { + "gzip" => { + let mut enc = GzEncoder::new(Vec::new(), Compression::default()); + let _ = enc.write_all(ser.into_inner().as_slice()); + match enc.finish() { + Ok(buf) => { + local_req_builder = local_req_builder.body(buf); + } + Err(e) => return Err(datadog::Error::Io(e)), + } + } + "deflate" => { + let mut enc = ZlibEncoder::new(Vec::new(), Compression::default()); + let _ = enc.write_all(ser.into_inner().as_slice()); + match enc.finish() { + Ok(buf) => { + local_req_builder = local_req_builder.body(buf); + } + Err(e) => return Err(datadog::Error::Io(e)), + } + } + #[cfg(feature = "zstd")] + "zstd1" => { + let mut enc = zstd::stream::Encoder::new(Vec::new(), 0).unwrap(); + let _ = enc.write_all(ser.into_inner().as_slice()); + match enc.finish() { + Ok(buf) => { + local_req_builder = local_req_builder.body(buf); + } + Err(e) => return Err(datadog::Error::Io(e)), + } + } + _ => { + local_req_builder = local_req_builder.body(ser.into_inner()); + } + } + } else { + local_req_builder = local_req_builder.body(ser.into_inner()); + } + } + + local_req_builder = local_req_builder.headers(headers); + let local_req = local_req_builder.build()?; + log::debug!("request content: {:?}", local_req.body()); + let local_resp = local_client.execute(local_req).await?; + + let local_status = local_resp.status(); + let local_content = local_resp.text().await?; + log::debug!("response content: {}", local_content); + + if !local_status.is_client_error() && !local_status.is_server_error() { + match serde_json::from_str::( + &local_content, + ) { + Ok(e) => { + return Ok(datadog::ResponseContent { + status: local_status, + content: local_content, + entity: Some(e), + }) + } + Err(e) => return Err(datadog::Error::Serde(e)), + }; + } else { + let local_entity: Option = + serde_json::from_str(&local_content).ok(); + let local_error = datadog::ResponseContent { + status: local_status, + content: local_content, + entity: local_entity, + }; + Err(datadog::Error::ResponseError(local_error)) + } + } +} diff --git a/src/datadogV2/api/mod.rs b/src/datadogV2/api/mod.rs index 42b1ca1f29..2def51fe78 100644 --- a/src/datadogV2/api/mod.rs +++ b/src/datadogV2/api/mod.rs @@ -5,6 +5,7 @@ pub mod api_action_connection; pub mod api_actions_datastores; pub mod api_agentless_scanning; +pub mod api_ai_guard; pub mod api_annotations; pub mod api_api_management; pub mod api_apm; diff --git a/src/datadogV2/mod.rs b/src/datadogV2/mod.rs index 11d24b3113..5ad592c142 100644 --- a/src/datadogV2/mod.rs +++ b/src/datadogV2/mod.rs @@ -6,6 +6,7 @@ pub mod api; pub use self::api::api_action_connection; pub use self::api::api_actions_datastores; pub use self::api::api_agentless_scanning; +pub use self::api::api_ai_guard; pub use self::api::api_annotations; pub use self::api::api_api_management; pub use self::api::api_apm; diff --git a/src/datadogV2/model/mod.rs b/src/datadogV2/model/mod.rs index dd3a7a3c19..464b4d38ae 100644 --- a/src/datadogV2/model/mod.rs +++ b/src/datadogV2/model/mod.rs @@ -778,6 +778,32 @@ pub mod model_aws_on_demand_create_attributes; pub use self::model_aws_on_demand_create_attributes::AwsOnDemandCreateAttributes; pub mod model_aws_on_demand_response; pub use self::model_aws_on_demand_response::AwsOnDemandResponse; +pub mod model_ai_guard_evaluate_request; +pub use self::model_ai_guard_evaluate_request::AIGuardEvaluateRequest; +pub mod model_ai_guard_message; +pub use self::model_ai_guard_message::AIGuardMessage; +pub mod model_ai_guard_content_part; +pub use self::model_ai_guard_content_part::AIGuardContentPart; +pub mod model_ai_guard_image_url; +pub use self::model_ai_guard_image_url::AIGuardImageURL; +pub mod model_ai_guard_message_content; +pub use self::model_ai_guard_message_content::AIGuardMessageContent; +pub mod model_ai_guard_message_role; +pub use self::model_ai_guard_message_role::AIGuardMessageRole; +pub mod model_ai_guard_tool_call; +pub use self::model_ai_guard_tool_call::AIGuardToolCall; +pub mod model_ai_guard_tool_call_function; +pub use self::model_ai_guard_tool_call_function::AIGuardToolCallFunction; +pub mod model_ai_guard_meta; +pub use self::model_ai_guard_meta::AIGuardMeta; +pub mod model_ai_guard_evaluate_response; +pub use self::model_ai_guard_evaluate_response::AIGuardEvaluateResponse; +pub mod model_ai_guard_action; +pub use self::model_ai_guard_action::AIGuardAction; +pub mod model_ai_guard_sds_finding; +pub use self::model_ai_guard_sds_finding::AIGuardSdsFinding; +pub mod model_ai_guard_sds_finding_location; +pub use self::model_ai_guard_sds_finding_location::AIGuardSdsFindingLocation; pub mod model_annotations_response; pub use self::model_annotations_response::AnnotationsResponse; pub mod model_annotation_data; diff --git a/src/datadogV2/model/model_ai_guard_action.rs b/src/datadogV2/model/model_ai_guard_action.rs new file mode 100644 index 0000000000..ecb865b8b5 --- /dev/null +++ b/src/datadogV2/model/model_ai_guard_action.rs @@ -0,0 +1,54 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[non_exhaustive] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum AIGuardAction { + ALLOW, + DENY, + ABORT, + UnparsedObject(crate::datadog::UnparsedObject), +} + +impl ToString for AIGuardAction { + fn to_string(&self) -> String { + match self { + Self::ALLOW => String::from("ALLOW"), + Self::DENY => String::from("DENY"), + Self::ABORT => String::from("ABORT"), + Self::UnparsedObject(v) => v.value.to_string(), + } + } +} + +impl Serialize for AIGuardAction { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Self::UnparsedObject(v) => v.serialize(serializer), + _ => serializer.serialize_str(self.to_string().as_str()), + } + } +} + +impl<'de> Deserialize<'de> for AIGuardAction { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s: String = String::deserialize(deserializer)?; + Ok(match s.as_str() { + "ALLOW" => Self::ALLOW, + "DENY" => Self::DENY, + "ABORT" => Self::ABORT, + _ => Self::UnparsedObject(crate::datadog::UnparsedObject { + value: serde_json::Value::String(s.into()), + }), + }) + } +} diff --git a/src/datadogV2/model/model_ai_guard_content_part.rs b/src/datadogV2/model/model_ai_guard_content_part.rs new file mode 100644 index 0000000000..8efbbbdbd3 --- /dev/null +++ b/src/datadogV2/model/model_ai_guard_content_part.rs @@ -0,0 +1,126 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. +use serde::de::{Error, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::skip_serializing_none; +use std::fmt::{self, Formatter}; + +/// A single part of a multipart message content. +#[non_exhaustive] +#[skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct AIGuardContentPart { + /// An image URL reference for multimodal content. + #[serde(rename = "image_url")] + pub image_url: Option, + /// The text content of this part, required when type is text. + #[serde(rename = "text")] + pub text: Option, + /// The type of content part, either text or image_url. + #[serde(rename = "type")] + pub type_: String, + #[serde(flatten)] + pub additional_properties: std::collections::BTreeMap, + #[serde(skip)] + #[serde(default)] + pub(crate) _unparsed: bool, +} + +impl AIGuardContentPart { + pub fn new(type_: String) -> AIGuardContentPart { + AIGuardContentPart { + image_url: None, + text: None, + type_, + additional_properties: std::collections::BTreeMap::new(), + _unparsed: false, + } + } + + pub fn image_url(mut self, value: crate::datadogV2::model::AIGuardImageURL) -> Self { + self.image_url = Some(value); + self + } + + pub fn text(mut self, value: String) -> Self { + self.text = Some(value); + self + } + + pub fn additional_properties( + mut self, + value: std::collections::BTreeMap, + ) -> Self { + self.additional_properties = value; + self + } +} + +impl<'de> Deserialize<'de> for AIGuardContentPart { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AIGuardContentPartVisitor; + impl<'a> Visitor<'a> for AIGuardContentPartVisitor { + type Value = AIGuardContentPart; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("a mapping") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'a>, + { + let mut image_url: Option = None; + let mut text: Option = None; + let mut type_: Option = None; + let mut additional_properties: std::collections::BTreeMap< + String, + serde_json::Value, + > = std::collections::BTreeMap::new(); + let mut _unparsed = false; + + while let Some((k, v)) = map.next_entry::()? { + match k.as_str() { + "image_url" => { + if v.is_null() { + continue; + } + image_url = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "text" => { + if v.is_null() { + continue; + } + text = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "type" => { + type_ = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + &_ => { + if let Ok(value) = serde_json::from_value(v.clone()) { + additional_properties.insert(k, value); + } + } + } + } + let type_ = type_.ok_or_else(|| M::Error::missing_field("type_"))?; + + let content = AIGuardContentPart { + image_url, + text, + type_, + additional_properties, + _unparsed, + }; + + Ok(content) + } + } + + deserializer.deserialize_any(AIGuardContentPartVisitor) + } +} diff --git a/src/datadogV2/model/model_ai_guard_evaluate_request.rs b/src/datadogV2/model/model_ai_guard_evaluate_request.rs new file mode 100644 index 0000000000..839cd370d2 --- /dev/null +++ b/src/datadogV2/model/model_ai_guard_evaluate_request.rs @@ -0,0 +1,109 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. +use serde::de::{Error, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::skip_serializing_none; +use std::fmt::{self, Formatter}; + +/// The evaluation request payload containing conversation messages and optional metadata. +#[non_exhaustive] +#[skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct AIGuardEvaluateRequest { + /// The list of conversation messages to evaluate. Must contain at least one message. + #[serde(rename = "messages")] + pub messages: Vec, + /// Optional metadata providing context about the originating service and request. + #[serde(rename = "meta")] + pub meta: Option, + #[serde(flatten)] + pub additional_properties: std::collections::BTreeMap, + #[serde(skip)] + #[serde(default)] + pub(crate) _unparsed: bool, +} + +impl AIGuardEvaluateRequest { + pub fn new(messages: Vec) -> AIGuardEvaluateRequest { + AIGuardEvaluateRequest { + messages, + meta: None, + additional_properties: std::collections::BTreeMap::new(), + _unparsed: false, + } + } + + pub fn meta(mut self, value: crate::datadogV2::model::AIGuardMeta) -> Self { + self.meta = Some(value); + self + } + + pub fn additional_properties( + mut self, + value: std::collections::BTreeMap, + ) -> Self { + self.additional_properties = value; + self + } +} + +impl<'de> Deserialize<'de> for AIGuardEvaluateRequest { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AIGuardEvaluateRequestVisitor; + impl<'a> Visitor<'a> for AIGuardEvaluateRequestVisitor { + type Value = AIGuardEvaluateRequest; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("a mapping") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'a>, + { + let mut messages: Option> = None; + let mut meta: Option = None; + let mut additional_properties: std::collections::BTreeMap< + String, + serde_json::Value, + > = std::collections::BTreeMap::new(); + let mut _unparsed = false; + + while let Some((k, v)) = map.next_entry::()? { + match k.as_str() { + "messages" => { + messages = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "meta" => { + if v.is_null() { + continue; + } + meta = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + &_ => { + if let Ok(value) = serde_json::from_value(v.clone()) { + additional_properties.insert(k, value); + } + } + } + } + let messages = messages.ok_or_else(|| M::Error::missing_field("messages"))?; + + let content = AIGuardEvaluateRequest { + messages, + meta, + additional_properties, + _unparsed, + }; + + Ok(content) + } + } + + deserializer.deserialize_any(AIGuardEvaluateRequestVisitor) + } +} diff --git a/src/datadogV2/model/model_ai_guard_evaluate_response.rs b/src/datadogV2/model/model_ai_guard_evaluate_response.rs new file mode 100644 index 0000000000..fb993ea229 --- /dev/null +++ b/src/datadogV2/model/model_ai_guard_evaluate_response.rs @@ -0,0 +1,187 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. +use serde::de::{Error, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::skip_serializing_none; +use std::fmt::{self, Formatter}; + +/// The result of the AI Guard evaluation. +#[non_exhaustive] +#[skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct AIGuardEvaluateResponse { + /// The action recommendation from the AI Guard evaluation. + #[serde(rename = "action")] + pub action: crate::datadogV2::model::AIGuardAction, + /// The overall threat probability score across all evaluated tags. + #[serde(rename = "global_prob")] + pub global_prob: Option, + /// Whether blocking mode is enabled for this organization. + #[serde(rename = "is_blocking_enabled")] + pub is_blocking_enabled: bool, + /// A human-readable explanation of the action recommendation. + #[serde(rename = "reason")] + pub reason: String, + /// Sensitive data findings detected in the evaluated conversation. + #[serde(rename = "sds_findings")] + pub sds_findings: Option>, + /// Probability scores for each evaluated threat tag. + #[serde(rename = "tag_probs")] + pub tag_probs: std::collections::BTreeMap, + /// Security threat tags detected in the evaluated conversation. + #[serde(rename = "tags")] + pub tags: Vec, + #[serde(flatten)] + pub additional_properties: std::collections::BTreeMap, + #[serde(skip)] + #[serde(default)] + pub(crate) _unparsed: bool, +} + +impl AIGuardEvaluateResponse { + pub fn new( + action: crate::datadogV2::model::AIGuardAction, + is_blocking_enabled: bool, + reason: String, + tag_probs: std::collections::BTreeMap, + tags: Vec, + ) -> AIGuardEvaluateResponse { + AIGuardEvaluateResponse { + action, + global_prob: None, + is_blocking_enabled, + reason, + sds_findings: None, + tag_probs, + tags, + additional_properties: std::collections::BTreeMap::new(), + _unparsed: false, + } + } + + pub fn global_prob(mut self, value: f64) -> Self { + self.global_prob = Some(value); + self + } + + pub fn sds_findings(mut self, value: Vec) -> Self { + self.sds_findings = Some(value); + self + } + + pub fn additional_properties( + mut self, + value: std::collections::BTreeMap, + ) -> Self { + self.additional_properties = value; + self + } +} + +impl<'de> Deserialize<'de> for AIGuardEvaluateResponse { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AIGuardEvaluateResponseVisitor; + impl<'a> Visitor<'a> for AIGuardEvaluateResponseVisitor { + type Value = AIGuardEvaluateResponse; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("a mapping") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'a>, + { + let mut action: Option = None; + let mut global_prob: Option = None; + let mut is_blocking_enabled: Option = None; + let mut reason: Option = None; + let mut sds_findings: Option> = + None; + let mut tag_probs: Option> = None; + let mut tags: Option> = None; + let mut additional_properties: std::collections::BTreeMap< + String, + serde_json::Value, + > = std::collections::BTreeMap::new(); + let mut _unparsed = false; + + while let Some((k, v)) = map.next_entry::()? { + match k.as_str() { + "action" => { + action = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + if let Some(ref _action) = action { + match _action { + crate::datadogV2::model::AIGuardAction::UnparsedObject( + _action, + ) => { + _unparsed = true; + } + _ => {} + } + } + } + "global_prob" => { + if v.is_null() || v.as_str() == Some("") { + continue; + } + global_prob = + Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "is_blocking_enabled" => { + is_blocking_enabled = + Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "reason" => { + reason = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "sds_findings" => { + if v.is_null() { + continue; + } + sds_findings = + Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "tag_probs" => { + tag_probs = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "tags" => { + tags = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + &_ => { + if let Ok(value) = serde_json::from_value(v.clone()) { + additional_properties.insert(k, value); + } + } + } + } + let action = action.ok_or_else(|| M::Error::missing_field("action"))?; + let is_blocking_enabled = is_blocking_enabled + .ok_or_else(|| M::Error::missing_field("is_blocking_enabled"))?; + let reason = reason.ok_or_else(|| M::Error::missing_field("reason"))?; + let tag_probs = tag_probs.ok_or_else(|| M::Error::missing_field("tag_probs"))?; + let tags = tags.ok_or_else(|| M::Error::missing_field("tags"))?; + + let content = AIGuardEvaluateResponse { + action, + global_prob, + is_blocking_enabled, + reason, + sds_findings, + tag_probs, + tags, + additional_properties, + _unparsed, + }; + + Ok(content) + } + } + + deserializer.deserialize_any(AIGuardEvaluateResponseVisitor) + } +} diff --git a/src/datadogV2/model/model_ai_guard_image_url.rs b/src/datadogV2/model/model_ai_guard_image_url.rs new file mode 100644 index 0000000000..ea5364571a --- /dev/null +++ b/src/datadogV2/model/model_ai_guard_image_url.rs @@ -0,0 +1,92 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. +use serde::de::{Error, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::skip_serializing_none; +use std::fmt::{self, Formatter}; + +/// An image URL reference for multimodal content. +#[non_exhaustive] +#[skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct AIGuardImageURL { + /// The URL pointing to the image. + #[serde(rename = "url")] + pub url: String, + #[serde(flatten)] + pub additional_properties: std::collections::BTreeMap, + #[serde(skip)] + #[serde(default)] + pub(crate) _unparsed: bool, +} + +impl AIGuardImageURL { + pub fn new(url: String) -> AIGuardImageURL { + AIGuardImageURL { + url, + additional_properties: std::collections::BTreeMap::new(), + _unparsed: false, + } + } + + pub fn additional_properties( + mut self, + value: std::collections::BTreeMap, + ) -> Self { + self.additional_properties = value; + self + } +} + +impl<'de> Deserialize<'de> for AIGuardImageURL { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AIGuardImageURLVisitor; + impl<'a> Visitor<'a> for AIGuardImageURLVisitor { + type Value = AIGuardImageURL; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("a mapping") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'a>, + { + let mut url: Option = None; + let mut additional_properties: std::collections::BTreeMap< + String, + serde_json::Value, + > = std::collections::BTreeMap::new(); + let mut _unparsed = false; + + while let Some((k, v)) = map.next_entry::()? { + match k.as_str() { + "url" => { + url = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + &_ => { + if let Ok(value) = serde_json::from_value(v.clone()) { + additional_properties.insert(k, value); + } + } + } + } + let url = url.ok_or_else(|| M::Error::missing_field("url"))?; + + let content = AIGuardImageURL { + url, + additional_properties, + _unparsed, + }; + + Ok(content) + } + } + + deserializer.deserialize_any(AIGuardImageURLVisitor) + } +} diff --git a/src/datadogV2/model/model_ai_guard_message.rs b/src/datadogV2/model/model_ai_guard_message.rs new file mode 100644 index 0000000000..b696b41541 --- /dev/null +++ b/src/datadogV2/model/model_ai_guard_message.rs @@ -0,0 +1,162 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. +use serde::de::{Error, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::skip_serializing_none; +use std::fmt::{self, Formatter}; + +/// A single message in the conversation to evaluate. +#[non_exhaustive] +#[skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct AIGuardMessage { + /// The message content, either a plain string or an array of content parts. + #[serde(rename = "content")] + pub content: Option, + /// The role of the message author in the conversation. + #[serde(rename = "role")] + pub role: crate::datadogV2::model::AIGuardMessageRole, + /// The ID of the tool call this message is responding to, required for tool messages. + #[serde(rename = "tool_call_id")] + pub tool_call_id: Option, + /// Tool calls issued by the assistant in this message. + #[serde(rename = "tool_calls")] + pub tool_calls: Option>, + #[serde(flatten)] + pub additional_properties: std::collections::BTreeMap, + #[serde(skip)] + #[serde(default)] + pub(crate) _unparsed: bool, +} + +impl AIGuardMessage { + pub fn new(role: crate::datadogV2::model::AIGuardMessageRole) -> AIGuardMessage { + AIGuardMessage { + content: None, + role, + tool_call_id: None, + tool_calls: None, + additional_properties: std::collections::BTreeMap::new(), + _unparsed: false, + } + } + + pub fn content(mut self, value: crate::datadogV2::model::AIGuardMessageContent) -> Self { + self.content = Some(value); + self + } + + pub fn tool_call_id(mut self, value: String) -> Self { + self.tool_call_id = Some(value); + self + } + + pub fn tool_calls(mut self, value: Vec) -> Self { + self.tool_calls = Some(value); + self + } + + pub fn additional_properties( + mut self, + value: std::collections::BTreeMap, + ) -> Self { + self.additional_properties = value; + self + } +} + +impl<'de> Deserialize<'de> for AIGuardMessage { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AIGuardMessageVisitor; + impl<'a> Visitor<'a> for AIGuardMessageVisitor { + type Value = AIGuardMessage; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("a mapping") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'a>, + { + let mut content: Option = None; + let mut role: Option = None; + let mut tool_call_id: Option = None; + let mut tool_calls: Option> = None; + let mut additional_properties: std::collections::BTreeMap< + String, + serde_json::Value, + > = std::collections::BTreeMap::new(); + let mut _unparsed = false; + + while let Some((k, v)) = map.next_entry::()? { + match k.as_str() { + "content" => { + if v.is_null() { + continue; + } + content = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + if let Some(ref _content) = content { + match _content { + crate::datadogV2::model::AIGuardMessageContent::UnparsedObject(_content) => { + _unparsed = true; + }, + _ => {} + } + } + } + "role" => { + role = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + if let Some(ref _role) = role { + match _role { + crate::datadogV2::model::AIGuardMessageRole::UnparsedObject( + _role, + ) => { + _unparsed = true; + } + _ => {} + } + } + } + "tool_call_id" => { + if v.is_null() { + continue; + } + tool_call_id = + Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "tool_calls" => { + if v.is_null() { + continue; + } + tool_calls = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + &_ => { + if let Ok(value) = serde_json::from_value(v.clone()) { + additional_properties.insert(k, value); + } + } + } + } + let role = role.ok_or_else(|| M::Error::missing_field("role"))?; + + let content = AIGuardMessage { + content, + role, + tool_call_id, + tool_calls, + additional_properties, + _unparsed, + }; + + Ok(content) + } + } + + deserializer.deserialize_any(AIGuardMessageVisitor) + } +} diff --git a/src/datadogV2/model/model_ai_guard_message_content.rs b/src/datadogV2/model/model_ai_guard_message_content.rs new file mode 100644 index 0000000000..fbaad020e0 --- /dev/null +++ b/src/datadogV2/model/model_ai_guard_message_content.rs @@ -0,0 +1,35 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. +use serde::{Deserialize, Deserializer, Serialize}; + +/// The message content, either a plain string or an array of content parts. +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(untagged)] +pub enum AIGuardMessageContent { + String(String), + AIGuardContentPartList(Vec), + UnparsedObject(crate::datadog::UnparsedObject), +} + +impl<'de> Deserialize<'de> for AIGuardMessageContent { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value: serde_json::Value = Deserialize::deserialize(deserializer)?; + if let Ok(_v) = serde_json::from_value::(value.clone()) { + return Ok(AIGuardMessageContent::String(_v)); + } + if let Ok(_v) = serde_json::from_value::>( + value.clone(), + ) { + return Ok(AIGuardMessageContent::AIGuardContentPartList(_v)); + } + + return Ok(AIGuardMessageContent::UnparsedObject( + crate::datadog::UnparsedObject { value }, + )); + } +} diff --git a/src/datadogV2/model/model_ai_guard_message_role.rs b/src/datadogV2/model/model_ai_guard_message_role.rs new file mode 100644 index 0000000000..26116c7e96 --- /dev/null +++ b/src/datadogV2/model/model_ai_guard_message_role.rs @@ -0,0 +1,60 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[non_exhaustive] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum AIGuardMessageRole { + USER, + ASSISTANT, + SYSTEM, + TOOL, + DEVELOPER, + UnparsedObject(crate::datadog::UnparsedObject), +} + +impl ToString for AIGuardMessageRole { + fn to_string(&self) -> String { + match self { + Self::USER => String::from("user"), + Self::ASSISTANT => String::from("assistant"), + Self::SYSTEM => String::from("system"), + Self::TOOL => String::from("tool"), + Self::DEVELOPER => String::from("developer"), + Self::UnparsedObject(v) => v.value.to_string(), + } + } +} + +impl Serialize for AIGuardMessageRole { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Self::UnparsedObject(v) => v.serialize(serializer), + _ => serializer.serialize_str(self.to_string().as_str()), + } + } +} + +impl<'de> Deserialize<'de> for AIGuardMessageRole { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s: String = String::deserialize(deserializer)?; + Ok(match s.as_str() { + "user" => Self::USER, + "assistant" => Self::ASSISTANT, + "system" => Self::SYSTEM, + "tool" => Self::TOOL, + "developer" => Self::DEVELOPER, + _ => Self::UnparsedObject(crate::datadog::UnparsedObject { + value: serde_json::Value::String(s.into()), + }), + }) + } +} diff --git a/src/datadogV2/model/model_ai_guard_meta.rs b/src/datadogV2/model/model_ai_guard_meta.rs new file mode 100644 index 0000000000..0a0283167c --- /dev/null +++ b/src/datadogV2/model/model_ai_guard_meta.rs @@ -0,0 +1,176 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. +use serde::de::{Error, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::skip_serializing_none; +use std::fmt::{self, Formatter}; + +/// Optional metadata providing context about the originating service and request. +#[non_exhaustive] +#[skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct AIGuardMeta { + /// Identifier of the coding agent sending the request, if applicable. + #[serde(rename = "coding_agent")] + pub coding_agent: Option, + /// Override for the default threat detection confidence threshold, between 0.0 and 1.0. + #[serde(rename = "confidence_threshold")] + pub confidence_threshold: Option, + /// The deployment environment of the originating service. + #[serde(rename = "env")] + pub env: Option, + /// Override whether sensitive data scanning is applied to this request. + #[serde(rename = "is_sds_enabled_override")] + pub is_sds_enabled_override: Option, + /// The name of the service sending the evaluation request. + #[serde(rename = "service")] + pub service: Option, + #[serde(flatten)] + pub additional_properties: std::collections::BTreeMap, + #[serde(skip)] + #[serde(default)] + pub(crate) _unparsed: bool, +} + +impl AIGuardMeta { + pub fn new() -> AIGuardMeta { + AIGuardMeta { + coding_agent: None, + confidence_threshold: None, + env: None, + is_sds_enabled_override: None, + service: None, + additional_properties: std::collections::BTreeMap::new(), + _unparsed: false, + } + } + + pub fn coding_agent(mut self, value: String) -> Self { + self.coding_agent = Some(value); + self + } + + pub fn confidence_threshold(mut self, value: f64) -> Self { + self.confidence_threshold = Some(value); + self + } + + pub fn env(mut self, value: String) -> Self { + self.env = Some(value); + self + } + + pub fn is_sds_enabled_override(mut self, value: bool) -> Self { + self.is_sds_enabled_override = Some(value); + self + } + + pub fn service(mut self, value: String) -> Self { + self.service = Some(value); + self + } + + pub fn additional_properties( + mut self, + value: std::collections::BTreeMap, + ) -> Self { + self.additional_properties = value; + self + } +} + +impl Default for AIGuardMeta { + fn default() -> Self { + Self::new() + } +} + +impl<'de> Deserialize<'de> for AIGuardMeta { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AIGuardMetaVisitor; + impl<'a> Visitor<'a> for AIGuardMetaVisitor { + type Value = AIGuardMeta; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("a mapping") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'a>, + { + let mut coding_agent: Option = None; + let mut confidence_threshold: Option = None; + let mut env: Option = None; + let mut is_sds_enabled_override: Option = None; + let mut service: Option = None; + let mut additional_properties: std::collections::BTreeMap< + String, + serde_json::Value, + > = std::collections::BTreeMap::new(); + let mut _unparsed = false; + + while let Some((k, v)) = map.next_entry::()? { + match k.as_str() { + "coding_agent" => { + if v.is_null() { + continue; + } + coding_agent = + Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "confidence_threshold" => { + if v.is_null() || v.as_str() == Some("") { + continue; + } + confidence_threshold = + Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "env" => { + if v.is_null() { + continue; + } + env = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "is_sds_enabled_override" => { + if v.is_null() { + continue; + } + is_sds_enabled_override = + Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "service" => { + if v.is_null() { + continue; + } + service = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + &_ => { + if let Ok(value) = serde_json::from_value(v.clone()) { + additional_properties.insert(k, value); + } + } + } + } + + let content = AIGuardMeta { + coding_agent, + confidence_threshold, + env, + is_sds_enabled_override, + service, + additional_properties, + _unparsed, + }; + + Ok(content) + } + } + + deserializer.deserialize_any(AIGuardMetaVisitor) + } +} diff --git a/src/datadogV2/model/model_ai_guard_sds_finding.rs b/src/datadogV2/model/model_ai_guard_sds_finding.rs new file mode 100644 index 0000000000..ac4247e6e8 --- /dev/null +++ b/src/datadogV2/model/model_ai_guard_sds_finding.rs @@ -0,0 +1,129 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. +use serde::de::{Error, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::skip_serializing_none; +use std::fmt::{self, Formatter}; + +/// A sensitive data finding detected by the SDS scanner. +#[non_exhaustive] +#[skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct AIGuardSdsFinding { + /// The category of sensitive data detected. + #[serde(rename = "category")] + pub category: String, + /// The location of a sensitive data match within the evaluated request. + #[serde(rename = "location")] + pub location: crate::datadogV2::model::AIGuardSdsFindingLocation, + /// The human-readable name of the SDS rule that triggered. + #[serde(rename = "rule_display_name")] + pub rule_display_name: String, + /// The tag identifier of the SDS rule that triggered. + #[serde(rename = "rule_tag")] + pub rule_tag: String, + #[serde(flatten)] + pub additional_properties: std::collections::BTreeMap, + #[serde(skip)] + #[serde(default)] + pub(crate) _unparsed: bool, +} + +impl AIGuardSdsFinding { + pub fn new( + category: String, + location: crate::datadogV2::model::AIGuardSdsFindingLocation, + rule_display_name: String, + rule_tag: String, + ) -> AIGuardSdsFinding { + AIGuardSdsFinding { + category, + location, + rule_display_name, + rule_tag, + additional_properties: std::collections::BTreeMap::new(), + _unparsed: false, + } + } + + pub fn additional_properties( + mut self, + value: std::collections::BTreeMap, + ) -> Self { + self.additional_properties = value; + self + } +} + +impl<'de> Deserialize<'de> for AIGuardSdsFinding { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AIGuardSdsFindingVisitor; + impl<'a> Visitor<'a> for AIGuardSdsFindingVisitor { + type Value = AIGuardSdsFinding; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("a mapping") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'a>, + { + let mut category: Option = None; + let mut location: Option = None; + let mut rule_display_name: Option = None; + let mut rule_tag: Option = None; + let mut additional_properties: std::collections::BTreeMap< + String, + serde_json::Value, + > = std::collections::BTreeMap::new(); + let mut _unparsed = false; + + while let Some((k, v)) = map.next_entry::()? { + match k.as_str() { + "category" => { + category = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "location" => { + location = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "rule_display_name" => { + rule_display_name = + Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "rule_tag" => { + rule_tag = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + &_ => { + if let Ok(value) = serde_json::from_value(v.clone()) { + additional_properties.insert(k, value); + } + } + } + } + let category = category.ok_or_else(|| M::Error::missing_field("category"))?; + let location = location.ok_or_else(|| M::Error::missing_field("location"))?; + let rule_display_name = rule_display_name + .ok_or_else(|| M::Error::missing_field("rule_display_name"))?; + let rule_tag = rule_tag.ok_or_else(|| M::Error::missing_field("rule_tag"))?; + + let content = AIGuardSdsFinding { + category, + location, + rule_display_name, + rule_tag, + additional_properties, + _unparsed, + }; + + Ok(content) + } + } + + deserializer.deserialize_any(AIGuardSdsFindingVisitor) + } +} diff --git a/src/datadogV2/model/model_ai_guard_sds_finding_location.rs b/src/datadogV2/model/model_ai_guard_sds_finding_location.rs new file mode 100644 index 0000000000..b3700118a1 --- /dev/null +++ b/src/datadogV2/model/model_ai_guard_sds_finding_location.rs @@ -0,0 +1,120 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. +use serde::de::{Error, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::skip_serializing_none; +use std::fmt::{self, Formatter}; + +/// The location of a sensitive data match within the evaluated request. +#[non_exhaustive] +#[skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct AIGuardSdsFindingLocation { + /// The end character index (exclusive) of the sensitive data match. + #[serde(rename = "end_index_exclusive")] + pub end_index_exclusive: i64, + /// The JSON path to the field containing the sensitive data. + #[serde(rename = "path")] + pub path: String, + /// The start character index of the sensitive data match. + #[serde(rename = "start_index")] + pub start_index: i64, + #[serde(flatten)] + pub additional_properties: std::collections::BTreeMap, + #[serde(skip)] + #[serde(default)] + pub(crate) _unparsed: bool, +} + +impl AIGuardSdsFindingLocation { + pub fn new( + end_index_exclusive: i64, + path: String, + start_index: i64, + ) -> AIGuardSdsFindingLocation { + AIGuardSdsFindingLocation { + end_index_exclusive, + path, + start_index, + additional_properties: std::collections::BTreeMap::new(), + _unparsed: false, + } + } + + pub fn additional_properties( + mut self, + value: std::collections::BTreeMap, + ) -> Self { + self.additional_properties = value; + self + } +} + +impl<'de> Deserialize<'de> for AIGuardSdsFindingLocation { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AIGuardSdsFindingLocationVisitor; + impl<'a> Visitor<'a> for AIGuardSdsFindingLocationVisitor { + type Value = AIGuardSdsFindingLocation; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("a mapping") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'a>, + { + let mut end_index_exclusive: Option = None; + let mut path: Option = None; + let mut start_index: Option = None; + let mut additional_properties: std::collections::BTreeMap< + String, + serde_json::Value, + > = std::collections::BTreeMap::new(); + let mut _unparsed = false; + + while let Some((k, v)) = map.next_entry::()? { + match k.as_str() { + "end_index_exclusive" => { + end_index_exclusive = + Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "path" => { + path = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "start_index" => { + start_index = + Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + &_ => { + if let Ok(value) = serde_json::from_value(v.clone()) { + additional_properties.insert(k, value); + } + } + } + } + let end_index_exclusive = end_index_exclusive + .ok_or_else(|| M::Error::missing_field("end_index_exclusive"))?; + let path = path.ok_or_else(|| M::Error::missing_field("path"))?; + let start_index = + start_index.ok_or_else(|| M::Error::missing_field("start_index"))?; + + let content = AIGuardSdsFindingLocation { + end_index_exclusive, + path, + start_index, + additional_properties, + _unparsed, + }; + + Ok(content) + } + } + + deserializer.deserialize_any(AIGuardSdsFindingLocationVisitor) + } +} diff --git a/src/datadogV2/model/model_ai_guard_tool_call.rs b/src/datadogV2/model/model_ai_guard_tool_call.rs new file mode 100644 index 0000000000..6e1d85acad --- /dev/null +++ b/src/datadogV2/model/model_ai_guard_tool_call.rs @@ -0,0 +1,105 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. +use serde::de::{Error, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::skip_serializing_none; +use std::fmt::{self, Formatter}; + +/// A tool call issued by the assistant. +#[non_exhaustive] +#[skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct AIGuardToolCall { + /// The function definition within a tool call. + #[serde(rename = "function")] + pub function: crate::datadogV2::model::AIGuardToolCallFunction, + /// The unique identifier of the tool call. + #[serde(rename = "id")] + pub id: String, + #[serde(flatten)] + pub additional_properties: std::collections::BTreeMap, + #[serde(skip)] + #[serde(default)] + pub(crate) _unparsed: bool, +} + +impl AIGuardToolCall { + pub fn new( + function: crate::datadogV2::model::AIGuardToolCallFunction, + id: String, + ) -> AIGuardToolCall { + AIGuardToolCall { + function, + id, + additional_properties: std::collections::BTreeMap::new(), + _unparsed: false, + } + } + + pub fn additional_properties( + mut self, + value: std::collections::BTreeMap, + ) -> Self { + self.additional_properties = value; + self + } +} + +impl<'de> Deserialize<'de> for AIGuardToolCall { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AIGuardToolCallVisitor; + impl<'a> Visitor<'a> for AIGuardToolCallVisitor { + type Value = AIGuardToolCall; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("a mapping") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'a>, + { + let mut function: Option = None; + let mut id: Option = None; + let mut additional_properties: std::collections::BTreeMap< + String, + serde_json::Value, + > = std::collections::BTreeMap::new(); + let mut _unparsed = false; + + while let Some((k, v)) = map.next_entry::()? { + match k.as_str() { + "function" => { + function = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "id" => { + id = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + &_ => { + if let Ok(value) = serde_json::from_value(v.clone()) { + additional_properties.insert(k, value); + } + } + } + } + let function = function.ok_or_else(|| M::Error::missing_field("function"))?; + let id = id.ok_or_else(|| M::Error::missing_field("id"))?; + + let content = AIGuardToolCall { + function, + id, + additional_properties, + _unparsed, + }; + + Ok(content) + } + } + + deserializer.deserialize_any(AIGuardToolCallVisitor) + } +} diff --git a/src/datadogV2/model/model_ai_guard_tool_call_function.rs b/src/datadogV2/model/model_ai_guard_tool_call_function.rs new file mode 100644 index 0000000000..a17de5b3f5 --- /dev/null +++ b/src/datadogV2/model/model_ai_guard_tool_call_function.rs @@ -0,0 +1,102 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. +use serde::de::{Error, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::skip_serializing_none; +use std::fmt::{self, Formatter}; + +/// The function definition within a tool call. +#[non_exhaustive] +#[skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct AIGuardToolCallFunction { + /// The JSON-encoded arguments passed to the function. + #[serde(rename = "arguments")] + pub arguments: String, + /// The name of the function being called. + #[serde(rename = "name")] + pub name: String, + #[serde(flatten)] + pub additional_properties: std::collections::BTreeMap, + #[serde(skip)] + #[serde(default)] + pub(crate) _unparsed: bool, +} + +impl AIGuardToolCallFunction { + pub fn new(arguments: String, name: String) -> AIGuardToolCallFunction { + AIGuardToolCallFunction { + arguments, + name, + additional_properties: std::collections::BTreeMap::new(), + _unparsed: false, + } + } + + pub fn additional_properties( + mut self, + value: std::collections::BTreeMap, + ) -> Self { + self.additional_properties = value; + self + } +} + +impl<'de> Deserialize<'de> for AIGuardToolCallFunction { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AIGuardToolCallFunctionVisitor; + impl<'a> Visitor<'a> for AIGuardToolCallFunctionVisitor { + type Value = AIGuardToolCallFunction; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("a mapping") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'a>, + { + let mut arguments: Option = None; + let mut name: Option = None; + let mut additional_properties: std::collections::BTreeMap< + String, + serde_json::Value, + > = std::collections::BTreeMap::new(); + let mut _unparsed = false; + + while let Some((k, v)) = map.next_entry::()? { + match k.as_str() { + "arguments" => { + arguments = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + "name" => { + name = Some(serde_json::from_value(v).map_err(M::Error::custom)?); + } + &_ => { + if let Ok(value) = serde_json::from_value(v.clone()) { + additional_properties.insert(k, value); + } + } + } + } + let arguments = arguments.ok_or_else(|| M::Error::missing_field("arguments"))?; + let name = name.ok_or_else(|| M::Error::missing_field("name"))?; + + let content = AIGuardToolCallFunction { + arguments, + name, + additional_properties, + _unparsed, + }; + + Ok(content) + } + } + + deserializer.deserialize_any(AIGuardToolCallFunctionVisitor) + } +} diff --git a/tests/scenarios/features/v2/ai_guard.feature b/tests/scenarios/features/v2/ai_guard.feature new file mode 100644 index 0000000000..46d17eb315 --- /dev/null +++ b/tests/scenarios/features/v2/ai_guard.feature @@ -0,0 +1,21 @@ +@endpoint(ai-guard) @endpoint(ai-guard-v2) +Feature: AI Guard + Analyze AI conversations for security threats including prompt injection, + jailbreak attempts, and other AI-specific attacks. + + Background: + Given a valid "apiKeyAuth" key in the system + And a valid "appKeyAuth" key in the system + And an instance of "AIGuard" API + And new "EvaluateAIGuardRequest" request + And body with value {"messages": [{"content": "How do I delete all files on the system?", "role": "user"}], "meta": {"env": "production", "service": "my-llm-service"}} + + @generated @skip @team:DataDog/k9-ai-guard + Scenario: Evaluate an AI Guard request returns "Bad Request" response + When the request is sent + Then the response status is 400 Bad Request + + @generated @skip @team:DataDog/k9-ai-guard + Scenario: Evaluate an AI Guard request returns "Evaluation result with action recommendation" response + When the request is sent + Then the response status is 200 Evaluation result with action recommendation diff --git a/tests/scenarios/features/v2/undo.json b/tests/scenarios/features/v2/undo.json index 3a15f0b470..3ba957a655 100644 --- a/tests/scenarios/features/v2/undo.json +++ b/tests/scenarios/features/v2/undo.json @@ -399,6 +399,12 @@ "type": "safe" } }, + "EvaluateAIGuardRequest": { + "tag": "AI Guard", + "undo": { + "type": "unsafe" + } + }, "ListAnnotations": { "tag": "Annotations", "undo": { diff --git a/tests/scenarios/function_mappings.rs b/tests/scenarios/function_mappings.rs index c5ffd865a6..ab4aeed5f0 100644 --- a/tests/scenarios/function_mappings.rs +++ b/tests/scenarios/function_mappings.rs @@ -54,6 +54,7 @@ pub struct ApiInstances { pub v2_api_actions_datastores: Option, pub v2_api_action_connection: Option, pub v2_api_agentless_scanning: Option, + pub v2_api_ai_guard: Option, pub v2_api_annotations: Option, pub v2_api_users: Option, pub v2_api_key_management: Option, @@ -608,6 +609,13 @@ pub fn initialize_api_instance(world: &mut DatadogWorld, api: String) { ), ); } + "AIGuard" => { + world.api_instances.v2_api_ai_guard = + Some(datadogV2::api_ai_guard::AIGuardAPI::with_client_and_config( + world.config.clone(), + world.http_client.as_ref().unwrap().clone(), + )); + } "Annotations" => { world.api_instances.v2_api_annotations = Some( datadogV2::api_annotations::AnnotationsAPI::with_client_and_config( @@ -2642,6 +2650,10 @@ pub fn collect_function_calls(world: &mut DatadogWorld) { "v2.GetAwsOnDemandTask".into(), test_v2_get_aws_on_demand_task, ); + world.function_mappings.insert( + "v2.EvaluateAIGuardRequest".into(), + test_v2_evaluate_ai_guard_request, + ); world .function_mappings .insert("v2.ListAnnotations".into(), test_v2_list_annotations); @@ -17858,6 +17870,34 @@ fn test_v2_get_aws_on_demand_task(world: &mut DatadogWorld, _parameters: &HashMa world.response.code = response.status.as_u16(); } +fn test_v2_evaluate_ai_guard_request( + world: &mut DatadogWorld, + _parameters: &HashMap, +) { + let api = world + .api_instances + .v2_api_ai_guard + .as_ref() + .expect("api instance not found"); + let body = serde_json::from_value(_parameters.get("body").unwrap().clone()).unwrap(); + let response = match block_on(api.evaluate_ai_guard_request_with_http_info(body)) { + Ok(response) => response, + Err(error) => { + return match error { + Error::ResponseError(e) => { + world.response.code = e.status.as_u16(); + if let Some(entity) = e.entity { + world.response.object = serde_json::to_value(entity).unwrap(); + } + } + _ => panic!("error parsing response: {error}"), + }; + } + }; + world.response.object = serde_json::to_value(response.entity).unwrap(); + world.response.code = response.status.as_u16(); +} + fn test_v2_list_annotations(world: &mut DatadogWorld, _parameters: &HashMap) { let api = world .api_instances