Skip to main content

acp_utils/
content.rs

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
51/// Converts ACP `ContentBlock` to plain text.
52///
53/// Embedded resources (e.g., file attachments) are formatted with their URI
54/// and content for inclusion in the agent's context.
55pub 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
67/// Formats an embedded resource as text for inclusion in agent context.
68pub 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}