librechat-data-provider 0.3.9 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,350 @@
1
+ import { OpenAPIV3 } from 'openapi-types';
2
+
3
+ export type FlowchartSchema = {
4
+ mermaid: {
5
+ type: 'string';
6
+ description: 'Flowchart to be rendered, in Mermaid syntax';
7
+ };
8
+ title: {
9
+ type: 'string';
10
+ description: 'Title of the flowchart';
11
+ };
12
+ };
13
+
14
+ export const getWeatherOpenapiSpec: OpenAPIV3.Document = {
15
+ openapi: '3.1.0',
16
+ info: {
17
+ title: 'Get weather data',
18
+ description: 'Retrieves current weather data for a location.',
19
+ version: 'v1.0.0',
20
+ },
21
+ servers: [
22
+ {
23
+ url: 'https://weather.example.com',
24
+ },
25
+ ],
26
+ paths: {
27
+ '/location': {
28
+ get: {
29
+ description: 'Get temperature for a specific location',
30
+ operationId: 'GetCurrentWeather',
31
+ parameters: [
32
+ {
33
+ name: 'location',
34
+ in: 'query',
35
+ description: 'The city and state to retrieve the weather for',
36
+ required: true,
37
+ schema: {
38
+ type: 'string',
39
+ },
40
+ },
41
+ ],
42
+ requestBody: {
43
+ required: true,
44
+ content: {
45
+ 'application/json': {
46
+ schema: {
47
+ type: 'object',
48
+ properties: {
49
+ locations: {
50
+ type: 'array',
51
+ items: {
52
+ type: 'object',
53
+ properties: {
54
+ city: {
55
+ type: 'string',
56
+ example: 'San Francisco',
57
+ },
58
+ state: {
59
+ type: 'string',
60
+ example: 'CA',
61
+ },
62
+ countryCode: {
63
+ type: 'string',
64
+ description: 'ISO 3166-1 alpha-2 country code',
65
+ example: 'US',
66
+ },
67
+ time: {
68
+ type: 'string',
69
+ description:
70
+ 'Optional time for which the weather is requested, in ISO 8601 format.',
71
+ example: '2023-12-04T14:00:00Z',
72
+ },
73
+ },
74
+ required: ['city', 'state', 'countryCode'],
75
+ description:
76
+ 'Details of the location for which the weather data is requested.',
77
+ },
78
+ description: 'A list of locations to retrieve the weather for.',
79
+ },
80
+ },
81
+ },
82
+ },
83
+ },
84
+ },
85
+ deprecated: false,
86
+ responses: {},
87
+ },
88
+ },
89
+ },
90
+ components: {
91
+ schemas: {},
92
+ },
93
+ };
94
+
95
+ export const whimsicalOpenapiSpec: OpenAPIV3.Document = {
96
+ openapi: '3.0.0',
97
+ info: {
98
+ version: '1.0.0',
99
+ title: 'Diagram to Image API',
100
+ description: 'A simple API to generate flowchart, mindmap, or sequence diagram images.',
101
+ },
102
+ servers: [{ url: 'https://whimsical.com/api' }],
103
+ paths: {
104
+ '/ai.chatgpt.render-flowchart': {
105
+ post: {
106
+ operationId: 'postRenderFlowchart',
107
+ // 'x-openai-isConsequential': false,
108
+ summary: 'Renders a flowchart',
109
+ description:
110
+ 'Accepts a string describing a flowchart and returns a URL to a rendered image',
111
+ requestBody: {
112
+ content: {
113
+ 'application/json': {
114
+ schema: {
115
+ $ref: '#/components/schemas/FlowchartRequest',
116
+ },
117
+ },
118
+ },
119
+ required: true,
120
+ },
121
+ responses: {
122
+ '200': {
123
+ description: 'URL to the rendered image',
124
+ content: {
125
+ 'application/json': {
126
+ schema: {
127
+ $ref: '#/components/schemas/FlowchartResponse',
128
+ },
129
+ },
130
+ },
131
+ },
132
+ },
133
+ },
134
+ },
135
+ },
136
+ components: {
137
+ schemas: {
138
+ FlowchartRequest: {
139
+ type: 'object',
140
+ properties: {
141
+ mermaid: {
142
+ type: 'string',
143
+ description: 'Flowchart to be rendered, in Mermaid syntax',
144
+ },
145
+ title: {
146
+ type: 'string',
147
+ description: 'Title of the flowchart',
148
+ },
149
+ },
150
+ required: ['mermaid'],
151
+ },
152
+ FlowchartResponse: {
153
+ type: 'object',
154
+ properties: {
155
+ imageURL: {
156
+ type: 'string',
157
+ description: 'URL of the rendered image',
158
+ },
159
+ },
160
+ },
161
+ },
162
+ },
163
+ };
164
+
165
+ export const scholarAIOpenapiSpec = `
166
+ openapi: 3.0.1
167
+ info:
168
+ title: ScholarAI
169
+ description: Allows the user to search facts and findings from scientific articles
170
+ version: 'v1'
171
+ servers:
172
+ - url: https://scholar-ai.net
173
+ paths:
174
+ /api/abstracts:
175
+ get:
176
+ operationId: searchAbstracts
177
+ summary: Get relevant paper abstracts by keywords search
178
+ parameters:
179
+ - name: keywords
180
+ in: query
181
+ description: Keywords of inquiry which should appear in article. Must be in English.
182
+ required: true
183
+ schema:
184
+ type: string
185
+ - name: sort
186
+ in: query
187
+ description: The sort order for results. Valid values are cited_by_count or publication_date. Excluding this value does a relevance based search.
188
+ required: false
189
+ schema:
190
+ type: string
191
+ enum:
192
+ - cited_by_count
193
+ - publication_date
194
+ - name: query
195
+ in: query
196
+ description: The user query
197
+ required: true
198
+ schema:
199
+ type: string
200
+ - name: peer_reviewed_only
201
+ in: query
202
+ description: Whether to only return peer reviewed articles. Defaults to true, ChatGPT should cautiously suggest this value can be set to false
203
+ required: false
204
+ schema:
205
+ type: string
206
+ - name: start_year
207
+ in: query
208
+ description: The first year, inclusive, to include in the search range. Excluding this value will include all years.
209
+ required: false
210
+ schema:
211
+ type: string
212
+ - name: end_year
213
+ in: query
214
+ description: The last year, inclusive, to include in the search range. Excluding this value will include all years.
215
+ required: false
216
+ schema:
217
+ type: string
218
+ - name: offset
219
+ in: query
220
+ description: The offset of the first result to return. Defaults to 0.
221
+ required: false
222
+ schema:
223
+ type: string
224
+ responses:
225
+ "200":
226
+ description: OK
227
+ content:
228
+ application/json:
229
+ schema:
230
+ $ref: '#/components/schemas/searchAbstractsResponse'
231
+ /api/fulltext:
232
+ get:
233
+ operationId: getFullText
234
+ summary: Get full text of a paper by URL for PDF
235
+ parameters:
236
+ - name: pdf_url
237
+ in: query
238
+ description: URL for PDF
239
+ required: true
240
+ schema:
241
+ type: string
242
+ - name: chunk
243
+ in: query
244
+ description: chunk number to retrieve, defaults to 1
245
+ required: false
246
+ schema:
247
+ type: number
248
+ responses:
249
+ "200":
250
+ description: OK
251
+ content:
252
+ application/json:
253
+ schema:
254
+ $ref: '#/components/schemas/getFullTextResponse'
255
+ /api/save-citation:
256
+ get:
257
+ operationId: saveCitation
258
+ summary: Save citation to reference manager
259
+ parameters:
260
+ - name: doi
261
+ in: query
262
+ description: Digital Object Identifier (DOI) of article
263
+ required: true
264
+ schema:
265
+ type: string
266
+ - name: zotero_user_id
267
+ in: query
268
+ description: Zotero User ID
269
+ required: true
270
+ schema:
271
+ type: string
272
+ - name: zotero_api_key
273
+ in: query
274
+ description: Zotero API Key
275
+ required: true
276
+ schema:
277
+ type: string
278
+ responses:
279
+ "200":
280
+ description: OK
281
+ content:
282
+ application/json:
283
+ schema:
284
+ $ref: '#/components/schemas/saveCitationResponse'
285
+ components:
286
+ schemas:
287
+ searchAbstractsResponse:
288
+ type: object
289
+ properties:
290
+ next_offset:
291
+ type: number
292
+ description: The offset of the next page of results.
293
+ total_num_results:
294
+ type: number
295
+ description: The total number of results.
296
+ abstracts:
297
+ type: array
298
+ items:
299
+ type: object
300
+ properties:
301
+ title:
302
+ type: string
303
+ abstract:
304
+ type: string
305
+ description: Summary of the context, methods, results, and conclusions of the paper.
306
+ doi:
307
+ type: string
308
+ description: The DOI of the paper.
309
+ landing_page_url:
310
+ type: string
311
+ description: Link to the paper on its open-access host.
312
+ pdf_url:
313
+ type: string
314
+ description: Link to the paper PDF.
315
+ publicationDate:
316
+ type: string
317
+ description: The date the paper was published in YYYY-MM-DD format.
318
+ relevance:
319
+ type: number
320
+ description: The relevance of the paper to the search query. 1 is the most relevant.
321
+ creators:
322
+ type: array
323
+ items:
324
+ type: string
325
+ description: The name of the creator.
326
+ cited_by_count:
327
+ type: number
328
+ description: The number of citations of the article.
329
+ description: The list of relevant abstracts.
330
+ getFullTextResponse:
331
+ type: object
332
+ properties:
333
+ full_text:
334
+ type: string
335
+ description: The full text of the paper.
336
+ pdf_url:
337
+ type: string
338
+ description: The PDF URL of the paper.
339
+ chunk:
340
+ type: number
341
+ description: The chunk of the paper.
342
+ total_chunk_num:
343
+ type: number
344
+ description: The total chunks of the paper.
345
+ saveCitationResponse:
346
+ type: object
347
+ properties:
348
+ message:
349
+ type: string
350
+ description: Confirmation of successful save or error message.`;
package/src/actions.ts ADDED
@@ -0,0 +1,347 @@
1
+ import axios from 'axios';
2
+ import { URL } from 'url';
3
+ import crypto from 'crypto';
4
+ import { load } from 'js-yaml';
5
+ import type { FunctionTool, Schema, Reference, ActionMetadata } from './types/assistants';
6
+ import type { OpenAPIV3 } from 'openapi-types';
7
+ import { Tools, AuthTypeEnum, AuthorizationTypeEnum } from './types/assistants';
8
+
9
+ export type ParametersSchema = {
10
+ type: string;
11
+ properties: Record<string, Reference | Schema>;
12
+ required: string[];
13
+ };
14
+
15
+ export type ApiKeyCredentials = {
16
+ api_key: string;
17
+ custom_auth_header?: string;
18
+ authorization_type?: AuthorizationTypeEnum;
19
+ };
20
+
21
+ export type OAuthCredentials = {
22
+ tokenUrl: string;
23
+ clientId: string;
24
+ clientSecret: string;
25
+ scope: string;
26
+ };
27
+
28
+ export type Credentials = ApiKeyCredentials | OAuthCredentials;
29
+
30
+ export function sha1(input: string) {
31
+ return crypto.createHash('sha1').update(input).digest('hex');
32
+ }
33
+
34
+ export function createURL(domain: string, path: string) {
35
+ const myURL = new URL(path, domain);
36
+ return myURL.toString();
37
+ }
38
+
39
+ export class FunctionSignature {
40
+ name: string;
41
+ description: string;
42
+ parameters: ParametersSchema;
43
+
44
+ constructor(name: string, description: string, parameters: ParametersSchema) {
45
+ this.name = name;
46
+ this.description = description;
47
+ if (parameters.properties?.['requestBody']) {
48
+ this.parameters = parameters.properties?.['requestBody'] as ParametersSchema;
49
+ } else {
50
+ this.parameters = parameters;
51
+ }
52
+ }
53
+
54
+ toObjectTool(): FunctionTool {
55
+ return {
56
+ type: Tools.function,
57
+ function: {
58
+ name: this.name,
59
+ description: this.description,
60
+ parameters: this.parameters,
61
+ },
62
+ };
63
+ }
64
+ }
65
+
66
+ export class ActionRequest {
67
+ domain: string;
68
+ path: string;
69
+ method: string;
70
+ operation: string;
71
+ operationHash?: string;
72
+ isConsequential: boolean;
73
+ contentType: string;
74
+ params?: object;
75
+
76
+ constructor(
77
+ domain: string,
78
+ path: string,
79
+ method: string,
80
+ operation: string,
81
+ isConsequential: boolean,
82
+ contentType: string,
83
+ ) {
84
+ this.domain = domain;
85
+ this.path = path;
86
+ this.method = method;
87
+ this.operation = operation;
88
+ this.isConsequential = isConsequential;
89
+ this.contentType = contentType;
90
+ }
91
+
92
+ private authHeaders: Record<string, string> = {};
93
+ private authToken?: string;
94
+
95
+ async setParams(params: object) {
96
+ this.operationHash = sha1(JSON.stringify(params));
97
+ this.params = params;
98
+ }
99
+
100
+ async setAuth(metadata: ActionMetadata) {
101
+ if (!metadata.auth) {
102
+ return;
103
+ }
104
+
105
+ const {
106
+ type,
107
+ /* API Key */
108
+ authorization_type,
109
+ custom_auth_header,
110
+ /* OAuth */
111
+ authorization_url,
112
+ client_url,
113
+ scope,
114
+ token_exchange_method,
115
+ } = metadata.auth;
116
+
117
+ const {
118
+ /* API Key */
119
+ api_key,
120
+ /* OAuth */
121
+ oauth_client_id,
122
+ oauth_client_secret,
123
+ } = metadata;
124
+
125
+ const isApiKey = api_key && type === AuthTypeEnum.ServiceHttp;
126
+ const isOAuth =
127
+ oauth_client_id &&
128
+ oauth_client_secret &&
129
+ type === AuthTypeEnum.OAuth &&
130
+ authorization_url &&
131
+ client_url &&
132
+ scope &&
133
+ token_exchange_method;
134
+
135
+ if (isApiKey && authorization_type === AuthorizationTypeEnum.Basic) {
136
+ const basicToken = Buffer.from(api_key).toString('base64');
137
+ this.authHeaders['Authorization'] = `Basic ${basicToken}`;
138
+ } else if (isApiKey && authorization_type === AuthorizationTypeEnum.Bearer) {
139
+ this.authHeaders['Authorization'] = `Bearer ${api_key}`;
140
+ } else if (
141
+ isApiKey &&
142
+ authorization_type === AuthorizationTypeEnum.Custom &&
143
+ custom_auth_header
144
+ ) {
145
+ this.authHeaders[custom_auth_header] = api_key;
146
+ } else if (isOAuth) {
147
+ // TODO: WIP - OAuth support
148
+ if (!this.authToken) {
149
+ const tokenResponse = await axios.post(
150
+ client_url,
151
+ {
152
+ client_id: oauth_client_id,
153
+ client_secret: oauth_client_secret,
154
+ scope: scope,
155
+ grant_type: 'client_credentials',
156
+ },
157
+ {
158
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
159
+ },
160
+ );
161
+ this.authToken = tokenResponse.data.access_token;
162
+ }
163
+ this.authHeaders['Authorization'] = `Bearer ${this.authToken}`;
164
+ }
165
+ }
166
+
167
+ async execute() {
168
+ const url = createURL(this.domain, this.path);
169
+ const headers = {
170
+ ...this.authHeaders,
171
+ 'Content-Type': this.contentType,
172
+ };
173
+
174
+ const method = this.method.toLowerCase();
175
+
176
+ if (method === 'get') {
177
+ return axios.get(url, { headers, params: this.params });
178
+ } else if (method === 'post') {
179
+ return axios.post(url, this.params, { headers });
180
+ } else if (method === 'put') {
181
+ return axios.put(url, this.params, { headers });
182
+ } else if (method === 'delete') {
183
+ return axios.delete(url, { headers, data: this.params });
184
+ } else if (method === 'patch') {
185
+ return axios.patch(url, this.params, { headers });
186
+ } else {
187
+ throw new Error(`Unsupported HTTP method: ${this.method}`);
188
+ }
189
+ }
190
+ }
191
+
192
+ export function resolveRef(
193
+ schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | OpenAPIV3.RequestBodyObject,
194
+ components?: OpenAPIV3.ComponentsObject,
195
+ ): OpenAPIV3.SchemaObject {
196
+ if ('$ref' in schema && components) {
197
+ const refPath = schema.$ref.replace(/^#\/components\/schemas\//, '');
198
+ const resolvedSchema = components.schemas?.[refPath];
199
+ if (!resolvedSchema) {
200
+ throw new Error(`Reference ${schema.$ref} not found`);
201
+ }
202
+ return resolveRef(resolvedSchema, components);
203
+ }
204
+ return schema as OpenAPIV3.SchemaObject;
205
+ }
206
+
207
+ /** Function to convert OpenAPI spec to function signatures and request builders */
208
+ export function openapiToFunction(openapiSpec: OpenAPIV3.Document): {
209
+ functionSignatures: FunctionSignature[];
210
+ requestBuilders: Record<string, ActionRequest>;
211
+ } {
212
+ const functionSignatures: FunctionSignature[] = [];
213
+ const requestBuilders: Record<string, ActionRequest> = {};
214
+
215
+ // Base URL from OpenAPI spec servers
216
+ const baseUrl = openapiSpec.servers?.[0]?.url ?? '';
217
+
218
+ // Iterate over each path and method in the OpenAPI spec
219
+ for (const [path, methods] of Object.entries(openapiSpec.paths)) {
220
+ for (const [method, operation] of Object.entries(methods as OpenAPIV3.PathsObject)) {
221
+ const operationObj = operation as OpenAPIV3.OperationObject & {
222
+ 'x-openai-isConsequential'?: boolean;
223
+ };
224
+
225
+ // Operation ID is used as the function name
226
+ const operationId = operationObj.operationId || `${method}_${path}`;
227
+ const description = operationObj.summary || operationObj.description || '';
228
+
229
+ const parametersSchema: ParametersSchema = { type: 'object', properties: {}, required: [] };
230
+
231
+ if (operationObj.requestBody) {
232
+ const requestBody = operationObj.requestBody as OpenAPIV3.RequestBodyObject;
233
+ const content = requestBody.content;
234
+ const contentType = Object.keys(content)[0];
235
+ const schema = content[contentType]?.schema;
236
+ const resolvedSchema = resolveRef(
237
+ schema as OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject,
238
+ openapiSpec.components,
239
+ );
240
+ parametersSchema.properties['requestBody'] = resolvedSchema;
241
+ }
242
+
243
+ if (operationObj.parameters) {
244
+ for (const param of operationObj.parameters) {
245
+ const paramObj = param as OpenAPIV3.ParameterObject;
246
+ const resolvedSchema = resolveRef(
247
+ { ...paramObj.schema } as OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject,
248
+ openapiSpec.components,
249
+ );
250
+ parametersSchema.properties[paramObj.name] = resolvedSchema;
251
+ if (paramObj.required) {
252
+ parametersSchema.required.push(paramObj.name);
253
+ }
254
+ if (paramObj.description && !('$$ref' in parametersSchema.properties[paramObj.name])) {
255
+ parametersSchema.properties[paramObj.name].description = paramObj.description;
256
+ }
257
+ }
258
+ }
259
+
260
+ const functionSignature = new FunctionSignature(operationId, description, parametersSchema);
261
+ functionSignatures.push(functionSignature);
262
+
263
+ const actionRequest = new ActionRequest(
264
+ baseUrl,
265
+ path,
266
+ method,
267
+ operationId,
268
+ !!operationObj['x-openai-isConsequential'], // Custom extension for consequential actions
269
+ operationObj.requestBody ? 'application/json' : 'application/x-www-form-urlencoded',
270
+ );
271
+
272
+ requestBuilders[operationId] = actionRequest;
273
+ }
274
+ }
275
+
276
+ return { functionSignatures, requestBuilders };
277
+ }
278
+
279
+ export type ValidationResult = {
280
+ status: boolean;
281
+ message: string;
282
+ spec?: OpenAPIV3.Document;
283
+ };
284
+
285
+ export function validateAndParseOpenAPISpec(specString: string): ValidationResult {
286
+ try {
287
+ let parsedSpec;
288
+ try {
289
+ parsedSpec = JSON.parse(specString);
290
+ } catch {
291
+ parsedSpec = load(specString);
292
+ }
293
+
294
+ // Check for servers
295
+ if (
296
+ !parsedSpec.servers ||
297
+ !Array.isArray(parsedSpec.servers) ||
298
+ parsedSpec.servers.length === 0
299
+ ) {
300
+ return { status: false, message: 'Could not find a valid URL in `servers`' };
301
+ }
302
+
303
+ if (!parsedSpec.servers[0].url) {
304
+ return { status: false, message: 'Could not find a valid URL in `servers`' };
305
+ }
306
+
307
+ // Check for paths
308
+ const paths = parsedSpec.paths;
309
+ if (!paths || typeof paths !== 'object' || Object.keys(paths).length === 0) {
310
+ return { status: false, message: 'No paths found in the OpenAPI spec.' };
311
+ }
312
+
313
+ const components = parsedSpec.components?.schemas || {};
314
+ const messages = [];
315
+
316
+ for (const [path, methods] of Object.entries(paths)) {
317
+ for (const [httpMethod, operation] of Object.entries(methods as OpenAPIV3.PathItemObject)) {
318
+ // Ensure operation is a valid operation object
319
+ const { responses } = operation as OpenAPIV3.OperationObject;
320
+ if (typeof operation === 'object' && responses) {
321
+ for (const [statusCode, response] of Object.entries(responses)) {
322
+ const content = (response as OpenAPIV3.ResponseObject).content;
323
+ if (content && content['application/json'] && content['application/json'].schema) {
324
+ const schema = content['application/json'].schema;
325
+ if ('$ref' in schema && typeof schema.$ref === 'string') {
326
+ const refName = schema.$ref.split('/').pop();
327
+ if (refName && !components[refName]) {
328
+ messages.push(
329
+ `In context=('paths', '${path}', '${httpMethod}', '${statusCode}', 'response', 'content', 'application/json', 'schema'), reference to unknown component ${refName}; using empty schema`,
330
+ );
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+
339
+ return {
340
+ status: true,
341
+ message: messages.join('\n') || 'OpenAPI spec is valid.',
342
+ spec: parsedSpec,
343
+ };
344
+ } catch (error) {
345
+ return { status: false, message: 'Error parsing OpenAPI spec.' };
346
+ }
347
+ }