@samchon/openapi
Version:
Universal OpenAPI to LLM function calling schemas. Transform any Swagger/OpenAPI document into type-safe schemas for OpenAI, Claude, Qwen, and more.
192 lines (184 loc) • 8.8 kB
JavaScript
import { OpenApiValidator } from "../utils/OpenApiValidator.mjs";
import { LlmSchemaComposer } from "./LlmSchemaComposer.mjs";
var HttpLlmComposer;
(function(HttpLlmComposer) {
HttpLlmComposer.application = props => {
const config = {
separate: props.config?.separate ?? null,
maxLength: props.config?.maxLength ?? 64,
equals: props.config?.equals ?? false,
reference: props.config?.reference ?? true,
strict: props.config?.strict ?? false
};
const errors = props.migrate.errors.filter(e => e.operation()["x-samchon-human"] !== true).map(e => ({
method: e.method,
path: e.path,
messages: e.messages,
operation: () => e.operation(),
route: () => undefined
}));
const functions = props.migrate.routes.filter(e => e.operation()["x-samchon-human"] !== true).map((route, i) => {
if (route.method === "head") {
errors.push({
method: route.method,
path: route.path,
messages: [ "HEAD method is not supported in the LLM application." ],
operation: () => route.operation(),
route: () => route
});
return null;
} else if (route.body?.type === "multipart/form-data" || route.success?.type === "multipart/form-data") {
errors.push({
method: route.method,
path: route.path,
messages: [ `The "multipart/form-data" content type is not supported in the LLM application.` ],
operation: () => route.operation(),
route: () => route
});
return null;
}
const localErrors = [];
const func = composeFunction({
components: props.migrate.document().components,
config,
route,
errors: localErrors
});
if (func === null) errors.push({
method: route.method,
path: route.path,
messages: localErrors,
operation: () => route.operation(),
route: () => route
});
return func;
}).filter(v => v !== null);
const app = {
config,
functions,
errors
};
HttpLlmComposer.shorten(app, props.config?.maxLength ?? 64);
return app;
};
const composeFunction = props => {
const endpoint = `$input.paths[${JSON.stringify(props.route.path)}][${JSON.stringify(props.route.method)}]`;
const operation = props.route.operation();
const description = (() => {
if (!operation.summary?.length || !operation.description?.length) return [ operation.summary || operation.description, operation.summary?.length ?? operation.description?.length ?? 0 ];
const summary = operation.summary.endsWith(".") ? operation.summary.slice(0, -1) : operation.summary;
const final = operation.description.startsWith(summary) ? operation.description : summary + ".\n\n" + operation.description;
return [ final, final.length ];
})();
if (description[1] > 1024) {
props.errors.push(`The description of the function is too long (must be equal or less than 1,024 characters, but ${description[1].toLocaleString()} length).`);
}
const name = emend(props.route.accessor.join("_"));
const isNameVariable = /^[a-zA-Z0-9_-]+$/.test(name);
const isNameStartsWithNumber = /^[0-9]/.test(name[0] ?? "");
if (isNameVariable === false) props.errors.push(`Elements of path (separated by '/') must be composed with alphabets, numbers, underscores, and hyphens`);
const parameters = {
type: "object",
properties: Object.fromEntries([ ...props.route.parameters.map(s => [ s.key, {
...s.schema,
description: s.parameter().description ?? s.schema.description
} ]), ...props.route.query ? [ [ props.route.query.key, {
...props.route.query.schema,
title: props.route.query.title() ?? props.route.query.schema.title,
description: props.route.query.description() ?? props.route.query.schema.description
} ] ] : [], ...props.route.body ? [ [ props.route.body.key, {
...props.route.body.schema,
description: props.route.body.description() ?? props.route.body.schema.description
} ] ] : [] ])
};
parameters.required = Object.keys(parameters.properties ?? {});
const llmParameters = LlmSchemaComposer.parameters({
config: props.config,
components: props.components,
schema: parameters,
accessor: `${endpoint}.parameters`
});
const output = props.route.success ? LlmSchemaComposer.schema({
config: props.config,
components: props.components,
schema: props.route.success.schema,
accessor: `${endpoint}.responses[${JSON.stringify(props.route.success.status)}][${JSON.stringify(props.route.success.type)}].schema`,
$defs: llmParameters.success ? llmParameters.value.$defs : {}
}) : undefined;
if (output?.success === false || llmParameters.success === false || isNameVariable === false || isNameStartsWithNumber === true || description[1] > 1024) {
if (output?.success === false) props.errors.push(...output.error.reasons.map(r => `${r.accessor}: ${r.message}`));
if (llmParameters.success === false) props.errors.push(...llmParameters.error.reasons.map(r => {
const accessor = r.accessor.replace(`parameters.properties["body"]`, `requestBody.content[${JSON.stringify(props.route.body?.type ?? "application/json")}].schema`);
return `${accessor}: ${r.message}`;
}));
return null;
}
return {
method: props.route.method,
path: props.route.path,
name,
parameters: llmParameters.value,
separated: props.config.separate ? LlmSchemaComposer.separate({
predicate: props.config.separate,
parameters: llmParameters.value,
equals: props.config.equals ?? false
}) : undefined,
output: output?.value,
description: description[0],
deprecated: operation.deprecated,
tags: operation.tags,
validate: OpenApiValidator.create({
components: props.components,
schema: parameters,
required: true,
equals: props.config.equals ?? false
}),
route: () => props.route,
operation: () => props.route.operation()
};
};
HttpLlmComposer.shorten = (app, limit = 64) => {
const dictionary = new Set;
const longFunctions = [];
for (const func of app.functions) {
dictionary.add(func.name);
if (func.name.length > limit) {
longFunctions.push(func);
}
}
if (longFunctions.length === 0) return;
let index = 0;
for (const func of longFunctions) {
let success = false;
let rename = str => {
dictionary.delete(func.name);
dictionary.add(str);
func.name = str;
success = true;
};
for (let i = 1; i < func.route().accessor.length; ++i) {
const shortName = func.route().accessor.slice(i).join("_");
if (shortName.length > limit - 8) continue; else if (dictionary.has(shortName) === false) rename(shortName); else {
const newName = `_${index}_${shortName}`;
if (dictionary.has(newName) === true) continue;
rename(newName);
++index;
}
break;
}
if (success === false) rename(randomFormatUuid());
}
};
})(HttpLlmComposer || (HttpLlmComposer = {}));
const randomFormatUuid = () => "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0;
const v = c === "x" ? r : r & 3 | 8;
return v.toString(16);
});
const emend = str => {
for (const ch of FORBIDDEN) str = str.split(ch).join("_");
return str;
};
const FORBIDDEN = [ "$", "%", "." ];
export { HttpLlmComposer };
//# sourceMappingURL=HttpLlmApplicationComposer.mjs.map