feat: add resource_link support to tools and prompts (#381)

* feat: add resource_link support to tools and prompts

* chore: remove unused serde_json import from test_resource_link_integration.rs

* chore: remove unused serde_json import from test_resource_link.rs
This commit is contained in:
Andrew Harvard 2025-08-29 06:13:38 -04:00 • committed by GitHub
commit 98b77fd94a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 2129 additions and 7 deletions

View file

@ -51,13 +51,14 @@ pub struct RawAudioContent {
pub type AudioContent = Annotated<RawAudioContent>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
#[serde(tag = "type", rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum RawContent {
Text(RawTextContent),
Image(RawImageContent),
Resource(RawEmbeddedResource),
Audio(AudioContent),
ResourceLink(super::resource::RawResource),
}
pub type Content = Annotated<RawContent>;
@ -123,6 +124,19 @@ impl RawContent {
_ => None,
}
}
/// Get the resource link if this is a ResourceLink variant
pub fn as_resource_link(&self) -> Option<&super::resource::RawResource> {
match self {
RawContent::ResourceLink(link) => Some(link),
_ => None,
}
}
/// Create a resource link content
pub fn resource_link(resource: super::resource::RawResource) -> Self {
RawContent::ResourceLink(resource)
}
}
impl Content {
@ -145,6 +159,11 @@ impl Content {
pub fn json<S: Serialize>(json: S) -> Result<Self, crate::ErrorData> {
RawContent::json(json).map(|c| c.no_annotation())
}
/// Create a resource link content
pub fn resource_link(resource: super::resource::RawResource) -> Self {
RawContent::resource_link(resource).no_annotation()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -207,4 +226,47 @@ mod tests {
assert!(json.contains("mimeType"));
assert!(!json.contains("mime_type"));
}
#[test]
fn test_resource_link_serialization() {
use super::super::resource::RawResource;
let resource_link = RawContent::ResourceLink(RawResource {
uri: "file:///test.txt".to_string(),
name: "test.txt".to_string(),
description: Some("A test file".to_string()),
mime_type: Some("text/plain".to_string()),
size: Some(100),
});
let json = serde_json::to_string(&resource_link).unwrap();
println!("ResourceLink JSON: {}", json);
// Verify it contains the correct type tag
assert!(json.contains("\"type\":\"resource_link\""));
assert!(json.contains("\"uri\":\"file:///test.txt\""));
assert!(json.contains("\"name\":\"test.txt\""));
}
#[test]
fn test_resource_link_deserialization() {
let json = r#"{
"type": "resource_link",
"uri": "file:///example.txt",
"name": "example.txt",
"description": "Example file",
"mimeType": "text/plain"
}"#;
let content: RawContent = serde_json::from_str(json).unwrap();
if let RawContent::ResourceLink(resource) = content {
assert_eq!(resource.uri, "file:///example.txt");
assert_eq!(resource.name, "example.txt");
assert_eq!(resource.description, Some("Example file".to_string()));
assert_eq!(resource.mime_type, Some("text/plain".to_string()));
} else {
panic!("Expected ResourceLink variant");
}
}
}

View file

@ -66,7 +66,7 @@ pub enum PromptMessageRole {
/// Content types that can be included in prompt messages
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
#[serde(tag = "type", rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum PromptMessageContent {
/// Plain text content
@ -78,12 +78,22 @@ pub enum PromptMessageContent {
},
/// Embedded server-side resource
Resource { resource: EmbeddedResource },
/// A link to a resource that can be fetched separately
ResourceLink {
#[serde(flatten)]
link: super::resource::Resource,
},
}
impl PromptMessageContent {
pub fn text(text: impl Into<String>) -> Self {
Self::Text { text: text.into() }
}
/// Create a resource link content
pub fn resource_link(resource: super::resource::Resource) -> Self {
Self::ResourceLink { link: resource }
}
}
/// A message in a prompt conversation
@ -151,6 +161,14 @@ impl PromptMessage {
},
}
}
/// Create a new resource link message
pub fn new_resource_link(role: PromptMessageRole, resource: super::resource::Resource) -> Self {
Self {
role,
content: PromptMessageContent::ResourceLink { link: resource },
}
}
}
#[cfg(test)]
@ -173,4 +191,43 @@ mod tests {
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);
// Verify it contains the correct type tag
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");
}
}
}