Implement SEP-1577: Sampling With Tools (#628)
* feat: implement SEP-1577 sampling with tools support * feat: add TryFrom<Content> for backward-compatible migration
This commit is contained in:
parent
be23334f9d
commit
8bd3fcb890
12 changed files with 1850 additions and 141 deletions
|
|
@ -1209,6 +1209,152 @@ pub enum Role {
|
|||
Assistant,
|
||||
}
|
||||
|
||||
/// Tool selection mode (SEP-1577).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum ToolChoiceMode {
|
||||
/// Model decides whether to use tools
|
||||
Auto,
|
||||
/// Model must use at least one tool
|
||||
Required,
|
||||
/// Model must not use tools
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for ToolChoiceMode {
|
||||
fn default() -> Self {
|
||||
Self::Auto
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool choice configuration (SEP-1577).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ToolChoice {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mode: Option<ToolChoiceMode>,
|
||||
}
|
||||
|
||||
impl ToolChoice {
|
||||
pub fn auto() -> Self {
|
||||
Self {
|
||||
mode: Some(ToolChoiceMode::Auto),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn required() -> Self {
|
||||
Self {
|
||||
mode: Some(ToolChoiceMode::Required),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn none() -> Self {
|
||||
Self {
|
||||
mode: Some(ToolChoiceMode::None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Single or array content wrapper (SEP-1577).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum SamplingContent<T> {
|
||||
Single(T),
|
||||
Multiple(Vec<T>),
|
||||
}
|
||||
|
||||
impl<T> SamplingContent<T> {
|
||||
/// Convert to a Vec regardless of whether it's single or multiple
|
||||
pub fn into_vec(self) -> Vec<T> {
|
||||
match self {
|
||||
SamplingContent::Single(item) => vec![item],
|
||||
SamplingContent::Multiple(items) => items,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the content is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
SamplingContent::Single(_) => false,
|
||||
SamplingContent::Multiple(items) => items.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of content items
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
SamplingContent::Single(_) => 1,
|
||||
SamplingContent::Multiple(items) => items.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for SamplingContent<T> {
|
||||
fn default() -> Self {
|
||||
SamplingContent::Multiple(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SamplingContent<T> {
|
||||
/// Get the first item if present
|
||||
pub fn first(&self) -> Option<&T> {
|
||||
match self {
|
||||
SamplingContent::Single(item) => Some(item),
|
||||
SamplingContent::Multiple(items) => items.first(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over all content items
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
let items: Vec<&T> = match self {
|
||||
SamplingContent::Single(item) => vec![item],
|
||||
SamplingContent::Multiple(items) => items.iter().collect(),
|
||||
};
|
||||
items.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl SamplingMessageContent {
|
||||
/// Get the text content if this is a Text variant
|
||||
pub fn as_text(&self) -> Option<&RawTextContent> {
|
||||
match self {
|
||||
SamplingMessageContent::Text(text) => Some(text),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the tool use content if this is a ToolUse variant
|
||||
pub fn as_tool_use(&self) -> Option<&ToolUseContent> {
|
||||
match self {
|
||||
SamplingMessageContent::ToolUse(tool_use) => Some(tool_use),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the tool result content if this is a ToolResult variant
|
||||
pub fn as_tool_result(&self) -> Option<&ToolResultContent> {
|
||||
match self {
|
||||
SamplingMessageContent::ToolResult(tool_result) => Some(tool_result),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for SamplingContent<T> {
|
||||
fn from(item: T) -> Self {
|
||||
SamplingContent::Single(item)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Vec<T>> for SamplingContent<T> {
|
||||
fn from(items: Vec<T>) -> Self {
|
||||
SamplingContent::Multiple(items)
|
||||
}
|
||||
}
|
||||
|
||||
/// A message in a sampling conversation, containing a role and content.
|
||||
///
|
||||
/// This represents a single message in a conversation flow, used primarily
|
||||
|
|
@ -1219,8 +1365,135 @@ pub enum Role {
|
|||
pub struct SamplingMessage {
|
||||
/// The role of the message sender (User or Assistant)
|
||||
pub role: Role,
|
||||
/// The actual content of the message (text, image, etc.)
|
||||
pub content: Content,
|
||||
/// The actual content of the message (text, image, audio, tool use, or tool result)
|
||||
pub content: SamplingContent<SamplingMessageContent>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
}
|
||||
|
||||
/// Content types for sampling messages (SEP-1577).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum SamplingMessageContent {
|
||||
Text(RawTextContent),
|
||||
Image(RawImageContent),
|
||||
Audio(RawAudioContent),
|
||||
/// Assistant only
|
||||
ToolUse(ToolUseContent),
|
||||
/// User only
|
||||
ToolResult(ToolResultContent),
|
||||
}
|
||||
|
||||
impl SamplingMessageContent {
|
||||
/// Create a text content
|
||||
pub fn text(text: impl Into<String>) -> Self {
|
||||
Self::Text(RawTextContent {
|
||||
text: text.into(),
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tool_use(id: impl Into<String>, name: impl Into<String>, input: JsonObject) -> Self {
|
||||
Self::ToolUse(ToolUseContent::new(id, name, input))
|
||||
}
|
||||
|
||||
pub fn tool_result(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
|
||||
Self::ToolResult(ToolResultContent::new(tool_use_id, content))
|
||||
}
|
||||
}
|
||||
|
||||
impl SamplingMessage {
|
||||
pub fn new(role: Role, content: impl Into<SamplingMessageContent>) -> Self {
|
||||
Self {
|
||||
role,
|
||||
content: SamplingContent::Single(content.into()),
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_multiple(role: Role, contents: Vec<SamplingMessageContent>) -> Self {
|
||||
Self {
|
||||
role,
|
||||
content: SamplingContent::Multiple(contents),
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_text(text: impl Into<String>) -> Self {
|
||||
Self::new(Role::User, SamplingMessageContent::text(text))
|
||||
}
|
||||
|
||||
pub fn assistant_text(text: impl Into<String>) -> Self {
|
||||
Self::new(Role::Assistant, SamplingMessageContent::text(text))
|
||||
}
|
||||
|
||||
pub fn user_tool_result(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
|
||||
Self::new(
|
||||
Role::User,
|
||||
SamplingMessageContent::tool_result(tool_use_id, content),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn assistant_tool_use(
|
||||
id: impl Into<String>,
|
||||
name: impl Into<String>,
|
||||
input: JsonObject,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
Role::Assistant,
|
||||
SamplingMessageContent::tool_use(id, name, input),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Conversion from RawTextContent to SamplingMessageContent
|
||||
impl From<RawTextContent> for SamplingMessageContent {
|
||||
fn from(text: RawTextContent) -> Self {
|
||||
SamplingMessageContent::Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
// Conversion from String to SamplingMessageContent (as text)
|
||||
impl From<String> for SamplingMessageContent {
|
||||
fn from(text: String) -> Self {
|
||||
SamplingMessageContent::text(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for SamplingMessageContent {
|
||||
fn from(text: &str) -> Self {
|
||||
SamplingMessageContent::text(text)
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility: Convert Content to SamplingMessageContent
|
||||
// Note: Resource and ResourceLink variants are not supported in sampling messages
|
||||
impl TryFrom<Content> for SamplingMessageContent {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(content: Content) -> Result<Self, Self::Error> {
|
||||
match content.raw {
|
||||
RawContent::Text(text) => Ok(SamplingMessageContent::Text(text)),
|
||||
RawContent::Image(image) => Ok(SamplingMessageContent::Image(image)),
|
||||
RawContent::Audio(audio) => Ok(SamplingMessageContent::Audio(audio)),
|
||||
RawContent::Resource(_) => {
|
||||
Err("Resource content is not supported in sampling messages")
|
||||
}
|
||||
RawContent::ResourceLink(_) => {
|
||||
Err("ResourceLink content is not supported in sampling messages")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility: Convert Content to SamplingContent<SamplingMessageContent>
|
||||
impl TryFrom<Content> for SamplingContent<SamplingMessageContent> {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(content: Content) -> Result<Self, Self::Error> {
|
||||
Ok(SamplingContent::Single(content.try_into()?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies how much context should be included in sampling requests.
|
||||
|
|
@ -1281,6 +1554,12 @@ pub struct CreateMessageRequestParams {
|
|||
/// Additional metadata for the request
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub metadata: Option<Value>,
|
||||
/// Tools available for the model to call (SEP-1577)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<Tool>>,
|
||||
/// Tool selection behavior (SEP-1577)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_choice: Option<ToolChoice>,
|
||||
}
|
||||
|
||||
impl RequestParamsMeta for CreateMessageRequestParams {
|
||||
|
|
@ -1926,6 +2205,7 @@ pub type CallToolRequestParam = CallToolRequestParams;
|
|||
/// Request to call a specific tool
|
||||
pub type CallToolRequest = Request<CallToolRequestMethod, CallToolRequestParams>;
|
||||
|
||||
/// Result of sampling/createMessage (SEP-1577).
|
||||
/// The result of a sampling/createMessage request containing the generated response.
|
||||
///
|
||||
/// This structure contains the generated message along with metadata about
|
||||
|
|
@ -1948,6 +2228,7 @@ impl CreateMessageResult {
|
|||
pub const STOP_REASON_END_TURN: &str = "endTurn";
|
||||
pub const STOP_REASON_END_SEQUENCE: &str = "stopSequence";
|
||||
pub const STOP_REASON_END_MAX_TOKEN: &str = "maxTokens";
|
||||
pub const STOP_REASON_TOOL_USE: &str = "toolUse";
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
|
|
@ -2477,7 +2758,9 @@ mod tests {
|
|||
..
|
||||
}) => {
|
||||
assert_eq!(capabilities.roots.unwrap().list_changed, Some(true));
|
||||
assert_eq!(capabilities.sampling.unwrap().len(), 0);
|
||||
let sampling = capabilities.sampling.unwrap();
|
||||
assert_eq!(sampling.tools, None);
|
||||
assert_eq!(sampling.context, None);
|
||||
assert_eq!(client_info.name, "ExampleClient");
|
||||
assert_eq!(client_info.version, "1.0.0");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,6 +194,19 @@ pub struct ElicitationCapability {
|
|||
pub schema_validation: Option<bool>,
|
||||
}
|
||||
|
||||
/// Sampling capability with optional sub-capabilities (SEP-1577).
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct SamplingCapability {
|
||||
/// Support for `tools` and `toolChoice` parameters
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<JsonObject>,
|
||||
/// Support for `includeContext` (soft-deprecated)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub context: Option<JsonObject>,
|
||||
}
|
||||
|
||||
///
|
||||
/// # Builder
|
||||
/// ```rust
|
||||
|
|
@ -217,8 +230,9 @@ pub struct ClientCapabilities {
|
|||
pub extensions: Option<ExtensionCapabilities>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub roots: Option<RootsCapabilities>,
|
||||
/// Capability for LLM sampling requests (SEP-1577)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sampling: Option<JsonObject>,
|
||||
pub sampling: Option<SamplingCapability>,
|
||||
/// Capability to handle elicitation requests from servers for interactive user input
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub elicitation: Option<ElicitationCapability>,
|
||||
|
|
@ -449,7 +463,7 @@ builder! {
|
|||
experimental: ExperimentalCapabilities,
|
||||
extensions: ExtensionCapabilities,
|
||||
roots: RootsCapabilities,
|
||||
sampling: JsonObject,
|
||||
sampling: SamplingCapability,
|
||||
elicitation: ElicitationCapability,
|
||||
tasks: TasksCapability,
|
||||
}
|
||||
|
|
@ -466,6 +480,26 @@ impl<const E: bool, const EXT: bool, const S: bool, const EL: bool, const TASKS:
|
|||
}
|
||||
}
|
||||
|
||||
impl<const E: bool, const EXT: bool, const R: bool, const EL: bool, const TASKS: bool>
|
||||
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, EXT, R, true, EL, TASKS>>
|
||||
{
|
||||
/// Enable tool calling in sampling requests
|
||||
pub fn enable_sampling_tools(mut self) -> Self {
|
||||
if let Some(c) = self.sampling.as_mut() {
|
||||
c.tools = Some(JsonObject::default());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable context inclusion in sampling (soft-deprecated)
|
||||
pub fn enable_sampling_context(mut self) -> Self {
|
||||
if let Some(c) = self.sampling.as_mut() {
|
||||
c.context = Some(JsonObject::default());
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "elicitation")]
|
||||
impl<const E: bool, const EXT: bool, const R: bool, const S: bool, const TASKS: bool>
|
||||
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, EXT, R, S, true, TASKS>>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,137 @@ pub struct RawAudioContent {
|
|||
|
||||
pub type AudioContent = Annotated<RawAudioContent>;
|
||||
|
||||
/// Tool call request from assistant (SEP-1577).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ToolUseContent {
|
||||
/// Unique identifier for this tool call
|
||||
pub id: String,
|
||||
/// Name of the tool to call
|
||||
pub name: String,
|
||||
/// Input arguments for the tool
|
||||
pub input: super::JsonObject,
|
||||
/// Optional metadata (preserved for caching)
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<super::Meta>,
|
||||
}
|
||||
|
||||
/// Tool execution result in user message (SEP-1577).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ToolResultContent {
|
||||
/// Optional metadata
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<super::Meta>,
|
||||
/// ID of the corresponding tool use
|
||||
pub tool_use_id: String,
|
||||
/// Content blocks returned by the tool
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub content: Vec<Content>,
|
||||
/// Optional structured result
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub structured_content: Option<super::JsonObject>,
|
||||
/// Whether tool execution failed
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_error: Option<bool>,
|
||||
}
|
||||
|
||||
impl ToolUseContent {
|
||||
pub fn new(id: impl Into<String>, name: impl Into<String>, input: super::JsonObject) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
name: name.into(),
|
||||
input,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolResultContent {
|
||||
pub fn new(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
|
||||
Self {
|
||||
meta: None,
|
||||
tool_use_id: tool_use_id.into(),
|
||||
content,
|
||||
structured_content: None,
|
||||
is_error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
|
||||
Self {
|
||||
meta: None,
|
||||
tool_use_id: tool_use_id.into(),
|
||||
content,
|
||||
structured_content: None,
|
||||
is_error: Some(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Assistant message content types (SEP-1577).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum AssistantMessageContent {
|
||||
Text(RawTextContent),
|
||||
Image(RawImageContent),
|
||||
Audio(RawAudioContent),
|
||||
ToolUse(ToolUseContent),
|
||||
}
|
||||
|
||||
/// User message content types (SEP-1577).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum UserMessageContent {
|
||||
Text(RawTextContent),
|
||||
Image(RawImageContent),
|
||||
Audio(RawAudioContent),
|
||||
ToolResult(ToolResultContent),
|
||||
}
|
||||
|
||||
impl AssistantMessageContent {
|
||||
/// Create a text content
|
||||
pub fn text(text: impl Into<String>) -> Self {
|
||||
Self::Text(RawTextContent {
|
||||
text: text.into(),
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a tool use content
|
||||
pub fn tool_use(
|
||||
id: impl Into<String>,
|
||||
name: impl Into<String>,
|
||||
input: super::JsonObject,
|
||||
) -> Self {
|
||||
Self::ToolUse(ToolUseContent::new(id, name, input))
|
||||
}
|
||||
}
|
||||
|
||||
impl UserMessageContent {
|
||||
/// Create a text content
|
||||
pub fn text(text: impl Into<String>) -> Self {
|
||||
Self::Text(RawTextContent {
|
||||
text: text.into(),
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a tool result content
|
||||
pub fn tool_result(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
|
||||
Self::ToolResult(ToolResultContent::new(tool_use_id, content))
|
||||
}
|
||||
|
||||
/// Create an error tool result content
|
||||
pub fn tool_result_error(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
|
||||
Self::ToolResult(ToolResultContent::error(tool_use_id, content))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
|
|
|
|||
|
|
@ -72,10 +72,7 @@ impl ClientHandler for TestClientHandler {
|
|||
};
|
||||
|
||||
Ok(CreateMessageResult {
|
||||
message: SamplingMessage {
|
||||
role: Role::Assistant,
|
||||
content: Content::text(response.to_string()),
|
||||
},
|
||||
message: SamplingMessage::assistant_text(response.to_string()),
|
||||
model: "test-model".to_string(),
|
||||
stop_reason: Some(CreateMessageResult::STOP_REASON_END_TURN.to_string()),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,14 +13,8 @@ use tokio_util::sync::CancellationToken;
|
|||
#[tokio::test]
|
||||
async fn test_message_roles() {
|
||||
let messages = vec![
|
||||
SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("user message"),
|
||||
},
|
||||
SamplingMessage {
|
||||
role: Role::Assistant,
|
||||
content: Content::text("assistant message"),
|
||||
},
|
||||
SamplingMessage::user_text("user message"),
|
||||
SamplingMessage::assistant_text("assistant message"),
|
||||
];
|
||||
|
||||
// Verify all roles can be serialized/deserialized correctly
|
||||
|
|
@ -50,10 +44,7 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
|
|||
params: CreateMessageRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
messages: vec![SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("test message"),
|
||||
}],
|
||||
messages: vec![SamplingMessage::user_text("test message")],
|
||||
include_context: Some(ContextInclusion::ThisServer),
|
||||
model_preferences: None,
|
||||
system_prompt: None,
|
||||
|
|
@ -61,6 +52,8 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
|
|||
max_tokens: 100,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
|
|
@ -79,7 +72,15 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
|
|||
.await?;
|
||||
|
||||
if let ClientResult::CreateMessageResult(result) = result {
|
||||
let text = result.message.content.as_text().unwrap().text.as_str();
|
||||
let text = result
|
||||
.message
|
||||
.content
|
||||
.first()
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap()
|
||||
.text
|
||||
.as_str();
|
||||
assert!(
|
||||
text.contains("test context"),
|
||||
"Response should include context for ThisServer"
|
||||
|
|
@ -94,10 +95,7 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
|
|||
params: CreateMessageRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
messages: vec![SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("test message"),
|
||||
}],
|
||||
messages: vec![SamplingMessage::user_text("test message")],
|
||||
include_context: Some(ContextInclusion::AllServers),
|
||||
model_preferences: None,
|
||||
system_prompt: None,
|
||||
|
|
@ -105,6 +103,8 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
|
|||
max_tokens: 100,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
|
|
@ -123,7 +123,15 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
|
|||
.await?;
|
||||
|
||||
if let ClientResult::CreateMessageResult(result) = result {
|
||||
let text = result.message.content.as_text().unwrap().text.as_str();
|
||||
let text = result
|
||||
.message
|
||||
.content
|
||||
.first()
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap()
|
||||
.text
|
||||
.as_str();
|
||||
assert!(
|
||||
text.contains("test context"),
|
||||
"Response should include context for AllServers"
|
||||
|
|
@ -138,10 +146,7 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
|
|||
params: CreateMessageRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
messages: vec![SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("test message"),
|
||||
}],
|
||||
messages: vec![SamplingMessage::user_text("test message")],
|
||||
include_context: Some(ContextInclusion::None),
|
||||
model_preferences: None,
|
||||
system_prompt: None,
|
||||
|
|
@ -149,6 +154,8 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
|
|||
max_tokens: 100,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
|
|
@ -167,7 +174,15 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
|
|||
.await?;
|
||||
|
||||
if let ClientResult::CreateMessageResult(result) = result {
|
||||
let text = result.message.content.as_text().unwrap().text.as_str();
|
||||
let text = result
|
||||
.message
|
||||
.content
|
||||
.first()
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap()
|
||||
.text
|
||||
.as_str();
|
||||
assert!(
|
||||
!text.contains("test context"),
|
||||
"Response should not include context for None"
|
||||
|
|
@ -202,10 +217,7 @@ async fn test_context_inclusion_ignored_integration() -> anyhow::Result<()> {
|
|||
params: CreateMessageRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
messages: vec![SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("test message"),
|
||||
}],
|
||||
messages: vec![SamplingMessage::user_text("test message")],
|
||||
include_context: Some(ContextInclusion::ThisServer),
|
||||
model_preferences: None,
|
||||
system_prompt: None,
|
||||
|
|
@ -213,6 +225,8 @@ async fn test_context_inclusion_ignored_integration() -> anyhow::Result<()> {
|
|||
max_tokens: 100,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
|
|
@ -231,7 +245,15 @@ async fn test_context_inclusion_ignored_integration() -> anyhow::Result<()> {
|
|||
.await?;
|
||||
|
||||
if let ClientResult::CreateMessageResult(result) = result {
|
||||
let text = result.message.content.as_text().unwrap().text.as_str();
|
||||
let text = result
|
||||
.message
|
||||
.content
|
||||
.first()
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap()
|
||||
.text
|
||||
.as_str();
|
||||
assert!(
|
||||
!text.contains("test context"),
|
||||
"Context should be ignored when client chooses not to honor requests"
|
||||
|
|
@ -266,14 +288,8 @@ async fn test_message_sequence_integration() -> anyhow::Result<()> {
|
|||
meta: None,
|
||||
task: None,
|
||||
messages: vec![
|
||||
SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("first message"),
|
||||
},
|
||||
SamplingMessage {
|
||||
role: Role::Assistant,
|
||||
content: Content::text("second message"),
|
||||
},
|
||||
SamplingMessage::user_text("first message"),
|
||||
SamplingMessage::assistant_text("second message"),
|
||||
],
|
||||
include_context: Some(ContextInclusion::ThisServer),
|
||||
model_preferences: None,
|
||||
|
|
@ -282,6 +298,8 @@ async fn test_message_sequence_integration() -> anyhow::Result<()> {
|
|||
max_tokens: 100,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
|
|
@ -300,7 +318,15 @@ async fn test_message_sequence_integration() -> anyhow::Result<()> {
|
|||
.await?;
|
||||
|
||||
if let ClientResult::CreateMessageResult(result) = result {
|
||||
let text = result.message.content.as_text().unwrap().text.as_str();
|
||||
let text = result
|
||||
.message
|
||||
.content
|
||||
.first()
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap()
|
||||
.text
|
||||
.as_str();
|
||||
assert!(
|
||||
text.contains("test context"),
|
||||
"Response should include context when ThisServer is specified"
|
||||
|
|
@ -339,18 +365,9 @@ async fn test_message_sequence_validation_integration() -> anyhow::Result<()> {
|
|||
meta: None,
|
||||
task: None,
|
||||
messages: vec![
|
||||
SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("first user message"),
|
||||
},
|
||||
SamplingMessage {
|
||||
role: Role::Assistant,
|
||||
content: Content::text("first assistant response"),
|
||||
},
|
||||
SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("second user message"),
|
||||
},
|
||||
SamplingMessage::user_text("first user message"),
|
||||
SamplingMessage::assistant_text("first assistant response"),
|
||||
SamplingMessage::user_text("second user message"),
|
||||
],
|
||||
include_context: None,
|
||||
model_preferences: None,
|
||||
|
|
@ -359,6 +376,8 @@ async fn test_message_sequence_validation_integration() -> anyhow::Result<()> {
|
|||
max_tokens: 100,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
|
|
@ -384,10 +403,7 @@ async fn test_message_sequence_validation_integration() -> anyhow::Result<()> {
|
|||
params: CreateMessageRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
messages: vec![SamplingMessage {
|
||||
role: Role::Assistant,
|
||||
content: Content::text("assistant message"),
|
||||
}],
|
||||
messages: vec![SamplingMessage::assistant_text("assistant message")],
|
||||
include_context: None,
|
||||
model_preferences: None,
|
||||
system_prompt: None,
|
||||
|
|
@ -395,6 +411,8 @@ async fn test_message_sequence_validation_integration() -> anyhow::Result<()> {
|
|||
max_tokens: 100,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
|
|
@ -439,10 +457,7 @@ async fn test_selective_context_handling_integration() -> anyhow::Result<()> {
|
|||
params: CreateMessageRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
messages: vec![SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("test message"),
|
||||
}],
|
||||
messages: vec![SamplingMessage::user_text("test message")],
|
||||
include_context: Some(ContextInclusion::ThisServer),
|
||||
model_preferences: None,
|
||||
system_prompt: None,
|
||||
|
|
@ -450,6 +465,8 @@ async fn test_selective_context_handling_integration() -> anyhow::Result<()> {
|
|||
max_tokens: 100,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
|
|
@ -468,7 +485,15 @@ async fn test_selective_context_handling_integration() -> anyhow::Result<()> {
|
|||
.await?;
|
||||
|
||||
if let ClientResult::CreateMessageResult(result) = result {
|
||||
let text = result.message.content.as_text().unwrap().text.as_str();
|
||||
let text = result
|
||||
.message
|
||||
.content
|
||||
.first()
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap()
|
||||
.text
|
||||
.as_str();
|
||||
assert!(
|
||||
text.contains("test context"),
|
||||
"ThisServer context request should be honored"
|
||||
|
|
@ -481,10 +506,7 @@ async fn test_selective_context_handling_integration() -> anyhow::Result<()> {
|
|||
params: CreateMessageRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
messages: vec![SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("test message"),
|
||||
}],
|
||||
messages: vec![SamplingMessage::user_text("test message")],
|
||||
include_context: Some(ContextInclusion::AllServers),
|
||||
model_preferences: None,
|
||||
system_prompt: None,
|
||||
|
|
@ -492,6 +514,8 @@ async fn test_selective_context_handling_integration() -> anyhow::Result<()> {
|
|||
max_tokens: 100,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
|
|
@ -510,7 +534,15 @@ async fn test_selective_context_handling_integration() -> anyhow::Result<()> {
|
|||
.await?;
|
||||
|
||||
if let ClientResult::CreateMessageResult(result) = result {
|
||||
let text = result.message.content.as_text().unwrap().text.as_str();
|
||||
let text = result
|
||||
.message
|
||||
.content
|
||||
.first()
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap()
|
||||
.text
|
||||
.as_str();
|
||||
assert!(
|
||||
!text.contains("test context"),
|
||||
"AllServers context request should be ignored"
|
||||
|
|
@ -540,10 +572,7 @@ async fn test_context_inclusion() -> anyhow::Result<()> {
|
|||
params: CreateMessageRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
messages: vec![SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("test"),
|
||||
}],
|
||||
messages: vec![SamplingMessage::user_text("test")],
|
||||
include_context: Some(ContextInclusion::ThisServer),
|
||||
model_preferences: None,
|
||||
system_prompt: None,
|
||||
|
|
@ -551,6 +580,8 @@ async fn test_context_inclusion() -> anyhow::Result<()> {
|
|||
max_tokens: 100,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
|
|
@ -569,7 +600,15 @@ async fn test_context_inclusion() -> anyhow::Result<()> {
|
|||
.await?;
|
||||
|
||||
if let ClientResult::CreateMessageResult(result) = result {
|
||||
let text = result.message.content.as_text().unwrap().text.as_str();
|
||||
let text = result
|
||||
.message
|
||||
.content
|
||||
.first()
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap()
|
||||
.text
|
||||
.as_str();
|
||||
assert!(text.contains("test context"));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -318,11 +318,15 @@
|
|||
]
|
||||
},
|
||||
"sampling": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
"description": "Capability for LLM sampling requests (SEP-1577)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SamplingCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tasks": {
|
||||
"anyOf": [
|
||||
|
|
@ -431,14 +435,21 @@
|
|||
]
|
||||
},
|
||||
"CreateMessageResult": {
|
||||
"description": "The result of a sampling/createMessage request containing the generated response.\n\nThis structure contains the generated message along with metadata about\nhow the generation was performed and why it stopped.",
|
||||
"description": "Result of sampling/createMessage (SEP-1577).\nThe result of a sampling/createMessage request containing the generated response.\n\nThis structure contains the generated message along with metadata about\nhow the generation was performed and why it stopped.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"content": {
|
||||
"description": "The actual content of the message (text, image, etc.)",
|
||||
"description": "The actual content of the message (text, image, audio, tool use, or tool result)",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Annotated"
|
||||
"$ref": "#/definitions/SamplingContent"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1741,6 +1752,134 @@
|
|||
"format": "const",
|
||||
"const": "notifications/roots/list_changed"
|
||||
},
|
||||
"SamplingCapability": {
|
||||
"description": "Sampling capability with optional sub-capabilities (SEP-1577).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"context": {
|
||||
"description": "Support for `includeContext` (soft-deprecated)",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"tools": {
|
||||
"description": "Support for `tools` and `toolChoice` parameters",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"SamplingContent": {
|
||||
"description": "Single or array content wrapper (SEP-1577).",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SamplingMessageContent"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SamplingMessageContent"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"SamplingMessageContent": {
|
||||
"description": "Content types for sampling messages (SEP-1577).",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "text"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawTextContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "image"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawImageContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "audio"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawAudioContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Assistant only",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "tool_use"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolUseContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "User only",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "tool_result"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolResultContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"SamplingTaskCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1875,6 +2014,81 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ToolResultContent": {
|
||||
"description": "Tool execution result in user message (SEP-1577).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Optional metadata",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"content": {
|
||||
"description": "Content blocks returned by the tool",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Annotated"
|
||||
}
|
||||
},
|
||||
"isError": {
|
||||
"description": "Whether tool execution failed",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"structuredContent": {
|
||||
"description": "Optional structured result",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"toolUseId": {
|
||||
"description": "ID of the corresponding tool use",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"toolUseId"
|
||||
]
|
||||
},
|
||||
"ToolUseContent": {
|
||||
"description": "Tool call request from assistant (SEP-1577).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Optional metadata (preserved for caching)",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"id": {
|
||||
"description": "Unique identifier for this tool call",
|
||||
"type": "string"
|
||||
},
|
||||
"input": {
|
||||
"description": "Input arguments for the tool",
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the tool to call",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"input"
|
||||
]
|
||||
},
|
||||
"ToolsTaskCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -318,11 +318,15 @@
|
|||
]
|
||||
},
|
||||
"sampling": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
"description": "Capability for LLM sampling requests (SEP-1577)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SamplingCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tasks": {
|
||||
"anyOf": [
|
||||
|
|
@ -431,14 +435,21 @@
|
|||
]
|
||||
},
|
||||
"CreateMessageResult": {
|
||||
"description": "The result of a sampling/createMessage request containing the generated response.\n\nThis structure contains the generated message along with metadata about\nhow the generation was performed and why it stopped.",
|
||||
"description": "Result of sampling/createMessage (SEP-1577).\nThe result of a sampling/createMessage request containing the generated response.\n\nThis structure contains the generated message along with metadata about\nhow the generation was performed and why it stopped.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"content": {
|
||||
"description": "The actual content of the message (text, image, etc.)",
|
||||
"description": "The actual content of the message (text, image, audio, tool use, or tool result)",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Annotated"
|
||||
"$ref": "#/definitions/SamplingContent"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1741,6 +1752,134 @@
|
|||
"format": "const",
|
||||
"const": "notifications/roots/list_changed"
|
||||
},
|
||||
"SamplingCapability": {
|
||||
"description": "Sampling capability with optional sub-capabilities (SEP-1577).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"context": {
|
||||
"description": "Support for `includeContext` (soft-deprecated)",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"tools": {
|
||||
"description": "Support for `tools` and `toolChoice` parameters",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"SamplingContent": {
|
||||
"description": "Single or array content wrapper (SEP-1577).",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SamplingMessageContent"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SamplingMessageContent"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"SamplingMessageContent": {
|
||||
"description": "Content types for sampling messages (SEP-1577).",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "text"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawTextContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "image"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawImageContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "audio"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawAudioContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Assistant only",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "tool_use"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolUseContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "User only",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "tool_result"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolResultContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"SamplingTaskCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1875,6 +2014,81 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ToolResultContent": {
|
||||
"description": "Tool execution result in user message (SEP-1577).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Optional metadata",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"content": {
|
||||
"description": "Content blocks returned by the tool",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Annotated"
|
||||
}
|
||||
},
|
||||
"isError": {
|
||||
"description": "Whether tool execution failed",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"structuredContent": {
|
||||
"description": "Optional structured result",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"toolUseId": {
|
||||
"description": "ID of the corresponding tool use",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"toolUseId"
|
||||
]
|
||||
},
|
||||
"ToolUseContent": {
|
||||
"description": "Tool call request from assistant (SEP-1577).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Optional metadata (preserved for caching)",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"id": {
|
||||
"description": "Unique identifier for this tool call",
|
||||
"type": "string"
|
||||
},
|
||||
"input": {
|
||||
"description": "Input arguments for the tool",
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the tool to call",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"input"
|
||||
]
|
||||
},
|
||||
"ToolsTaskCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -641,6 +641,27 @@
|
|||
"null"
|
||||
],
|
||||
"format": "float"
|
||||
},
|
||||
"toolChoice": {
|
||||
"description": "Tool selection behavior (SEP-1577)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolChoice"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"description": "Tools available for the model to call (SEP-1577)",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Tool"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -2309,15 +2330,36 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"SamplingContent": {
|
||||
"description": "Single or array content wrapper (SEP-1577).",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SamplingMessageContent"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SamplingMessageContent"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"SamplingMessage": {
|
||||
"description": "A message in a sampling conversation, containing a role and content.\n\nThis represents a single message in a conversation flow, used primarily\nin LLM sampling requests where the conversation history is important\nfor generating appropriate responses.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"content": {
|
||||
"description": "The actual content of the message (text, image, etc.)",
|
||||
"description": "The actual content of the message (text, image, audio, tool use, or tool result)",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Annotated"
|
||||
"$ref": "#/definitions/SamplingContent"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -2335,6 +2377,98 @@
|
|||
"content"
|
||||
]
|
||||
},
|
||||
"SamplingMessageContent": {
|
||||
"description": "Content types for sampling messages (SEP-1577).",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "text"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawTextContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "image"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawImageContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "audio"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawAudioContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Assistant only",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "tool_use"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolUseContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "User only",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "tool_result"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolResultContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"SamplingTaskCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -3008,6 +3142,42 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ToolChoice": {
|
||||
"description": "Tool choice configuration (SEP-1577).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolChoiceMode"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ToolChoiceMode": {
|
||||
"description": "Tool selection mode (SEP-1577).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Model decides whether to use tools",
|
||||
"type": "string",
|
||||
"const": "auto"
|
||||
},
|
||||
{
|
||||
"description": "Model must use at least one tool",
|
||||
"type": "string",
|
||||
"const": "required"
|
||||
},
|
||||
{
|
||||
"description": "Model must not use tools",
|
||||
"type": "string",
|
||||
"const": "none"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ToolExecution": {
|
||||
"description": "Execution-related configuration for a tool.\n\nThis struct contains settings that control how a tool should be executed,\nincluding task support configuration.",
|
||||
"type": "object",
|
||||
|
|
@ -3030,6 +3200,81 @@
|
|||
"format": "const",
|
||||
"const": "notifications/tools/list_changed"
|
||||
},
|
||||
"ToolResultContent": {
|
||||
"description": "Tool execution result in user message (SEP-1577).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Optional metadata",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"content": {
|
||||
"description": "Content blocks returned by the tool",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Annotated"
|
||||
}
|
||||
},
|
||||
"isError": {
|
||||
"description": "Whether tool execution failed",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"structuredContent": {
|
||||
"description": "Optional structured result",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"toolUseId": {
|
||||
"description": "ID of the corresponding tool use",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"toolUseId"
|
||||
]
|
||||
},
|
||||
"ToolUseContent": {
|
||||
"description": "Tool call request from assistant (SEP-1577).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Optional metadata (preserved for caching)",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"id": {
|
||||
"description": "Unique identifier for this tool call",
|
||||
"type": "string"
|
||||
},
|
||||
"input": {
|
||||
"description": "Input arguments for the tool",
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the tool to call",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"input"
|
||||
]
|
||||
},
|
||||
"ToolsCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -641,6 +641,27 @@
|
|||
"null"
|
||||
],
|
||||
"format": "float"
|
||||
},
|
||||
"toolChoice": {
|
||||
"description": "Tool selection behavior (SEP-1577)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolChoice"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"description": "Tools available for the model to call (SEP-1577)",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Tool"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -2309,15 +2330,36 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"SamplingContent": {
|
||||
"description": "Single or array content wrapper (SEP-1577).",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SamplingMessageContent"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SamplingMessageContent"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"SamplingMessage": {
|
||||
"description": "A message in a sampling conversation, containing a role and content.\n\nThis represents a single message in a conversation flow, used primarily\nin LLM sampling requests where the conversation history is important\nfor generating appropriate responses.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"content": {
|
||||
"description": "The actual content of the message (text, image, etc.)",
|
||||
"description": "The actual content of the message (text, image, audio, tool use, or tool result)",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Annotated"
|
||||
"$ref": "#/definitions/SamplingContent"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -2335,6 +2377,98 @@
|
|||
"content"
|
||||
]
|
||||
},
|
||||
"SamplingMessageContent": {
|
||||
"description": "Content types for sampling messages (SEP-1577).",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "text"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawTextContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "image"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawImageContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "audio"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawAudioContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Assistant only",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "tool_use"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolUseContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "User only",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "tool_result"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolResultContent"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"SamplingTaskCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -3008,6 +3142,42 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ToolChoice": {
|
||||
"description": "Tool choice configuration (SEP-1577).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolChoiceMode"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ToolChoiceMode": {
|
||||
"description": "Tool selection mode (SEP-1577).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Model decides whether to use tools",
|
||||
"type": "string",
|
||||
"const": "auto"
|
||||
},
|
||||
{
|
||||
"description": "Model must use at least one tool",
|
||||
"type": "string",
|
||||
"const": "required"
|
||||
},
|
||||
{
|
||||
"description": "Model must not use tools",
|
||||
"type": "string",
|
||||
"const": "none"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ToolExecution": {
|
||||
"description": "Execution-related configuration for a tool.\n\nThis struct contains settings that control how a tool should be executed,\nincluding task support configuration.",
|
||||
"type": "object",
|
||||
|
|
@ -3030,6 +3200,81 @@
|
|||
"format": "const",
|
||||
"const": "notifications/tools/list_changed"
|
||||
},
|
||||
"ToolResultContent": {
|
||||
"description": "Tool execution result in user message (SEP-1577).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Optional metadata",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"content": {
|
||||
"description": "Content blocks returned by the tool",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Annotated"
|
||||
}
|
||||
},
|
||||
"isError": {
|
||||
"description": "Whether tool execution failed",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"structuredContent": {
|
||||
"description": "Optional structured result",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"toolUseId": {
|
||||
"description": "ID of the corresponding tool use",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"toolUseId"
|
||||
]
|
||||
},
|
||||
"ToolUseContent": {
|
||||
"description": "Tool call request from assistant (SEP-1577).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Optional metadata (preserved for caching)",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"id": {
|
||||
"description": "Unique identifier for this tool call",
|
||||
"type": "string"
|
||||
},
|
||||
"input": {
|
||||
"description": "Input arguments for the tool",
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the tool to call",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"input"
|
||||
]
|
||||
},
|
||||
"ToolsCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -13,13 +13,8 @@ use tokio_util::sync::CancellationToken;
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_basic_sampling_message_creation() -> Result<()> {
|
||||
// Test basic sampling message structure
|
||||
let message = SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("What is the capital of France?"),
|
||||
};
|
||||
let message = SamplingMessage::user_text("What is the capital of France?");
|
||||
|
||||
// Verify serialization/deserialization
|
||||
let json = serde_json::to_string(&message)?;
|
||||
let deserialized: SamplingMessage = serde_json::from_str(&json)?;
|
||||
assert_eq!(message, deserialized);
|
||||
|
|
@ -30,14 +25,10 @@ async fn test_basic_sampling_message_creation() -> Result<()> {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_sampling_request_params() -> Result<()> {
|
||||
// Test sampling request parameters structure
|
||||
let params = CreateMessageRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
messages: vec![SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("Hello, world!"),
|
||||
}],
|
||||
messages: vec![SamplingMessage::user_text("Hello, world!")],
|
||||
model_preferences: Some(ModelPreferences {
|
||||
hints: Some(vec![ModelHint {
|
||||
name: Some("claude".to_string()),
|
||||
|
|
@ -52,14 +43,14 @@ async fn test_sampling_request_params() -> Result<()> {
|
|||
stop_sequences: Some(vec!["STOP".to_string()]),
|
||||
include_context: Some(ContextInclusion::None),
|
||||
metadata: Some(serde_json::json!({"test": "value"})),
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
};
|
||||
|
||||
// Verify serialization/deserialization
|
||||
let json = serde_json::to_string(¶ms)?;
|
||||
let deserialized: CreateMessageRequestParams = serde_json::from_str(&json)?;
|
||||
assert_eq!(params, deserialized);
|
||||
|
||||
// Verify specific fields
|
||||
assert_eq!(params.messages.len(), 1);
|
||||
assert_eq!(params.max_tokens, 100);
|
||||
assert_eq!(params.temperature, Some(0.7));
|
||||
|
|
@ -69,22 +60,16 @@ async fn test_sampling_request_params() -> Result<()> {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_sampling_result_structure() -> Result<()> {
|
||||
// Test sampling result structure
|
||||
let result = CreateMessageResult {
|
||||
message: SamplingMessage {
|
||||
role: Role::Assistant,
|
||||
content: Content::text("The capital of France is Paris."),
|
||||
},
|
||||
message: SamplingMessage::assistant_text("The capital of France is Paris."),
|
||||
model: "test-model".to_string(),
|
||||
stop_reason: Some(CreateMessageResult::STOP_REASON_END_TURN.to_string()),
|
||||
};
|
||||
|
||||
// Verify serialization/deserialization
|
||||
let json = serde_json::to_string(&result)?;
|
||||
let deserialized: CreateMessageResult = serde_json::from_str(&json)?;
|
||||
assert_eq!(result, deserialized);
|
||||
|
||||
// Verify specific fields
|
||||
assert_eq!(result.message.role, Role::Assistant);
|
||||
assert_eq!(result.model, "test-model");
|
||||
assert_eq!(
|
||||
|
|
@ -97,7 +82,6 @@ async fn test_sampling_result_structure() -> Result<()> {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_sampling_context_inclusion_enum() -> Result<()> {
|
||||
// Test context inclusion enum values
|
||||
let test_cases = vec![
|
||||
(ContextInclusion::None, "none"),
|
||||
(ContextInclusion::ThisServer, "thisServer"),
|
||||
|
|
@ -139,10 +123,7 @@ async fn test_sampling_integration_with_test_handlers() -> Result<()> {
|
|||
params: CreateMessageRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
messages: vec![SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("What is the capital of France?"),
|
||||
}],
|
||||
messages: vec![SamplingMessage::user_text("What is the capital of France?")],
|
||||
include_context: Some(ContextInclusion::ThisServer),
|
||||
model_preferences: Some(ModelPreferences {
|
||||
hints: Some(vec![ModelHint {
|
||||
|
|
@ -157,6 +138,8 @@ async fn test_sampling_integration_with_test_handlers() -> Result<()> {
|
|||
max_tokens: 100,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
|
|
@ -183,7 +166,15 @@ async fn test_sampling_integration_with_test_handlers() -> Result<()> {
|
|||
Some(CreateMessageResult::STOP_REASON_END_TURN.to_string())
|
||||
);
|
||||
|
||||
let response_text = result.message.content.as_text().unwrap().text.as_str();
|
||||
let response_text = result
|
||||
.message
|
||||
.content
|
||||
.first()
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap()
|
||||
.text
|
||||
.as_str();
|
||||
assert!(
|
||||
response_text.contains("test context"),
|
||||
"Response should include context for ThisServer inclusion"
|
||||
|
|
@ -221,10 +212,7 @@ async fn test_sampling_no_context_inclusion() -> Result<()> {
|
|||
params: CreateMessageRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
messages: vec![SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text("Hello"),
|
||||
}],
|
||||
messages: vec![SamplingMessage::user_text("Hello")],
|
||||
include_context: Some(ContextInclusion::None),
|
||||
model_preferences: None,
|
||||
system_prompt: None,
|
||||
|
|
@ -232,6 +220,8 @@ async fn test_sampling_no_context_inclusion() -> Result<()> {
|
|||
max_tokens: 50,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
|
|
@ -254,7 +244,15 @@ async fn test_sampling_no_context_inclusion() -> Result<()> {
|
|||
assert_eq!(result.message.role, Role::Assistant);
|
||||
assert_eq!(result.model, "test-model");
|
||||
|
||||
let response_text = result.message.content.as_text().unwrap().text.as_str();
|
||||
let response_text = result
|
||||
.message
|
||||
.content
|
||||
.first()
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap()
|
||||
.text
|
||||
.as_str();
|
||||
assert!(
|
||||
!response_text.contains("test context"),
|
||||
"Response should not include context for None inclusion"
|
||||
|
|
@ -292,10 +290,9 @@ async fn test_sampling_error_invalid_message_sequence() -> Result<()> {
|
|||
params: CreateMessageRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
messages: vec![SamplingMessage {
|
||||
role: Role::Assistant,
|
||||
content: Content::text("I'm an assistant message without a user message"),
|
||||
}],
|
||||
messages: vec![SamplingMessage::assistant_text(
|
||||
"I'm an assistant message without a user message",
|
||||
)],
|
||||
include_context: Some(ContextInclusion::None),
|
||||
model_preferences: None,
|
||||
system_prompt: None,
|
||||
|
|
@ -303,6 +300,8 @@ async fn test_sampling_error_invalid_message_sequence() -> Result<()> {
|
|||
max_tokens: 50,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
|
|
@ -327,3 +326,314 @@ async fn test_sampling_error_invalid_message_sequence() -> Result<()> {
|
|||
server_handle.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tool_choice_serialization() -> Result<()> {
|
||||
let auto = ToolChoice::auto();
|
||||
let json = serde_json::to_string(&auto)?;
|
||||
assert!(json.contains("auto"));
|
||||
let deserialized: ToolChoice = serde_json::from_str(&json)?;
|
||||
assert_eq!(auto, deserialized);
|
||||
|
||||
let required = ToolChoice::required();
|
||||
let json = serde_json::to_string(&required)?;
|
||||
assert!(json.contains("required"));
|
||||
let deserialized: ToolChoice = serde_json::from_str(&json)?;
|
||||
assert_eq!(required, deserialized);
|
||||
|
||||
let none = ToolChoice::none();
|
||||
let json = serde_json::to_string(&none)?;
|
||||
assert!(json.contains("none"));
|
||||
let deserialized: ToolChoice = serde_json::from_str(&json)?;
|
||||
assert_eq!(none, deserialized);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sampling_with_tools() -> Result<()> {
|
||||
use std::sync::Arc;
|
||||
|
||||
let tool = Tool::new(
|
||||
"get_weather",
|
||||
"Get the current weather for a location",
|
||||
Arc::new(
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "The city and state, e.g. San Francisco, CA"
|
||||
}
|
||||
},
|
||||
"required": ["location"]
|
||||
})
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.clone(),
|
||||
),
|
||||
);
|
||||
|
||||
let params = CreateMessageRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
messages: vec![SamplingMessage::user_text(
|
||||
"What's the weather in San Francisco?",
|
||||
)],
|
||||
model_preferences: None,
|
||||
system_prompt: None,
|
||||
include_context: None,
|
||||
temperature: None,
|
||||
max_tokens: 100,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: Some(vec![tool]),
|
||||
tool_choice: Some(ToolChoice::auto()),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(¶ms)?;
|
||||
let deserialized: CreateMessageRequestParams = serde_json::from_str(&json)?;
|
||||
|
||||
assert!(deserialized.tools.is_some());
|
||||
assert_eq!(deserialized.tools.as_ref().unwrap().len(), 1);
|
||||
assert_eq!(deserialized.tools.as_ref().unwrap()[0].name, "get_weather");
|
||||
assert!(deserialized.tool_choice.is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tool_use_content_serialization() -> Result<()> {
|
||||
let tool_use = ToolUseContent::new(
|
||||
"call_123",
|
||||
"get_weather",
|
||||
serde_json::json!({
|
||||
"location": "San Francisco, CA"
|
||||
})
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.clone(),
|
||||
);
|
||||
|
||||
let json = serde_json::to_string(&tool_use)?;
|
||||
let deserialized: ToolUseContent = serde_json::from_str(&json)?;
|
||||
assert_eq!(tool_use, deserialized);
|
||||
assert_eq!(deserialized.id, "call_123");
|
||||
assert_eq!(deserialized.name, "get_weather");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tool_result_content_serialization() -> Result<()> {
|
||||
let tool_result = ToolResultContent::new(
|
||||
"call_123",
|
||||
vec![Content::text(
|
||||
"The weather in San Francisco is 72°F and sunny.",
|
||||
)],
|
||||
);
|
||||
|
||||
let json = serde_json::to_string(&tool_result)?;
|
||||
let deserialized: ToolResultContent = serde_json::from_str(&json)?;
|
||||
assert_eq!(tool_result, deserialized);
|
||||
assert_eq!(deserialized.tool_use_id, "call_123");
|
||||
assert!(!deserialized.content.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sampling_message_with_tool_use() -> Result<()> {
|
||||
let message = SamplingMessage::assistant_tool_use(
|
||||
"call_123",
|
||||
"get_weather",
|
||||
serde_json::json!({
|
||||
"location": "San Francisco, CA"
|
||||
})
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.clone(),
|
||||
);
|
||||
|
||||
let json = serde_json::to_string(&message)?;
|
||||
let deserialized: SamplingMessage = serde_json::from_str(&json)?;
|
||||
assert_eq!(message, deserialized);
|
||||
assert_eq!(deserialized.role, Role::Assistant);
|
||||
|
||||
let tool_use = deserialized.content.first().unwrap().as_tool_use().unwrap();
|
||||
assert_eq!(tool_use.name, "get_weather");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sampling_message_with_tool_result() -> Result<()> {
|
||||
let message =
|
||||
SamplingMessage::user_tool_result("call_123", vec![Content::text("72°F and sunny")]);
|
||||
|
||||
let json = serde_json::to_string(&message)?;
|
||||
let deserialized: SamplingMessage = serde_json::from_str(&json)?;
|
||||
assert_eq!(message, deserialized);
|
||||
assert_eq!(deserialized.role, Role::User);
|
||||
|
||||
let tool_result = deserialized
|
||||
.content
|
||||
.first()
|
||||
.unwrap()
|
||||
.as_tool_result()
|
||||
.unwrap();
|
||||
assert_eq!(tool_result.tool_use_id, "call_123");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_message_result_tool_use_stop_reason() -> Result<()> {
|
||||
let result = CreateMessageResult {
|
||||
message: SamplingMessage::assistant_tool_use(
|
||||
"call_123",
|
||||
"get_weather",
|
||||
serde_json::json!({
|
||||
"location": "San Francisco"
|
||||
})
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.clone(),
|
||||
),
|
||||
model: "test-model".to_string(),
|
||||
stop_reason: Some(CreateMessageResult::STOP_REASON_TOOL_USE.to_string()),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&result)?;
|
||||
let deserialized: CreateMessageResult = serde_json::from_str(&json)?;
|
||||
assert_eq!(result, deserialized);
|
||||
assert_eq!(deserialized.stop_reason, Some("toolUse".to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sampling_capability() -> Result<()> {
|
||||
let cap = SamplingCapability {
|
||||
tools: Some(JsonObject::default()),
|
||||
context: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&cap)?;
|
||||
let deserialized: SamplingCapability = serde_json::from_str(&json)?;
|
||||
assert_eq!(cap, deserialized);
|
||||
assert!(deserialized.tools.is_some());
|
||||
assert!(deserialized.context.is_none());
|
||||
|
||||
let client_cap = ClientCapabilities::builder()
|
||||
.enable_sampling()
|
||||
.enable_sampling_tools()
|
||||
.build();
|
||||
|
||||
assert!(client_cap.sampling.is_some());
|
||||
assert!(client_cap.sampling.as_ref().unwrap().tools.is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_backward_compat_sampling_message_deserialization() -> Result<()> {
|
||||
let old_format_json = r#"{
|
||||
"role": "user",
|
||||
"content": {
|
||||
"type": "text",
|
||||
"text": "Hello, world!"
|
||||
}
|
||||
}"#;
|
||||
|
||||
let message: SamplingMessage = serde_json::from_str(old_format_json)?;
|
||||
assert_eq!(message.role, Role::User);
|
||||
let text = message.content.first().unwrap().as_text().unwrap();
|
||||
assert_eq!(text.text, "Hello, world!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_backward_compat_sampling_message_with_image() -> Result<()> {
|
||||
let old_format_json = r#"{
|
||||
"role": "user",
|
||||
"content": {
|
||||
"type": "image",
|
||||
"data": "base64data",
|
||||
"mimeType": "image/png"
|
||||
}
|
||||
}"#;
|
||||
|
||||
let message: SamplingMessage = serde_json::from_str(old_format_json)?;
|
||||
assert_eq!(message.role, Role::User);
|
||||
assert_eq!(message.content.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_backward_compat_sampling_capability_empty_object() -> Result<()> {
|
||||
let empty_json = "{}";
|
||||
let cap: SamplingCapability = serde_json::from_str(empty_json)?;
|
||||
assert!(cap.tools.is_none());
|
||||
assert!(cap.context.is_none());
|
||||
|
||||
let client_cap_json = r#"{"sampling": {}}"#;
|
||||
let client_cap: ClientCapabilities = serde_json::from_str(client_cap_json)?;
|
||||
assert!(client_cap.sampling.is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_content_to_sampling_message_content_conversion() -> Result<()> {
|
||||
use std::convert::TryInto;
|
||||
|
||||
let content = Content::text("Hello");
|
||||
let sampling_content: SamplingMessageContent =
|
||||
content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?;
|
||||
assert!(sampling_content.as_text().is_some());
|
||||
assert_eq!(sampling_content.as_text().unwrap().text, "Hello");
|
||||
|
||||
let content = Content::image("base64data", "image/png");
|
||||
let sampling_content: SamplingMessageContent =
|
||||
content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?;
|
||||
assert!(matches!(sampling_content, SamplingMessageContent::Image(_)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_content_to_sampling_content_conversion() -> Result<()> {
|
||||
use std::convert::TryInto;
|
||||
|
||||
let content = Content::text("Hello");
|
||||
let sampling_content: SamplingContent<SamplingMessageContent> =
|
||||
content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?;
|
||||
assert_eq!(sampling_content.len(), 1);
|
||||
assert!(sampling_content.first().unwrap().as_text().is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_content_conversion_unsupported_variants() {
|
||||
use std::convert::TryInto;
|
||||
|
||||
use rmcp::model::ResourceContents;
|
||||
|
||||
let resource_content = Content::resource(ResourceContents::TextResourceContents {
|
||||
uri: "file:///test.txt".to_string(),
|
||||
mime_type: Some("text/plain".to_string()),
|
||||
text: "test".to_string(),
|
||||
meta: None,
|
||||
});
|
||||
|
||||
let result: Result<SamplingMessageContent, _> = resource_content.try_into();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err(),
|
||||
"Resource content is not supported in sampling messages"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,10 +41,7 @@ impl ClientHandler for SamplingDemoClient {
|
|||
self.mock_llm_response(¶ms.messages, params.system_prompt.as_deref());
|
||||
|
||||
Ok(CreateMessageResult {
|
||||
message: SamplingMessage {
|
||||
role: Role::Assistant,
|
||||
content: Content::text(response_text),
|
||||
},
|
||||
message: SamplingMessage::assistant_text(response_text),
|
||||
model: "mock_llm".to_string(),
|
||||
stop_reason: Some(CreateMessageResult::STOP_REASON_END_TURN.to_string()),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -51,10 +51,7 @@ impl ServerHandler for SamplingDemoServer {
|
|||
.create_message(CreateMessageRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
messages: vec![SamplingMessage {
|
||||
role: Role::User,
|
||||
content: Content::text(question),
|
||||
}],
|
||||
messages: vec![SamplingMessage::user_text(question)],
|
||||
model_preferences: Some(ModelPreferences {
|
||||
hints: Some(vec![ModelHint {
|
||||
name: Some("claude".to_string()),
|
||||
|
|
@ -69,6 +66,8 @@ impl ServerHandler for SamplingDemoServer {
|
|||
max_tokens: 150,
|
||||
stop_sequences: None,
|
||||
metadata: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
|
|
@ -85,7 +84,8 @@ impl ServerHandler for SamplingDemoServer {
|
|||
response
|
||||
.message
|
||||
.content
|
||||
.as_text()
|
||||
.first()
|
||||
.and_then(|c| c.as_text())
|
||||
.map(|t| &t.text)
|
||||
.unwrap_or(&"No text response".to_string())
|
||||
))]))
|
||||
|
|
|
|||
Loading…
Reference in a new issue