in src/lib/server/tools/index.ts [132:307]
export function getCallMethod(tool: Omit<BaseTool, "call">): BackendCall {
return async function* (params, ctx, uuid) {
if (
tool.endpoint === null ||
!tool.baseUrl ||
!tool.outputComponent ||
tool.outputComponentIdx === null
) {
throw new Error(`Tool function ${tool.name} has no endpoint`);
}
const ipToken = await getIpToken(ctx.ip, ctx.username);
function coerceInput(value: unknown, type: ToolInput["type"]) {
const valueStr = String(value);
switch (type) {
case "str":
return valueStr;
case "int":
return parseInt(valueStr);
case "float":
return parseFloat(valueStr);
case "bool":
return valueStr === "true";
default:
throw new Error(`Unsupported type ${type}`);
}
}
const inputs = tool.inputs.map(async (input) => {
if (input.type === "file" && input.paramType !== "required") {
throw new Error("File inputs are always required and cannot be optional or fixed");
}
if (input.paramType === "fixed") {
return coerceInput(input.value, input.type);
} else if (input.paramType === "optional") {
return coerceInput(params[input.name] ?? input.default, input.type);
} else if (input.paramType === "required") {
if (params[input.name] === undefined) {
throw new Error(`Missing required input ${input.name}`);
}
if (input.type === "file") {
// todo: parse file here !
// structure is {input|output}-{msgIdx}-{fileIdx}-{filename}
const filename = params[input.name];
if (!filename || typeof filename !== "string") {
throw new Error(`Filename is not a string`);
}
const messages = ctx.messages;
const msgIdx = parseInt(filename.split("_")[1]);
const fileIdx = parseInt(filename.split("_")[2]);
if (Number.isNaN(msgIdx) || Number.isNaN(fileIdx)) {
throw Error(`Message index or file index is missing`);
}
if (msgIdx >= messages.length) {
throw Error(`Message index ${msgIdx} is out of bounds`);
}
const file = messages[msgIdx].files?.[fileIdx];
if (!file) {
throw Error(`File index ${fileIdx} is out of bounds`);
}
const blob = await downloadFile(file.value, ctx.conv._id)
.then((file) => fetch(`data:${file.mime};base64,${file.value}`))
.then((res) => res.blob())
.catch((err) => {
throw Error("Failed to download file", { cause: err });
});
return blob;
} else {
return coerceInput(params[input.name], input.type);
}
}
});
const outputs = yield* callSpace(
tool.baseUrl,
tool.endpoint,
await Promise.all(inputs),
ipToken,
uuid
);
if (!isValidOutputComponent(tool.outputComponent)) {
throw new Error(`Tool output component is not defined`);
}
const { type, path } = ToolOutputPaths[tool.outputComponent];
if (!path || !type) {
throw new Error(`Tool output type ${tool.outputComponent} is not supported`);
}
const files: MessageFile[] = [];
const toolOutputs: Array<Record<string, string>> = [];
if (outputs.length <= tool.outputComponentIdx) {
throw new Error(`Tool output component index is out of bounds`);
}
// if its not an object, return directly
if (
outputs[tool.outputComponentIdx] !== undefined &&
typeof outputs[tool.outputComponentIdx] !== "object"
) {
return {
outputs: [{ [tool.name + "-0"]: outputs[tool.outputComponentIdx] }],
display: tool.showOutput,
};
}
await Promise.all(
jp
.query(outputs[tool.outputComponentIdx], path)
.map(async (output: string | string[], idx) => {
const arrayedOutput = Array.isArray(output) ? output : [output];
if (type === "file") {
// output files are actually URLs
await Promise.all(
arrayedOutput.map(async (output, idx) => {
await fetch(output)
.then((res) => res.blob())
.then(async (blob) => {
const { ext, mime } = (await fileTypeFromBlob(blob)) ?? { ext: "octet-stream" };
return new File(
[blob],
`${idx}-${await sha256(JSON.stringify(params))}.${ext}`,
{
type: mime,
}
);
})
.then((file) => uploadFile(file, ctx.conv))
.then((file) => files.push(file));
})
);
toolOutputs.push({
[tool.name + "-" + idx.toString()]:
`Only and always answer: 'I used the tool ${tool.displayName}, here is the result.' Don't add anything else.`,
});
} else {
for (const output of arrayedOutput) {
toolOutputs.push({
[tool.name + "-" + idx.toString()]: output,
});
}
}
})
);
for (const file of files) {
yield {
type: MessageUpdateType.File,
name: file.name,
sha: file.value,
mime: file.mime,
};
}
return { outputs: toolOutputs, display: tool.showOutput };
};
}