1use agent_client_protocol::schema::v2 as acp;
2use llm::ContentBlock as LlmContentBlock;
3
4pub fn map_acp_to_content_blocks(blocks: Vec<acp::ContentBlock>) -> Vec<LlmContentBlock> {
5 blocks
6 .into_iter()
7 .map(|block| match block {
8 acp::ContentBlock::Text(text) => LlmContentBlock::text(text.text),
9 acp::ContentBlock::Image(image) => {
10 LlmContentBlock::Image { data: image.data, mime_type: image.mime_type.to_string() }
11 }
12 acp::ContentBlock::Audio(audio) => {
13 LlmContentBlock::Audio { data: audio.data, mime_type: audio.mime_type.to_string() }
14 }
15 acp::ContentBlock::ResourceLink(link) => LlmContentBlock::text(format!("[Resource: {}]", link.uri)),
16 acp::ContentBlock::Resource(resource) => LlmContentBlock::text(format_embedded_resource(&resource)),
17 _ => LlmContentBlock::text("[Unknown content]"),
18 })
19 .collect()
20}
21
22pub fn map_user_content_block(block: &LlmContentBlock) -> acp::ContentBlock {
23 match block {
24 LlmContentBlock::Text { text } => acp::ContentBlock::from(text.clone()),
25 LlmContentBlock::Image { data, mime_type } => {
26 acp::ContentBlock::Image(acp::ImageContent::new(data.clone(), mime_type.clone()))
27 }
28 LlmContentBlock::Audio { data, mime_type } => {
29 acp::ContentBlock::Audio(acp::AudioContent::new(data.clone(), mime_type.clone()))
30 }
31 }
32}
33
34pub fn display_content_blocks(blocks: &[acp::ContentBlock]) -> Vec<acp::ContentBlock> {
35 blocks
36 .iter()
37 .map(|block| match block {
38 acp::ContentBlock::Resource(resource) => {
39 let uri = match &resource.resource {
40 acp::EmbeddedResourceResource::TextResourceContents(text) => &text.uri,
41 acp::EmbeddedResourceResource::BlobResourceContents(blob) => &blob.uri,
42 _ => return acp::ContentBlock::from("[Unknown resource type]"),
43 };
44 acp::ContentBlock::from(format!("[Resource: {uri}]"))
45 }
46 block => block.clone(),
47 })
48 .collect()
49}
50
51pub fn map_content_blocks_to_text(blocks: Vec<acp::ContentBlock>) -> String {
56 map_acp_to_content_blocks(blocks)
57 .into_iter()
58 .map(|block| match block {
59 LlmContentBlock::Text { text } => text,
60 LlmContentBlock::Image { .. } => "[Image content]".to_string(),
61 LlmContentBlock::Audio { .. } => "[Audio content]".to_string(),
62 })
63 .collect::<Vec<_>>()
64 .join("\n")
65}
66
67pub fn format_embedded_resource(resource: &acp::EmbeddedResource) -> String {
69 match &resource.resource {
70 acp::EmbeddedResourceResource::TextResourceContents(text) => {
71 format!("<file uri=\"{}\">\n{}\n</file>", text.uri, text.text)
72 }
73 acp::EmbeddedResourceResource::BlobResourceContents(blob) => {
74 format!("[Binary resource: {}]", blob.uri)
75 }
76 _ => "[Unknown resource type]".to_string(),
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn prompt_conversion_preserves_media_and_shares_resource_display() {
86 let media = vec![
87 acp::ContentBlock::Image(acp::ImageContent::new("image-data", "image/png")),
88 acp::ContentBlock::Audio(acp::AudioContent::new("audio-data", "audio/wav")),
89 ];
90 let converted = map_acp_to_content_blocks(media.clone());
91 assert_eq!(converted.iter().map(map_user_content_block).collect::<Vec<_>>(), media);
92 assert_eq!(map_content_blocks_to_text(media), "[Image content]\n[Audio content]");
93 let resources = vec![
94 acp::ContentBlock::ResourceLink(acp::ResourceLink::new("readme", "file://readme")),
95 acp::ContentBlock::Resource(acp::EmbeddedResource::new(
96 acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents::new(
97 "contents",
98 "file://source",
99 )),
100 )),
101 ];
102 let converted = map_acp_to_content_blocks(resources.clone());
103 let text = converted
104 .iter()
105 .map(|block| match block {
106 LlmContentBlock::Text { text } => text.as_str(),
107 _ => panic!("resources must become text"),
108 })
109 .collect::<Vec<_>>()
110 .join("\n");
111 assert_eq!(text, map_content_blocks_to_text(resources));
112 }
113
114 #[test]
115 fn test_format_embedded_resource_text() {
116 let resource = acp::EmbeddedResource::new(acp::EmbeddedResourceResource::TextResourceContents(
117 acp::TextResourceContents::new("let x = 1;", "file://test.rs"),
118 ));
119
120 let result = format_embedded_resource(&resource);
121
122 assert_eq!(result, "<file uri=\"file://test.rs\">\nlet x = 1;\n</file>");
123 }
124
125 #[test]
126 fn test_format_embedded_resource_blob() {
127 let resource = acp::EmbeddedResource::new(acp::EmbeddedResourceResource::BlobResourceContents(
128 acp::BlobResourceContents::new("base64data", "file://image.png"),
129 ));
130
131 let result = format_embedded_resource(&resource);
132
133 assert_eq!(result, "[Binary resource: file://image.png]");
134 }
135
136 #[test]
137 fn test_map_content_blocks_to_text_with_embedded_resource() {
138 let blocks = vec![
139 acp::ContentBlock::Text(acp::TextContent::new("Check this file:")),
140 acp::ContentBlock::Resource(acp::EmbeddedResource::new(
141 acp::EmbeddedResourceResource::TextResourceContents(
142 acp::TextResourceContents::new("pub fn hello() {}", "file://src/lib.rs").mime_type("text/x-rust"),
143 ),
144 )),
145 ];
146
147 let result = map_content_blocks_to_text(blocks);
148
149 assert!(result.contains("Check this file:"));
150 assert!(result.contains("<file uri=\"file://src/lib.rs\">"));
151 assert!(result.contains("pub fn hello() {}"));
152 assert!(result.contains("</file>"));
153 }
154
155 #[test]
156 fn test_map_content_blocks_text_only() {
157 let blocks = vec![
158 acp::ContentBlock::Text(acp::TextContent::new("Hello")),
159 acp::ContentBlock::Text(acp::TextContent::new("World")),
160 ];
161
162 assert_eq!(map_content_blocks_to_text(blocks), "Hello\nWorld");
163 }
164
165 #[test]
166 fn test_map_content_blocks_empty() {
167 assert_eq!(map_content_blocks_to_text(vec![]), "");
168 }
169
170 #[test]
171 fn test_map_content_blocks_resource_link() {
172 let blocks = vec![acp::ContentBlock::ResourceLink(acp::ResourceLink::new("readme.md", "file://readme.md"))];
173
174 let result = map_content_blocks_to_text(blocks);
175 assert_eq!(result, "[Resource: file://readme.md]");
176 }
177}