use serde::{Deserialize, Serialize};
use std::time::SystemTime;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GuardrailRequest {
pub id: Uuid,
#[serde(with = "system_time_serde")]
pub received_at: SystemTime,
pub messages: Vec<ChatMessage>,
pub provider: Provider,
pub model: String,
pub stream: bool,
pub extra: serde_json::Value,
}
impl GuardrailRequest {
pub fn new(messages: Vec<ChatMessage>, model: String, provider: Provider) -> Self {
Self {
id: Uuid::new_v4(),
received_at: SystemTime::now(),
messages,
provider,
model,
stream: false,
extra: serde_json::Value::Null,
}
}
pub fn user_text(&self) -> String {
self.messages
.iter()
.filter(|m| matches!(m.role, Role::User))
.map(|m| m.content.as_text())
.collect::<Vec<_>>()
.join("\n")
}
pub fn text_for_roles(&self, roles: &[Role]) -> String {
self.messages
.iter()
.filter(|m| roles.contains(&m.role))
.map(|m| m.content.as_text())
.collect::<Vec<_>>()
.join("\n")
}
pub fn all_text(&self) -> String {
self.messages
.iter()
.map(|m| m.content.as_text())
.collect::<Vec<_>>()
.join("\n")
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ChatMessage {
pub role: Role,
pub content: MessageContent,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Parts(Vec<ContentPart>),
}
impl MessageContent {
pub fn as_text(&self) -> String {
match self {
MessageContent::Text(t) => t.clone(),
MessageContent::Parts(parts) => parts
.iter()
.filter_map(|p| {
if let ContentPart::Text { text } = p {
Some(text.as_str())
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
Text {
text: String,
},
ImageUrl {
image_url: ImageUrl,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ImageUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Provider {
OpenAI,
Anthropic,
Azure,
Other(String),
}
mod system_time_serde {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::time::{SystemTime, UNIX_EPOCH};
pub fn serialize<S: Serializer>(t: &SystemTime, s: S) -> Result<S::Ok, S::Error> {
let secs = t
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64();
secs.serialize(s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<SystemTime, D::Error> {
let secs = f64::deserialize(d)?;
Ok(UNIX_EPOCH + std::time::Duration::from_secs_f64(secs))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_text_extraction() {
let req = GuardrailRequest::new(
vec![
ChatMessage {
role: Role::System,
content: MessageContent::Text("You are a helpful assistant.".into()),
},
ChatMessage {
role: Role::User,
content: MessageContent::Text("Hello!".into()),
},
],
"gpt-4o".into(),
Provider::OpenAI,
);
let text = req.user_text();
assert_eq!(text, "Hello!");
}
#[test]
fn test_message_content_as_text_parts() {
let content = MessageContent::Parts(vec![
ContentPart::Text {
text: "Hello".into(),
},
ContentPart::ImageUrl {
image_url: ImageUrl {
url: "https://example.com/img.png".into(),
detail: None,
},
},
ContentPart::Text {
text: "World".into(),
},
]);
assert_eq!(content.as_text(), "Hello\nWorld");
}
#[test]
fn test_request_roundtrip_json() {
let req = GuardrailRequest::new(
vec![ChatMessage {
role: Role::User,
content: MessageContent::Text("Test".into()),
}],
"gpt-4o".into(),
Provider::OpenAI,
);
let json = serde_json::to_string(&req).unwrap();
let decoded: GuardrailRequest = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.model, req.model);
assert_eq!(decoded.id, req.id);
}
}