export function getCallMethod()

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 };
	};
}