mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00

* ci(backend-review.yml): add linter step to the backend review workflow * chore(backend-review.yml): remove prettier from lint-action configuration * chore: apply new linting workflow * chore(lint-staged.config.js): reorder lint-staged tasks for JavaScript and TypeScript files * chore(eslint): update ignorePatterns in .eslintrc.js chore(lint-action): remove prettier option in backend-review.yml chore(package.json): add lint and lint:fix scripts * chore(lint-staged.config.js): remove prettier --write command for js, jsx, ts, tsx files * chore(titleConvo.js): remove unnecessary console.log statement chore(titleConvo.js): add missing comma in options object * chore: apply linting to all files * chore(lint-staged.config.js): update lint-staged configuration to include prettier formatting
238 lines
7.1 KiB
JavaScript
238 lines
7.1 KiB
JavaScript
const { Tool } = require('langchain/tools');
|
|
const yaml = require('js-yaml');
|
|
|
|
/*
|
|
export interface AIPluginToolParams {
|
|
name: string;
|
|
description: string;
|
|
apiSpec: string;
|
|
openaiSpec: string;
|
|
model: BaseLanguageModel;
|
|
}
|
|
|
|
export interface PathParameter {
|
|
name: string;
|
|
description: string;
|
|
}
|
|
|
|
export interface Info {
|
|
title: string;
|
|
description: string;
|
|
version: string;
|
|
}
|
|
export interface PathMethod {
|
|
summary: string;
|
|
operationId: string;
|
|
parameters?: PathParameter[];
|
|
}
|
|
|
|
interface ApiSpec {
|
|
openapi: string;
|
|
info: Info;
|
|
paths: { [key: string]: { [key: string]: PathMethod } };
|
|
}
|
|
*/
|
|
|
|
function isJson(str) {
|
|
try {
|
|
JSON.parse(str);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function convertJsonToYamlIfApplicable(spec) {
|
|
if (isJson(spec)) {
|
|
const jsonData = JSON.parse(spec);
|
|
return yaml.dump(jsonData);
|
|
}
|
|
return spec;
|
|
}
|
|
|
|
function extractShortVersion(openapiSpec) {
|
|
openapiSpec = convertJsonToYamlIfApplicable(openapiSpec);
|
|
try {
|
|
const fullApiSpec = yaml.load(openapiSpec);
|
|
const shortApiSpec = {
|
|
openapi: fullApiSpec.openapi,
|
|
info: fullApiSpec.info,
|
|
paths: {},
|
|
};
|
|
|
|
for (let path in fullApiSpec.paths) {
|
|
shortApiSpec.paths[path] = {};
|
|
for (let method in fullApiSpec.paths[path]) {
|
|
shortApiSpec.paths[path][method] = {
|
|
summary: fullApiSpec.paths[path][method].summary,
|
|
operationId: fullApiSpec.paths[path][method].operationId,
|
|
parameters: fullApiSpec.paths[path][method].parameters?.map((parameter) => ({
|
|
name: parameter.name,
|
|
description: parameter.description,
|
|
})),
|
|
};
|
|
}
|
|
}
|
|
|
|
return yaml.dump(shortApiSpec);
|
|
} catch (e) {
|
|
console.log(e);
|
|
return '';
|
|
}
|
|
}
|
|
function printOperationDetails(operationId, openapiSpec) {
|
|
openapiSpec = convertJsonToYamlIfApplicable(openapiSpec);
|
|
let returnText = '';
|
|
try {
|
|
let doc = yaml.load(openapiSpec);
|
|
let servers = doc.servers;
|
|
let paths = doc.paths;
|
|
let components = doc.components;
|
|
|
|
for (let path in paths) {
|
|
for (let method in paths[path]) {
|
|
let operation = paths[path][method];
|
|
if (operation.operationId === operationId) {
|
|
returnText += `The API request to do for operationId "${operationId}" is:\n`;
|
|
returnText += `Method: ${method.toUpperCase()}\n`;
|
|
|
|
let url = servers[0].url + path;
|
|
returnText += `Path: ${url}\n`;
|
|
|
|
returnText += 'Parameters:\n';
|
|
if (operation.parameters) {
|
|
for (let param of operation.parameters) {
|
|
let required = param.required ? '' : ' (optional),';
|
|
returnText += `- ${param.name} (${param.in},${required} ${param.schema.type}): ${param.description}\n`;
|
|
}
|
|
} else {
|
|
returnText += ' None\n';
|
|
}
|
|
returnText += '\n';
|
|
|
|
let responseSchema = operation.responses['200'].content['application/json'].schema;
|
|
|
|
// Check if schema is a reference
|
|
if (responseSchema.$ref) {
|
|
// Extract schema name from reference
|
|
let schemaName = responseSchema.$ref.split('/').pop();
|
|
// Look up schema in components
|
|
responseSchema = components.schemas[schemaName];
|
|
}
|
|
|
|
returnText += 'Response schema:\n';
|
|
returnText += '- Type: ' + responseSchema.type + '\n';
|
|
returnText += '- Additional properties:\n';
|
|
returnText += ' - Type: ' + responseSchema.additionalProperties?.type + '\n';
|
|
if (responseSchema.additionalProperties?.properties) {
|
|
returnText += ' - Properties:\n';
|
|
for (let prop in responseSchema.additionalProperties.properties) {
|
|
returnText += ` - ${prop} (${responseSchema.additionalProperties.properties[prop].type}): Description not provided in OpenAPI spec\n`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (returnText === '') {
|
|
returnText += `No operation with operationId "${operationId}" found.`;
|
|
}
|
|
return returnText;
|
|
} catch (e) {
|
|
console.log(e);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
class AIPluginTool extends Tool {
|
|
/*
|
|
private _name: string;
|
|
private _description: string;
|
|
apiSpec: string;
|
|
openaiSpec: string;
|
|
model: BaseLanguageModel;
|
|
*/
|
|
|
|
get name() {
|
|
return this._name;
|
|
}
|
|
|
|
get description() {
|
|
return this._description;
|
|
}
|
|
|
|
constructor(params) {
|
|
super();
|
|
this._name = params.name;
|
|
this._description = params.description;
|
|
this.apiSpec = params.apiSpec;
|
|
this.openaiSpec = params.openaiSpec;
|
|
this.model = params.model;
|
|
}
|
|
|
|
async _call(input) {
|
|
let date = new Date();
|
|
let fullDate = `Date: ${date.getDate()}/${
|
|
date.getMonth() + 1
|
|
}/${date.getFullYear()}, Time: ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
|
|
const prompt = `${fullDate}\nQuestion: ${input} \n${this.apiSpec}.`;
|
|
console.log(prompt);
|
|
const gptResponse = await this.model.predict(prompt);
|
|
let operationId = gptResponse.match(/operationId: (.*)/)?.[1];
|
|
if (!operationId) {
|
|
return 'No operationId found in the response';
|
|
}
|
|
if (operationId == 'No API path found to answer the question') {
|
|
return 'No API path found to answer the question';
|
|
}
|
|
|
|
let openApiData = printOperationDetails(operationId, this.openaiSpec);
|
|
|
|
return openApiData;
|
|
}
|
|
|
|
static async fromPluginUrl(url, model) {
|
|
const aiPluginRes = await fetch(url, {});
|
|
if (!aiPluginRes.ok) {
|
|
throw new Error(`Failed to fetch plugin from ${url} with status ${aiPluginRes.status}`);
|
|
}
|
|
const aiPluginJson = await aiPluginRes.json();
|
|
const apiUrlRes = await fetch(aiPluginJson.api.url, {});
|
|
if (!apiUrlRes.ok) {
|
|
throw new Error(
|
|
`Failed to fetch API spec from ${aiPluginJson.api.url} with status ${apiUrlRes.status}`,
|
|
);
|
|
}
|
|
const apiUrlJson = await apiUrlRes.text();
|
|
const shortApiSpec = extractShortVersion(apiUrlJson);
|
|
return new AIPluginTool({
|
|
name: aiPluginJson.name_for_model.toLowerCase(),
|
|
description: `A \`tool\` to learn the API documentation for ${aiPluginJson.name_for_model.toLowerCase()}, after which you can use 'http_request' to make the actual API call. Short description of how to use the API's results: ${
|
|
aiPluginJson.description_for_model
|
|
})`,
|
|
apiSpec: `
|
|
As an AI, your task is to identify the operationId of the relevant API path based on the condensed OpenAPI specifications provided.
|
|
|
|
Please note:
|
|
|
|
1. Do not imagine URLs. Only use the information provided in the condensed OpenAPI specifications.
|
|
|
|
2. Do not guess the operationId. Identify it strictly based on the API paths and their descriptions.
|
|
|
|
Your output should only include:
|
|
- operationId: The operationId of the relevant API path
|
|
|
|
If you cannot find a suitable API path based on the OpenAPI specifications, please answer only "operationId: No API path found to answer the question".
|
|
|
|
Now, based on the question above and the condensed OpenAPI specifications given below, identify the operationId:
|
|
|
|
\`\`\`
|
|
${shortApiSpec}
|
|
\`\`\`
|
|
`,
|
|
openaiSpec: apiUrlJson,
|
|
model: model,
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = AIPluginTool;
|