librechat-data-provider 0.7.78 → 0.7.82
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.
- package/dist/index.es.js +1 -1
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react-query/index.es.js +1 -1
- package/dist/react-query/index.es.js.map +1 -1
- package/package.json +2 -1
- package/specs/actions.spec.ts +238 -0
- package/specs/mcp.spec.ts +126 -1
- package/specs/parsers.spec.ts +125 -0
- package/src/actions.ts +73 -19
- package/src/api-endpoints.ts +42 -10
- package/src/config.ts +30 -2
- package/src/createPayload.ts +13 -2
- package/src/data-service.ts +47 -69
- package/src/file-config.ts +3 -2
- package/src/index.ts +1 -0
- package/src/mcp.ts +12 -8
- package/src/parsers.ts +47 -16
- package/src/permissions.ts +90 -0
- package/src/react-query/react-query-service.ts +5 -34
- package/src/roles.ts +72 -126
- package/src/schemas.ts +152 -178
- package/src/types/assistants.ts +16 -18
- package/src/types/mutations.ts +8 -2
- package/src/types/queries.ts +28 -13
- package/src/types.ts +10 -0
- package/src/zod.spec.ts +569 -1
- package/src/zod.ts +318 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "librechat-data-provider",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.82",
|
|
4
4
|
"description": "data services for librechat apps",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.es.js",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"homepage": "https://librechat.ai",
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"axios": "^1.8.2",
|
|
43
|
+
"dayjs": "^1.11.13",
|
|
43
44
|
"js-yaml": "^4.1.0",
|
|
44
45
|
"zod": "^3.22.4"
|
|
45
46
|
},
|
package/specs/actions.spec.ts
CHANGED
|
@@ -206,6 +206,244 @@ describe('ActionRequest', () => {
|
|
|
206
206
|
},
|
|
207
207
|
});
|
|
208
208
|
});
|
|
209
|
+
|
|
210
|
+
it('handles GET requests with header and query parameters', async () => {
|
|
211
|
+
mockedAxios.get.mockResolvedValue({ data: { success: true } });
|
|
212
|
+
|
|
213
|
+
const data: Record<string, unknown> = {
|
|
214
|
+
'api-version': '2025-01-01',
|
|
215
|
+
'some-header': 'header-var',
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
|
|
219
|
+
'api-version': 'query',
|
|
220
|
+
'some-header': 'header',
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const actionRequest = new ActionRequest(
|
|
224
|
+
'https://example.com',
|
|
225
|
+
'/get',
|
|
226
|
+
'GET',
|
|
227
|
+
'testGET',
|
|
228
|
+
false,
|
|
229
|
+
'',
|
|
230
|
+
loc,
|
|
231
|
+
);
|
|
232
|
+
const executer = actionRequest.setParams(data);
|
|
233
|
+
const response = await executer.execute();
|
|
234
|
+
expect(mockedAxios.get).toHaveBeenCalled();
|
|
235
|
+
|
|
236
|
+
const [url, config] = mockedAxios.get.mock.calls[0];
|
|
237
|
+
expect(url).toBe('https://example.com/get');
|
|
238
|
+
expect(config?.headers).toEqual({
|
|
239
|
+
'some-header': 'header-var',
|
|
240
|
+
});
|
|
241
|
+
expect(config?.params).toEqual({
|
|
242
|
+
'api-version': '2025-01-01',
|
|
243
|
+
});
|
|
244
|
+
expect(response.data.success).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('handles GET requests with header and path parameters', async () => {
|
|
248
|
+
mockedAxios.get.mockResolvedValue({ data: { success: true } });
|
|
249
|
+
|
|
250
|
+
const data: Record<string, unknown> = {
|
|
251
|
+
'user-id': '1',
|
|
252
|
+
'some-header': 'header-var',
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
|
|
256
|
+
'user-id': 'path',
|
|
257
|
+
'some-header': 'header',
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const actionRequest = new ActionRequest(
|
|
261
|
+
'https://example.com',
|
|
262
|
+
'/getwithpath/{user-id}',
|
|
263
|
+
'GET',
|
|
264
|
+
'testGETwithpath',
|
|
265
|
+
false,
|
|
266
|
+
'',
|
|
267
|
+
loc,
|
|
268
|
+
);
|
|
269
|
+
const executer = actionRequest.setParams(data);
|
|
270
|
+
const response = await executer.execute();
|
|
271
|
+
expect(mockedAxios.get).toHaveBeenCalled();
|
|
272
|
+
|
|
273
|
+
const [url, config] = mockedAxios.get.mock.calls[0];
|
|
274
|
+
expect(url).toBe('https://example.com/getwithpath/1');
|
|
275
|
+
expect(config?.headers).toEqual({
|
|
276
|
+
'some-header': 'header-var',
|
|
277
|
+
});
|
|
278
|
+
expect(config?.params).toEqual({
|
|
279
|
+
});
|
|
280
|
+
expect(response.data.success).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('handles POST requests with body, header and query parameters', async () => {
|
|
284
|
+
mockedAxios.post.mockResolvedValue({ data: { success: true } });
|
|
285
|
+
|
|
286
|
+
const data: Record<string, unknown> = {
|
|
287
|
+
'api-version': '2025-01-01',
|
|
288
|
+
'message': 'a body parameter',
|
|
289
|
+
'some-header': 'header-var',
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
|
|
293
|
+
'api-version': 'query',
|
|
294
|
+
'message': 'body',
|
|
295
|
+
'some-header': 'header',
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const actionRequest = new ActionRequest(
|
|
299
|
+
'https://example.com',
|
|
300
|
+
'/post',
|
|
301
|
+
'POST',
|
|
302
|
+
'testPost',
|
|
303
|
+
false,
|
|
304
|
+
'application/json',
|
|
305
|
+
loc,
|
|
306
|
+
);
|
|
307
|
+
const executer = actionRequest.setParams(data);
|
|
308
|
+
const response = await executer.execute();
|
|
309
|
+
expect(mockedAxios.post).toHaveBeenCalled();
|
|
310
|
+
|
|
311
|
+
const [url, body, config] = mockedAxios.post.mock.calls[0];
|
|
312
|
+
expect(url).toBe('https://example.com/post');
|
|
313
|
+
expect(body).toEqual({ message: 'a body parameter' });
|
|
314
|
+
expect(config?.headers).toEqual({
|
|
315
|
+
'some-header': 'header-var',
|
|
316
|
+
'Content-Type': 'application/json',
|
|
317
|
+
});
|
|
318
|
+
expect(config?.params).toEqual({
|
|
319
|
+
'api-version': '2025-01-01',
|
|
320
|
+
});
|
|
321
|
+
expect(response.data.success).toBe(true);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('handles PUT requests with body, header and query parameters', async () => {
|
|
325
|
+
mockedAxios.put.mockResolvedValue({ data: { success: true } });
|
|
326
|
+
|
|
327
|
+
const data: Record<string, unknown> = {
|
|
328
|
+
'api-version': '2025-01-01',
|
|
329
|
+
'message': 'a body parameter',
|
|
330
|
+
'some-header': 'header-var',
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
|
|
334
|
+
'api-version': 'query',
|
|
335
|
+
'message': 'body',
|
|
336
|
+
'some-header': 'header',
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const actionRequest = new ActionRequest(
|
|
340
|
+
'https://example.com',
|
|
341
|
+
'/put',
|
|
342
|
+
'PUT',
|
|
343
|
+
'testPut',
|
|
344
|
+
false,
|
|
345
|
+
'application/json',
|
|
346
|
+
loc,
|
|
347
|
+
);
|
|
348
|
+
const executer = actionRequest.setParams(data);
|
|
349
|
+
const response = await executer.execute();
|
|
350
|
+
expect(mockedAxios.put).toHaveBeenCalled();
|
|
351
|
+
|
|
352
|
+
const [url, body, config] = mockedAxios.put.mock.calls[0];
|
|
353
|
+
expect(url).toBe('https://example.com/put');
|
|
354
|
+
expect(body).toEqual({ message: 'a body parameter' });
|
|
355
|
+
expect(config?.headers).toEqual({
|
|
356
|
+
'some-header': 'header-var',
|
|
357
|
+
'Content-Type': 'application/json',
|
|
358
|
+
});
|
|
359
|
+
expect(config?.params).toEqual({
|
|
360
|
+
'api-version': '2025-01-01',
|
|
361
|
+
});
|
|
362
|
+
expect(response.data.success).toBe(true);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('handles PATCH requests with body, header and query parameters', async () => {
|
|
366
|
+
mockedAxios.patch.mockResolvedValue({ data: { success: true } });
|
|
367
|
+
|
|
368
|
+
const data: Record<string, unknown> = {
|
|
369
|
+
'api-version': '2025-01-01',
|
|
370
|
+
'message': 'a body parameter',
|
|
371
|
+
'some-header': 'header-var',
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
|
|
375
|
+
'api-version': 'query',
|
|
376
|
+
'message': 'body',
|
|
377
|
+
'some-header': 'header',
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const actionRequest = new ActionRequest(
|
|
381
|
+
'https://example.com',
|
|
382
|
+
'/patch',
|
|
383
|
+
'PATCH',
|
|
384
|
+
'testPatch',
|
|
385
|
+
false,
|
|
386
|
+
'application/json',
|
|
387
|
+
loc,
|
|
388
|
+
);
|
|
389
|
+
const executer = actionRequest.setParams(data);
|
|
390
|
+
const response = await executer.execute();
|
|
391
|
+
expect(mockedAxios.patch).toHaveBeenCalled();
|
|
392
|
+
|
|
393
|
+
const [url, body, config] = mockedAxios.patch.mock.calls[0];
|
|
394
|
+
expect(url).toBe('https://example.com/patch');
|
|
395
|
+
expect(body).toEqual({ message: 'a body parameter' });
|
|
396
|
+
expect(config?.headers).toEqual({
|
|
397
|
+
'some-header': 'header-var',
|
|
398
|
+
'Content-Type': 'application/json',
|
|
399
|
+
});
|
|
400
|
+
expect(config?.params).toEqual({
|
|
401
|
+
'api-version': '2025-01-01',
|
|
402
|
+
});
|
|
403
|
+
expect(response.data.success).toBe(true);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('handles DELETE requests with body, header and query parameters', async () => {
|
|
407
|
+
mockedAxios.delete.mockResolvedValue({ data: { success: true } });
|
|
408
|
+
|
|
409
|
+
const data: Record<string, unknown> = {
|
|
410
|
+
'api-version': '2025-01-01',
|
|
411
|
+
'message-id': '1',
|
|
412
|
+
'some-header': 'header-var',
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
|
|
416
|
+
'api-version': 'query',
|
|
417
|
+
'message-id': 'body',
|
|
418
|
+
'some-header': 'header',
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const actionRequest = new ActionRequest(
|
|
422
|
+
'https://example.com',
|
|
423
|
+
'/delete',
|
|
424
|
+
'DELETE',
|
|
425
|
+
'testDelete',
|
|
426
|
+
false,
|
|
427
|
+
'application/json',
|
|
428
|
+
loc,
|
|
429
|
+
);
|
|
430
|
+
const executer = actionRequest.setParams(data);
|
|
431
|
+
const response = await executer.execute();
|
|
432
|
+
expect(mockedAxios.delete).toHaveBeenCalled();
|
|
433
|
+
|
|
434
|
+
const [url, config] = mockedAxios.delete.mock.calls[0];
|
|
435
|
+
expect(url).toBe('https://example.com/delete');
|
|
436
|
+
expect(config?.data).toEqual({ 'message-id': '1' });
|
|
437
|
+
expect(config?.headers).toEqual({
|
|
438
|
+
'some-header': 'header-var',
|
|
439
|
+
'Content-Type': 'application/json',
|
|
440
|
+
});
|
|
441
|
+
expect(config?.params).toEqual({
|
|
442
|
+
'api-version': '2025-01-01',
|
|
443
|
+
});
|
|
444
|
+
expect(response.data.success).toBe(true);
|
|
445
|
+
});
|
|
446
|
+
|
|
209
447
|
});
|
|
210
448
|
|
|
211
449
|
it('throws an error for unsupported HTTP method', async () => {
|
package/specs/mcp.spec.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StdioOptionsSchema } from '../src/mcp';
|
|
1
|
+
import { StdioOptionsSchema, processMCPEnv, MCPOptions } from '../src/mcp';
|
|
2
2
|
|
|
3
3
|
describe('Environment Variable Extraction (MCP)', () => {
|
|
4
4
|
const originalEnv = process.env;
|
|
@@ -49,4 +49,129 @@ describe('Environment Variable Extraction (MCP)', () => {
|
|
|
49
49
|
expect(result.env).toBeUndefined();
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
describe('processMCPEnv', () => {
|
|
54
|
+
it('should create a deep clone of the input object', () => {
|
|
55
|
+
const originalObj: MCPOptions = {
|
|
56
|
+
command: 'node',
|
|
57
|
+
args: ['server.js'],
|
|
58
|
+
env: {
|
|
59
|
+
API_KEY: '${TEST_API_KEY}',
|
|
60
|
+
PLAIN_VALUE: 'plain-value',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const result = processMCPEnv(originalObj);
|
|
65
|
+
|
|
66
|
+
// Verify it's not the same object reference
|
|
67
|
+
expect(result).not.toBe(originalObj);
|
|
68
|
+
|
|
69
|
+
// Modify the result and ensure original is unchanged
|
|
70
|
+
if ('env' in result && result.env) {
|
|
71
|
+
result.env.API_KEY = 'modified-value';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
expect(originalObj.env?.API_KEY).toBe('${TEST_API_KEY}');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should process environment variables in env field', () => {
|
|
78
|
+
const obj: MCPOptions = {
|
|
79
|
+
command: 'node',
|
|
80
|
+
args: ['server.js'],
|
|
81
|
+
env: {
|
|
82
|
+
API_KEY: '${TEST_API_KEY}',
|
|
83
|
+
ANOTHER_KEY: '${ANOTHER_SECRET}',
|
|
84
|
+
PLAIN_VALUE: 'plain-value',
|
|
85
|
+
NON_EXISTENT: '${NON_EXISTENT_VAR}',
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const result = processMCPEnv(obj);
|
|
90
|
+
|
|
91
|
+
expect('env' in result && result.env).toEqual({
|
|
92
|
+
API_KEY: 'test-api-key-value',
|
|
93
|
+
ANOTHER_KEY: 'another-secret-value',
|
|
94
|
+
PLAIN_VALUE: 'plain-value',
|
|
95
|
+
NON_EXISTENT: '${NON_EXISTENT_VAR}',
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should process user ID in headers field', () => {
|
|
100
|
+
const userId = 'test-user-123';
|
|
101
|
+
const obj: MCPOptions = {
|
|
102
|
+
type: 'sse',
|
|
103
|
+
url: 'https://example.com',
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: '${TEST_API_KEY}',
|
|
106
|
+
'User-Id': '{{LIBRECHAT_USER_ID}}',
|
|
107
|
+
'Content-Type': 'application/json',
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const result = processMCPEnv(obj, userId);
|
|
112
|
+
|
|
113
|
+
expect('headers' in result && result.headers).toEqual({
|
|
114
|
+
Authorization: 'test-api-key-value',
|
|
115
|
+
'User-Id': 'test-user-123',
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should handle null or undefined input', () => {
|
|
121
|
+
// @ts-ignore - Testing null/undefined handling
|
|
122
|
+
expect(processMCPEnv(null)).toBeNull();
|
|
123
|
+
// @ts-ignore - Testing null/undefined handling
|
|
124
|
+
expect(processMCPEnv(undefined)).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should not modify objects without env or headers', () => {
|
|
128
|
+
const obj: MCPOptions = {
|
|
129
|
+
command: 'node',
|
|
130
|
+
args: ['server.js'],
|
|
131
|
+
timeout: 5000,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const result = processMCPEnv(obj);
|
|
135
|
+
|
|
136
|
+
expect(result).toEqual(obj);
|
|
137
|
+
expect(result).not.toBe(obj); // Still a different object (deep clone)
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should ensure different users with same starting config get separate values', () => {
|
|
141
|
+
// Create a single base configuration
|
|
142
|
+
const baseConfig: MCPOptions = {
|
|
143
|
+
type: 'sse',
|
|
144
|
+
url: 'https://example.com',
|
|
145
|
+
headers: {
|
|
146
|
+
'User-Id': '{{LIBRECHAT_USER_ID}}',
|
|
147
|
+
'API-Key': '${TEST_API_KEY}',
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Process for two different users
|
|
152
|
+
const user1Id = 'user-123';
|
|
153
|
+
const user2Id = 'user-456';
|
|
154
|
+
|
|
155
|
+
const resultUser1 = processMCPEnv(baseConfig, user1Id);
|
|
156
|
+
const resultUser2 = processMCPEnv(baseConfig, user2Id);
|
|
157
|
+
|
|
158
|
+
// Verify each has the correct user ID
|
|
159
|
+
expect('headers' in resultUser1 && resultUser1.headers?.['User-Id']).toBe(user1Id);
|
|
160
|
+
expect('headers' in resultUser2 && resultUser2.headers?.['User-Id']).toBe(user2Id);
|
|
161
|
+
|
|
162
|
+
// Verify they're different objects
|
|
163
|
+
expect(resultUser1).not.toBe(resultUser2);
|
|
164
|
+
|
|
165
|
+
// Modify one result and ensure it doesn't affect the other
|
|
166
|
+
if ('headers' in resultUser1 && resultUser1.headers) {
|
|
167
|
+
resultUser1.headers['User-Id'] = 'modified-user';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Original config should be unchanged
|
|
171
|
+
expect(baseConfig.headers?.['User-Id']).toBe('{{LIBRECHAT_USER_ID}}');
|
|
172
|
+
|
|
173
|
+
// Second user's config should be unchanged
|
|
174
|
+
expect('headers' in resultUser2 && resultUser2.headers?.['User-Id']).toBe(user2Id);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
52
177
|
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { replaceSpecialVars } from '../src/parsers';
|
|
2
|
+
import { specialVariables } from '../src/config';
|
|
3
|
+
import type { TUser } from '../src/types';
|
|
4
|
+
|
|
5
|
+
// Mock dayjs module with consistent date/time values regardless of environment
|
|
6
|
+
jest.mock('dayjs', () => {
|
|
7
|
+
// Create a mock implementation that returns fixed values
|
|
8
|
+
const mockDayjs = () => ({
|
|
9
|
+
format: (format: string) => {
|
|
10
|
+
if (format === 'YYYY-MM-DD') {
|
|
11
|
+
return '2024-04-29';
|
|
12
|
+
}
|
|
13
|
+
if (format === 'YYYY-MM-DD HH:mm:ss') {
|
|
14
|
+
return '2024-04-29 12:34:56';
|
|
15
|
+
}
|
|
16
|
+
return format; // fallback
|
|
17
|
+
},
|
|
18
|
+
day: () => 1, // 1 = Monday
|
|
19
|
+
toISOString: () => '2024-04-29T16:34:56.000Z',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Add any static methods needed
|
|
23
|
+
mockDayjs.extend = jest.fn();
|
|
24
|
+
|
|
25
|
+
return mockDayjs;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('replaceSpecialVars', () => {
|
|
29
|
+
// Create a partial user object for testing
|
|
30
|
+
const mockUser = {
|
|
31
|
+
name: 'Test User',
|
|
32
|
+
id: 'user123',
|
|
33
|
+
} as TUser;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
jest.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('should return the original text if text is empty', () => {
|
|
40
|
+
expect(replaceSpecialVars({ text: '' })).toBe('');
|
|
41
|
+
expect(replaceSpecialVars({ text: null as unknown as string })).toBe(null);
|
|
42
|
+
expect(replaceSpecialVars({ text: undefined as unknown as string })).toBe(undefined);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should replace {{current_date}} with the current date', () => {
|
|
46
|
+
const result = replaceSpecialVars({ text: 'Today is {{current_date}}' });
|
|
47
|
+
// dayjs().day() returns 1 for Monday (April 29, 2024 is a Monday)
|
|
48
|
+
expect(result).toBe('Today is 2024-04-29 (1)');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should replace {{current_datetime}} with the current datetime', () => {
|
|
52
|
+
const result = replaceSpecialVars({ text: 'Now is {{current_datetime}}' });
|
|
53
|
+
expect(result).toBe('Now is 2024-04-29 12:34:56 (1)');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should replace {{iso_datetime}} with the ISO datetime', () => {
|
|
57
|
+
const result = replaceSpecialVars({ text: 'ISO time: {{iso_datetime}}' });
|
|
58
|
+
expect(result).toBe('ISO time: 2024-04-29T16:34:56.000Z');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('should replace {{current_user}} with the user name if provided', () => {
|
|
62
|
+
const result = replaceSpecialVars({
|
|
63
|
+
text: 'Hello {{current_user}}!',
|
|
64
|
+
user: mockUser,
|
|
65
|
+
});
|
|
66
|
+
expect(result).toBe('Hello Test User!');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should not replace {{current_user}} if user is not provided', () => {
|
|
70
|
+
const result = replaceSpecialVars({
|
|
71
|
+
text: 'Hello {{current_user}}!',
|
|
72
|
+
});
|
|
73
|
+
expect(result).toBe('Hello {{current_user}}!');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should not replace {{current_user}} if user has no name', () => {
|
|
77
|
+
const result = replaceSpecialVars({
|
|
78
|
+
text: 'Hello {{current_user}}!',
|
|
79
|
+
user: { id: 'user123' } as TUser,
|
|
80
|
+
});
|
|
81
|
+
expect(result).toBe('Hello {{current_user}}!');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('should handle multiple replacements in the same text', () => {
|
|
85
|
+
const result = replaceSpecialVars({
|
|
86
|
+
text: 'Hello {{current_user}}! Today is {{current_date}} and the time is {{current_datetime}}. ISO: {{iso_datetime}}',
|
|
87
|
+
user: mockUser,
|
|
88
|
+
});
|
|
89
|
+
expect(result).toBe(
|
|
90
|
+
'Hello Test User! Today is 2024-04-29 (1) and the time is 2024-04-29 12:34:56 (1). ISO: 2024-04-29T16:34:56.000Z',
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('should be case-insensitive when replacing variables', () => {
|
|
95
|
+
const result = replaceSpecialVars({
|
|
96
|
+
text: 'Date: {{CURRENT_DATE}}, User: {{Current_User}}',
|
|
97
|
+
user: mockUser,
|
|
98
|
+
});
|
|
99
|
+
expect(result).toBe('Date: 2024-04-29 (1), User: Test User');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('should confirm all specialVariables from config.ts get parsed', () => {
|
|
103
|
+
// Create a text that includes all special variables
|
|
104
|
+
const specialVarsText = Object.keys(specialVariables)
|
|
105
|
+
.map((key) => `{{${key}}}`)
|
|
106
|
+
.join(' ');
|
|
107
|
+
|
|
108
|
+
const result = replaceSpecialVars({
|
|
109
|
+
text: specialVarsText,
|
|
110
|
+
user: mockUser,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Verify none of the original variable placeholders remain in the result
|
|
114
|
+
Object.keys(specialVariables).forEach((key) => {
|
|
115
|
+
const placeholder = `{{${key}}}`;
|
|
116
|
+
expect(result).not.toContain(placeholder);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Verify the expected replacements
|
|
120
|
+
expect(result).toContain('2024-04-29 (1)'); // current_date
|
|
121
|
+
expect(result).toContain('2024-04-29 12:34:56 (1)'); // current_datetime
|
|
122
|
+
expect(result).toContain('2024-04-29T16:34:56.000Z'); // iso_datetime
|
|
123
|
+
expect(result).toContain('Test User'); // current_user
|
|
124
|
+
});
|
|
125
|
+
});
|
package/src/actions.ts
CHANGED
|
@@ -167,12 +167,13 @@ class RequestConfig {
|
|
|
167
167
|
readonly operation: string,
|
|
168
168
|
readonly isConsequential: boolean,
|
|
169
169
|
readonly contentType: string,
|
|
170
|
+
readonly parameterLocations?: Record<string, 'query' | 'path' | 'header' | 'body'>,
|
|
170
171
|
) {}
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
class RequestExecutor {
|
|
174
175
|
path: string;
|
|
175
|
-
params?:
|
|
176
|
+
params?: Record<string, unknown>;
|
|
176
177
|
private operationHash?: string;
|
|
177
178
|
private authHeaders: Record<string, string> = {};
|
|
178
179
|
private authToken?: string;
|
|
@@ -181,15 +182,28 @@ class RequestExecutor {
|
|
|
181
182
|
this.path = config.basePath;
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
setParams(params:
|
|
185
|
+
setParams(params: Record<string, unknown>) {
|
|
185
186
|
this.operationHash = sha1(JSON.stringify(params));
|
|
186
|
-
this.params =
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
this.params = { ...params } as Record<string, unknown>;
|
|
188
|
+
if (this.config.parameterLocations) {
|
|
189
|
+
//Substituting “Path” Parameters:
|
|
190
|
+
for (const [key, value] of Object.entries(params)) {
|
|
191
|
+
if (this.config.parameterLocations[key] === 'path') {
|
|
192
|
+
const paramPattern = `{${key}}`;
|
|
193
|
+
if (this.path.includes(paramPattern)) {
|
|
194
|
+
this.path = this.path.replace(paramPattern, encodeURIComponent(String(value)));
|
|
195
|
+
delete this.params[key];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
// Fallback: if no locations are defined, perform path substitution for all keys.
|
|
201
|
+
for (const [key, value] of Object.entries(params)) {
|
|
202
|
+
const paramPattern = `{${key}}`;
|
|
203
|
+
if (this.path.includes(paramPattern)) {
|
|
204
|
+
this.path = this.path.replace(paramPattern, encodeURIComponent(String(value)));
|
|
205
|
+
delete this.params[key];
|
|
206
|
+
}
|
|
193
207
|
}
|
|
194
208
|
}
|
|
195
209
|
return this;
|
|
@@ -275,23 +289,46 @@ class RequestExecutor {
|
|
|
275
289
|
|
|
276
290
|
async execute() {
|
|
277
291
|
const url = createURL(this.config.domain, this.path);
|
|
278
|
-
const headers = {
|
|
292
|
+
const headers: Record<string, string> = {
|
|
279
293
|
...this.authHeaders,
|
|
280
|
-
'Content-Type': this.config.contentType,
|
|
294
|
+
...(this.config.contentType ? { 'Content-Type': this.config.contentType } : {}),
|
|
281
295
|
};
|
|
282
|
-
|
|
283
296
|
const method = this.config.method.toLowerCase();
|
|
284
297
|
const axios = _axios.create();
|
|
298
|
+
|
|
299
|
+
// Initialize separate containers for query and body parameters.
|
|
300
|
+
const queryParams: Record<string, unknown> = {};
|
|
301
|
+
const bodyParams: Record<string, unknown> = {};
|
|
302
|
+
|
|
303
|
+
if (this.config.parameterLocations && this.params) {
|
|
304
|
+
for (const key of Object.keys(this.params)) {
|
|
305
|
+
// Determine parameter placement; default to "query" for GET and "body" for others.
|
|
306
|
+
const loc: 'query' | 'path' | 'header' | 'body' = this.config.parameterLocations[key] || (method === 'get' ? 'query' : 'body');
|
|
307
|
+
|
|
308
|
+
const val = this.params[key];
|
|
309
|
+
if (loc === 'query') {
|
|
310
|
+
queryParams[key] = val;
|
|
311
|
+
} else if (loc === 'header') {
|
|
312
|
+
headers[key] = String(val);
|
|
313
|
+
} else if (loc === 'body') {
|
|
314
|
+
bodyParams[key] = val;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} else if (this.params) {
|
|
318
|
+
Object.assign(queryParams, this.params);
|
|
319
|
+
Object.assign(bodyParams, this.params);
|
|
320
|
+
}
|
|
321
|
+
|
|
285
322
|
if (method === 'get') {
|
|
286
|
-
return axios.get(url, { headers, params:
|
|
323
|
+
return axios.get(url, { headers, params: queryParams });
|
|
287
324
|
} else if (method === 'post') {
|
|
288
|
-
return axios.post(url,
|
|
325
|
+
return axios.post(url, bodyParams, { headers, params: queryParams });
|
|
289
326
|
} else if (method === 'put') {
|
|
290
|
-
return axios.put(url,
|
|
327
|
+
return axios.put(url, bodyParams, { headers, params: queryParams });
|
|
291
328
|
} else if (method === 'delete') {
|
|
292
|
-
return axios.delete(url, { headers, data:
|
|
329
|
+
return axios.delete(url, { headers, data: bodyParams, params: queryParams });
|
|
293
330
|
} else if (method === 'patch') {
|
|
294
|
-
return axios.patch(url,
|
|
331
|
+
return axios.patch(url, bodyParams, { headers, params: queryParams });
|
|
295
332
|
} else {
|
|
296
333
|
throw new Error(`Unsupported HTTP method: ${method}`);
|
|
297
334
|
}
|
|
@@ -312,8 +349,9 @@ export class ActionRequest {
|
|
|
312
349
|
operation: string,
|
|
313
350
|
isConsequential: boolean,
|
|
314
351
|
contentType: string,
|
|
352
|
+
parameterLocations?: Record<string, 'query' | 'path' | 'header' | 'body'>,
|
|
315
353
|
) {
|
|
316
|
-
this.config = new RequestConfig(domain, path, method, operation, isConsequential, contentType);
|
|
354
|
+
this.config = new RequestConfig(domain, path, method, operation, isConsequential, contentType, parameterLocations);
|
|
317
355
|
}
|
|
318
356
|
|
|
319
357
|
// Add getters to maintain backward compatibility
|
|
@@ -341,7 +379,7 @@ export class ActionRequest {
|
|
|
341
379
|
}
|
|
342
380
|
|
|
343
381
|
// Maintain backward compatibility by delegating to a new executor
|
|
344
|
-
setParams(params:
|
|
382
|
+
setParams(params: Record<string, unknown>) {
|
|
345
383
|
const executor = this.createExecutor();
|
|
346
384
|
executor.setParams(params);
|
|
347
385
|
return executor;
|
|
@@ -406,6 +444,7 @@ export function openapiToFunction(
|
|
|
406
444
|
// Iterate over each path and method in the OpenAPI spec
|
|
407
445
|
for (const [path, methods] of Object.entries(openapiSpec.paths)) {
|
|
408
446
|
for (const [method, operation] of Object.entries(methods as OpenAPIV3.PathsObject)) {
|
|
447
|
+
const paramLocations: Record<string, 'query' | 'path' | 'header' | 'body'> = {};
|
|
409
448
|
const operationObj = operation as OpenAPIV3.OperationObject & {
|
|
410
449
|
'x-openai-isConsequential'?: boolean;
|
|
411
450
|
} & {
|
|
@@ -445,6 +484,14 @@ export function openapiToFunction(
|
|
|
445
484
|
if (resolvedParam.required) {
|
|
446
485
|
parametersSchema.required.push(paramName);
|
|
447
486
|
}
|
|
487
|
+
// Record the parameter location from the OpenAPI "in" field.
|
|
488
|
+
paramLocations[paramName] =
|
|
489
|
+
(resolvedParam.in === 'query' ||
|
|
490
|
+
resolvedParam.in === 'path' ||
|
|
491
|
+
resolvedParam.in === 'header' ||
|
|
492
|
+
resolvedParam.in === 'body')
|
|
493
|
+
? resolvedParam.in
|
|
494
|
+
: 'query';
|
|
448
495
|
}
|
|
449
496
|
}
|
|
450
497
|
|
|
@@ -464,6 +511,12 @@ export function openapiToFunction(
|
|
|
464
511
|
if (resolvedSchema.required) {
|
|
465
512
|
parametersSchema.required.push(...resolvedSchema.required);
|
|
466
513
|
}
|
|
514
|
+
// Mark requestBody properties as belonging to the "body"
|
|
515
|
+
if (resolvedSchema.properties) {
|
|
516
|
+
for (const key in resolvedSchema.properties) {
|
|
517
|
+
paramLocations[key] = 'body';
|
|
518
|
+
}
|
|
519
|
+
}
|
|
467
520
|
}
|
|
468
521
|
|
|
469
522
|
const functionSignature = new FunctionSignature(
|
|
@@ -481,6 +534,7 @@ export function openapiToFunction(
|
|
|
481
534
|
operationId,
|
|
482
535
|
!!(operationObj['x-openai-isConsequential'] ?? false),
|
|
483
536
|
operationObj.requestBody ? 'application/json' : '',
|
|
537
|
+
paramLocations,
|
|
484
538
|
);
|
|
485
539
|
|
|
486
540
|
requestBuilders[operationId] = actionRequest;
|