fix(api): use camelCase in CallToolResponse and add type discriminators to ContentBlock (#7487)
Co-authored-by: Goose <opensource@block.xyz> Co-authored-by: Jack Amadeo <jackamadeo@block.xyz>
This commit is contained in:
@@ -11,8 +11,8 @@ use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata, ProviderTyp
|
||||
use goose::session::{Session, SessionInsights, SessionType, SystemInfo};
|
||||
use rmcp::model::{
|
||||
Annotations, Content, EmbeddedResource, Icon, ImageContent, JsonObject, RawAudioContent,
|
||||
RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ResourceContents, Role,
|
||||
TaskSupport, TextContent, Tool, ToolAnnotations, ToolExecution,
|
||||
RawContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent,
|
||||
ResourceContents, Role, TaskSupport, TextContent, Tool, ToolAnnotations, ToolExecution,
|
||||
};
|
||||
use utoipa::{OpenApi, ToSchema};
|
||||
|
||||
@@ -36,7 +36,7 @@ use utoipa::openapi::{AllOfBuilder, Ref, RefOr};
|
||||
|
||||
macro_rules! derive_utoipa {
|
||||
($inner_type:ident as $schema_name:ident) => {
|
||||
struct $schema_name {}
|
||||
pub struct $schema_name {}
|
||||
|
||||
impl<'__s> ToSchema<'__s> for $schema_name {
|
||||
fn schema() -> (&'__s str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
|
||||
@@ -47,6 +47,23 @@ macro_rules! derive_utoipa {
|
||||
(stringify!($inner_type), schema)
|
||||
}
|
||||
|
||||
fn aliases() -> Vec<(&'__s str, utoipa::openapi::schema::Schema)> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
};
|
||||
($inner_type:ident as $schema_name:ident => $output_name:expr) => {
|
||||
pub struct $schema_name {}
|
||||
|
||||
impl<'__s> ToSchema<'__s> for $schema_name {
|
||||
fn schema() -> (&'__s str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
|
||||
let settings = rmcp::schemars::generate::SchemaSettings::openapi3();
|
||||
let generator = settings.into_generator();
|
||||
let schema = generator.into_root_schema_for::<$inner_type>();
|
||||
let schema = convert_schemars_to_utoipa(schema);
|
||||
($output_name, schema)
|
||||
}
|
||||
|
||||
fn aliases() -> Vec<(&'__s str, utoipa::openapi::schema::Schema)> {
|
||||
Vec::new()
|
||||
}
|
||||
@@ -89,7 +106,26 @@ fn convert_json_object_to_utoipa(
|
||||
return RefOr::T(Schema::OneOf(builder.build()));
|
||||
}
|
||||
|
||||
// Handle the discriminated union pattern from schemars: an object with
|
||||
// `type`, `properties`, `required` AND `allOf` (e.g. each variant of a
|
||||
// `#[serde(tag = "type")]` enum). We merge the inline object (which carries
|
||||
// the discriminator property) with the `allOf` refs into a single `allOf`.
|
||||
if let Some(Value::Array(all_of)) = obj.get("allOf") {
|
||||
let has_inline_properties = obj.contains_key("properties") || obj.contains_key("type");
|
||||
if has_inline_properties {
|
||||
let mut builder = AllOfBuilder::new();
|
||||
// Build an object schema from the inline properties/required
|
||||
let mut obj_without_allof = obj.clone();
|
||||
obj_without_allof.remove("allOf");
|
||||
builder = builder.item(convert_json_object_to_utoipa(&obj_without_allof));
|
||||
for item in all_of {
|
||||
if let Ok(schema) = rmcp::schemars::Schema::try_from(item.clone()) {
|
||||
builder = builder.item(convert_schemars_to_utoipa(schema));
|
||||
}
|
||||
}
|
||||
return RefOr::T(Schema::AllOf(builder.build()));
|
||||
}
|
||||
|
||||
let mut builder = AllOfBuilder::new();
|
||||
for item in all_of {
|
||||
if let Ok(schema) = rmcp::schemars::Schema::try_from(item.clone()) {
|
||||
@@ -215,6 +251,22 @@ fn convert_typed_schema(
|
||||
"string" => {
|
||||
let mut object_builder = ObjectBuilder::new().schema_type(SchemaType::String);
|
||||
|
||||
if let Some(Value::Array(enum_values)) = obj.get("enum") {
|
||||
let values: Vec<serde_json::Value> = enum_values
|
||||
.iter()
|
||||
.filter_map(|v| {
|
||||
if let Value::String(s) = v {
|
||||
Some(Value::String(s.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if !values.is_empty() {
|
||||
object_builder = object_builder.enum_values(Some(values));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Value::Number(min_length)) = obj.get("minLength") {
|
||||
if let Some(min) = min_length.as_u64() {
|
||||
object_builder = object_builder.min_length(Some(min as usize));
|
||||
@@ -310,6 +362,7 @@ fn convert_typed_schema(
|
||||
|
||||
derive_utoipa!(Role as RoleSchema);
|
||||
derive_utoipa!(Content as ContentSchema);
|
||||
derive_utoipa!(RawContent as ContentBlockSchema => "ContentBlock");
|
||||
derive_utoipa!(EmbeddedResource as EmbeddedResourceSchema);
|
||||
derive_utoipa!(ImageContent as ImageContentSchema);
|
||||
derive_utoipa!(TextContent as TextContentSchema);
|
||||
@@ -577,6 +630,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::agent::ReadResourceResponse,
|
||||
super::routes::agent::CallToolRequest,
|
||||
super::routes::agent::CallToolResponse,
|
||||
ContentBlockSchema,
|
||||
super::routes::agent::ListAppsRequest,
|
||||
super::routes::agent::ListAppsResponse,
|
||||
super::routes::agent::ImportAppRequest,
|
||||
|
||||
@@ -27,7 +27,7 @@ use goose::{
|
||||
agents::{extension::ToolInfo, extension_manager::get_parameter_names},
|
||||
config::permission::PermissionLevel,
|
||||
};
|
||||
use rmcp::model::{CallToolRequestParams, Content};
|
||||
use rmcp::model::CallToolRequestParams;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
@@ -134,13 +134,31 @@ pub struct CallToolRequest {
|
||||
arguments: Value,
|
||||
}
|
||||
|
||||
/// Ref-only alias so utoipa emits `$ref: "#/components/schemas/ContentBlock"`.
|
||||
/// The actual schema is registered via `derive_utoipa!(RawContent as ContentBlockSchema => "ContentBlock")`.
|
||||
#[allow(dead_code)]
|
||||
pub enum ContentBlock {}
|
||||
|
||||
impl<'s> utoipa::ToSchema<'s> for ContentBlock {
|
||||
fn schema() -> (
|
||||
&'s str,
|
||||
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
|
||||
) {
|
||||
// Delegate to the auto-generated schema
|
||||
crate::openapi::ContentBlockSchema::schema()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CallToolResponse {
|
||||
content: Vec<Content>,
|
||||
#[schema(value_type = Vec<ContentBlock>)]
|
||||
content: Vec<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
structured_content: Option<Value>,
|
||||
is_error: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "_meta")]
|
||||
_meta: Option<Value>,
|
||||
}
|
||||
|
||||
@@ -992,8 +1010,15 @@ async fn call_tool(
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let content = result
|
||||
.content
|
||||
.into_iter()
|
||||
.map(serde_json::to_value)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(CallToolResponse {
|
||||
content: result.content,
|
||||
content,
|
||||
structured_content: result.structured_content,
|
||||
is_error: result.is_error.unwrap_or(false),
|
||||
_meta: result.meta.and_then(|m| serde_json::to_value(m).ok()),
|
||||
|
||||
Reference in New Issue
Block a user