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 { createClawdisCodingTools } from "./pi-tools.js";
|
||||
|
||||
const PNG_1x1 =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=";
|
||||
|
||||
describe("createClawdisCodingTools", () => {
|
||||
it("sniffs mime from bytes when extension lies", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-pi-"));
|
||||
const filePath = path.join(tmpDir, "image.jpg"); // actually PNG bytes
|
||||
await fs.writeFile(filePath, Buffer.from(PNG_1x1, "base64"));
|
||||
|
||||
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 } =>
|
||||
!!b &&
|
||||
typeof b === "object" &&
|
||||
(b as Record<string, unknown>).type === "image" &&
|
||||
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);
|
||||
it("merges properties for union tool schemas", () => {
|
||||
const tools = createClawdisCodingTools();
|
||||
const browser = tools.find((tool) => tool.name === "clawdis_browser");
|
||||
expect(browser).toBeDefined();
|
||||
const parameters = browser?.parameters as {
|
||||
anyOf?: unknown[];
|
||||
properties?: Record<string, unknown>;
|
||||
required?: string[];
|
||||
};
|
||||
expect(parameters.anyOf?.length ?? 0).toBeGreaterThan(0);
|
||||
expect(parameters.properties?.action).toBeDefined();
|
||||
expect(parameters.properties?.controlUrl).toBeDefined();
|
||||
expect(parameters.properties?.targetUrl).toBeDefined();
|
||||
expect(parameters.properties?.request).toBeDefined();
|
||||
expect(parameters.required ?? []).toContain("action");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,12 +107,53 @@ function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool {
|
||||
if (!schema) return tool;
|
||||
if ("type" in schema && "properties" in schema) 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 {
|
||||
...tool,
|
||||
parameters: {
|
||||
...schema,
|
||||
type: "object",
|
||||
properties: schema.properties ?? {},
|
||||
properties:
|
||||
Object.keys(mergedProperties).length > 0
|
||||
? mergedProperties
|
||||
: schema.properties ?? {},
|
||||
...(mergedRequired && mergedRequired.length > 0
|
||||
? { required: mergedRequired }
|
||||
: {}),
|
||||
additionalProperties:
|
||||
"additionalProperties" in schema ? schema.additionalProperties : true,
|
||||
} as unknown as TSchema,
|
||||
|
||||
Reference in New Issue
Block a user