Spaces:
Paused
Paused
| import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core"; | |
| import fs from "node:fs/promises"; | |
| import { detectMime } from "../../media/mime.js"; | |
| import { sanitizeToolResultImages } from "../tool-images.js"; | |
| // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance. | |
| export type AnyAgentTool = AgentTool<any, unknown>; | |
| export type StringParamOptions = { | |
| required?: boolean; | |
| trim?: boolean; | |
| label?: string; | |
| allowEmpty?: boolean; | |
| }; | |
| export type ActionGate<T extends Record<string, boolean | undefined>> = ( | |
| key: keyof T, | |
| defaultValue?: boolean, | |
| ) => boolean; | |
| export function createActionGate<T extends Record<string, boolean | undefined>>( | |
| actions: T | undefined, | |
| ): ActionGate<T> { | |
| return (key, defaultValue = true) => { | |
| const value = actions?.[key]; | |
| if (value === undefined) { | |
| return defaultValue; | |
| } | |
| return value !== false; | |
| }; | |
| } | |
| export function readStringParam( | |
| params: Record<string, unknown>, | |
| key: string, | |
| options: StringParamOptions & { required: true }, | |
| ): string; | |
| export function readStringParam( | |
| params: Record<string, unknown>, | |
| key: string, | |
| options?: StringParamOptions, | |
| ): string | undefined; | |
| export function readStringParam( | |
| params: Record<string, unknown>, | |
| key: string, | |
| options: StringParamOptions = {}, | |
| ) { | |
| const { required = false, trim = true, label = key, allowEmpty = false } = options; | |
| const raw = params[key]; | |
| if (typeof raw !== "string") { | |
| if (required) { | |
| throw new Error(`${label} required`); | |
| } | |
| return undefined; | |
| } | |
| const value = trim ? raw.trim() : raw; | |
| if (!value && !allowEmpty) { | |
| if (required) { | |
| throw new Error(`${label} required`); | |
| } | |
| return undefined; | |
| } | |
| return value; | |
| } | |
| export function readStringOrNumberParam( | |
| params: Record<string, unknown>, | |
| key: string, | |
| options: { required?: boolean; label?: string } = {}, | |
| ): string | undefined { | |
| const { required = false, label = key } = options; | |
| const raw = params[key]; | |
| if (typeof raw === "number" && Number.isFinite(raw)) { | |
| return String(raw); | |
| } | |
| if (typeof raw === "string") { | |
| const value = raw.trim(); | |
| if (value) { | |
| return value; | |
| } | |
| } | |
| if (required) { | |
| throw new Error(`${label} required`); | |
| } | |
| return undefined; | |
| } | |
| export function readNumberParam( | |
| params: Record<string, unknown>, | |
| key: string, | |
| options: { required?: boolean; label?: string; integer?: boolean } = {}, | |
| ): number | undefined { | |
| const { required = false, label = key, integer = false } = options; | |
| const raw = params[key]; | |
| let value: number | undefined; | |
| if (typeof raw === "number" && Number.isFinite(raw)) { | |
| value = raw; | |
| } else if (typeof raw === "string") { | |
| const trimmed = raw.trim(); | |
| if (trimmed) { | |
| const parsed = Number.parseFloat(trimmed); | |
| if (Number.isFinite(parsed)) { | |
| value = parsed; | |
| } | |
| } | |
| } | |
| if (value === undefined) { | |
| if (required) { | |
| throw new Error(`${label} required`); | |
| } | |
| return undefined; | |
| } | |
| return integer ? Math.trunc(value) : value; | |
| } | |
| export function readStringArrayParam( | |
| params: Record<string, unknown>, | |
| key: string, | |
| options: StringParamOptions & { required: true }, | |
| ): string[]; | |
| export function readStringArrayParam( | |
| params: Record<string, unknown>, | |
| key: string, | |
| options?: StringParamOptions, | |
| ): string[] | undefined; | |
| export function readStringArrayParam( | |
| params: Record<string, unknown>, | |
| key: string, | |
| options: StringParamOptions = {}, | |
| ) { | |
| const { required = false, label = key } = options; | |
| const raw = params[key]; | |
| if (Array.isArray(raw)) { | |
| const values = raw | |
| .filter((entry) => typeof entry === "string") | |
| .map((entry) => entry.trim()) | |
| .filter(Boolean); | |
| if (values.length === 0) { | |
| if (required) { | |
| throw new Error(`${label} required`); | |
| } | |
| return undefined; | |
| } | |
| return values; | |
| } | |
| if (typeof raw === "string") { | |
| const value = raw.trim(); | |
| if (!value) { | |
| if (required) { | |
| throw new Error(`${label} required`); | |
| } | |
| return undefined; | |
| } | |
| return [value]; | |
| } | |
| if (required) { | |
| throw new Error(`${label} required`); | |
| } | |
| return undefined; | |
| } | |
| export type ReactionParams = { | |
| emoji: string; | |
| remove: boolean; | |
| isEmpty: boolean; | |
| }; | |
| export function readReactionParams( | |
| params: Record<string, unknown>, | |
| options: { | |
| emojiKey?: string; | |
| removeKey?: string; | |
| removeErrorMessage: string; | |
| }, | |
| ): ReactionParams { | |
| const emojiKey = options.emojiKey ?? "emoji"; | |
| const removeKey = options.removeKey ?? "remove"; | |
| const remove = typeof params[removeKey] === "boolean" ? params[removeKey] : false; | |
| const emoji = readStringParam(params, emojiKey, { | |
| required: true, | |
| allowEmpty: true, | |
| }); | |
| if (remove && !emoji) { | |
| throw new Error(options.removeErrorMessage); | |
| } | |
| return { emoji, remove, isEmpty: !emoji }; | |
| } | |
| export function jsonResult(payload: unknown): AgentToolResult<unknown> { | |
| return { | |
| content: [ | |
| { | |
| type: "text", | |
| text: JSON.stringify(payload, null, 2), | |
| }, | |
| ], | |
| details: payload, | |
| }; | |
| } | |
| export async function imageResult(params: { | |
| label: string; | |
| path: string; | |
| base64: string; | |
| mimeType: string; | |
| extraText?: string; | |
| details?: Record<string, unknown>; | |
| }): Promise<AgentToolResult<unknown>> { | |
| const content: AgentToolResult<unknown>["content"] = [ | |
| { | |
| type: "text", | |
| text: params.extraText ?? `MEDIA:${params.path}`, | |
| }, | |
| { | |
| type: "image", | |
| data: params.base64, | |
| mimeType: params.mimeType, | |
| }, | |
| ]; | |
| const result: AgentToolResult<unknown> = { | |
| content, | |
| details: { path: params.path, ...params.details }, | |
| }; | |
| return await sanitizeToolResultImages(result, params.label); | |
| } | |
| export async function imageResultFromFile(params: { | |
| label: string; | |
| path: string; | |
| extraText?: string; | |
| details?: Record<string, unknown>; | |
| }): Promise<AgentToolResult<unknown>> { | |
| const buf = await fs.readFile(params.path); | |
| const mimeType = (await detectMime({ buffer: buf.slice(0, 256) })) ?? "image/png"; | |
| return await imageResult({ | |
| label: params.label, | |
| path: params.path, | |
| base64: buf.toString("base64"), | |
| mimeType, | |
| extraText: params.extraText, | |
| details: params.details, | |
| }); | |
| } | |