genai-lite 0.1.3 → 0.1.4

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 CHANGED
@@ -11,6 +11,7 @@ A lightweight, portable Node.js/TypeScript library providing a unified interface
11
11
  - ⚡ **Lightweight** - Minimal dependencies, focused functionality
12
12
  - 🛡️ **Provider Normalization** - Consistent responses across different AI APIs
13
13
  - 🎨 **Configurable Model Presets** - Built-in presets with full customization options
14
+ - 🎭 **Template Engine** - Sophisticated templating with conditionals and variable substitution
14
15
 
15
16
  ## Installation
16
17
 
@@ -388,6 +389,108 @@ const response = await llm.sendMessage({
388
389
  });
389
390
  ```
390
391
 
392
+ ### Template Engine
393
+
394
+ Generate dynamic prompts and content using the built-in template engine that supports variable substitution and conditional logic:
395
+
396
+ ```typescript
397
+ import { renderTemplate } from 'genai-lite/utils';
398
+
399
+ // Simple variable substitution
400
+ const greeting = renderTemplate('Hello, {{ name }}!', { name: 'World' });
401
+ // Result: "Hello, World!"
402
+
403
+ // Conditional rendering with ternary syntax
404
+ const prompt = renderTemplate(
405
+ 'Analyze this {{ language }} code:\n{{ hasContext ? `Context: {{context}}\n` : `` }}```\n{{ code }}\n```',
406
+ {
407
+ language: 'TypeScript',
408
+ hasContext: true,
409
+ context: 'React component for user authentication',
410
+ code: 'export const Login = () => { ... }'
411
+ }
412
+ );
413
+ // Result includes the context line when hasContext is true
414
+
415
+ // Complex template with multiple conditionals
416
+ const complexTemplate = `
417
+ System: You are a {{ role }} assistant.
418
+ {{ hasExpertise ? `Expertise: {{expertise}}` : `General knowledge assistant` }}
419
+
420
+ Task: {{ task }}
421
+ {{ hasFiles ? `
422
+ Files to analyze:
423
+ {{ fileList }}` : `` }}
424
+ {{ requiresOutput ? `
425
+ Expected output format:
426
+ {{ outputFormat }}` : `` }}
427
+ `;
428
+
429
+ const result = renderTemplate(complexTemplate, {
430
+ role: 'coding',
431
+ hasExpertise: true,
432
+ expertise: 'TypeScript, React, Node.js',
433
+ task: 'Review the code for best practices',
434
+ hasFiles: true,
435
+ fileList: '- src/index.ts\n- src/utils.ts',
436
+ requiresOutput: false
437
+ });
438
+ ```
439
+
440
+ Template syntax supports:
441
+ - **Simple substitution**: `{{ variableName }}`
442
+ - **Ternary conditionals**: `{{ condition ? `true result` : `false result` }}`
443
+ - **Nested variables**: `{{ show ? `Name: {{name}}` : `Anonymous` }}`
444
+ - **Multi-line strings**: Use backticks to preserve formatting
445
+ - **Intelligent newline handling**: Empty results remove trailing newlines
446
+
447
+ ### Example: Building Dynamic LLM Prompts
448
+
449
+ Combine the template engine with other utilities for powerful prompt generation:
450
+
451
+ ```typescript
452
+ import { LLMService, fromEnvironment } from 'genai-lite';
453
+ import { renderTemplate, countTokens } from 'genai-lite/utils';
454
+
455
+ const llm = new LLMService(fromEnvironment);
456
+
457
+ // Define a reusable prompt template
458
+ const codeReviewTemplate = `
459
+ You are an expert {{ language }} developer.
460
+
461
+ {{ hasGuidelines ? `Follow these coding guidelines:
462
+ {{ guidelines }}
463
+
464
+ ` : `` }}Review the following code:
465
+ \`\`\`{{ language }}
466
+ {{ code }}
467
+ \`\`\`
468
+
469
+ {{ hasFocus ? `Focus on: {{ focusAreas }}` : `Provide a comprehensive review covering all aspects.` }}
470
+ `;
471
+
472
+ // Render the prompt with specific values
473
+ const prompt = renderTemplate(codeReviewTemplate, {
474
+ language: 'TypeScript',
475
+ hasGuidelines: true,
476
+ guidelines: '- Use functional components\n- Prefer composition over inheritance',
477
+ code: sourceCode,
478
+ hasFocus: true,
479
+ focusAreas: 'performance optimizations and error handling'
480
+ });
481
+
482
+ // Check token count before sending
483
+ const tokenCount = countTokens(prompt, 'gpt-4.1-mini');
484
+ console.log(`Prompt uses ${tokenCount} tokens`);
485
+
486
+ // Send to LLM
487
+ const response = await llm.sendMessage({
488
+ providerId: 'openai',
489
+ modelId: 'gpt-4.1-mini',
490
+ messages: [{ role: 'user', content: prompt }]
491
+ });
492
+ ```
493
+
391
494
  ## Contributing
392
495
 
393
496
  Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
package/dist/index.d.ts CHANGED
@@ -5,3 +5,4 @@ export type { ModelPreset } from "./types/presets";
5
5
  export * from "./llm/types";
6
6
  export * from "./llm/clients/types";
7
7
  export { fromEnvironment } from "./providers/fromEnvironment";
8
+ export { renderTemplate } from "./utils/templateEngine";
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.fromEnvironment = exports.LLMService = void 0;
17
+ exports.renderTemplate = exports.fromEnvironment = exports.LLMService = void 0;
18
18
  // --- LLM Service ---
19
19
  var LLMService_1 = require("./llm/LLMService");
20
20
  Object.defineProperty(exports, "LLMService", { enumerable: true, get: function () { return LLMService_1.LLMService; } });
@@ -25,3 +25,6 @@ __exportStar(require("./llm/clients/types"), exports);
25
25
  // --- API Key Providers ---
26
26
  var fromEnvironment_1 = require("./providers/fromEnvironment");
27
27
  Object.defineProperty(exports, "fromEnvironment", { enumerable: true, get: function () { return fromEnvironment_1.fromEnvironment; } });
28
+ // --- Utilities ---
29
+ var templateEngine_1 = require("./utils/templateEngine");
30
+ Object.defineProperty(exports, "renderTemplate", { enumerable: true, get: function () { return templateEngine_1.renderTemplate; } });
@@ -1 +1,2 @@
1
1
  export * from './prompt';
2
+ export * from './templateEngine';
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./prompt"), exports);
18
+ __exportStar(require("./templateEngine"), exports);
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Renders a template string by substituting variables and evaluating conditional expressions.
3
+ *
4
+ * Supports:
5
+ * - Simple variable substitution: {{ variableName }}
6
+ * - Conditional rendering: {{ condition ? `true result` : `false result` }}
7
+ * - Conditional with only true branch: {{ condition ? `true result` }}
8
+ * - Multi-line strings in backticks
9
+ * - Intelligent newline handling (removes empty lines when result is empty)
10
+ *
11
+ * @param template The template string containing placeholders
12
+ * @param variables Object containing variable values
13
+ * @returns The rendered template string
14
+ */
15
+ export declare function renderTemplate(template: string, variables: Record<string, any>): string;
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderTemplate = renderTemplate;
4
+ /**
5
+ * Renders a template string by substituting variables and evaluating conditional expressions.
6
+ *
7
+ * Supports:
8
+ * - Simple variable substitution: {{ variableName }}
9
+ * - Conditional rendering: {{ condition ? `true result` : `false result` }}
10
+ * - Conditional with only true branch: {{ condition ? `true result` }}
11
+ * - Multi-line strings in backticks
12
+ * - Intelligent newline handling (removes empty lines when result is empty)
13
+ *
14
+ * @param template The template string containing placeholders
15
+ * @param variables Object containing variable values
16
+ * @returns The rendered template string
17
+ */
18
+ function renderTemplate(template, variables) {
19
+ // We need to handle nested {{ }} properly, so we'll use a custom approach
20
+ let result = '';
21
+ let lastIndex = 0;
22
+ while (lastIndex < template.length) {
23
+ // Find the next {{
24
+ const startIndex = template.indexOf('{{', lastIndex);
25
+ if (startIndex === -1) {
26
+ // No more placeholders
27
+ result += template.substring(lastIndex);
28
+ break;
29
+ }
30
+ // Add everything before the placeholder
31
+ result += template.substring(lastIndex, startIndex);
32
+ // Check for leading newline
33
+ const leadingNewline = startIndex > 0 && template[startIndex - 1] === '\n' ? '\n' : '';
34
+ // Find the matching }}
35
+ let depth = 1;
36
+ let i = startIndex + 2;
37
+ let inBacktick = false;
38
+ let expression = '';
39
+ while (i < template.length && depth > 0) {
40
+ if (template[i] === '`' && (i === startIndex + 2 || template[i - 1] !== '\\')) {
41
+ inBacktick = !inBacktick;
42
+ }
43
+ else if (!inBacktick) {
44
+ if (template[i] === '{' && i + 1 < template.length && template[i + 1] === '{') {
45
+ depth++;
46
+ i++; // Skip the second {
47
+ }
48
+ else if (template[i] === '}' && i + 1 < template.length && template[i + 1] === '}') {
49
+ depth--;
50
+ if (depth === 0) {
51
+ expression = template.substring(startIndex + 2, i);
52
+ i++; // Skip the second }
53
+ break;
54
+ }
55
+ i++; // Skip the second }
56
+ }
57
+ }
58
+ i++;
59
+ }
60
+ if (depth > 0) {
61
+ // Unmatched {{, treat as literal
62
+ result += template.substring(startIndex, i);
63
+ lastIndex = i;
64
+ continue;
65
+ }
66
+ // Check for trailing newline
67
+ const nextCharIndex = i + 1;
68
+ const trailingNewline = nextCharIndex < template.length && template[nextCharIndex] === '\n' ? '\n' : '';
69
+ // Process the expression
70
+ const processedResult = processExpression(expression.trim(), variables, leadingNewline, trailingNewline);
71
+ // Add the processed result (which already includes newlines when appropriate)
72
+ result += processedResult;
73
+ // Update lastIndex, skipping the trailing newline if the result was empty
74
+ if (processedResult === '' && trailingNewline) {
75
+ lastIndex = nextCharIndex + 1;
76
+ }
77
+ else {
78
+ lastIndex = nextCharIndex;
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+ function processExpression(expression, variables, leadingNewline, trailingNewline) {
84
+ const conditionalMarkerIndex = expression.indexOf('?');
85
+ let result;
86
+ if (conditionalMarkerIndex === -1) {
87
+ // --- Simple variable substitution (backward compatible) ---
88
+ const key = expression;
89
+ const value = variables[key];
90
+ // Handle task context specially - only include if it exists
91
+ if (key === 'task_context' && (!value || (typeof value === 'string' && value.trim() === ''))) {
92
+ result = '';
93
+ }
94
+ else {
95
+ result = value !== undefined ? String(value) : '';
96
+ }
97
+ }
98
+ else {
99
+ // --- Conditional 'ternary' substitution ---
100
+ const conditionKey = expression.substring(0, conditionalMarkerIndex).trim();
101
+ const rest = expression.substring(conditionalMarkerIndex + 1).trim();
102
+ // Parse ternary expression with backtick-delimited strings
103
+ // We need to handle nested {{ }} within backticks
104
+ let trueText;
105
+ let falseText = ''; // Default to empty string if no 'else' part
106
+ // Try to match backtick format with better handling of nested content
107
+ if (rest.startsWith('`')) {
108
+ // Find the matching closing backtick, accounting for escaped backticks
109
+ let i = 1;
110
+ let depth = 1;
111
+ while (i < rest.length && depth > 0) {
112
+ if (rest[i] === '\\' && i + 1 < rest.length && rest[i + 1] === '`') {
113
+ i += 2; // Skip escaped backtick
114
+ }
115
+ else if (rest[i] === '`') {
116
+ depth--;
117
+ i++;
118
+ }
119
+ else {
120
+ i++;
121
+ }
122
+ }
123
+ if (depth === 0) {
124
+ // Found matching backtick
125
+ trueText = rest.substring(1, i - 1);
126
+ const afterTrue = rest.substring(i).trim();
127
+ // Check for false part
128
+ if (afterTrue.startsWith(':')) {
129
+ const falsePart = afterTrue.substring(1).trim();
130
+ if (falsePart.startsWith('`')) {
131
+ // Find matching backtick for false part
132
+ let j = 1;
133
+ let falseDepth = 1;
134
+ while (j < falsePart.length && falseDepth > 0) {
135
+ if (falsePart[j] === '\\' && j + 1 < falsePart.length && falsePart[j + 1] === '`') {
136
+ j += 2;
137
+ }
138
+ else if (falsePart[j] === '`') {
139
+ falseDepth--;
140
+ j++;
141
+ }
142
+ else {
143
+ j++;
144
+ }
145
+ }
146
+ if (falseDepth === 0) {
147
+ falseText = falsePart.substring(1, j - 1);
148
+ }
149
+ }
150
+ }
151
+ // Unescape any escaped backticks
152
+ trueText = trueText.replace(/\\`/g, '`');
153
+ falseText = falseText.replace(/\\`/g, '`');
154
+ }
155
+ else {
156
+ // No matching backtick found
157
+ trueText = rest;
158
+ }
159
+ }
160
+ else {
161
+ // Fallback to old quote-based parsing for backward compatibility
162
+ const elseMarkerIndex = rest.indexOf(':');
163
+ if (elseMarkerIndex === -1) {
164
+ trueText = rest;
165
+ }
166
+ else {
167
+ trueText = rest.substring(0, elseMarkerIndex).trim();
168
+ falseText = rest.substring(elseMarkerIndex + 1).trim();
169
+ }
170
+ // Remove quotes from the start and end of the text parts
171
+ const unquote = (text) => {
172
+ if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
173
+ return text.slice(1, -1);
174
+ }
175
+ return text;
176
+ };
177
+ trueText = unquote(trueText);
178
+ falseText = unquote(falseText);
179
+ }
180
+ const conditionValue = !!variables[conditionKey]; // Evaluate truthiness
181
+ result = conditionValue ? trueText : falseText;
182
+ // Recursively process the result to handle nested variables
183
+ if (result.includes('{{')) {
184
+ result = renderTemplate(result, variables);
185
+ }
186
+ }
187
+ // If result is empty, return empty string without newlines
188
+ if (result === '') {
189
+ return '';
190
+ }
191
+ // If result is not empty, return just the result without the captured newlines
192
+ // The newlines are already part of the template structure
193
+ return result;
194
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const templateEngine_1 = require("./templateEngine");
4
+ describe('renderTemplate', () => {
5
+ it('should handle simple variable substitution', () => {
6
+ const template = 'Hello, {{ name }}!';
7
+ const result = (0, templateEngine_1.renderTemplate)(template, { name: 'World' });
8
+ expect(result).toBe('Hello, World!');
9
+ });
10
+ it('should handle intelligent newline trimming for empty results', () => {
11
+ const template = 'Start\n{{ showDetails ? `Details here` : `` }}\nEnd';
12
+ const result = (0, templateEngine_1.renderTemplate)(template, { showDetails: false });
13
+ expect(result).toBe('Start\nEnd');
14
+ });
15
+ it('should preserve newlines for non-empty results', () => {
16
+ const template = 'Start\n{{ showDetails ? `Details here` : `` }}\nEnd';
17
+ const result = (0, templateEngine_1.renderTemplate)(template, { showDetails: true });
18
+ expect(result).toBe('Start\nDetails here\nEnd');
19
+ });
20
+ it('should correctly parse ternary logic with backtick-delimited strings', () => {
21
+ const template = '{{ condition ? `A` : `B` }}';
22
+ expect((0, templateEngine_1.renderTemplate)(template, { condition: true })).toBe('A');
23
+ expect((0, templateEngine_1.renderTemplate)(template, { condition: false })).toBe('B');
24
+ });
25
+ it('should handle multi-line strings in backticks', () => {
26
+ const template = '{{ condition ? `Line 1\nLine 2` : `Single Line` }}';
27
+ const result = (0, templateEngine_1.renderTemplate)(template, { condition: true });
28
+ expect(result).toBe('Line 1\nLine 2');
29
+ });
30
+ it('should handle ternary expressions with only a true part', () => {
31
+ const template = 'Data: {{ data ? `{{data}}` : `` }}';
32
+ expect((0, templateEngine_1.renderTemplate)(template, { data: 'Exists' })).toBe('Data: Exists');
33
+ expect((0, templateEngine_1.renderTemplate)(template, { data: false })).toBe('Data: ');
34
+ });
35
+ it('should handle escaped backticks within the template strings', () => {
36
+ const template = '{{ flag ? `This is a \\`backtick\\`` : `` }}';
37
+ const result = (0, templateEngine_1.renderTemplate)(template, { flag: true });
38
+ expect(result).toBe('This is a `backtick`');
39
+ });
40
+ it('should handle undefined variables', () => {
41
+ const template = 'Value: {{ undefinedVar }}';
42
+ const result = (0, templateEngine_1.renderTemplate)(template, {});
43
+ expect(result).toBe('Value: ');
44
+ });
45
+ it('should handle boolean values', () => {
46
+ const template = 'Boolean: {{ boolValue }}';
47
+ expect((0, templateEngine_1.renderTemplate)(template, { boolValue: true })).toBe('Boolean: true');
48
+ expect((0, templateEngine_1.renderTemplate)(template, { boolValue: false })).toBe('Boolean: false');
49
+ });
50
+ it('should handle numeric values', () => {
51
+ const template = 'Number: {{ numValue }}';
52
+ expect((0, templateEngine_1.renderTemplate)(template, { numValue: 42 })).toBe('Number: 42');
53
+ expect((0, templateEngine_1.renderTemplate)(template, { numValue: 0 })).toBe('Number: 0');
54
+ });
55
+ it('should handle nested variable substitution in ternary true branch', () => {
56
+ const template = '{{ showName ? `Hello, {{name}}!` : `Hello, stranger!` }}';
57
+ const result = (0, templateEngine_1.renderTemplate)(template, { showName: true, name: 'Alice' });
58
+ expect(result).toBe('Hello, Alice!');
59
+ });
60
+ it('should handle nested variable substitution in ternary false branch', () => {
61
+ const template = '{{ showName ? `Hello, {{name}}!` : `Hello, {{fallback}}!` }}';
62
+ const result = (0, templateEngine_1.renderTemplate)(template, { showName: false, fallback: 'Guest' });
63
+ expect(result).toBe('Hello, Guest!');
64
+ });
65
+ it('should handle multiple variable substitutions', () => {
66
+ const template = '{{ greeting }}, {{ name }}! {{ farewell }}';
67
+ const result = (0, templateEngine_1.renderTemplate)(template, {
68
+ greeting: 'Hello',
69
+ name: 'World',
70
+ farewell: 'Goodbye'
71
+ });
72
+ expect(result).toBe('Hello, World! Goodbye');
73
+ });
74
+ it('should handle special task_context variable with empty string', () => {
75
+ const template = 'Context: {{ task_context }}';
76
+ expect((0, templateEngine_1.renderTemplate)(template, { task_context: '' })).toBe('Context: ');
77
+ expect((0, templateEngine_1.renderTemplate)(template, { task_context: ' ' })).toBe('Context: ');
78
+ expect((0, templateEngine_1.renderTemplate)(template, { task_context: 'Some context' })).toBe('Context: Some context');
79
+ });
80
+ it('should handle complex nested templates', () => {
81
+ const template = `
82
+ Project: {{ projectName }}
83
+ {{ hasFiles ? \`## Files
84
+ {{ fileList }}\` : \`No files selected\` }}
85
+ {{ showSummary ? \`
86
+ ## Summary
87
+ {{ summary }}\` : \`\` }}
88
+ End
89
+ `;
90
+ const result = (0, templateEngine_1.renderTemplate)(template, {
91
+ projectName: 'MyProject',
92
+ hasFiles: true,
93
+ fileList: '- file1.js\n- file2.js',
94
+ showSummary: false,
95
+ summary: 'This is a summary'
96
+ });
97
+ expect(result).toBe(`
98
+ Project: MyProject
99
+ ## Files
100
+ - file1.js
101
+ - file2.js
102
+ End
103
+ `);
104
+ });
105
+ it('should handle backward compatible quote-based ternary syntax', () => {
106
+ const template = '{{ condition ? "True value" : "False value" }}';
107
+ expect((0, templateEngine_1.renderTemplate)(template, { condition: true })).toBe('True value');
108
+ expect((0, templateEngine_1.renderTemplate)(template, { condition: false })).toBe('False value');
109
+ });
110
+ it('should handle single quotes in backward compatible syntax', () => {
111
+ const template = "{{ condition ? 'True value' : 'False value' }}";
112
+ expect((0, templateEngine_1.renderTemplate)(template, { condition: true })).toBe('True value');
113
+ expect((0, templateEngine_1.renderTemplate)(template, { condition: false })).toBe('False value');
114
+ });
115
+ it('should handle whitespace around expressions', () => {
116
+ const template = '{{ name }}';
117
+ const result = (0, templateEngine_1.renderTemplate)(template, { name: 'Test' });
118
+ expect(result).toBe('Test');
119
+ });
120
+ it('should handle whitespace in ternary expressions', () => {
121
+ const template = '{{ condition ? `True` : `False` }}';
122
+ expect((0, templateEngine_1.renderTemplate)(template, { condition: true })).toBe('True');
123
+ });
124
+ it('should handle empty template', () => {
125
+ const template = '';
126
+ const result = (0, templateEngine_1.renderTemplate)(template, { any: 'value' });
127
+ expect(result).toBe('');
128
+ });
129
+ it('should handle template with no placeholders', () => {
130
+ const template = 'Just plain text';
131
+ const result = (0, templateEngine_1.renderTemplate)(template, { any: 'value' });
132
+ expect(result).toBe('Just plain text');
133
+ });
134
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genai-lite",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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",