Skip to main content

agenterra_rmcp/model/
content.rs

1//! Content sent around agents, extensions, and LLMs
2//! The various content types can be display to humans but also understood by models
3//! They include optional annotations used to help inform agent usage
4use serde::{Deserialize, Serialize};
5use serde_json::json;
6
7use super::{AnnotateAble, Annotated, resource::ResourceContents};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
12pub struct RawTextContent {
13    pub text: String,
14}
15pub type TextContent = Annotated<RawTextContent>;
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
19pub struct RawImageContent {
20    /// The base64-encoded image
21    pub data: String,
22    pub mime_type: String,
23}
24
25pub type ImageContent = Annotated<RawImageContent>;
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
29pub struct RawEmbeddedResource {
30    pub resource: ResourceContents,
31}
32pub type EmbeddedResource = Annotated<RawEmbeddedResource>;
33
34impl EmbeddedResource {
35    pub fn get_text(&self) -> String {
36        match &self.resource {
37            ResourceContents::TextResourceContents { text, .. } => text.clone(),
38            _ => String::new(),
39        }
40    }
41}
42
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
46pub struct RawAudioContent {
47    pub data: String,
48    pub mime_type: String,
49}
50
51pub type AudioContent = Annotated<RawAudioContent>;
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54#[serde(tag = "type", rename_all = "camelCase")]
55#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
56pub enum RawContent {
57    Text(RawTextContent),
58    Image(RawImageContent),
59    Resource(RawEmbeddedResource),
60    Audio(AudioContent),
61}
62
63pub type Content = Annotated<RawContent>;
64
65impl RawContent {
66    pub fn json<S: Serialize>(json: S) -> Result<Self, crate::Error> {
67        let json = serde_json::to_string(&json).map_err(|e| {
68            crate::Error::internal_error(
69                "fail to serialize response to json",
70                Some(json!(
71                    {"reason": e.to_string()}
72                )),
73            )
74        })?;
75        Ok(RawContent::text(json))
76    }
77
78    pub fn text<S: Into<String>>(text: S) -> Self {
79        RawContent::Text(RawTextContent { text: text.into() })
80    }
81
82    pub fn image<S: Into<String>, T: Into<String>>(data: S, mime_type: T) -> Self {
83        RawContent::Image(RawImageContent {
84            data: data.into(),
85            mime_type: mime_type.into(),
86        })
87    }
88
89    pub fn resource(resource: ResourceContents) -> Self {
90        RawContent::Resource(RawEmbeddedResource { resource })
91    }
92
93    pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
94        RawContent::Resource(RawEmbeddedResource {
95            resource: ResourceContents::TextResourceContents {
96                uri: uri.into(),
97                mime_type: Some("text".to_string()),
98                text: content.into(),
99            },
100        })
101    }
102
103    /// Get the text content if this is a TextContent variant
104    pub fn as_text(&self) -> Option<&RawTextContent> {
105        match self {
106            RawContent::Text(text) => Some(text),
107            _ => None,
108        }
109    }
110
111    /// Get the image content if this is an ImageContent variant
112    pub fn as_image(&self) -> Option<&RawImageContent> {
113        match self {
114            RawContent::Image(image) => Some(image),
115            _ => None,
116        }
117    }
118
119    /// Get the resource content if this is an ImageContent variant
120    pub fn as_resource(&self) -> Option<&RawEmbeddedResource> {
121        match self {
122            RawContent::Resource(resource) => Some(resource),
123            _ => None,
124        }
125    }
126}
127
128impl Content {
129    pub fn text<S: Into<String>>(text: S) -> Self {
130        RawContent::text(text).no_annotation()
131    }
132
133    pub fn image<S: Into<String>, T: Into<String>>(data: S, mime_type: T) -> Self {
134        RawContent::image(data, mime_type).no_annotation()
135    }
136
137    pub fn resource(resource: ResourceContents) -> Self {
138        RawContent::resource(resource).no_annotation()
139    }
140
141    pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
142        RawContent::embedded_text(uri, content).no_annotation()
143    }
144
145    pub fn json<S: Serialize>(json: S) -> Result<Self, crate::Error> {
146        RawContent::json(json).map(|c| c.no_annotation())
147    }
148}
149
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151pub struct JsonContent<S: Serialize>(S);
152/// Types that can be converted into a list of contents
153pub trait IntoContents {
154    fn into_contents(self) -> Vec<Content>;
155}
156
157impl IntoContents for Content {
158    fn into_contents(self) -> Vec<Content> {
159        vec![self]
160    }
161}
162
163impl IntoContents for String {
164    fn into_contents(self) -> Vec<Content> {
165        vec![Content::text(self)]
166    }
167}