Files
oh-my-pi/packages/coding-agent/src/tools/inspect-image.ts
T
can1357 f1f6516056 refactor: reorganized exports and removed obsolete helper branches
- Removed export leakage by demoting many helper and const symbols to module-local scope.
- Renamed underscore-prefixed internals and cache fields, then updated related references and `satisfies never` checks.
- Deleted obsolete logic branches and helpers, including harmony-stream interruption flow and unused benchmark runtime helpers.
- Updated Biome config and manifests by broadening lint coverage and removing an unused `@napi-rs/cli` dev dependency.
- Adjusted tests and utilities to use renamed test helpers and remove redundant private test-only helpers/locals.
2026-05-14 04:36:19 +02:00

164 lines
5.3 KiB
TypeScript

import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { type Api, completeSimple, type Model } from "@oh-my-pi/pi-ai";
import { prompt } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { extractTextContent } from "../commit/utils";
import { expandRoleAlias, resolveModelFromString } from "../config/model-resolver";
import inspectImageDescription from "../prompts/tools/inspect-image.md" with { type: "text" };
import inspectImageSystemPromptTemplate from "../prompts/tools/inspect-image-system.md" with { type: "text" };
import {
ImageInputTooLargeError,
type LoadedImageInput,
loadImageInput,
MAX_IMAGE_INPUT_BYTES,
} from "../utils/image-loading";
import type { ToolSession } from "./index";
import { ToolError } from "./tool-errors";
const inspectImageSchema = Type.Object(
{
path: Type.String({ description: "image path", examples: ["image.png"] }),
question: Type.String({ description: "question about image", examples: ["What is in this image?"] }),
},
{ additionalProperties: false },
);
export type InspectImageParams = Static<typeof inspectImageSchema>;
export interface InspectImageToolDetails {
model: string;
imagePath: string;
mimeType: string;
}
export class InspectImageTool implements AgentTool<typeof inspectImageSchema, InspectImageToolDetails> {
readonly name = "inspect_image";
readonly label = "InspectImage";
readonly loadMode = "discoverable";
readonly summary = "Describe or analyze an image file";
readonly description: string;
readonly parameters = inspectImageSchema;
readonly strict = false;
constructor(
private readonly session: ToolSession,
private readonly completeImageRequest: typeof completeSimple = completeSimple,
) {
this.description = prompt.render(inspectImageDescription);
}
async execute(
_toolCallId: string,
params: InspectImageParams,
signal?: AbortSignal,
_onUpdate?: AgentToolUpdateCallback<InspectImageToolDetails>,
_context?: AgentToolContext,
): Promise<AgentToolResult<InspectImageToolDetails>> {
if (this.session.settings.get("images.blockImages")) {
throw new ToolError(
"Image submission is disabled by settings (images.blockImages=true). Disable it to use inspect_image.",
);
}
const modelRegistry = this.session.modelRegistry;
if (!modelRegistry) {
throw new ToolError("Model registry is unavailable for inspect_image.");
}
const availableModels = modelRegistry.getAvailable();
if (availableModels.length === 0) {
throw new ToolError("No models available for inspect_image.");
}
const matchPreferences = { usageOrder: this.session.settings.getStorage()?.getModelUsageOrder() };
const resolvePattern = (pattern: string | undefined): Model<Api> | undefined => {
if (!pattern) return undefined;
const expanded = expandRoleAlias(pattern, this.session.settings);
return resolveModelFromString(expanded, availableModels, matchPreferences, modelRegistry);
};
const activeModelPattern = this.session.getActiveModelString?.() ?? this.session.getModelString?.();
const model =
resolvePattern("pi/vision") ??
resolvePattern("pi/default") ??
resolvePattern(activeModelPattern) ??
availableModels[0];
if (!model) {
throw new ToolError("Unable to resolve a model for inspect_image.");
}
if (!model.input.includes("image")) {
throw new ToolError(
`Resolved model ${model.provider}/${model.id} does not support image input. Configure a vision-capable model for modelRoles.vision.`,
);
}
const apiKey = await modelRegistry.getApiKey(model);
if (!apiKey) {
throw new ToolError(
`No API key available for ${model.provider}/${model.id}. Configure credentials for this provider or choose another vision-capable model.`,
);
}
let imageInput: LoadedImageInput | null;
try {
imageInput = await loadImageInput({
path: params.path,
cwd: this.session.cwd,
autoResize: this.session.settings.get("images.autoResize"),
maxBytes: MAX_IMAGE_INPUT_BYTES,
});
} catch (error) {
if (error instanceof ImageInputTooLargeError) {
throw new ToolError(error.message);
}
throw error;
}
if (!imageInput) {
throw new ToolError("inspect_image only supports PNG, JPEG, GIF, and WEBP files detected by file content.");
}
const response = await this.completeImageRequest(
model,
{
systemPrompt: [prompt.render(inspectImageSystemPromptTemplate)],
messages: [
{
role: "user",
content: [
{ type: "image", data: imageInput.data, mimeType: imageInput.mimeType },
{ type: "text", text: params.question },
],
timestamp: Date.now(),
},
],
},
{ apiKey, signal },
);
if (response.stopReason === "error") {
throw new ToolError(response.errorMessage ?? "inspect_image request failed.");
}
if (response.stopReason === "aborted") {
throw new ToolError("inspect_image request aborted.");
}
const text = extractTextContent(response);
if (!text) {
throw new ToolError("inspect_image model returned no text output.");
}
return {
content: [{ type: "text", text }],
details: {
model: `${model.provider}/${model.id}`,
imagePath: imageInput.resolvedPath,
mimeType: imageInput.mimeType,
},
};
}
}
export { inspectImageToolRenderer } from "./inspect-image-renderer";