1use serde::{Deserialize, Serialize};
16
17use super::content::*;
18use super::enums::{ContentType, Role};
19use super::message::Message;
20use crate::hooks::payload::Extensions;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum ViewKind {
30 Text,
31 Thinking,
32 ToolCall,
33 ToolResult,
34 Resource,
35 ResourceRef,
36 PromptRequest,
37 PromptResult,
38 Image,
39 Video,
40 Audio,
41 Document,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum ViewAction {
48 Read,
49 Write,
50 Execute,
51 Invoke,
52 Send,
53 Receive,
54 Generate,
55}
56
57impl ViewKind {
58 pub fn from_content_type(ct: ContentType) -> Self {
60 match ct {
61 ContentType::Text => ViewKind::Text,
62 ContentType::Thinking => ViewKind::Thinking,
63 ContentType::ToolCall => ViewKind::ToolCall,
64 ContentType::ToolResult => ViewKind::ToolResult,
65 ContentType::Resource => ViewKind::Resource,
66 ContentType::ResourceRef => ViewKind::ResourceRef,
67 ContentType::PromptRequest => ViewKind::PromptRequest,
68 ContentType::PromptResult => ViewKind::PromptResult,
69 ContentType::Image => ViewKind::Image,
70 ContentType::Video => ViewKind::Video,
71 ContentType::Audio => ViewKind::Audio,
72 ContentType::Document => ViewKind::Document,
73 }
74 }
75
76 pub fn default_action(&self, role: Role) -> ViewAction {
78 match self {
79 ViewKind::ToolCall => ViewAction::Execute,
80 ViewKind::ToolResult => ViewAction::Receive,
81 ViewKind::Resource | ViewKind::ResourceRef => ViewAction::Read,
82 ViewKind::PromptRequest => ViewAction::Invoke,
83 ViewKind::PromptResult => ViewAction::Receive,
84 ViewKind::Text
86 | ViewKind::Thinking
87 | ViewKind::Image
88 | ViewKind::Video
89 | ViewKind::Audio
90 | ViewKind::Document => match role {
91 Role::User => ViewAction::Send,
92 Role::Assistant => ViewAction::Generate,
93 Role::Tool => ViewAction::Receive,
94 Role::System | Role::Developer => ViewAction::Write,
95 },
96 }
97 }
98
99 pub fn is_tool(&self) -> bool {
101 matches!(self, ViewKind::ToolCall | ViewKind::ToolResult)
102 }
103
104 pub fn is_resource(&self) -> bool {
106 matches!(self, ViewKind::Resource | ViewKind::ResourceRef)
107 }
108
109 pub fn is_prompt(&self) -> bool {
111 matches!(self, ViewKind::PromptRequest | ViewKind::PromptResult)
112 }
113
114 pub fn is_media(&self) -> bool {
116 matches!(
117 self,
118 ViewKind::Image | ViewKind::Video | ViewKind::Audio | ViewKind::Document
119 )
120 }
121
122 pub fn is_text(&self) -> bool {
124 matches!(self, ViewKind::Text | ViewKind::Thinking)
125 }
126}
127
128pub struct MessageView<'a> {
140 part: &'a ContentPart,
142 kind: ViewKind,
144 role: Role,
146 hook: Option<&'a str>,
148 extensions: Option<&'a Extensions>,
150}
151
152impl<'a> MessageView<'a> {
153 pub fn new(
155 part: &'a ContentPart,
156 role: Role,
157 hook: Option<&'a str>,
158 extensions: Option<&'a Extensions>,
159 ) -> Self {
160 let kind = match part {
161 ContentPart::Text { .. } => ViewKind::Text,
162 ContentPart::Thinking { .. } => ViewKind::Thinking,
163 ContentPart::ToolCall { .. } => ViewKind::ToolCall,
164 ContentPart::ToolResult { .. } => ViewKind::ToolResult,
165 ContentPart::Resource { .. } => ViewKind::Resource,
166 ContentPart::ResourceRef { .. } => ViewKind::ResourceRef,
167 ContentPart::PromptRequest { .. } => ViewKind::PromptRequest,
168 ContentPart::PromptResult { .. } => ViewKind::PromptResult,
169 ContentPart::Image { .. } => ViewKind::Image,
170 ContentPart::Video { .. } => ViewKind::Video,
171 ContentPart::Audio { .. } => ViewKind::Audio,
172 ContentPart::Document { .. } => ViewKind::Document,
173 };
174
175 Self {
176 part,
177 kind,
178 role,
179 hook,
180 extensions,
181 }
182 }
183
184 pub fn kind(&self) -> ViewKind {
188 self.kind
189 }
190
191 pub fn role(&self) -> Role {
193 self.role
194 }
195
196 pub fn raw(&self) -> &'a ContentPart {
198 self.part
199 }
200
201 pub fn hook(&self) -> Option<&str> {
203 self.hook
204 }
205
206 pub fn action(&self) -> ViewAction {
208 self.kind.default_action(self.role)
209 }
210
211 pub fn is_pre(&self) -> bool {
215 self.hook.is_some_and(|h| h.contains("pre"))
216 }
217
218 pub fn is_post(&self) -> bool {
220 self.hook.is_some_and(|h| h.contains("post"))
221 }
222
223 pub fn content(&self) -> Option<&str> {
227 match self.part {
228 ContentPart::Text { text } | ContentPart::Thinking { text } => Some(text),
229 ContentPart::ToolResult { content: tr } => {
230 tr.content.as_str().map(Some).unwrap_or(None)
231 },
232 ContentPart::Resource { content: r } => r.content.as_deref(),
233 ContentPart::PromptResult { content: pr } => pr.content.as_deref(),
234 _ => None,
235 }
236 }
237
238 pub fn name(&self) -> Option<&str> {
240 match self.part {
241 ContentPart::ToolCall { content: tc } => Some(&tc.name),
242 ContentPart::ToolResult { content: tr } => Some(&tr.tool_name),
243 ContentPart::Resource { content: r } => r.name.as_deref().or(Some(&r.uri)),
244 ContentPart::ResourceRef { content: rr } => rr.name.as_deref().or(Some(&rr.uri)),
245 ContentPart::PromptRequest { content: pr } => Some(&pr.name),
246 ContentPart::PromptResult { content: pr } => Some(&pr.prompt_name),
247 _ => None,
248 }
249 }
250
251 pub fn uri(&self) -> Option<String> {
253 match self.part {
254 ContentPart::ToolCall { content: tc } => Some(format!("tool://_/{}", tc.name)),
255 ContentPart::Resource { content: r } => Some(r.uri.clone()),
256 ContentPart::ResourceRef { content: rr } => Some(rr.uri.clone()),
257 ContentPart::PromptRequest { content: pr } => Some(format!("prompt://_/{}", pr.name)),
258 _ => None,
259 }
260 }
261
262 pub fn args(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
264 match self.part {
265 ContentPart::ToolCall { content: tc } => Some(&tc.arguments),
266 ContentPart::PromptRequest { content: pr } => Some(&pr.arguments),
267 _ => None,
268 }
269 }
270
271 pub fn get_arg(&self, name: &str) -> Option<&serde_json::Value> {
273 self.args().and_then(|a| a.get(name))
274 }
275
276 pub fn has_arg(&self, name: &str) -> bool {
278 self.get_arg(name).is_some()
279 }
280
281 pub fn mime_type(&self) -> Option<&str> {
283 match self.part {
284 ContentPart::Resource { content: r } => r.mime_type.as_deref(),
285 ContentPart::Image { content: img } => img.media_type.as_deref(),
286 ContentPart::Video { content: vid } => vid.media_type.as_deref(),
287 ContentPart::Audio { content: aud } => aud.media_type.as_deref(),
288 ContentPart::Document { content: doc } => doc.media_type.as_deref(),
289 _ => None,
290 }
291 }
292
293 pub fn is_error(&self) -> bool {
295 match self.part {
296 ContentPart::ToolResult { content: tr } => tr.is_error,
297 ContentPart::PromptResult { content: pr } => pr.is_error,
298 _ => false,
299 }
300 }
301
302 pub fn is_tool(&self) -> bool {
305 self.kind.is_tool()
306 }
307 pub fn is_resource(&self) -> bool {
308 self.kind.is_resource()
309 }
310 pub fn is_prompt(&self) -> bool {
311 self.kind.is_prompt()
312 }
313 pub fn is_media(&self) -> bool {
314 self.kind.is_media()
315 }
316 pub fn is_text(&self) -> bool {
317 self.kind.is_text()
318 }
319
320 pub fn extensions(&self) -> Option<&'a Extensions> {
324 self.extensions
325 }
326
327 pub fn has_label(&self, label: &str) -> bool {
329 self.extensions
330 .and_then(|e| e.security.as_ref())
331 .map(|s| s.has_label(label))
332 .unwrap_or(false)
333 }
334
335 pub fn get_header(&self, name: &str) -> Option<&str> {
337 self.extensions
338 .and_then(|e| e.http.as_ref())
339 .and_then(|h| h.get_header(name))
340 }
341
342 const SENSITIVE_HEADERS: &'static [&'static str] = &["authorization", "cookie", "x-api-key"];
346
347 pub fn to_dict(&self, include_content: bool, include_context: bool) -> serde_json::Value {
353 use super::constants::*;
354
355 let mut result = serde_json::Map::new();
356
357 result.insert(FIELD_KIND.into(), serde_json::json!(self.kind));
359 result.insert(FIELD_ROLE.into(), serde_json::json!(self.role));
360 result.insert(FIELD_IS_PRE.into(), serde_json::json!(self.is_pre()));
361 result.insert(FIELD_IS_POST.into(), serde_json::json!(self.is_post()));
362 result.insert(FIELD_ACTION.into(), serde_json::json!(self.action()));
363
364 if let Some(hook) = self.hook {
365 result.insert(FIELD_HOOK.into(), serde_json::json!(hook));
366 }
367
368 if let Some(uri) = self.uri() {
369 result.insert(FIELD_URI.into(), serde_json::json!(uri));
370 }
371
372 if let Some(name) = self.name() {
373 result.insert(FIELD_NAME.into(), serde_json::json!(name));
374 }
375
376 if include_content {
378 if let Some(text) = self.content() {
379 result.insert(FIELD_SIZE_BYTES.into(), serde_json::json!(text.len()));
380 result.insert(FIELD_CONTENT.into(), serde_json::json!(text));
381 }
382 }
383
384 if let Some(mime) = self.mime_type() {
385 result.insert(FIELD_MIME_TYPE.into(), serde_json::json!(mime));
386 }
387
388 if let Some(args) = self.args() {
390 result.insert(FIELD_ARGUMENTS.into(), serde_json::json!(args));
391 }
392
393 if include_context {
395 if let Some(ext) = self.extensions {
396 let mut ext_map = serde_json::Map::new();
397
398 if let Some(ref sec) = ext.security {
400 if let Some(ref subject) = sec.subject {
401 let mut sub_map = serde_json::Map::new();
402 if let Some(ref id) = subject.id {
403 sub_map.insert(FIELD_ID.into(), serde_json::json!(id));
404 }
405 if let Some(ref st) = subject.subject_type {
406 sub_map.insert(FIELD_TYPE.into(), serde_json::json!(st));
407 }
408 if !subject.roles.is_empty() {
409 let mut roles: Vec<&String> = subject.roles.iter().collect();
410 roles.sort();
411 sub_map.insert(FIELD_ROLES.into(), serde_json::json!(roles));
412 }
413 if !subject.permissions.is_empty() {
414 let mut perms: Vec<&String> = subject.permissions.iter().collect();
415 perms.sort();
416 sub_map.insert(FIELD_PERMISSIONS.into(), serde_json::json!(perms));
417 }
418 if !subject.teams.is_empty() {
419 let mut teams: Vec<&String> = subject.teams.iter().collect();
420 teams.sort();
421 sub_map.insert(FIELD_TEAMS.into(), serde_json::json!(teams));
422 }
423 if !sub_map.is_empty() {
424 ext_map
425 .insert(FIELD_SUBJECT.into(), serde_json::Value::Object(sub_map));
426 }
427 }
428
429 if !sec.labels.is_empty() {
431 let mut labels: Vec<&String> = sec.labels.iter().collect();
432 labels.sort();
433 ext_map.insert(FIELD_LABELS.into(), serde_json::json!(labels));
434 }
435 }
436
437 if let Some(ref req) = ext.request {
439 if let Some(ref env) = req.environment {
440 ext_map.insert(FIELD_ENVIRONMENT.into(), serde_json::json!(env));
441 }
442 }
443
444 if let Some(ref http) = ext.http {
446 let safe: std::collections::HashMap<&String, &String> = http
447 .request_headers
448 .iter()
449 .filter(|(k, _)| {
450 !Self::SENSITIVE_HEADERS.contains(&k.to_lowercase().as_str())
451 })
452 .collect();
453 if !safe.is_empty() {
454 ext_map.insert(FIELD_HEADERS.into(), serde_json::json!(safe));
455 }
456 }
457
458 if let Some(ref agent) = ext.agent {
460 let mut agent_map = serde_json::Map::new();
461 if let Some(ref input) = agent.input {
462 agent_map.insert(FIELD_INPUT.into(), serde_json::json!(input));
463 }
464 if let Some(ref sid) = agent.session_id {
465 agent_map.insert(FIELD_SESSION_ID.into(), serde_json::json!(sid));
466 }
467 if let Some(ref cid) = agent.conversation_id {
468 agent_map.insert(FIELD_CONVERSATION_ID.into(), serde_json::json!(cid));
469 }
470 if let Some(turn) = agent.turn {
471 agent_map.insert(FIELD_TURN.into(), serde_json::json!(turn));
472 }
473 if let Some(ref aid) = agent.agent_id {
474 agent_map.insert(FIELD_AGENT_ID.into(), serde_json::json!(aid));
475 }
476 if let Some(ref paid) = agent.parent_agent_id {
477 agent_map.insert(FIELD_PARENT_AGENT_ID.into(), serde_json::json!(paid));
478 }
479 if !agent_map.is_empty() {
480 ext_map.insert(FIELD_AGENT.into(), serde_json::Value::Object(agent_map));
481 }
482 }
483
484 if let Some(ref meta) = ext.meta {
486 let mut meta_map = serde_json::Map::new();
487 if let Some(ref et) = meta.entity_type {
488 meta_map.insert(FIELD_ENTITY_TYPE.into(), serde_json::json!(et));
489 }
490 if let Some(ref en) = meta.entity_name {
491 meta_map.insert(FIELD_ENTITY_NAME.into(), serde_json::json!(en));
492 }
493 if !meta.tags.is_empty() {
494 let mut tags: Vec<&String> = meta.tags.iter().collect();
495 tags.sort();
496 meta_map.insert(FIELD_TAGS.into(), serde_json::json!(tags));
497 }
498 if !meta_map.is_empty() {
499 ext_map.insert(FIELD_META.into(), serde_json::Value::Object(meta_map));
500 }
501 }
502
503 if !ext_map.is_empty() {
504 result.insert(FIELD_EXTENSIONS.into(), serde_json::Value::Object(ext_map));
505 }
506 }
507 }
508
509 serde_json::Value::Object(result)
510 }
511
512 pub fn to_opa_input(&self, include_content: bool) -> serde_json::Value {
517 use super::constants::FIELD_OPA_INPUT;
518 serde_json::json!({
519 FIELD_OPA_INPUT: self.to_dict(include_content, true)
520 })
521 }
522}
523
524impl<'a> std::fmt::Debug for MessageView<'a> {
525 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
526 f.debug_struct("MessageView")
527 .field("kind", &self.kind)
528 .field("role", &self.role)
529 .field("name", &self.name())
530 .field("hook", &self.hook)
531 .finish()
532 }
533}
534
535pub fn iter_views<'a>(
544 message: &'a Message,
545 hook: Option<&'a str>,
546 extensions: Option<&'a Extensions>,
547) -> impl Iterator<Item = MessageView<'a>> {
548 message
549 .content
550 .iter()
551 .map(move |part| MessageView::new(part, message.role, hook, extensions))
552}
553
554impl Message {
556 pub fn iter_views<'a>(
561 &'a self,
562 hook: Option<&'a str>,
563 extensions: Option<&'a Extensions>,
564 ) -> impl Iterator<Item = MessageView<'a>> {
565 iter_views(self, hook, extensions)
566 }
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572 use crate::cmf::enums::Role;
573 use crate::hooks::payload::MetaExtension;
574
575 fn make_test_message() -> Message {
576 Message {
577 schema_version: "2.0".into(),
578 role: Role::Assistant,
579 content: vec![
580 ContentPart::Thinking {
581 text: "Let me think...".into(),
582 },
583 ContentPart::Text {
584 text: "Here's the answer.".into(),
585 },
586 ContentPart::ToolCall {
587 content: ToolCall {
588 tool_call_id: "tc_001".into(),
589 name: "get_weather".into(),
590 arguments: [("city".to_string(), serde_json::json!("London"))].into(),
591 namespace: None,
592 },
593 },
594 ContentPart::Resource {
595 content: Resource {
596 resource_request_id: "rr_001".into(),
597 uri: "file:///data.csv".into(),
598 name: Some("Data File".into()),
599 resource_type: crate::cmf::enums::ResourceType::File,
600 content: Some("col1,col2".into()),
601 mime_type: Some("text/csv".into()),
602 ..Default::default()
603 },
604 },
605 ],
606 channel: None,
607 }
608 }
609
610 #[test]
611 fn test_iter_views_count() {
612 let msg = make_test_message();
613 let views: Vec<_> = msg.iter_views(None, None).collect();
614 assert_eq!(views.len(), 4);
615 }
616
617 #[test]
618 fn test_view_kinds() {
619 let msg = make_test_message();
620 let views: Vec<_> = msg.iter_views(None, None).collect();
621 assert_eq!(views[0].kind(), ViewKind::Thinking);
622 assert_eq!(views[1].kind(), ViewKind::Text);
623 assert_eq!(views[2].kind(), ViewKind::ToolCall);
624 assert_eq!(views[3].kind(), ViewKind::Resource);
625 }
626
627 #[test]
628 fn test_view_content() {
629 let msg = make_test_message();
630 let views: Vec<_> = msg.iter_views(None, None).collect();
631 assert_eq!(views[0].content(), Some("Let me think..."));
632 assert_eq!(views[1].content(), Some("Here's the answer."));
633 assert!(views[2].content().is_none()); assert_eq!(views[3].content(), Some("col1,col2")); }
636
637 #[test]
638 fn test_view_name() {
639 let msg = make_test_message();
640 let views: Vec<_> = msg.iter_views(None, None).collect();
641 assert!(views[0].name().is_none()); assert!(views[1].name().is_none()); assert_eq!(views[2].name(), Some("get_weather"));
644 assert_eq!(views[3].name(), Some("Data File"));
645 }
646
647 #[test]
648 fn test_view_uri() {
649 let msg = make_test_message();
650 let views: Vec<_> = msg.iter_views(None, None).collect();
651 assert_eq!(views[2].uri(), Some("tool://_/get_weather".to_string()));
652 assert_eq!(views[3].uri(), Some("file:///data.csv".to_string()));
653 }
654
655 #[test]
656 fn test_view_args() {
657 let msg = make_test_message();
658 let views: Vec<_> = msg.iter_views(None, None).collect();
659 let tool_view = &views[2];
660 assert!(tool_view.has_arg("city"));
661 assert_eq!(tool_view.get_arg("city").unwrap(), "London");
662 assert!(!tool_view.has_arg("nonexistent"));
663 }
664
665 #[test]
666 fn test_view_action() {
667 let msg = make_test_message();
668 let views: Vec<_> = msg.iter_views(None, None).collect();
669 assert_eq!(views[0].action(), ViewAction::Generate); assert_eq!(views[1].action(), ViewAction::Generate); assert_eq!(views[2].action(), ViewAction::Execute); assert_eq!(views[3].action(), ViewAction::Read); }
674
675 #[test]
676 fn test_view_action_user_role() {
677 let msg = Message::text(Role::User, "Hello");
678 let views: Vec<_> = msg.iter_views(None, None).collect();
679 assert_eq!(views[0].action(), ViewAction::Send); }
681
682 #[test]
683 fn test_view_hook_pre_post() {
684 let msg = make_test_message();
685 let pre_views: Vec<_> = msg.iter_views(Some("tool_pre_invoke"), None).collect();
686 assert!(pre_views[0].is_pre());
687 assert!(!pre_views[0].is_post());
688
689 let post_views: Vec<_> = msg.iter_views(Some("tool_post_invoke"), None).collect();
690 assert!(post_views[0].is_post());
691 assert!(!post_views[0].is_pre());
692 }
693
694 #[test]
695 fn test_view_type_helpers() {
696 let msg = make_test_message();
697 let views: Vec<_> = msg.iter_views(None, None).collect();
698 assert!(views[0].is_text()); assert!(views[1].is_text()); assert!(views[2].is_tool()); assert!(views[3].is_resource()); }
703
704 #[test]
705 fn test_view_mime_type() {
706 let msg = make_test_message();
707 let views: Vec<_> = msg.iter_views(None, None).collect();
708 assert_eq!(views[3].mime_type(), Some("text/csv"));
709 }
710
711 #[test]
712 fn test_view_with_extensions() {
713 use crate::extensions::{HttpExtension, SecurityExtension};
714 use std::sync::Arc;
715
716 let mut security = SecurityExtension::default();
717 security.add_label("PII");
718
719 let mut http = HttpExtension::default();
720 http.set_header("Authorization", "Bearer tok");
721
722 let ext = Extensions {
723 security: Some(Arc::new(security)),
724 http: Some(Arc::new(http)),
725 ..Default::default()
726 };
727
728 let msg = make_test_message();
729 let views: Vec<_> = msg.iter_views(None, Some(&ext)).collect();
730
731 assert!(views[0].has_label("PII"));
732 assert!(!views[0].has_label("HIPAA"));
733 assert_eq!(views[0].get_header("Authorization"), Some("Bearer tok"));
734 }
735
736 #[test]
737 fn test_to_dict_basic() {
738 let msg = Message::text(Role::User, "Hello world");
739 let views: Vec<_> = msg.iter_views(Some("llm_input"), None).collect();
740 let dict = views[0].to_dict(true, false);
741
742 assert_eq!(dict["kind"], "text");
743 assert_eq!(dict["role"], "user");
744 assert_eq!(dict["action"], "send");
745 assert_eq!(dict["hook"], "llm_input");
746 assert_eq!(dict["content"], "Hello world");
747 assert_eq!(dict["size_bytes"], 11);
748 assert_eq!(dict["is_pre"], false);
749 assert_eq!(dict["is_post"], false);
750 }
751
752 #[test]
753 fn test_to_dict_tool_call() {
754 let msg = make_test_message();
755 let views: Vec<_> = msg.iter_views(Some("tool_pre_invoke"), None).collect();
756 let dict = views[2].to_dict(true, false); assert_eq!(dict["kind"], "tool_call");
759 assert_eq!(dict["name"], "get_weather");
760 assert_eq!(dict["uri"], "tool://_/get_weather");
761 assert_eq!(dict["action"], "execute");
762 assert_eq!(dict["is_pre"], true);
763 assert!(dict["arguments"].is_object());
764 assert_eq!(dict["arguments"]["city"], "London");
765 }
766
767 #[test]
768 fn test_to_dict_without_content() {
769 let msg = Message::text(Role::User, "Secret message");
770 let views: Vec<_> = msg.iter_views(None, None).collect();
771 let dict = views[0].to_dict(false, false);
772
773 assert!(dict.get("content").is_none());
774 assert!(dict.get("size_bytes").is_none());
775 }
776
777 #[test]
778 fn test_to_dict_with_extensions() {
779 use crate::extensions::{
780 AgentExtension, HttpExtension, RequestExtension, SecurityExtension,
781 };
782 use std::sync::Arc;
783
784 let mut security = SecurityExtension::default();
785 security.add_label("PII");
786 security.subject = Some(crate::extensions::security::SubjectExtension {
787 id: Some("alice".into()),
788 subject_type: Some(crate::extensions::security::SubjectType::User),
789 roles: ["admin".to_string()].into(),
790 ..Default::default()
791 });
792
793 let mut http = HttpExtension::default();
794 http.set_header("Authorization", "Bearer secret");
795 http.set_header("X-Request-ID", "req-123");
796
797 let ext = Extensions {
798 security: Some(Arc::new(security)),
799 http: Some(Arc::new(http)),
800 request: Some(Arc::new(RequestExtension {
801 environment: Some("production".into()),
802 ..Default::default()
803 })),
804 agent: Some(Arc::new(AgentExtension {
805 session_id: Some("sess-001".into()),
806 agent_id: Some("agent-x".into()),
807 ..Default::default()
808 })),
809 meta: Some(Arc::new(MetaExtension {
810 entity_type: Some("tool".into()),
811 entity_name: Some("get_compensation".into()),
812 tags: ["pii".to_string()].into(),
813 ..Default::default()
814 })),
815 ..Default::default()
816 };
817
818 let msg = Message::text(Role::User, "test");
819 let views: Vec<_> = msg.iter_views(None, Some(&ext)).collect();
820 let dict = views[0].to_dict(true, true);
821
822 let extensions = &dict["extensions"];
823
824 assert_eq!(extensions["subject"]["id"], "alice");
826 assert!(extensions["subject"]["roles"]
827 .as_array()
828 .unwrap()
829 .contains(&serde_json::json!("admin")));
830
831 assert!(extensions["labels"]
833 .as_array()
834 .unwrap()
835 .contains(&serde_json::json!("PII")));
836
837 assert_eq!(extensions["environment"], "production");
839
840 assert!(extensions["headers"].get("Authorization").is_none());
842 assert_eq!(extensions["headers"]["X-Request-ID"], "req-123");
843
844 assert_eq!(extensions["agent"]["session_id"], "sess-001");
846 assert_eq!(extensions["agent"]["agent_id"], "agent-x");
847
848 assert_eq!(extensions["meta"]["entity_type"], "tool");
850 assert_eq!(extensions["meta"]["entity_name"], "get_compensation");
851 }
852
853 #[test]
854 fn test_to_opa_input() {
855 let msg = Message::text(Role::User, "Hello");
856 let views: Vec<_> = msg.iter_views(None, None).collect();
857 let opa = views[0].to_opa_input(true);
858
859 assert!(opa.get("input").is_some());
860 assert_eq!(opa["input"]["kind"], "text");
861 assert_eq!(opa["input"]["role"], "user");
862 assert_eq!(opa["input"]["content"], "Hello");
863 }
864}