art agent: enforce json schema
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Successful in 1m32s
Project CI / Native shell tests (pull_request) Successful in 1m57s

This commit is contained in:
2026-07-28 12:03:45 +08:00
parent 7f8abe02ea
commit 2b31d5e16e
10 changed files with 115 additions and 13 deletions
@@ -88,6 +88,7 @@ impl Tool for EditImageTool {
"type": "string",
"description": "编辑提示词,描述希望如何修改图片。例如「把背景换成红色」、「把人物改成坐着」。"
},
// TODO need to introduce size param, but that needs more metadata such as original image size, skip in this version
"model": {
"type": "string",
"enum": [GPT_IMAGE_2_MODEL],
@@ -6,7 +6,8 @@ use crate::agent::tools::generate_image::{
};
use crate::agent::tools::image_generation_options::{
default_image_aspect_ratio, default_image_model, default_image_size,
image_aspect_ratio_parameter_schema, image_model_parameter_schema, image_size_parameter_schema,
image_aspect_ratio_parameter_schema, image_model_parameter_schema,
image_model_size_constraint_schema, image_size_parameter_schema,
validate_image_generation_options,
};
use crate::framework::tool::ToolFailureKind;
@@ -53,7 +54,8 @@ impl Tool for GenerateCharacterTool {
"image_size": image_size_parameter_schema(),
},
"required": ["prompt"],
"additionalProperties": false
"additionalProperties": false,
"allOf": [image_model_size_constraint_schema()]
})
}
@@ -4,7 +4,8 @@ use crate::agent::tools::context::EditorToolContext;
use crate::agent::tools::image_generation_options::{
ImageGenerationOptionsError, default_image_aspect_ratio, default_image_model,
default_image_size, image_aspect_ratio_parameter_schema, image_model_parameter_schema,
image_size_parameter_schema, validate_image_generation_options,
image_model_size_constraint_schema, image_size_parameter_schema,
validate_image_generation_options,
};
use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind};
use platform_image::{GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL};
@@ -133,7 +134,8 @@ impl Tool for GenerateIconSpritesheetTool {
"image_size": image_size_parameter_schema()
},
"required": ["reference_image_id", "icon_descriptions"],
"additionalProperties": false
"additionalProperties": false,
"allOf": [image_model_size_constraint_schema()]
})
}
@@ -4,7 +4,8 @@ use crate::agent::tools::context::EditorToolContext;
use crate::agent::tools::image_generation_options::{
ImageGenerationOptionsError, default_image_aspect_ratio, default_image_model,
default_image_size, image_aspect_ratio_parameter_schema, image_model_parameter_schema,
image_size_parameter_schema, validate_image_generation_options,
image_model_size_constraint_schema, image_size_parameter_schema,
validate_image_generation_options,
};
use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind};
use platform_image::{GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL};
@@ -113,7 +114,8 @@ impl Tool for GenerateImageTool {
"image_size": image_size_parameter_schema()
},
"required": ["prompt"],
"additionalProperties": false
"additionalProperties": false,
"allOf": [image_model_size_constraint_schema()]
})
}
@@ -5,8 +5,8 @@ use crate::agent::tools::generate_image::{
EditorImageGenerationResult, GenerateImageError, GenerateImageToolOutput,
};
use crate::agent::tools::image_generation_options::{
default_image_aspect_ratio, default_image_size, image_aspect_ratio_parameter_schema,
image_size_parameter_schema, validate_image_generation_options,
default_image_aspect_ratio, default_image_size, gpt_image_2_size_parameter_schema,
image_aspect_ratio_parameter_schema, validate_image_generation_options,
};
use crate::framework::tool::ToolFailureKind;
use crate::framework::tool::{Tool, ToolFailure};
@@ -58,7 +58,7 @@ impl Tool for GenerateUiDesignTool {
},
"reference_image_ids": { "type": "array", "items": { "type": "string" }, "description": "image_id(s) for desc UI 风格或布局" },
"aspect_ratio": image_aspect_ratio_parameter_schema(),
"image_size": image_size_parameter_schema()
"image_size": gpt_image_2_size_parameter_schema()
},
"required": ["prompt", "model"],
"additionalProperties": false
@@ -169,6 +169,14 @@ mod tests {
parameters["properties"]["model"]["default"],
GPT_IMAGE_2_MODEL
);
assert_eq!(
parameters["properties"]["image_size"]["enum"],
json!(["1K", "2K"])
);
assert_eq!(
parameters["properties"]["image_size"]["default"],
json!("1K")
);
assert!(
parameters["required"]
.as_array()
@@ -125,9 +125,10 @@ impl Tool for GenerateVideoTool {
"additionalProperties": false,
"allOf": [
{
// model 省略时运行时会补为 fast;这里故意不要求 model 存在,
// 让 JSON Schema 对省略 model 的调用应用同一分辨率约束。
"if": {
"properties": { "model": { "const": GenerateVideoTool::SEEDANCE_2_FAST_MODEL } },
"required": ["model"]
"properties": { "model": { "const": GenerateVideoTool::SEEDANCE_2_FAST_MODEL } }
},
"then": {
"properties": { "resolution": { "enum": GenerateVideoTool::SEEDANCE_2_FAST_RESOLUTIONS } }
@@ -360,5 +361,15 @@ mod tests {
);
assert_eq!(schema["properties"]["sound"]["default"], json!("on"));
assert_eq!(schema["allOf"].as_array().map(Vec::len), Some(2));
let fast_model_condition = &schema["allOf"][0];
assert_eq!(
fast_model_condition["if"]["properties"]["model"]["const"],
json!(GenerateVideoTool::DEFAULT_VIDEO_MODEL)
);
assert!(fast_model_condition["if"].get("required").is_none());
assert_eq!(
fast_model_condition["then"]["properties"]["resolution"]["enum"],
json!(["480p", "720p"])
);
}
}
@@ -93,12 +93,36 @@ pub fn image_aspect_ratio_parameter_schema() -> Value {
pub fn image_size_parameter_schema() -> Value {
json!({
"type": "string",
"enum": ["0.5K", "1K", "2K"],
"enum": NANOBANANA_2_IMAGE_SIZES,
"default": DEFAULT_IMAGE_SIZE,
"description": "图片尺寸档位。nanobanana2 支持 0.5K、1K、2Kgpt-image-2 仅支持 1K、2K;默认 1K。"
})
}
pub fn gpt_image_2_size_parameter_schema() -> Value {
json!({
"type": "string",
"enum": GPT_IMAGE_2_IMAGE_SIZES,
"default": DEFAULT_IMAGE_SIZE,
"description": "图片尺寸档位。gpt-image-2 仅支持 1K、2K;默认 1K。"
})
}
pub fn image_model_size_constraint_schema() -> Value {
json!({
"if": {
"properties": { "model": { "const": GPT_IMAGE_2_MODEL } },
// model 省略时运行时默认 nanobanana2,仍允许 0.5K。
"required": ["model"]
},
"then": {
"properties": {
"image_size": { "enum": GPT_IMAGE_2_IMAGE_SIZES }
}
}
})
}
fn supported_image_sizes(model: &str) -> Option<&'static [&'static str]> {
match model {
NANOBANANA_2_MODEL => Some(NANOBANANA_2_IMAGE_SIZES),
@@ -157,5 +181,23 @@ mod tests {
json!(["0.5K", "1K", "2K"])
);
assert_eq!(image_size_parameter_schema()["default"], DEFAULT_IMAGE_SIZE);
assert_eq!(
gpt_image_2_size_parameter_schema()["enum"],
json!(["1K", "2K"])
);
assert_eq!(
gpt_image_2_size_parameter_schema()["default"],
DEFAULT_IMAGE_SIZE
);
let model_size_constraint = image_model_size_constraint_schema();
assert_eq!(
model_size_constraint["if"]["properties"]["model"]["const"],
GPT_IMAGE_2_MODEL
);
assert_eq!(model_size_constraint["if"]["required"], json!(["model"]));
assert_eq!(
model_size_constraint["then"]["properties"]["image_size"]["enum"],
json!(["1K", "2K"])
);
}
}
@@ -18,7 +18,9 @@ mod tests {
GenerateBackgroundMusicTool, GenerateBackgroundMusicToolArgs,
};
use super::generate_character::{GenerateCharacterTool, GenerateCharacterToolArgs};
use super::generate_icon_spritesheet::GenerateIconSpritesheetToolArgs;
use super::generate_icon_spritesheet::{
GenerateIconSpritesheetTool, GenerateIconSpritesheetToolArgs,
};
use super::generate_image::{GenerateImageError, GenerateImageTool, GenerateImageToolArgs};
use super::generate_sound_effect::{GenerateSoundEffectTool, GenerateSoundEffectToolArgs};
use super::generate_ui_design::{GenerateUiDesignTool, GenerateUiDesignToolArgs};
@@ -188,6 +190,18 @@ mod tests {
#[test]
fn confirmation_schemas_expose_all_finite_choices_as_enums() {
let context = EditorToolContext::default();
let image = GenerateImageTool {
context: context.clone(),
}
.parameters();
let character = GenerateCharacterTool {
context: context.clone(),
}
.parameters();
let icon = GenerateIconSpritesheetTool {
context: context.clone(),
}
.parameters();
let edit = EditImageTool {
context: context.clone(),
}
@@ -218,5 +232,16 @@ mod tests {
assert_eq!(sound["properties"]["duration"]["default"], json!(5));
assert_eq!(video["properties"]["sound"]["default"], json!("on"));
assert!(music["properties"].get("make_instrumental").is_none());
for schema in [&image, &character, &icon] {
assert_eq!(
schema["allOf"][0]["if"]["properties"]["model"]["const"],
json!(GPT_IMAGE_2_MODEL)
);
assert_eq!(
schema["allOf"][0]["then"]["properties"]["image_size"]["enum"],
json!(["1K", "2K"])
);
}
}
}