1use serde::{Deserialize, Serialize};
4use std::time::SystemTime;
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct GuardrailRequest {
13 pub id: Uuid,
15 #[serde(with = "system_time_serde")]
17 pub received_at: SystemTime,
18 pub messages: Vec<ChatMessage>,
20 pub provider: Provider,
22 pub model: String,
24 pub stream: bool,
26 pub extra: serde_json::Value,
28}
29
30impl GuardrailRequest {
31 pub fn new(messages: Vec<ChatMessage>, model: String, provider: Provider) -> Self {
46 Self {
47 id: Uuid::new_v4(),
48 received_at: SystemTime::now(),
49 messages,
50 provider,
51 model,
52 stream: false,
53 extra: serde_json::Value::Null,
54 }
55 }
56
57 pub fn user_text(&self) -> String {
61 self.messages
62 .iter()
63 .filter(|m| matches!(m.role, Role::User))
64 .map(|m| m.content.as_text())
65 .collect::<Vec<_>>()
66 .join("\n")
67 }
68
69 pub fn text_for_roles(&self, roles: &[Role]) -> String {
89 self.messages
90 .iter()
91 .filter(|m| roles.contains(&m.role))
92 .map(|m| m.content.as_text())
93 .collect::<Vec<_>>()
94 .join("\n")
95 }
96
97 pub fn all_text(&self) -> String {
99 self.messages
100 .iter()
101 .map(|m| m.content.as_text())
102 .collect::<Vec<_>>()
103 .join("\n")
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
109pub struct ChatMessage {
110 pub role: Role,
112 pub content: MessageContent,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
118#[serde(untagged)]
119pub enum MessageContent {
120 Text(String),
122 Parts(Vec<ContentPart>),
124}
125
126impl MessageContent {
127 pub fn as_text(&self) -> String {
129 match self {
130 MessageContent::Text(t) => t.clone(),
131 MessageContent::Parts(parts) => parts
132 .iter()
133 .filter_map(|p| {
134 if let ContentPart::Text { text } = p {
135 Some(text.as_str())
136 } else {
137 None
138 }
139 })
140 .collect::<Vec<_>>()
141 .join("\n"),
142 }
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
148#[serde(tag = "type", rename_all = "snake_case")]
149pub enum ContentPart {
150 Text {
152 text: String,
154 },
155 ImageUrl {
157 image_url: ImageUrl,
159 },
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
164pub struct ImageUrl {
165 pub url: String,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub detail: Option<String>,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(rename_all = "lowercase")]
175pub enum Role {
176 System,
178 User,
180 Assistant,
182 Tool,
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(rename_all = "lowercase")]
189pub enum Provider {
190 OpenAI,
192 Anthropic,
194 Azure,
196 Other(String),
198}
199
200mod system_time_serde {
202 use serde::{Deserialize, Deserializer, Serialize, Serializer};
203 use std::time::{SystemTime, UNIX_EPOCH};
204
205 pub fn serialize<S: Serializer>(t: &SystemTime, s: S) -> Result<S::Ok, S::Error> {
206 let secs = t
207 .duration_since(UNIX_EPOCH)
208 .unwrap_or_default()
209 .as_secs_f64();
210 secs.serialize(s)
211 }
212
213 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<SystemTime, D::Error> {
214 let secs = f64::deserialize(d)?;
215 Ok(UNIX_EPOCH + std::time::Duration::from_secs_f64(secs))
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_user_text_extraction() {
225 let req = GuardrailRequest::new(
226 vec![
227 ChatMessage {
228 role: Role::System,
229 content: MessageContent::Text("You are a helpful assistant.".into()),
230 },
231 ChatMessage {
232 role: Role::User,
233 content: MessageContent::Text("Hello!".into()),
234 },
235 ],
236 "gpt-4o".into(),
237 Provider::OpenAI,
238 );
239
240 let text = req.user_text();
241 assert_eq!(text, "Hello!");
242 }
243
244 #[test]
245 fn test_message_content_as_text_parts() {
246 let content = MessageContent::Parts(vec![
247 ContentPart::Text {
248 text: "Hello".into(),
249 },
250 ContentPart::ImageUrl {
251 image_url: ImageUrl {
252 url: "https://example.com/img.png".into(),
253 detail: None,
254 },
255 },
256 ContentPart::Text {
257 text: "World".into(),
258 },
259 ]);
260 assert_eq!(content.as_text(), "Hello\nWorld");
261 }
262
263 #[test]
264 fn test_request_roundtrip_json() {
265 let req = GuardrailRequest::new(
266 vec![ChatMessage {
267 role: Role::User,
268 content: MessageContent::Text("Test".into()),
269 }],
270 "gpt-4o".into(),
271 Provider::OpenAI,
272 );
273 let json = serde_json::to_string(&req).unwrap();
274 let decoded: GuardrailRequest = serde_json::from_str(&json).unwrap();
275 assert_eq!(decoded.model, req.model);
276 assert_eq!(decoded.id, req.id);
277 }
278}