genai-lite 0.2.0 → 0.3.0
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/README.md +508 -30
- package/dist/config/presets.json +121 -17
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -3
- package/dist/llm/LLMService.createMessages.test.d.ts +4 -0
- package/dist/llm/LLMService.createMessages.test.js +364 -0
- package/dist/llm/LLMService.d.ts +49 -47
- package/dist/llm/LLMService.js +208 -303
- package/dist/llm/LLMService.original.d.ts +147 -0
- package/dist/llm/LLMService.original.js +656 -0
- package/dist/llm/LLMService.prepareMessage.test.d.ts +1 -0
- package/dist/llm/LLMService.prepareMessage.test.js +303 -0
- package/dist/llm/LLMService.sendMessage.preset.test.d.ts +1 -0
- package/dist/llm/LLMService.sendMessage.preset.test.js +153 -0
- package/dist/llm/LLMService.test.js +275 -0
- package/dist/llm/clients/AnthropicClientAdapter.js +64 -10
- package/dist/llm/clients/AnthropicClientAdapter.test.js +11 -1
- package/dist/llm/clients/GeminiClientAdapter.js +70 -11
- package/dist/llm/clients/GeminiClientAdapter.test.js +125 -1
- package/dist/llm/clients/MockClientAdapter.js +9 -3
- package/dist/llm/clients/MockClientAdapter.test.js +11 -1
- package/dist/llm/clients/OpenAIClientAdapter.js +26 -10
- package/dist/llm/clients/OpenAIClientAdapter.test.js +11 -1
- package/dist/llm/config.js +117 -2
- package/dist/llm/config.test.js +17 -0
- package/dist/llm/services/AdapterRegistry.d.ts +59 -0
- package/dist/llm/services/AdapterRegistry.js +113 -0
- package/dist/llm/services/AdapterRegistry.test.d.ts +1 -0
- package/dist/llm/services/AdapterRegistry.test.js +239 -0
- package/dist/llm/services/ModelResolver.d.ts +35 -0
- package/dist/llm/services/ModelResolver.js +116 -0
- package/dist/llm/services/ModelResolver.test.d.ts +1 -0
- package/dist/llm/services/ModelResolver.test.js +158 -0
- package/dist/llm/services/PresetManager.d.ts +27 -0
- package/dist/llm/services/PresetManager.js +50 -0
- package/dist/llm/services/PresetManager.test.d.ts +1 -0
- package/dist/llm/services/PresetManager.test.js +210 -0
- package/dist/llm/services/RequestValidator.d.ts +31 -0
- package/dist/llm/services/RequestValidator.js +122 -0
- package/dist/llm/services/RequestValidator.test.d.ts +1 -0
- package/dist/llm/services/RequestValidator.test.js +159 -0
- package/dist/llm/services/SettingsManager.d.ts +32 -0
- package/dist/llm/services/SettingsManager.js +223 -0
- package/dist/llm/services/SettingsManager.test.d.ts +1 -0
- package/dist/llm/services/SettingsManager.test.js +266 -0
- package/dist/llm/types.d.ts +107 -0
- package/dist/prompting/builder.d.ts +4 -0
- package/dist/prompting/builder.js +12 -61
- package/dist/prompting/content.js +3 -9
- package/dist/prompting/index.d.ts +2 -3
- package/dist/prompting/index.js +4 -5
- package/dist/prompting/parser.d.ts +80 -0
- package/dist/prompting/parser.js +133 -0
- package/dist/prompting/parser.test.js +348 -0
- package/dist/prompting/template.d.ts +8 -0
- package/dist/prompting/template.js +89 -6
- package/dist/prompting/template.test.js +116 -0
- package/package.json +3 -2
- package/src/config/presets.json +122 -17
|
@@ -114,3 +114,351 @@ describe('parseStructuredContent', () => {
|
|
|
114
114
|
});
|
|
115
115
|
});
|
|
116
116
|
});
|
|
117
|
+
describe('extractInitialTaggedContent', () => {
|
|
118
|
+
it('should extract content from a tag at the beginning', () => {
|
|
119
|
+
const content = '<thinking>I am thinking about this problem.</thinking>Here is the answer.';
|
|
120
|
+
const result = (0, parser_1.extractInitialTaggedContent)(content, 'thinking');
|
|
121
|
+
expect(result.extracted).toBe('I am thinking about this problem.');
|
|
122
|
+
expect(result.remaining).toBe('Here is the answer.');
|
|
123
|
+
});
|
|
124
|
+
it('should handle leading whitespace', () => {
|
|
125
|
+
const content = ' \n <thinking>My thoughts here</thinking>The actual response';
|
|
126
|
+
const result = (0, parser_1.extractInitialTaggedContent)(content, 'thinking');
|
|
127
|
+
expect(result.extracted).toBe('My thoughts here');
|
|
128
|
+
expect(result.remaining).toBe('The actual response');
|
|
129
|
+
});
|
|
130
|
+
it('should not extract when tag is in the middle', () => {
|
|
131
|
+
const content = 'Some preamble <thinking>This is in the middle</thinking> and more text';
|
|
132
|
+
const result = (0, parser_1.extractInitialTaggedContent)(content, 'thinking');
|
|
133
|
+
expect(result.extracted).toBeNull();
|
|
134
|
+
expect(result.remaining).toBe('Some preamble <thinking>This is in the middle</thinking> and more text');
|
|
135
|
+
});
|
|
136
|
+
it('should handle no tag present', () => {
|
|
137
|
+
const content = 'This is just regular text without any special tags.';
|
|
138
|
+
const result = (0, parser_1.extractInitialTaggedContent)(content, 'thinking');
|
|
139
|
+
expect(result.extracted).toBeNull();
|
|
140
|
+
expect(result.remaining).toBe('This is just regular text without any special tags.');
|
|
141
|
+
});
|
|
142
|
+
it('should work with different tag names', () => {
|
|
143
|
+
const content = '<scratchpad>Working through the logic...</scratchpad>Final answer is 42.';
|
|
144
|
+
const result = (0, parser_1.extractInitialTaggedContent)(content, 'scratchpad');
|
|
145
|
+
expect(result.extracted).toBe('Working through the logic...');
|
|
146
|
+
expect(result.remaining).toBe('Final answer is 42.');
|
|
147
|
+
});
|
|
148
|
+
it('should handle unclosed tag at start', () => {
|
|
149
|
+
const content = '<thinking>This tag is never closed and continues...';
|
|
150
|
+
const result = (0, parser_1.extractInitialTaggedContent)(content, 'thinking');
|
|
151
|
+
expect(result.extracted).toBeNull();
|
|
152
|
+
expect(result.remaining).toBe('<thinking>This tag is never closed and continues...');
|
|
153
|
+
});
|
|
154
|
+
it('should handle multiline content within tags', () => {
|
|
155
|
+
const content = `<thinking>
|
|
156
|
+
Step 1: Analyze the problem
|
|
157
|
+
Step 2: Break it down
|
|
158
|
+
Step 3: Solve each part
|
|
159
|
+
</thinking>
|
|
160
|
+
The solution is to approach it systematically.`;
|
|
161
|
+
const result = (0, parser_1.extractInitialTaggedContent)(content, 'thinking');
|
|
162
|
+
expect(result.extracted).toBe(`Step 1: Analyze the problem
|
|
163
|
+
Step 2: Break it down
|
|
164
|
+
Step 3: Solve each part`);
|
|
165
|
+
expect(result.remaining).toBe('The solution is to approach it systematically.');
|
|
166
|
+
});
|
|
167
|
+
it('should handle empty tag', () => {
|
|
168
|
+
const content = '<thinking></thinking>Here is the response.';
|
|
169
|
+
const result = (0, parser_1.extractInitialTaggedContent)(content, 'thinking');
|
|
170
|
+
expect(result.extracted).toBe('');
|
|
171
|
+
expect(result.remaining).toBe('Here is the response.');
|
|
172
|
+
});
|
|
173
|
+
it('should handle empty content', () => {
|
|
174
|
+
const result = (0, parser_1.extractInitialTaggedContent)('', 'thinking');
|
|
175
|
+
expect(result.extracted).toBeNull();
|
|
176
|
+
expect(result.remaining).toBe('');
|
|
177
|
+
});
|
|
178
|
+
it('should preserve whitespace after the closing tag', () => {
|
|
179
|
+
const content = '<thinking>Thoughts</thinking> \n\n Response here';
|
|
180
|
+
const result = (0, parser_1.extractInitialTaggedContent)(content, 'thinking');
|
|
181
|
+
expect(result.extracted).toBe('Thoughts');
|
|
182
|
+
expect(result.remaining).toBe('Response here');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe('parseRoleTags', () => {
|
|
186
|
+
it('should parse basic role tags with variables', () => {
|
|
187
|
+
const template = `
|
|
188
|
+
<SYSTEM>You are a helpful {{expertise}} assistant.</SYSTEM>
|
|
189
|
+
<USER>Help me with {{task}}</USER>
|
|
190
|
+
<ASSISTANT>I'll help you with {{task}}.</ASSISTANT>
|
|
191
|
+
`;
|
|
192
|
+
const result = (0, parser_1.parseRoleTags)(template);
|
|
193
|
+
expect(result).toEqual([
|
|
194
|
+
{ role: 'system', content: 'You are a helpful {{expertise}} assistant.' },
|
|
195
|
+
{ role: 'user', content: 'Help me with {{task}}' },
|
|
196
|
+
{ role: 'assistant', content: "I'll help you with {{task}}." }
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
it('should handle multiple user/assistant turns', () => {
|
|
200
|
+
const template = `
|
|
201
|
+
<USER>First question</USER>
|
|
202
|
+
<ASSISTANT>First answer</ASSISTANT>
|
|
203
|
+
<USER>Follow-up question</USER>
|
|
204
|
+
<ASSISTANT>Follow-up answer</ASSISTANT>
|
|
205
|
+
`;
|
|
206
|
+
const result = (0, parser_1.parseRoleTags)(template);
|
|
207
|
+
expect(result).toEqual([
|
|
208
|
+
{ role: 'user', content: 'First question' },
|
|
209
|
+
{ role: 'assistant', content: 'First answer' },
|
|
210
|
+
{ role: 'user', content: 'Follow-up question' },
|
|
211
|
+
{ role: 'assistant', content: 'Follow-up answer' }
|
|
212
|
+
]);
|
|
213
|
+
});
|
|
214
|
+
it('should handle template with only system message', () => {
|
|
215
|
+
const template = '<SYSTEM>You are a coding assistant.</SYSTEM>';
|
|
216
|
+
const result = (0, parser_1.parseRoleTags)(template);
|
|
217
|
+
expect(result).toEqual([
|
|
218
|
+
{ role: 'system', content: 'You are a coding assistant.' }
|
|
219
|
+
]);
|
|
220
|
+
});
|
|
221
|
+
it('should treat content without tags as user message', () => {
|
|
222
|
+
const template = 'Hello, can you help me with {{problem}}?';
|
|
223
|
+
const result = (0, parser_1.parseRoleTags)(template);
|
|
224
|
+
expect(result).toEqual([
|
|
225
|
+
{ role: 'user', content: 'Hello, can you help me with {{problem}}?' }
|
|
226
|
+
]);
|
|
227
|
+
});
|
|
228
|
+
it('should handle empty template', () => {
|
|
229
|
+
const result = (0, parser_1.parseRoleTags)('');
|
|
230
|
+
expect(result).toEqual([]);
|
|
231
|
+
});
|
|
232
|
+
it('should handle whitespace-only template', () => {
|
|
233
|
+
const result = (0, parser_1.parseRoleTags)(' \n\t ');
|
|
234
|
+
expect(result).toEqual([]);
|
|
235
|
+
});
|
|
236
|
+
it('should preserve multiline content', () => {
|
|
237
|
+
const template = `<USER>
|
|
238
|
+
Please help me with:
|
|
239
|
+
1. {{task1}}
|
|
240
|
+
2. {{task2}}
|
|
241
|
+
3. {{task3}}
|
|
242
|
+
</USER>`;
|
|
243
|
+
const result = (0, parser_1.parseRoleTags)(template);
|
|
244
|
+
expect(result).toEqual([
|
|
245
|
+
{ role: 'user', content: 'Please help me with:\n 1. {{task1}}\n 2. {{task2}}\n 3. {{task3}}' }
|
|
246
|
+
]);
|
|
247
|
+
});
|
|
248
|
+
it('should ignore empty role tags', () => {
|
|
249
|
+
const template = `
|
|
250
|
+
<SYSTEM>System prompt</SYSTEM>
|
|
251
|
+
<USER></USER>
|
|
252
|
+
<ASSISTANT> </ASSISTANT>
|
|
253
|
+
<USER>Real question</USER>
|
|
254
|
+
`;
|
|
255
|
+
const result = (0, parser_1.parseRoleTags)(template);
|
|
256
|
+
expect(result).toEqual([
|
|
257
|
+
{ role: 'system', content: 'System prompt' },
|
|
258
|
+
{ role: 'user', content: 'Real question' }
|
|
259
|
+
]);
|
|
260
|
+
});
|
|
261
|
+
it('should handle mixed case tags', () => {
|
|
262
|
+
const template = '<System>System</System><User>User</User><Assistant>Assistant</Assistant>';
|
|
263
|
+
const result = (0, parser_1.parseRoleTags)(template);
|
|
264
|
+
// Should only match uppercase tags as per the regex
|
|
265
|
+
expect(result).toEqual([
|
|
266
|
+
{ role: 'user', content: '<System>System</System><User>User</User><Assistant>Assistant</Assistant>' }
|
|
267
|
+
]);
|
|
268
|
+
});
|
|
269
|
+
it('should handle complex templates with conditionals', () => {
|
|
270
|
+
const template = `
|
|
271
|
+
<SYSTEM>{{ systemPrompt ? systemPrompt : 'Default system prompt' }}</SYSTEM>
|
|
272
|
+
<USER>{{ hasContext ? 'Context: {{context}}\n\n' : '' }}{{question}}</USER>
|
|
273
|
+
`;
|
|
274
|
+
const result = (0, parser_1.parseRoleTags)(template);
|
|
275
|
+
expect(result).toEqual([
|
|
276
|
+
{ role: 'system', content: "{{ systemPrompt ? systemPrompt : 'Default system prompt' }}" },
|
|
277
|
+
{ role: 'user', content: "{{ hasContext ? 'Context: {{context}}\n\n' : '' }}{{question}}" }
|
|
278
|
+
]);
|
|
279
|
+
});
|
|
280
|
+
it('should preserve special characters and formatting', () => {
|
|
281
|
+
const template = `
|
|
282
|
+
<USER>Code: \`{{code}}\`
|
|
283
|
+
|
|
284
|
+
Error: "{{error}}"</USER>
|
|
285
|
+
<ASSISTANT>Let me analyze that code...</ASSISTANT>
|
|
286
|
+
`;
|
|
287
|
+
const result = (0, parser_1.parseRoleTags)(template);
|
|
288
|
+
expect(result).toEqual([
|
|
289
|
+
{ role: 'user', content: 'Code: `{{code}}`\n \n Error: "{{error}}"' },
|
|
290
|
+
{ role: 'assistant', content: 'Let me analyze that code...' }
|
|
291
|
+
]);
|
|
292
|
+
});
|
|
293
|
+
it('should handle unclosed tags as regular content', () => {
|
|
294
|
+
const template = 'This has <USER>unclosed tag content';
|
|
295
|
+
const result = (0, parser_1.parseRoleTags)(template);
|
|
296
|
+
expect(result).toEqual([
|
|
297
|
+
{ role: 'user', content: 'This has <USER>unclosed tag content' }
|
|
298
|
+
]);
|
|
299
|
+
});
|
|
300
|
+
it('should handle nested-looking structures', () => {
|
|
301
|
+
const template = `<USER>Can you parse <XML>tags</XML> inside content?</USER>`;
|
|
302
|
+
const result = (0, parser_1.parseRoleTags)(template);
|
|
303
|
+
expect(result).toEqual([
|
|
304
|
+
{ role: 'user', content: 'Can you parse <XML>tags</XML> inside content?' }
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
it('should throw error for non-string input', () => {
|
|
308
|
+
expect(() => (0, parser_1.parseRoleTags)(null)).toThrow('Template must be a string');
|
|
309
|
+
expect(() => (0, parser_1.parseRoleTags)(undefined)).toThrow('Template must be a string');
|
|
310
|
+
expect(() => (0, parser_1.parseRoleTags)(123)).toThrow('Template must be a string');
|
|
311
|
+
});
|
|
312
|
+
it('should handle text between role tags', () => {
|
|
313
|
+
const template = `
|
|
314
|
+
Some intro text
|
|
315
|
+
<SYSTEM>System message</SYSTEM>
|
|
316
|
+
Some middle text that should be ignored
|
|
317
|
+
<USER>User message</USER>
|
|
318
|
+
Some ending text
|
|
319
|
+
`;
|
|
320
|
+
const result = (0, parser_1.parseRoleTags)(template);
|
|
321
|
+
expect(result).toEqual([
|
|
322
|
+
{ role: 'system', content: 'System message' },
|
|
323
|
+
{ role: 'user', content: 'User message' }
|
|
324
|
+
]);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
describe('parseTemplateWithMetadata', () => {
|
|
328
|
+
it('should parse template with valid META block', () => {
|
|
329
|
+
const template = `<META>
|
|
330
|
+
{
|
|
331
|
+
"settings": {
|
|
332
|
+
"temperature": 0.9,
|
|
333
|
+
"thinkingExtraction": { "enabled": true, "tag": "reasoning" }
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
</META>
|
|
337
|
+
<SYSTEM>You are a creative writer.</SYSTEM>
|
|
338
|
+
<USER>Write a story</USER>`;
|
|
339
|
+
const result = (0, parser_1.parseTemplateWithMetadata)(template);
|
|
340
|
+
expect(result.metadata).toEqual({
|
|
341
|
+
settings: {
|
|
342
|
+
temperature: 0.9,
|
|
343
|
+
thinkingExtraction: { enabled: true, tag: "reasoning" }
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
expect(result.content).toBe('<SYSTEM>You are a creative writer.</SYSTEM>\n<USER>Write a story</USER>');
|
|
347
|
+
});
|
|
348
|
+
it('should handle template without META block', () => {
|
|
349
|
+
const template = `<SYSTEM>You are a helpful assistant.</SYSTEM>
|
|
350
|
+
<USER>Help me understand TypeScript</USER>`;
|
|
351
|
+
const result = (0, parser_1.parseTemplateWithMetadata)(template);
|
|
352
|
+
expect(result.metadata).toEqual({ settings: {} });
|
|
353
|
+
expect(result.content).toBe(template);
|
|
354
|
+
});
|
|
355
|
+
it('should handle invalid JSON in META block', () => {
|
|
356
|
+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
357
|
+
const template = `<META>
|
|
358
|
+
{
|
|
359
|
+
"settings": {
|
|
360
|
+
"temperature": 0.9,
|
|
361
|
+
invalid json here
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
</META>
|
|
365
|
+
<SYSTEM>Test</SYSTEM>`;
|
|
366
|
+
const result = (0, parser_1.parseTemplateWithMetadata)(template);
|
|
367
|
+
expect(result.metadata).toEqual({ settings: {} });
|
|
368
|
+
expect(result.content).toBe(template); // Original template returned on error
|
|
369
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith('Could not parse <META> block in template. Treating it as content.', expect.any(Error));
|
|
370
|
+
consoleWarnSpy.mockRestore();
|
|
371
|
+
});
|
|
372
|
+
it('should handle META block with missing settings', () => {
|
|
373
|
+
const template = `<META>
|
|
374
|
+
{
|
|
375
|
+
"name": "My Template",
|
|
376
|
+
"version": "1.0"
|
|
377
|
+
}
|
|
378
|
+
</META>
|
|
379
|
+
<USER>Hello</USER>`;
|
|
380
|
+
const result = (0, parser_1.parseTemplateWithMetadata)(template);
|
|
381
|
+
expect(result.metadata).toEqual({ settings: {} });
|
|
382
|
+
expect(result.content).toBe('<USER>Hello</USER>');
|
|
383
|
+
});
|
|
384
|
+
it('should handle META block with non-object settings', () => {
|
|
385
|
+
const template = `<META>
|
|
386
|
+
{
|
|
387
|
+
"settings": "not an object"
|
|
388
|
+
}
|
|
389
|
+
</META>
|
|
390
|
+
<USER>Test</USER>`;
|
|
391
|
+
const result = (0, parser_1.parseTemplateWithMetadata)(template);
|
|
392
|
+
expect(result.metadata).toEqual({ settings: {} });
|
|
393
|
+
expect(result.content).toBe('<USER>Test</USER>');
|
|
394
|
+
});
|
|
395
|
+
it('should handle empty META block', () => {
|
|
396
|
+
const template = `<META></META>
|
|
397
|
+
<USER>Test</USER>`;
|
|
398
|
+
const result = (0, parser_1.parseTemplateWithMetadata)(template);
|
|
399
|
+
expect(result.metadata).toEqual({ settings: {} });
|
|
400
|
+
expect(result.content).toBe('<USER>Test</USER>');
|
|
401
|
+
});
|
|
402
|
+
it('should handle META block with whitespace', () => {
|
|
403
|
+
const template = ` <META>
|
|
404
|
+
{
|
|
405
|
+
"settings": {
|
|
406
|
+
"temperature": 0.5
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
</META>
|
|
410
|
+
<SYSTEM>Assistant</SYSTEM>`;
|
|
411
|
+
const result = (0, parser_1.parseTemplateWithMetadata)(template);
|
|
412
|
+
expect(result.metadata).toEqual({
|
|
413
|
+
settings: { temperature: 0.5 }
|
|
414
|
+
});
|
|
415
|
+
expect(result.content).toBe('<SYSTEM>Assistant</SYSTEM>');
|
|
416
|
+
});
|
|
417
|
+
it('should handle complex settings in META block', () => {
|
|
418
|
+
const template = `<META>
|
|
419
|
+
{
|
|
420
|
+
"settings": {
|
|
421
|
+
"temperature": 0.8,
|
|
422
|
+
"maxTokens": 2000,
|
|
423
|
+
"stopSequences": ["\\n\\n", "END"],
|
|
424
|
+
"reasoning": {
|
|
425
|
+
"enabled": true,
|
|
426
|
+
"effort": "high",
|
|
427
|
+
"maxTokens": 5000
|
|
428
|
+
},
|
|
429
|
+
"thinkingExtraction": {
|
|
430
|
+
"enabled": false
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
</META>
|
|
435
|
+
<SYSTEM>Complex template</SYSTEM>`;
|
|
436
|
+
const result = (0, parser_1.parseTemplateWithMetadata)(template);
|
|
437
|
+
expect(result.metadata.settings).toEqual({
|
|
438
|
+
temperature: 0.8,
|
|
439
|
+
maxTokens: 2000,
|
|
440
|
+
stopSequences: ["\n\n", "END"],
|
|
441
|
+
reasoning: {
|
|
442
|
+
enabled: true,
|
|
443
|
+
effort: "high",
|
|
444
|
+
maxTokens: 5000
|
|
445
|
+
},
|
|
446
|
+
thinkingExtraction: {
|
|
447
|
+
enabled: false
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
it('should only parse META at the beginning of the template', () => {
|
|
452
|
+
const template = `<USER>Some content</USER>
|
|
453
|
+
<META>
|
|
454
|
+
{
|
|
455
|
+
"settings": { "temperature": 0.5 }
|
|
456
|
+
}
|
|
457
|
+
</META>
|
|
458
|
+
<USER>More content</USER>`;
|
|
459
|
+
const result = (0, parser_1.parseTemplateWithMetadata)(template);
|
|
460
|
+
// META block not at start, so it's not parsed
|
|
461
|
+
expect(result.metadata).toEqual({ settings: {} });
|
|
462
|
+
expect(result.content).toBe(template);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
@@ -12,9 +12,17 @@
|
|
|
12
12
|
* - Simple variable substitution: {{ variableName }}
|
|
13
13
|
* - Conditional rendering: {{ condition ? `true result` : `false result` }}
|
|
14
14
|
* - Conditional with only true branch: {{ condition ? `true result` }}
|
|
15
|
+
* - Logical operators in conditions:
|
|
16
|
+
* - NOT: {{ !isDisabled ? `enabled` : `disabled` }}
|
|
17
|
+
* - AND: {{ hasPermission && isActive ? `show` : `hide` }}
|
|
18
|
+
* - OR: {{ isAdmin || isOwner ? `allow` : `deny` }}
|
|
19
|
+
* - Combined: {{ !isDraft && isPublished ? `public` : `private` }}
|
|
15
20
|
* - Multi-line strings in backticks
|
|
16
21
|
* - Intelligent newline handling (removes empty lines when result is empty)
|
|
17
22
|
*
|
|
23
|
+
* Note: Logical operators support up to 2 operands. Complex expressions
|
|
24
|
+
* (parentheses, mixing && and ||, or 3+ operands) are not supported.
|
|
25
|
+
*
|
|
18
26
|
* @param template The template string containing placeholders
|
|
19
27
|
* @param variables Object containing variable values
|
|
20
28
|
* @returns The rendered template string
|
|
@@ -15,9 +15,17 @@ exports.renderTemplate = renderTemplate;
|
|
|
15
15
|
* - Simple variable substitution: {{ variableName }}
|
|
16
16
|
* - Conditional rendering: {{ condition ? `true result` : `false result` }}
|
|
17
17
|
* - Conditional with only true branch: {{ condition ? `true result` }}
|
|
18
|
+
* - Logical operators in conditions:
|
|
19
|
+
* - NOT: {{ !isDisabled ? `enabled` : `disabled` }}
|
|
20
|
+
* - AND: {{ hasPermission && isActive ? `show` : `hide` }}
|
|
21
|
+
* - OR: {{ isAdmin || isOwner ? `allow` : `deny` }}
|
|
22
|
+
* - Combined: {{ !isDraft && isPublished ? `public` : `private` }}
|
|
18
23
|
* - Multi-line strings in backticks
|
|
19
24
|
* - Intelligent newline handling (removes empty lines when result is empty)
|
|
20
25
|
*
|
|
26
|
+
* Note: Logical operators support up to 2 operands. Complex expressions
|
|
27
|
+
* (parentheses, mixing && and ||, or 3+ operands) are not supported.
|
|
28
|
+
*
|
|
21
29
|
* @param template The template string containing placeholders
|
|
22
30
|
* @param variables Object containing variable values
|
|
23
31
|
* @returns The rendered template string
|
|
@@ -87,6 +95,53 @@ function renderTemplate(template, variables) {
|
|
|
87
95
|
}
|
|
88
96
|
return result;
|
|
89
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Evaluates a condition string that may contain logical operators (&&, ||, !)
|
|
100
|
+
* Supports:
|
|
101
|
+
* - Simple variable: varName
|
|
102
|
+
* - Negation: !varName
|
|
103
|
+
* - AND: varName1 && varName2, !varName1 && varName2
|
|
104
|
+
* - OR: varName1 || varName2, !varName1 || varName2
|
|
105
|
+
*
|
|
106
|
+
* Does NOT support:
|
|
107
|
+
* - Parentheses for grouping
|
|
108
|
+
* - Mixing && and || in same expression
|
|
109
|
+
* - More than 2 operands
|
|
110
|
+
*/
|
|
111
|
+
function evaluateCondition(condition, variables) {
|
|
112
|
+
condition = condition.trim();
|
|
113
|
+
// Check for AND operator
|
|
114
|
+
if (condition.includes('&&')) {
|
|
115
|
+
const parts = condition.split('&&').map(p => p.trim());
|
|
116
|
+
if (parts.length !== 2) {
|
|
117
|
+
// Fallback to simple variable lookup for complex expressions
|
|
118
|
+
return !!variables[condition];
|
|
119
|
+
}
|
|
120
|
+
return evaluateSimpleCondition(parts[0], variables) && evaluateSimpleCondition(parts[1], variables);
|
|
121
|
+
}
|
|
122
|
+
// Check for OR operator
|
|
123
|
+
if (condition.includes('||')) {
|
|
124
|
+
const parts = condition.split('||').map(p => p.trim());
|
|
125
|
+
if (parts.length !== 2) {
|
|
126
|
+
// Fallback to simple variable lookup for complex expressions
|
|
127
|
+
return !!variables[condition];
|
|
128
|
+
}
|
|
129
|
+
return evaluateSimpleCondition(parts[0], variables) || evaluateSimpleCondition(parts[1], variables);
|
|
130
|
+
}
|
|
131
|
+
// Simple condition (possibly with negation)
|
|
132
|
+
return evaluateSimpleCondition(condition, variables);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Evaluates a simple condition that may have a ! prefix
|
|
136
|
+
*/
|
|
137
|
+
function evaluateSimpleCondition(condition, variables) {
|
|
138
|
+
condition = condition.trim();
|
|
139
|
+
if (condition.startsWith('!')) {
|
|
140
|
+
const varName = condition.substring(1).trim();
|
|
141
|
+
return !variables[varName];
|
|
142
|
+
}
|
|
143
|
+
return !!variables[condition];
|
|
144
|
+
}
|
|
90
145
|
function processExpression(expression, variables, leadingNewline, trailingNewline) {
|
|
91
146
|
const conditionalMarkerIndex = expression.indexOf('?');
|
|
92
147
|
let result;
|
|
@@ -104,7 +159,7 @@ function processExpression(expression, variables, leadingNewline, trailingNewlin
|
|
|
104
159
|
}
|
|
105
160
|
else {
|
|
106
161
|
// --- Conditional 'ternary' substitution ---
|
|
107
|
-
const
|
|
162
|
+
const conditionStr = expression.substring(0, conditionalMarkerIndex).trim();
|
|
108
163
|
const rest = expression.substring(conditionalMarkerIndex + 1).trim();
|
|
109
164
|
// Parse ternary expression with backtick-delimited strings
|
|
110
165
|
// We need to handle nested {{ }} within backticks
|
|
@@ -165,8 +220,31 @@ function processExpression(expression, variables, leadingNewline, trailingNewlin
|
|
|
165
220
|
}
|
|
166
221
|
}
|
|
167
222
|
else {
|
|
168
|
-
// Fallback to
|
|
169
|
-
|
|
223
|
+
// Fallback to quote-based parsing for backward compatibility
|
|
224
|
+
// Need to find the ':' that separates true/false parts, not one inside quotes
|
|
225
|
+
let elseMarkerIndex = -1;
|
|
226
|
+
let inSingleQuote = false;
|
|
227
|
+
let inDoubleQuote = false;
|
|
228
|
+
for (let i = 0; i < rest.length; i++) {
|
|
229
|
+
const char = rest[i];
|
|
230
|
+
const prevChar = i > 0 ? rest[i - 1] : '';
|
|
231
|
+
// Track quote state (ignoring escaped quotes)
|
|
232
|
+
if (char === "'" && prevChar !== '\\') {
|
|
233
|
+
if (!inDoubleQuote) {
|
|
234
|
+
inSingleQuote = !inSingleQuote;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else if (char === '"' && prevChar !== '\\') {
|
|
238
|
+
if (!inSingleQuote) {
|
|
239
|
+
inDoubleQuote = !inDoubleQuote;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else if (char === ':' && !inSingleQuote && !inDoubleQuote) {
|
|
243
|
+
// Found the separator outside of quotes
|
|
244
|
+
elseMarkerIndex = i;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
170
248
|
if (elseMarkerIndex === -1) {
|
|
171
249
|
trueText = rest;
|
|
172
250
|
}
|
|
@@ -176,15 +254,20 @@ function processExpression(expression, variables, leadingNewline, trailingNewlin
|
|
|
176
254
|
}
|
|
177
255
|
// Remove quotes from the start and end of the text parts
|
|
178
256
|
const unquote = (text) => {
|
|
179
|
-
if (
|
|
180
|
-
|
|
257
|
+
if (text.startsWith('"') && text.endsWith('"')) {
|
|
258
|
+
// Remove the outer double quotes and unescape inner quotes
|
|
259
|
+
return text.slice(1, -1).replace(/\\"/g, '"');
|
|
260
|
+
}
|
|
261
|
+
else if (text.startsWith("'") && text.endsWith("'")) {
|
|
262
|
+
// Remove the outer single quotes and unescape inner quotes
|
|
263
|
+
return text.slice(1, -1).replace(/\\'/g, "'");
|
|
181
264
|
}
|
|
182
265
|
return text;
|
|
183
266
|
};
|
|
184
267
|
trueText = unquote(trueText);
|
|
185
268
|
falseText = unquote(falseText);
|
|
186
269
|
}
|
|
187
|
-
const conditionValue =
|
|
270
|
+
const conditionValue = evaluateCondition(conditionStr, variables);
|
|
188
271
|
result = conditionValue ? trueText : falseText;
|
|
189
272
|
// Recursively process the result to handle nested variables
|
|
190
273
|
if (result.includes('{{')) {
|
|
@@ -131,4 +131,120 @@ End
|
|
|
131
131
|
const result = (0, template_1.renderTemplate)(template, { any: 'value' });
|
|
132
132
|
expect(result).toBe('Just plain text');
|
|
133
133
|
});
|
|
134
|
+
describe('colon handling in quoted strings', () => {
|
|
135
|
+
it('should handle colon in double-quoted true branch', () => {
|
|
136
|
+
const template = '{{ isDraft ? "Note: This is a draft" : "Published" }}';
|
|
137
|
+
expect((0, template_1.renderTemplate)(template, { isDraft: true })).toBe('Note: This is a draft');
|
|
138
|
+
expect((0, template_1.renderTemplate)(template, { isDraft: false })).toBe('Published');
|
|
139
|
+
});
|
|
140
|
+
it('should handle colon in single-quoted true branch', () => {
|
|
141
|
+
const template = "{{ isDraft ? 'Note: This is a draft' : 'Published' }}";
|
|
142
|
+
expect((0, template_1.renderTemplate)(template, { isDraft: true })).toBe('Note: This is a draft');
|
|
143
|
+
expect((0, template_1.renderTemplate)(template, { isDraft: false })).toBe('Published');
|
|
144
|
+
});
|
|
145
|
+
it('should handle colon in both branches', () => {
|
|
146
|
+
const template = '{{ isDraft ? "Status: Draft" : "Status: Published" }}';
|
|
147
|
+
expect((0, template_1.renderTemplate)(template, { isDraft: true })).toBe('Status: Draft');
|
|
148
|
+
expect((0, template_1.renderTemplate)(template, { isDraft: false })).toBe('Status: Published');
|
|
149
|
+
});
|
|
150
|
+
it('should handle multiple colons in quoted strings', () => {
|
|
151
|
+
const template = '{{ showTime ? "Time: 10:30:45" : "Time: Not available" }}';
|
|
152
|
+
expect((0, template_1.renderTemplate)(template, { showTime: true })).toBe('Time: 10:30:45');
|
|
153
|
+
expect((0, template_1.renderTemplate)(template, { showTime: false })).toBe('Time: Not available');
|
|
154
|
+
});
|
|
155
|
+
it('should handle escaped quotes with colons', () => {
|
|
156
|
+
const template = '{{ useAdvanced ? "He said: \\"Hello\\"" : "Simple greeting" }}';
|
|
157
|
+
expect((0, template_1.renderTemplate)(template, { useAdvanced: true })).toBe('He said: "Hello"');
|
|
158
|
+
});
|
|
159
|
+
it('should handle mixed quote types', () => {
|
|
160
|
+
const template = `{{ showBoth ? "Bob's message: 'Hi there'" : 'Alice said: "Hello"' }}`;
|
|
161
|
+
expect((0, template_1.renderTemplate)(template, { showBoth: true })).toBe("Bob's message: 'Hi there'");
|
|
162
|
+
expect((0, template_1.renderTemplate)(template, { showBoth: false })).toBe('Alice said: "Hello"');
|
|
163
|
+
});
|
|
164
|
+
it('should handle URL-like strings with colons', () => {
|
|
165
|
+
const template = '{{ useHttps ? "https://example.com" : "http://example.com" }}';
|
|
166
|
+
expect((0, template_1.renderTemplate)(template, { useHttps: true })).toBe('https://example.com');
|
|
167
|
+
expect((0, template_1.renderTemplate)(template, { useHttps: false })).toBe('http://example.com');
|
|
168
|
+
});
|
|
169
|
+
it('should work with backtick syntax (regression test)', () => {
|
|
170
|
+
const template = '{{ isDraft ? `Note: This is a draft` : `Status: Published` }}';
|
|
171
|
+
expect((0, template_1.renderTemplate)(template, { isDraft: true })).toBe('Note: This is a draft');
|
|
172
|
+
expect((0, template_1.renderTemplate)(template, { isDraft: false })).toBe('Status: Published');
|
|
173
|
+
});
|
|
174
|
+
it('should handle no false branch with colon in true branch', () => {
|
|
175
|
+
const template = '{{ showNote ? "Note: Important information" }}';
|
|
176
|
+
expect((0, template_1.renderTemplate)(template, { showNote: true })).toBe('Note: Important information');
|
|
177
|
+
expect((0, template_1.renderTemplate)(template, { showNote: false })).toBe('');
|
|
178
|
+
});
|
|
179
|
+
it('should handle colons with logical operators', () => {
|
|
180
|
+
const template = '{{ isActive && hasAccess ? "Status: Active" : "Status: Inactive" }}';
|
|
181
|
+
expect((0, template_1.renderTemplate)(template, { isActive: true, hasAccess: true })).toBe('Status: Active');
|
|
182
|
+
expect((0, template_1.renderTemplate)(template, { isActive: false, hasAccess: true })).toBe('Status: Inactive');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe('logical operators in conditions', () => {
|
|
186
|
+
it('should handle NOT operator (!)', () => {
|
|
187
|
+
const template = '{{ !isDisabled ? `Enabled` : `Disabled` }}';
|
|
188
|
+
expect((0, template_1.renderTemplate)(template, { isDisabled: false })).toBe('Enabled');
|
|
189
|
+
expect((0, template_1.renderTemplate)(template, { isDisabled: true })).toBe('Disabled');
|
|
190
|
+
});
|
|
191
|
+
it('should handle AND operator (&&)', () => {
|
|
192
|
+
const template = '{{ hasPermission && isActive ? `Show button` : `Hide button` }}';
|
|
193
|
+
expect((0, template_1.renderTemplate)(template, { hasPermission: true, isActive: true })).toBe('Show button');
|
|
194
|
+
expect((0, template_1.renderTemplate)(template, { hasPermission: true, isActive: false })).toBe('Hide button');
|
|
195
|
+
expect((0, template_1.renderTemplate)(template, { hasPermission: false, isActive: true })).toBe('Hide button');
|
|
196
|
+
expect((0, template_1.renderTemplate)(template, { hasPermission: false, isActive: false })).toBe('Hide button');
|
|
197
|
+
});
|
|
198
|
+
it('should handle OR operator (||)', () => {
|
|
199
|
+
const template = '{{ isAdmin || isOwner ? `Has access` : `No access` }}';
|
|
200
|
+
expect((0, template_1.renderTemplate)(template, { isAdmin: true, isOwner: false })).toBe('Has access');
|
|
201
|
+
expect((0, template_1.renderTemplate)(template, { isAdmin: false, isOwner: true })).toBe('Has access');
|
|
202
|
+
expect((0, template_1.renderTemplate)(template, { isAdmin: true, isOwner: true })).toBe('Has access');
|
|
203
|
+
expect((0, template_1.renderTemplate)(template, { isAdmin: false, isOwner: false })).toBe('No access');
|
|
204
|
+
});
|
|
205
|
+
it('should handle NOT with AND', () => {
|
|
206
|
+
const template = '{{ !isDraft && isPublished ? `Show public` : `Hide` }}';
|
|
207
|
+
expect((0, template_1.renderTemplate)(template, { isDraft: false, isPublished: true })).toBe('Show public');
|
|
208
|
+
expect((0, template_1.renderTemplate)(template, { isDraft: true, isPublished: true })).toBe('Hide');
|
|
209
|
+
});
|
|
210
|
+
it('should handle NOT with OR', () => {
|
|
211
|
+
const template = '{{ !isBlocked || isAdmin ? `Allow` : `Deny` }}';
|
|
212
|
+
expect((0, template_1.renderTemplate)(template, { isBlocked: false, isAdmin: false })).toBe('Allow');
|
|
213
|
+
expect((0, template_1.renderTemplate)(template, { isBlocked: true, isAdmin: true })).toBe('Allow');
|
|
214
|
+
expect((0, template_1.renderTemplate)(template, { isBlocked: true, isAdmin: false })).toBe('Deny');
|
|
215
|
+
});
|
|
216
|
+
it('should handle NOT on both sides of AND', () => {
|
|
217
|
+
const template = '{{ !isLoading && !hasError ? `Ready` : `Not ready` }}';
|
|
218
|
+
expect((0, template_1.renderTemplate)(template, { isLoading: false, hasError: false })).toBe('Ready');
|
|
219
|
+
expect((0, template_1.renderTemplate)(template, { isLoading: true, hasError: false })).toBe('Not ready');
|
|
220
|
+
expect((0, template_1.renderTemplate)(template, { isLoading: false, hasError: true })).toBe('Not ready');
|
|
221
|
+
});
|
|
222
|
+
it('should handle whitespace around operators', () => {
|
|
223
|
+
const template = '{{ !isDisabled && isVisible ? `Show` : `Hide` }}';
|
|
224
|
+
expect((0, template_1.renderTemplate)(template, { isDisabled: false, isVisible: true })).toBe('Show');
|
|
225
|
+
});
|
|
226
|
+
it('should handle undefined variables in logical expressions', () => {
|
|
227
|
+
const template = '{{ undefinedVar && definedVar ? `Both true` : `At least one false` }}';
|
|
228
|
+
expect((0, template_1.renderTemplate)(template, { definedVar: true })).toBe('At least one false');
|
|
229
|
+
});
|
|
230
|
+
it('should handle falsy values in logical expressions', () => {
|
|
231
|
+
const template = '{{ nullVar || zero ? `Has truthy` : `All falsy` }}';
|
|
232
|
+
expect((0, template_1.renderTemplate)(template, { nullVar: null, zero: 0 })).toBe('All falsy');
|
|
233
|
+
const template2 = '{{ emptyString || text ? `Has truthy` : `All falsy` }}';
|
|
234
|
+
expect((0, template_1.renderTemplate)(template2, { emptyString: '', text: 'hello' })).toBe('Has truthy');
|
|
235
|
+
});
|
|
236
|
+
it('should fallback to simple lookup for complex expressions', () => {
|
|
237
|
+
// More than 2 operands should fallback
|
|
238
|
+
const template = '{{ a && b && c ? `True` : `False` }}';
|
|
239
|
+
const result = (0, template_1.renderTemplate)(template, { 'a && b && c': true });
|
|
240
|
+
expect(result).toBe('True');
|
|
241
|
+
});
|
|
242
|
+
it('should handle nested templates with logical operators', () => {
|
|
243
|
+
const template = '{{ showDetails && hasData ? `Details: {{data}}` : `No details` }}';
|
|
244
|
+
expect((0, template_1.renderTemplate)(template, { showDetails: true, hasData: true, data: 'Important info' }))
|
|
245
|
+
.toBe('Details: Important info');
|
|
246
|
+
expect((0, template_1.renderTemplate)(template, { showDetails: false, hasData: true, data: 'Important info' }))
|
|
247
|
+
.toBe('No details');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
134
250
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genai-lite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A lightweight, portable toolkit for interacting with various Generative AI APIs.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"build": "tsc",
|
|
39
39
|
"test": "jest --coverage",
|
|
40
40
|
"test:watch": "jest --watch",
|
|
41
|
-
"test:e2e": "npm run build && jest --config jest.e2e.config.js"
|
|
41
|
+
"test:e2e": "npm run build && jest --config jest.e2e.config.js",
|
|
42
|
+
"test:e2e:reasoning": "npm run build && jest --config jest.e2e.config.js reasoning.e2e.test.ts"
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
44
45
|
"@anthropic-ai/sdk": "^0.56.0",
|