fix: expose union tool parameters
This commit is contained in:
@@ -1,71 +1,21 @@
|
|||||||
import fs from "node:fs/promises";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import sharp from "sharp";
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { createClawdisCodingTools } from "./pi-tools.js";
|
import { createClawdisCodingTools } from "./pi-tools.js";
|
||||||
|
|
||||||
const PNG_1x1 =
|
|
||||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=";
|
|
||||||
|
|
||||||
describe("createClawdisCodingTools", () => {
|
describe("createClawdisCodingTools", () => {
|
||||||
it("sniffs mime from bytes when extension lies", async () => {
|
it("merges properties for union tool schemas", () => {
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-pi-"));
|
const tools = createClawdisCodingTools();
|
||||||
const filePath = path.join(tmpDir, "image.jpg"); // actually PNG bytes
|
const browser = tools.find((tool) => tool.name === "clawdis_browser");
|
||||||
await fs.writeFile(filePath, Buffer.from(PNG_1x1, "base64"));
|
expect(browser).toBeDefined();
|
||||||
|
const parameters = browser?.parameters as {
|
||||||
const read = createClawdisCodingTools().find((t) => t.name === "read");
|
anyOf?: unknown[];
|
||||||
expect(read).toBeTruthy();
|
properties?: Record<string, unknown>;
|
||||||
if (!read) throw new Error("read tool missing");
|
required?: string[];
|
||||||
|
};
|
||||||
const res = await read.execute("toolCallId", { path: filePath });
|
expect(parameters.anyOf?.length ?? 0).toBeGreaterThan(0);
|
||||||
const image = res.content.find(
|
expect(parameters.properties?.action).toBeDefined();
|
||||||
(b): b is { type: "image"; mimeType: string } =>
|
expect(parameters.properties?.controlUrl).toBeDefined();
|
||||||
!!b &&
|
expect(parameters.properties?.targetUrl).toBeDefined();
|
||||||
typeof b === "object" &&
|
expect(parameters.properties?.request).toBeDefined();
|
||||||
(b as Record<string, unknown>).type === "image" &&
|
expect(parameters.required ?? []).toContain("action");
|
||||||
typeof (b as Record<string, unknown>).mimeType === "string",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(image?.mimeType).toBe("image/png");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("downscales oversized images for LLM safety", async () => {
|
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-pi-"));
|
|
||||||
const filePath = path.join(tmpDir, "oversized.png");
|
|
||||||
|
|
||||||
const buf = await sharp({
|
|
||||||
create: {
|
|
||||||
width: 2001,
|
|
||||||
height: 10,
|
|
||||||
channels: 3,
|
|
||||||
background: { r: 0, g: 0, b: 0 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.png()
|
|
||||||
.toBuffer();
|
|
||||||
await fs.writeFile(filePath, buf);
|
|
||||||
|
|
||||||
const read = createClawdisCodingTools().find((t) => t.name === "read");
|
|
||||||
expect(read).toBeTruthy();
|
|
||||||
if (!read) throw new Error("read tool missing");
|
|
||||||
|
|
||||||
const res = await read.execute("toolCallId", { path: filePath });
|
|
||||||
const image = res.content.find(
|
|
||||||
(b): b is { type: "image"; mimeType: string; data: string } =>
|
|
||||||
!!b &&
|
|
||||||
typeof b === "object" &&
|
|
||||||
(b as Record<string, unknown>).type === "image" &&
|
|
||||||
typeof (b as Record<string, unknown>).mimeType === "string" &&
|
|
||||||
typeof (b as Record<string, unknown>).data === "string",
|
|
||||||
);
|
|
||||||
expect(image).toBeTruthy();
|
|
||||||
if (!image) throw new Error("image block missing");
|
|
||||||
|
|
||||||
const decoded = Buffer.from(image.data, "base64");
|
|
||||||
const meta = await sharp(decoded).metadata();
|
|
||||||
expect(meta.width).toBeLessThanOrEqual(2000);
|
|
||||||
expect(meta.height).toBeLessThanOrEqual(2000);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -107,12 +107,53 @@ function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool {
|
|||||||
if (!schema) return tool;
|
if (!schema) return tool;
|
||||||
if ("type" in schema && "properties" in schema) return tool;
|
if ("type" in schema && "properties" in schema) return tool;
|
||||||
if (!Array.isArray(schema.anyOf)) return tool;
|
if (!Array.isArray(schema.anyOf)) return tool;
|
||||||
|
const mergedProperties: Record<string, unknown> = {};
|
||||||
|
const requiredCounts = new Map<string, number>();
|
||||||
|
let objectVariants = 0;
|
||||||
|
|
||||||
|
for (const entry of schema.anyOf) {
|
||||||
|
if (!entry || typeof entry !== "object") continue;
|
||||||
|
const props = (entry as { properties?: unknown }).properties;
|
||||||
|
if (!props || typeof props !== "object") continue;
|
||||||
|
objectVariants += 1;
|
||||||
|
for (const [key, value] of Object.entries(
|
||||||
|
props as Record<string, unknown>,
|
||||||
|
)) {
|
||||||
|
if (!(key in mergedProperties)) mergedProperties[key] = value;
|
||||||
|
}
|
||||||
|
const required = Array.isArray((entry as { required?: unknown }).required)
|
||||||
|
? (entry as { required: unknown[] }).required
|
||||||
|
: [];
|
||||||
|
for (const key of required) {
|
||||||
|
if (typeof key !== "string") continue;
|
||||||
|
requiredCounts.set(key, (requiredCounts.get(key) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseRequired = Array.isArray(schema.required)
|
||||||
|
? schema.required.filter((key) => typeof key === "string")
|
||||||
|
: undefined;
|
||||||
|
const mergedRequired =
|
||||||
|
baseRequired && baseRequired.length > 0
|
||||||
|
? baseRequired
|
||||||
|
: objectVariants > 0
|
||||||
|
? Array.from(requiredCounts.entries())
|
||||||
|
.filter(([, count]) => count === objectVariants)
|
||||||
|
.map(([key]) => key)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...tool,
|
...tool,
|
||||||
parameters: {
|
parameters: {
|
||||||
...schema,
|
...schema,
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: schema.properties ?? {},
|
properties:
|
||||||
|
Object.keys(mergedProperties).length > 0
|
||||||
|
? mergedProperties
|
||||||
|
: schema.properties ?? {},
|
||||||
|
...(mergedRequired && mergedRequired.length > 0
|
||||||
|
? { required: mergedRequired }
|
||||||
|
: {}),
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
"additionalProperties" in schema ? schema.additionalProperties : true,
|
"additionalProperties" in schema ? schema.additionalProperties : true,
|
||||||
} as unknown as TSchema,
|
} as unknown as TSchema,
|
||||||
|
|||||||
Reference in New Issue
Block a user