use serde::{Deserialize, Serialize};
use super::{
AnnotateAble, Annotations, Icon, Meta, RawEmbeddedResource,
content::{EmbeddedResource, ImageContent},
resource::ResourceContents,
};
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct Prompt {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Icon>>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
}
impl Prompt {
pub fn new<N, D>(
name: N,
description: Option<D>,
arguments: Option<Vec<PromptArgument>>,
) -> Self
where
N: Into<String>,
D: Into<String>,
{
Prompt {
name: name.into(),
title: None,
description: description.map(Into::into),
arguments,
icons: None,
meta: None,
}
}
pub fn from_raw(
name: impl Into<String>,
description: Option<impl Into<String>>,
arguments: Option<Vec<PromptArgument>>,
) -> Self {
Prompt {
name: name.into(),
title: None,
description: description.map(Into::into),
arguments,
icons: None,
meta: None,
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
self.icons = Some(icons);
self
}
pub fn with_meta(mut self, meta: Meta) -> Self {
self.meta = Some(meta);
self
}
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct PromptArgument {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
}
impl PromptArgument {
pub fn new<N: Into<String>>(name: N) -> Self {
PromptArgument {
name: name.into(),
title: None,
description: None,
required: None,
}
}
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
self.title = Some(title.into());
self
}
pub fn with_description<D: Into<String>>(mut self, description: D) -> Self {
self.description = Some(description.into());
self
}
pub fn with_required(mut self, required: bool) -> Self {
self.required = Some(required);
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
pub enum PromptMessageRole {
User,
Assistant,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
pub enum PromptMessageContent {
Text { text: String },
Image {
#[serde(flatten)]
image: ImageContent,
},
Resource { resource: EmbeddedResource },
ResourceLink {
#[serde(flatten)]
link: super::resource::Resource,
},
}
impl PromptMessageContent {
pub fn text(text: impl Into<String>) -> Self {
Self::Text { text: text.into() }
}
pub fn resource_link(resource: super::resource::Resource) -> Self {
Self::ResourceLink { link: resource }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct PromptMessage {
pub role: PromptMessageRole,
pub content: PromptMessageContent,
}
impl PromptMessage {
pub fn new(role: PromptMessageRole, content: PromptMessageContent) -> Self {
Self { role, content }
}
pub fn new_text<S: Into<String>>(role: PromptMessageRole, text: S) -> Self {
Self {
role,
content: PromptMessageContent::Text { text: text.into() },
}
}
#[cfg(feature = "base64")]
pub fn new_image(
role: PromptMessageRole,
data: &[u8],
mime_type: &str,
meta: Option<crate::model::Meta>,
annotations: Option<Annotations>,
) -> Self {
use base64::{Engine, prelude::BASE64_STANDARD};
let base64 = BASE64_STANDARD.encode(data);
Self {
role,
content: PromptMessageContent::Image {
image: crate::model::RawImageContent {
data: base64,
mime_type: mime_type.into(),
meta,
}
.optional_annotate(annotations),
},
}
}
pub fn new_resource(
role: PromptMessageRole,
uri: String,
mime_type: Option<String>,
text: Option<String>,
resource_meta: Option<crate::model::Meta>,
resource_content_meta: Option<crate::model::Meta>,
annotations: Option<Annotations>,
) -> Self {
let resource_contents = match text {
Some(t) => ResourceContents::TextResourceContents {
uri,
mime_type,
text: t,
meta: resource_content_meta,
},
None => ResourceContents::BlobResourceContents {
uri,
mime_type,
blob: String::new(),
meta: resource_content_meta,
},
};
Self {
role,
content: PromptMessageContent::Resource {
resource: RawEmbeddedResource {
meta: resource_meta,
resource: resource_contents,
}
.optional_annotate(annotations),
},
}
}
pub fn new_text_with_meta<S: Into<String>>(
role: PromptMessageRole,
text: S,
_meta: Option<crate::model::Meta>,
) -> Self {
Self::new_text(role, text)
}
pub fn new_resource_link(role: PromptMessageRole, resource: super::resource::Resource) -> Self {
Self {
role,
content: PromptMessageContent::ResourceLink { link: resource },
}
}
}
#[cfg(test)]
mod tests {
use serde_json;
use super::*;
#[test]
fn test_prompt_message_image_serialization() {
let image_content = crate::model::RawImageContent {
data: "base64data".to_string(),
mime_type: "image/png".to_string(),
meta: None,
};
let json = serde_json::to_string(&image_content).unwrap();
println!("PromptMessage ImageContent JSON: {}", json);
assert!(json.contains("mimeType"));
assert!(!json.contains("mime_type"));
}
#[test]
fn test_prompt_message_resource_link_serialization() {
use super::super::resource::RawResource;
let resource = RawResource::new("file:///test.txt", "test.txt");
let message =
PromptMessage::new_resource_link(PromptMessageRole::User, resource.no_annotation());
let json = serde_json::to_string(&message).unwrap();
println!("PromptMessage with ResourceLink JSON: {}", json);
assert!(json.contains("\"type\":\"resource_link\""));
assert!(json.contains("\"uri\":\"file:///test.txt\""));
assert!(json.contains("\"name\":\"test.txt\""));
}
#[test]
fn test_prompt_message_content_resource_link_deserialization() {
let json = r#"{
"type": "resource_link",
"uri": "file:///example.txt",
"name": "example.txt",
"description": "Example file",
"mimeType": "text/plain"
}"#;
let content: PromptMessageContent = serde_json::from_str(json).unwrap();
if let PromptMessageContent::ResourceLink { link } = content {
assert_eq!(link.uri, "file:///example.txt");
assert_eq!(link.name, "example.txt");
assert_eq!(link.description, Some("Example file".to_string()));
assert_eq!(link.mime_type, Some("text/plain".to_string()));
} else {
panic!("Expected ResourceLink variant");
}
}
}