| require('dotenv').config(); | |
| const { z } = require('zod'); | |
| const fs = require('fs'); | |
| const yaml = require('js-yaml'); | |
| const path = require('path'); | |
| const { DynamicStructuredTool } = require('langchain/tools'); | |
| const { createOpenAPIChain } = require('langchain/chains'); | |
| const { ChatPromptTemplate, HumanMessagePromptTemplate } = require('langchain/prompts'); | |
| function addLinePrefix(text, prefix = '// ') { | |
| return text | |
| .split('\n') | |
| .map((line) => prefix + line) | |
| .join('\n'); | |
| } | |
| function createPrompt(name, functions) { | |
| const prefix = `// The ${name} tool has the following functions. Determine the desired or most optimal function for the user's query:`; | |
| const functionDescriptions = functions | |
| .map((func) => `// - ${func.name}: ${func.description}`) | |
| .join('\n'); | |
| return `${prefix}\n${functionDescriptions} | |
| // You are an expert manager and scrum master. You must provide a detailed intent to better execute the function. | |
| // Always format as such: {{"func": "function_name", "intent": "intent and expected result"}}`; | |
| } | |
| const AuthBearer = z | |
| .object({ | |
| type: z.string().includes('service_http'), | |
| authorization_type: z.string().includes('bearer'), | |
| verification_tokens: z.object({ | |
| openai: z.string(), | |
| }), | |
| }) | |
| .catch(() => false); | |
| const AuthDefinition = z | |
| .object({ | |
| type: z.string(), | |
| authorization_type: z.string(), | |
| verification_tokens: z.object({ | |
| openai: z.string(), | |
| }), | |
| }) | |
| .catch(() => false); | |
| async function readSpecFile(filePath) { | |
| try { | |
| const fileContents = await fs.promises.readFile(filePath, 'utf8'); | |
| if (path.extname(filePath) === '.json') { | |
| return JSON.parse(fileContents); | |
| } | |
| return yaml.load(fileContents); | |
| } catch (e) { | |
| console.error(e); | |
| return false; | |
| } | |
| } | |
| async function getSpec(url) { | |
| const RegularUrl = z | |
| .string() | |
| .url() | |
| .catch(() => false); | |
| if (RegularUrl.parse(url) && path.extname(url) === '.json') { | |
| const response = await fetch(url); | |
| return await response.json(); | |
| } | |
| const ValidSpecPath = z | |
| .string() | |
| .url() | |
| .catch(async () => { | |
| const spec = path.join(__dirname, '..', '.well-known', 'openapi', url); | |
| if (!fs.existsSync(spec)) { | |
| return false; | |
| } | |
| return await readSpecFile(spec); | |
| }); | |
| return ValidSpecPath.parse(url); | |
| } | |
| async function createOpenAPIPlugin({ data, llm, user, message, memory, signal, verbose = false }) { | |
| let spec; | |
| try { | |
| spec = await getSpec(data.api.url, verbose); | |
| } catch (error) { | |
| verbose && console.debug('getSpec error', error); | |
| return null; | |
| } | |
| if (!spec) { | |
| verbose && console.debug('No spec found'); | |
| return null; | |
| } | |
| const headers = {}; | |
| const { auth, name_for_model, description_for_model, description_for_human } = data; | |
| if (auth && AuthDefinition.parse(auth)) { | |
| verbose && console.debug('auth detected', auth); | |
| const { openai } = auth.verification_tokens; | |
| if (AuthBearer.parse(auth)) { | |
| headers.authorization = `Bearer ${openai}`; | |
| verbose && console.debug('added auth bearer', headers); | |
| } | |
| } | |
| const chainOptions = { | |
| llm, | |
| verbose, | |
| }; | |
| if (data.headers && data.headers['librechat_user_id']) { | |
| verbose && console.debug('id detected', headers); | |
| headers[data.headers['librechat_user_id']] = user; | |
| } | |
| if (Object.keys(headers).length > 0) { | |
| verbose && console.debug('headers detected', headers); | |
| chainOptions.headers = headers; | |
| } | |
| if (data.params) { | |
| verbose && console.debug('params detected', data.params); | |
| chainOptions.params = data.params; | |
| } | |
| let history = ''; | |
| if (memory) { | |
| verbose && console.debug('openAPI chain: memory detected', memory); | |
| const { history: chat_history } = await memory.loadMemoryVariables({}); | |
| history = chat_history?.length > 0 ? `\n\n## Chat History:\n${chat_history}\n` : ''; | |
| } | |
| chainOptions.prompt = ChatPromptTemplate.fromMessages([ | |
| HumanMessagePromptTemplate.fromTemplate( | |
| `# Use the provided API's to respond to this query:\n\n{query}\n\n## Instructions:\n${addLinePrefix( | |
| description_for_model, | |
| )}${history}`, | |
| ), | |
| ]); | |
| const chain = await createOpenAPIChain(spec, chainOptions); | |
| const { functions } = chain.chains[0].lc_kwargs.llmKwargs; | |
| return new DynamicStructuredTool({ | |
| name: name_for_model, | |
| description_for_model: `${addLinePrefix(description_for_human)}${createPrompt( | |
| name_for_model, | |
| functions, | |
| )}`, | |
| description: `${description_for_human}`, | |
| schema: z.object({ | |
| func: z | |
| .string() | |
| .describe( | |
| `The function to invoke. The functions available are: ${functions | |
| .map((func) => func.name) | |
| .join(', ')}`, | |
| ), | |
| intent: z | |
| .string() | |
| .describe('Describe your intent with the function and your expected result'), | |
| }), | |
| func: async ({ func = '', intent = '' }) => { | |
| const filteredFunctions = functions.filter((f) => f.name === func); | |
| chain.chains[0].lc_kwargs.llmKwargs.functions = filteredFunctions; | |
| const query = `${message}${func?.length > 0 ? `\n// Intent: ${intent}` : ''}`; | |
| const result = await chain.call({ | |
| query, | |
| signal, | |
| }); | |
| return result.response; | |
| }, | |
| }); | |
| } | |
| module.exports = { | |
| getSpec, | |
| readSpecFile, | |
| createOpenAPIPlugin, | |
| }; | |