mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 22:41:47 -06:00
Make MCP aligns enums compatible with Kimi (#5592)
* M13: make MCP aligns enums Kimi-compatible Fixes #5588 * Drop the progress-log lines that conflict with main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
036811368b
commit
07582e9ed4
@@ -21,6 +21,9 @@ them with the image's current orientation. Full turns retain its appearance, rec
|
||||
and flips. Angles that are not multiples of 90 are refused without changing the document or
|
||||
its undo/redo history.
|
||||
|
||||
`page_add_text`, `content_update` and `text_edit` take per-line `aligns`. An `"inherit"` entry
|
||||
follows the box-level `align`; legacy `null` entries remain accepted with the same meaning.
|
||||
|
||||
`Content` is `Json(Value)` or `Png { data, width, height }`. Errors are `UnknownTool`, `InvalidArgs` (the call didn't match the schema) or `Failed` (a readable message for the agent).
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -83,10 +83,10 @@ pub(crate) fn parse_align(al: &str) -> Result<TextAlign> {
|
||||
/// Most entries `aligns` takes: one per paragraph, and no text has this many.
|
||||
const MAX_ALIGNS: usize = 4096;
|
||||
|
||||
/// `aligns`: each paragraph's (line's) own alignment, or null where it follows `align`.
|
||||
/// `aligns`: each paragraph's (line's) own alignment, or inherit/null where it follows `align`.
|
||||
pub(crate) fn opt_aligns(a: &Args) -> Result<Option<Vec<Option<TextAlign>>>> {
|
||||
let Some(v) = a.get("aligns") else { return Ok(None) };
|
||||
let items = v.as_array().ok_or_else(|| bad("aligns must be an array of left, center, right, justify or null"))?;
|
||||
let items = v.as_array().ok_or_else(|| bad("aligns must be an array of left, center, right, justify, inherit or null"))?;
|
||||
if items.len() > MAX_ALIGNS {
|
||||
return Err(bad(format!("aligns takes at most {MAX_ALIGNS} entries")));
|
||||
}
|
||||
@@ -94,8 +94,9 @@ pub(crate) fn opt_aligns(a: &Args) -> Result<Option<Vec<Option<TextAlign>>>> {
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
Value::Null => Ok(None),
|
||||
Value::String(s) if s == "inherit" => Ok(None),
|
||||
Value::String(s) => parse_align(s).map(Some),
|
||||
_ => Err(bad("each entry of aligns is left, center, right, justify or null")),
|
||||
_ => Err(bad("each entry of aligns is left, center, right, justify, inherit or null")),
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()
|
||||
.map(Some)
|
||||
|
||||
@@ -637,7 +637,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
"font": { "type": "string", "enum": ["helvetica", "times", "courier"] }, "size": { "type": "number", "minimum": 1, "maximum": 500 },
|
||||
"bold": { "type": "boolean" }, "italic": { "type": "boolean" }, "color": { "type": "string" },
|
||||
"align": { "type": "string", "enum": ["left", "center", "right", "justify"] },
|
||||
"aligns": { "type": "array", "maxItems": 4096, "items": { "enum": ["left", "center", "right", "justify", null] }, "description": "Each paragraph's (line's) own alignment, by index; null follows align." },
|
||||
"aligns": { "type": "array", "maxItems": 4096, "items": { "type": "string", "enum": ["left", "center", "right", "justify", "inherit"] }, "description": "Each paragraph's (line's) own alignment, by index; inherit (or legacy null) follows align." },
|
||||
"char_spacing": { "type": "number", "minimum": -500, "maximum": 500, "description": "Points after each character." },
|
||||
}),
|
||||
&["doc", "page", "text"],
|
||||
@@ -660,7 +660,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
"font": { "type": "string", "enum": ["helvetica", "times", "courier"] }, "size": { "type": "number", "minimum": 1, "maximum": 500 },
|
||||
"bold": { "type": "boolean" }, "italic": { "type": "boolean" }, "color": { "type": "string" },
|
||||
"align": { "type": "string", "enum": ["left", "center", "right", "justify"] },
|
||||
"aligns": { "type": "array", "maxItems": 4096, "items": { "enum": ["left", "center", "right", "justify", null] }, "description": "Each paragraph's (line's) own alignment, by index; null follows align." },
|
||||
"aligns": { "type": "array", "maxItems": 4096, "items": { "type": "string", "enum": ["left", "center", "right", "justify", "inherit"] }, "description": "Each paragraph's (line's) own alignment, by index; inherit (or legacy null) follows align." },
|
||||
"char_spacing": { "type": "number", "minimum": -500, "maximum": 500, "description": "Points after each character." },
|
||||
"rotate": { "type": "integer" }, "flip_h": { "type": "boolean" }, "flip_v": { "type": "boolean" },
|
||||
"crop": { "type": "array", "items": { "type": "number", "minimum": 0, "maximum": 0.95 }, "minItems": 4, "maxItems": 4, "description": "Fractions trimmed: left, bottom, right, top. Left and right together, and bottom and top together, trim at most 0.95." },
|
||||
@@ -1429,7 +1429,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
"size": { "type": "number", "exclusiveMinimum": 0 },
|
||||
"color": { "type": "string", "description": "#rrggbb" },
|
||||
"align": { "type": "string", "enum": ["left", "center", "right", "justify"] },
|
||||
"aligns": { "type": "array", "maxItems": 4096, "items": { "enum": ["left", "center", "right", "justify", null] }, "description": "Each paragraph's (line's) own alignment, by index; null follows align." },
|
||||
"aligns": { "type": "array", "maxItems": 4096, "items": { "type": "string", "enum": ["left", "center", "right", "justify", "inherit"] }, "description": "Each paragraph's (line's) own alignment, by index; inherit (or legacy null) follows align." },
|
||||
"underline": { "type": "boolean" },
|
||||
"line_spacing": { "type": "number", "description": "Multiple of the font size (1.2 is ordinary)." },
|
||||
"dx": { "type": "number", "description": "Paragraph only: move right by this many points (negative: left)." },
|
||||
|
||||
@@ -664,6 +664,44 @@ fn tool_table_is_well_formed() {
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_homogeneous_enums(schema: &Value, path: &str) {
|
||||
fn json_kind(value: &Value) -> &'static str {
|
||||
match value {
|
||||
Value::Null => "null",
|
||||
Value::Bool(_) => "boolean",
|
||||
Value::Number(_) => "number",
|
||||
Value::String(_) => "string",
|
||||
Value::Array(_) => "array",
|
||||
Value::Object(_) => "object",
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(values) = schema.get("enum").and_then(Value::as_array) {
|
||||
let first = values.first().map(json_kind);
|
||||
assert!(values.iter().map(json_kind).all(|kind| Some(kind) == first), "mixed enum types at {path}: {values:?}");
|
||||
}
|
||||
match schema {
|
||||
Value::Object(object) => {
|
||||
for (key, child) in object {
|
||||
assert_homogeneous_enums(child, &format!("{path}.{key}"));
|
||||
}
|
||||
}
|
||||
Value::Array(array) => {
|
||||
for (index, child) in array.iter().enumerate() {
|
||||
assert_homogeneous_enums(child, &format!("{path}[{index}]"));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_schema_enums_have_one_json_type() {
|
||||
for tool in tools() {
|
||||
assert_homogeneous_enums(&tool.input_schema, tool.name);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_font_and_disjoint_ranges_save_reopen_through_tools() {
|
||||
let dir = workdir("font-ranges");
|
||||
@@ -9878,7 +9916,11 @@ fn added_text_lines_take_their_own_alignments() {
|
||||
let dir = workdir("paragraph-aligns");
|
||||
let mut a = auto(&dir);
|
||||
let id = ok(&mut a, "doc_create", json!({"from":"blank","width":600,"height":400}))["doc"].as_u64().unwrap();
|
||||
ok(&mut a, "page_add_text", json!({"doc":id,"page":1,"text":"Left line\nRight line","at":[40,50],"width":500,"size":12,"aligns":[null,"right"]}));
|
||||
ok(
|
||||
&mut a,
|
||||
"page_add_text",
|
||||
json!({"doc":id,"page":1,"text":"Left line\nRight line\nInherited line","at":[40,50],"width":500,"size":12,"aligns":[null,"right","inherit"]}),
|
||||
);
|
||||
let rect = |a: &mut Automation, text: &str| -> Vec<f64> {
|
||||
let lines = ok(a, "text_lines", json!({"doc":id,"page":1}));
|
||||
let line = lines["lines"].as_array().unwrap().iter().find(|l| l["text"] == text).cloned().unwrap();
|
||||
@@ -9886,10 +9928,13 @@ fn added_text_lines_take_their_own_alignments() {
|
||||
};
|
||||
assert!((rect(&mut a, "Left line")[0] - 40.0).abs() < 1.0);
|
||||
assert!((rect(&mut a, "Right line")[2] - 540.0).abs() < 1.0, "{:?}", rect(&mut a, "Right line"));
|
||||
assert!((rect(&mut a, "Inherited line")[0] - 40.0).abs() < 1.0);
|
||||
// The whole box centred: the right-aligned line goes with it.
|
||||
ok(&mut a, "content_update", json!({"doc":id,"page":1,"index":1,"align":"center"}));
|
||||
let right = rect(&mut a, "Right line");
|
||||
assert!(((right[0] + right[2]) / 2.0 - 290.0).abs() < 1.0, "{right:?}");
|
||||
let inherited = rect(&mut a, "Inherited line");
|
||||
assert!(((inherited[0] + inherited[2]) / 2.0 - 290.0).abs() < 1.0, "{inherited:?}");
|
||||
assert!(a.call("content_update", &json!({"doc":id,"page":1,"index":1,"aligns":["sideways"]})).is_err());
|
||||
assert!(a.call("content_update", &json!({"doc":id,"page":1,"index":1,"aligns":"right"})).is_err());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user