accept path as silent fallback parameter

This commit is contained in:
Peter Steinberger 2025-06-08 04:05:13 +01:00
parent 76d0faef42
commit 17b74b4f1f
2 changed files with 97 additions and 6 deletions

View file

@ -11,6 +11,7 @@ import {
export const analyzeToolSchema = z.object({
image_path: z
.string()
.optional()
.describe(
"Required. Absolute path to image file (.png, .jpg, .webp) to be analyzed.",
),
@ -36,7 +37,15 @@ export const analyzeToolSchema = z.object({
.describe(
"Optional. Explicit provider/model. Validated against server's PEEKABOO_AI_PROVIDERS.",
),
});
// Silent fallback parameter (not advertised in schema)
path: z.string().optional(),
}).refine(
(data) => data.image_path || data.path,
{
message: "image_path is required",
path: ["image_path"],
}
);
export type AnalyzeToolInput = z.infer<typeof analyzeToolSchema>;
@ -47,13 +56,16 @@ export async function analyzeToolHandler(
const { logger } = context;
try {
// Determine the effective image path (prioritize image_path, fallback to path)
const effectiveImagePath = input.image_path || input.path!;
logger.debug(
{ input: { ...input, image_path: input.image_path.split("/").pop() } },
{ input: { ...input, effectiveImagePath: effectiveImagePath.split("/").pop() } },
"Processing peekaboo.analyze tool call",
);
// Validate image file extension
const ext = path.extname(input.image_path).toLowerCase();
const ext = path.extname(effectiveImagePath).toLowerCase();
if (![".png", ".jpg", ".jpeg", ".webp"].includes(ext)) {
return {
content: [
@ -117,10 +129,10 @@ export async function analyzeToolHandler(
// Read image as base64
let imageBase64: string;
try {
imageBase64 = await readImageAsBase64(input.image_path);
imageBase64 = await readImageAsBase64(effectiveImagePath);
} catch (error) {
logger.error(
{ error, path: input.image_path },
{ error, path: effectiveImagePath },
"Failed to read image file",
);
return {
@ -140,7 +152,7 @@ export async function analyzeToolHandler(
try {
analysisResult = await analyzeImageWithProvider(
{ provider, model },
input.image_path,
effectiveImagePath,
imageBase64,
input.question,
logger,

View file

@ -2,6 +2,7 @@ import { vi, beforeEach, describe, it, expect } from "vitest";
import { pino } from "pino";
import {
analyzeToolHandler,
analyzeToolSchema,
AnalyzeToolInput,
} from "../../../src/tools/analyze";
import { readImageAsBase64 } from "../../../src/utils/peekaboo-cli";
@ -567,4 +568,82 @@ describe("Analyze Tool", () => {
expect(result.isError).toBeUndefined();
});
});
describe("Schema Validation", () => {
it("should validate successfully with image_path", () => {
const input = {
image_path: "/path/to/image.png",
question: "What is this?",
};
const result = analyzeToolSchema.safeParse(input);
expect(result.success).toBe(true);
});
it("should validate successfully with path as silent fallback", () => {
const input = {
path: "/path/to/image.png",
question: "What is this?",
};
const result = analyzeToolSchema.safeParse(input);
expect(result.success).toBe(true);
});
it("should validate successfully when both image_path and path are provided", () => {
const input = {
image_path: "/priority/image.png",
path: "/fallback/image.png",
question: "What is this?",
};
const result = analyzeToolSchema.safeParse(input);
expect(result.success).toBe(true);
});
it("should fail validation when neither image_path nor path is provided", () => {
const input = {
question: "What is this?",
};
const result = analyzeToolSchema.safeParse(input);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.errors[0].message).toBe("image_path is required");
expect(result.error.errors[0].path).toEqual(["image_path"]);
}
});
it("should fail validation when question is missing", () => {
const input = {
image_path: "/path/to/image.png",
};
const result = analyzeToolSchema.safeParse(input);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.errors[0].message).toBe("Required");
expect(result.error.errors[0].path).toEqual(["question"]);
}
});
it("should validate optional provider_config correctly", () => {
const inputWithoutProvider = {
image_path: "/path/to/image.png",
question: "What is this?",
};
const inputWithProvider = {
image_path: "/path/to/image.png",
question: "What is this?",
provider_config: {
type: "openai" as const,
model: "gpt-4o",
},
};
expect(analyzeToolSchema.safeParse(inputWithoutProvider).success).toBe(true);
expect(analyzeToolSchema.safeParse(inputWithProvider).success).toBe(true);
});
});
});