mirror of
https://github.com/aljazceru/goose.git
synced 2026-01-07 00:14:23 +01:00
Replace mcp_core::content types with rmcp::model types (#3500)
This commit is contained in:
@@ -1,313 +0,0 @@
|
||||
/// Content sent around agents, extensions, and LLMs
|
||||
/// The various content types can be display to humans but also understood by models
|
||||
/// They include optional annotations used to help inform agent usage
|
||||
use super::Role;
|
||||
use crate::resource::ResourceContents;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Annotations {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub audience: Option<Vec<Role>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub priority: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(value_type = String, format = "date-time", example = "2023-01-01T00:00:00Z")]
|
||||
// for openapi
|
||||
pub timestamp: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl Annotations {
|
||||
/// Creates a new Annotations instance specifically for resources
|
||||
/// optional priority, and a timestamp (defaults to now if None)
|
||||
pub fn for_resource(priority: f32, timestamp: DateTime<Utc>) -> Self {
|
||||
assert!(
|
||||
(0.0..=1.0).contains(&priority),
|
||||
"Priority {priority} must be between 0.0 and 1.0"
|
||||
);
|
||||
Annotations {
|
||||
priority: Some(priority),
|
||||
timestamp: Some(timestamp),
|
||||
audience: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TextContent {
|
||||
pub text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
}
|
||||
|
||||
#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImageContent {
|
||||
pub data: String,
|
||||
pub mime_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
}
|
||||
|
||||
#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmbeddedResource {
|
||||
pub resource: ResourceContents,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
}
|
||||
|
||||
impl EmbeddedResource {
|
||||
pub fn get_text(&self) -> String {
|
||||
match &self.resource {
|
||||
ResourceContents::TextResourceContents { text, .. } => text.clone(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum Content {
|
||||
Text(TextContent),
|
||||
Image(ImageContent),
|
||||
Resource(EmbeddedResource),
|
||||
}
|
||||
|
||||
impl Content {
|
||||
pub fn text<S: Into<String>>(text: S) -> Self {
|
||||
Content::Text(TextContent {
|
||||
text: text.into(),
|
||||
annotations: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn image<S: Into<String>, T: Into<String>>(data: S, mime_type: T) -> Self {
|
||||
Content::Image(ImageContent {
|
||||
data: data.into(),
|
||||
mime_type: mime_type.into(),
|
||||
annotations: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resource(resource: ResourceContents) -> Self {
|
||||
Content::Resource(EmbeddedResource {
|
||||
resource,
|
||||
annotations: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
|
||||
Content::Resource(EmbeddedResource {
|
||||
resource: ResourceContents::TextResourceContents {
|
||||
uri: uri.into(),
|
||||
mime_type: Some("text".to_string()),
|
||||
text: content.into(),
|
||||
},
|
||||
annotations: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the text content if this is a TextContent variant
|
||||
pub fn as_text(&self) -> Option<&str> {
|
||||
match self {
|
||||
Content::Text(text) => Some(&text.text),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the image content if this is an ImageContent variant
|
||||
pub fn as_image(&self) -> Option<(&str, &str)> {
|
||||
match self {
|
||||
Content::Image(image) => Some((&image.data, &image.mime_type)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the audience for the content
|
||||
pub fn with_audience(mut self, audience: Vec<Role>) -> Self {
|
||||
let annotations = match &mut self {
|
||||
Content::Text(text) => &mut text.annotations,
|
||||
Content::Image(image) => &mut image.annotations,
|
||||
Content::Resource(resource) => &mut resource.annotations,
|
||||
};
|
||||
*annotations = Some(match annotations.take() {
|
||||
Some(mut a) => {
|
||||
a.audience = Some(audience);
|
||||
a
|
||||
}
|
||||
None => Annotations {
|
||||
audience: Some(audience),
|
||||
priority: None,
|
||||
timestamp: None,
|
||||
},
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the priority for the content
|
||||
/// # Panics
|
||||
/// Panics if priority is not between 0.0 and 1.0 inclusive
|
||||
pub fn with_priority(mut self, priority: f32) -> Self {
|
||||
if !(0.0..=1.0).contains(&priority) {
|
||||
panic!("Priority must be between 0.0 and 1.0");
|
||||
}
|
||||
let annotations = match &mut self {
|
||||
Content::Text(text) => &mut text.annotations,
|
||||
Content::Image(image) => &mut image.annotations,
|
||||
Content::Resource(resource) => &mut resource.annotations,
|
||||
};
|
||||
*annotations = Some(match annotations.take() {
|
||||
Some(mut a) => {
|
||||
a.priority = Some(priority);
|
||||
a
|
||||
}
|
||||
None => Annotations {
|
||||
audience: None,
|
||||
priority: Some(priority),
|
||||
timestamp: None,
|
||||
},
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the audience if set
|
||||
pub fn audience(&self) -> Option<&Vec<Role>> {
|
||||
match self {
|
||||
Content::Text(text) => text.annotations.as_ref().and_then(|a| a.audience.as_ref()),
|
||||
Content::Image(image) => image.annotations.as_ref().and_then(|a| a.audience.as_ref()),
|
||||
Content::Resource(resource) => resource
|
||||
.annotations
|
||||
.as_ref()
|
||||
.and_then(|a| a.audience.as_ref()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the priority if set
|
||||
pub fn priority(&self) -> Option<f32> {
|
||||
match self {
|
||||
Content::Text(text) => text.annotations.as_ref().and_then(|a| a.priority),
|
||||
Content::Image(image) => image.annotations.as_ref().and_then(|a| a.priority),
|
||||
Content::Resource(resource) => resource.annotations.as_ref().and_then(|a| a.priority),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unannotated(&self) -> Self {
|
||||
match self {
|
||||
Content::Text(text) => Content::text(text.text.clone()),
|
||||
Content::Image(image) => Content::image(image.data.clone(), image.mime_type.clone()),
|
||||
Content::Resource(resource) => Content::resource(resource.resource.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_content_text() {
|
||||
let content = Content::text("hello");
|
||||
assert_eq!(content.as_text(), Some("hello"));
|
||||
assert_eq!(content.as_image(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_image() {
|
||||
let content = Content::image("data", "image/png");
|
||||
assert_eq!(content.as_text(), None);
|
||||
assert_eq!(content.as_image(), Some(("data", "image/png")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_annotations_basic() {
|
||||
let content = Content::text("hello")
|
||||
.with_audience(vec![Role::User])
|
||||
.with_priority(0.5);
|
||||
assert_eq!(content.audience(), Some(&vec![Role::User]));
|
||||
assert_eq!(content.priority(), Some(0.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_annotations_order_independence() {
|
||||
let content1 = Content::text("hello")
|
||||
.with_audience(vec![Role::User])
|
||||
.with_priority(0.5);
|
||||
let content2 = Content::text("hello")
|
||||
.with_priority(0.5)
|
||||
.with_audience(vec![Role::User]);
|
||||
|
||||
assert_eq!(content1.audience(), content2.audience());
|
||||
assert_eq!(content1.priority(), content2.priority());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_annotations_overwrite() {
|
||||
let content = Content::text("hello")
|
||||
.with_audience(vec![Role::User])
|
||||
.with_priority(0.5)
|
||||
.with_audience(vec![Role::Assistant])
|
||||
.with_priority(0.8);
|
||||
|
||||
assert_eq!(content.audience(), Some(&vec![Role::Assistant]));
|
||||
assert_eq!(content.priority(), Some(0.8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_annotations_image() {
|
||||
let content = Content::image("data", "image/png")
|
||||
.with_audience(vec![Role::User])
|
||||
.with_priority(0.5);
|
||||
|
||||
assert_eq!(content.audience(), Some(&vec![Role::User]));
|
||||
assert_eq!(content.priority(), Some(0.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_annotations_preservation() {
|
||||
let text_content = Content::text("hello")
|
||||
.with_audience(vec![Role::User])
|
||||
.with_priority(0.5);
|
||||
|
||||
match &text_content {
|
||||
Content::Text(TextContent { annotations, .. }) => {
|
||||
assert!(annotations.is_some());
|
||||
let ann = annotations.as_ref().unwrap();
|
||||
assert_eq!(ann.audience, Some(vec![Role::User]));
|
||||
assert_eq!(ann.priority, Some(0.5));
|
||||
}
|
||||
_ => panic!("Expected Text content"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Priority must be between 0.0 and 1.0")]
|
||||
fn test_invalid_priority() {
|
||||
Content::text("hello").with_priority(1.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unannotated() {
|
||||
let content = Content::text("hello")
|
||||
.with_audience(vec![Role::User])
|
||||
.with_priority(0.5);
|
||||
let unannotated = content.unannotated();
|
||||
assert_eq!(unannotated.audience(), None);
|
||||
assert_eq!(unannotated.priority(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partial_annotations() {
|
||||
let content = Content::text("hello").with_priority(0.5);
|
||||
assert_eq!(content.audience(), None);
|
||||
assert_eq!(content.priority(), Some(0.5));
|
||||
|
||||
let content = Content::text("hello").with_audience(vec![Role::User]);
|
||||
assert_eq!(content.audience(), Some(&vec![Role::User]));
|
||||
assert_eq!(content.priority(), None);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
pub mod content;
|
||||
pub use content::{Annotations, Content, ImageContent, TextContent};
|
||||
pub mod handler;
|
||||
pub mod role;
|
||||
pub use role::Role;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::content::{Annotations, EmbeddedResource, ImageContent};
|
||||
use crate::handler::PromptError;
|
||||
use crate::resource::ResourceContents;
|
||||
use base64::engine::{general_purpose::STANDARD as BASE64_STANDARD, Engine};
|
||||
use rmcp::model::{Annotations, EmbeddedResource, ImageContent};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A prompt that can be used to generate text from a model
|
||||
@@ -113,8 +112,7 @@ impl PromptMessage {
|
||||
role,
|
||||
content: PromptMessageContent::Image {
|
||||
image: ImageContent {
|
||||
data,
|
||||
mime_type,
|
||||
raw: rmcp::model::RawImageContent { data, mime_type },
|
||||
annotations,
|
||||
},
|
||||
},
|
||||
@@ -129,7 +127,7 @@ impl PromptMessage {
|
||||
text: Option<String>,
|
||||
annotations: Option<Annotations>,
|
||||
) -> Self {
|
||||
let resource_contents = ResourceContents::TextResourceContents {
|
||||
let resource_contents = rmcp::model::ResourceContents::TextResourceContents {
|
||||
uri,
|
||||
mime_type: Some(mime_type),
|
||||
text: text.unwrap_or_default(),
|
||||
@@ -139,7 +137,9 @@ impl PromptMessage {
|
||||
role,
|
||||
content: PromptMessageContent::Resource {
|
||||
resource: EmbeddedResource {
|
||||
resource: resource_contents,
|
||||
raw: rmcp::model::RawEmbeddedResource {
|
||||
resource: resource_contents,
|
||||
},
|
||||
annotations,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/// The protocol messages exchanged between client and server
|
||||
use crate::{
|
||||
content::Content,
|
||||
prompt::{Prompt, PromptMessage},
|
||||
resource::Resource,
|
||||
resource::ResourceContents,
|
||||
tool::Tool,
|
||||
};
|
||||
use rmcp::model::Content;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::content::Annotations;
|
||||
/// Resources that servers provide to clients
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use rmcp::model::Annotations;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
Reference in New Issue
Block a user