Skip to main content

acp_utils/
content.rs

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