@output.ai/llm 0.2.2 → 0.2.3
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/package.json +1 -1
- package/src/load_content.js +3 -2
- package/src/parser.js +7 -6
- package/src/parser.spec.js +1 -1
- package/src/prompt_loader.js +10 -25
- package/src/prompt_loader.spec.js +86 -10
package/package.json
CHANGED
package/src/load_content.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
2
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { resolveInvocationDir } from '@output.ai/core/utils';
|
|
4
|
+
import { FatalError } from '@output.ai/core';
|
|
4
5
|
|
|
5
6
|
const scanDir = dir => {
|
|
6
7
|
try {
|
|
7
8
|
return readdirSync( dir, { withFileTypes: true } );
|
|
8
9
|
} catch ( error ) {
|
|
9
|
-
throw new
|
|
10
|
+
throw new FatalError( `Error scanning directory "${dir}"`, { cause: error } );
|
|
10
11
|
}
|
|
11
12
|
};
|
|
12
13
|
|
|
@@ -14,7 +15,7 @@ const loadFile = path => {
|
|
|
14
15
|
try {
|
|
15
16
|
return readFileSync( path, 'utf-8' );
|
|
16
17
|
} catch ( error ) {
|
|
17
|
-
throw new
|
|
18
|
+
throw new FatalError( `Error reading file "${path}"`, { cause: error } );
|
|
18
19
|
}
|
|
19
20
|
};
|
|
20
21
|
|
package/src/parser.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import matter from 'gray-matter';
|
|
2
|
+
import { FatalError } from '@output.ai/core';
|
|
2
3
|
|
|
3
4
|
export function parsePrompt( raw ) {
|
|
4
|
-
const { data, content } = matter( raw );
|
|
5
|
+
const { data: config, content } = matter( raw );
|
|
5
6
|
|
|
6
7
|
if ( !content || content.trim() === '' ) {
|
|
7
|
-
throw new
|
|
8
|
+
throw new FatalError( 'Prompt file has no content after frontmatter' );
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
const infoExtractor = /<(system|user|assistant|tool)>([\s\S]*?)<\/\1>/gm;
|
|
@@ -16,12 +17,12 @@ export function parsePrompt( raw ) {
|
|
|
16
17
|
const contentPreview = content.substring( 0, 200 );
|
|
17
18
|
const ellipsis = content.length > 200 ? '...' : '';
|
|
18
19
|
|
|
19
|
-
throw new
|
|
20
|
-
`No valid message blocks found in prompt file.
|
|
21
|
-
Expected format: <system>...</system>, <user>...</user>, etc.
|
|
20
|
+
throw new FatalError(
|
|
21
|
+
`No valid message blocks found in prompt file.
|
|
22
|
+
Expected format: <system>...</system>, <user>...</user>, etc.
|
|
22
23
|
Content preview: ${contentPreview}${ellipsis}`
|
|
23
24
|
);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
return {
|
|
27
|
+
return { config, messages };
|
|
27
28
|
}
|
package/src/parser.spec.js
CHANGED
package/src/prompt_loader.js
CHANGED
|
@@ -2,30 +2,15 @@ import { parsePrompt } from './parser.js';
|
|
|
2
2
|
import { Liquid } from 'liquidjs';
|
|
3
3
|
import { loadContent } from './load_content.js';
|
|
4
4
|
import { validatePrompt } from './prompt_validations.js';
|
|
5
|
+
import { FatalError } from '@output.ai/core';
|
|
5
6
|
|
|
6
7
|
const liquid = new Liquid();
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
* Render a single message with template variables.
|
|
10
|
-
*
|
|
11
|
-
* @param {string} role - The message role
|
|
12
|
-
* @param {string} content - The message content template
|
|
13
|
-
* @param {Record<string, string | number>} values - Variables to interpolate
|
|
14
|
-
* @param {string} promptName - Name of the prompt (for error messages)
|
|
15
|
-
* @param {number} index - Message index (for error messages)
|
|
16
|
-
* @returns {{role: string, content: string}} Rendered message object
|
|
17
|
-
*/
|
|
18
|
-
const renderMessage = ( role, content, values, promptName, index ) => {
|
|
9
|
+
const renderPrompt = ( name, content, values ) => {
|
|
19
10
|
try {
|
|
20
|
-
return
|
|
21
|
-
role,
|
|
22
|
-
content: liquid.parseAndRenderSync( content, values )
|
|
23
|
-
};
|
|
11
|
+
return liquid.parseAndRenderSync( content, values );
|
|
24
12
|
} catch ( error ) {
|
|
25
|
-
throw new
|
|
26
|
-
`Failed to render template in message ${index + 1} (role: ${role}) of prompt "${promptName}": ${error.message}`,
|
|
27
|
-
{ cause: error }
|
|
28
|
-
);
|
|
13
|
+
throw new FatalError( `Failed to render template in prompt "${name}": ${error.message}`, { cause: error } );
|
|
29
14
|
}
|
|
30
15
|
};
|
|
31
16
|
|
|
@@ -36,20 +21,20 @@ const renderMessage = ( role, content, values, promptName, index ) => {
|
|
|
36
21
|
* @param {Record<string, string | number>} [values] - Variables to interpolate
|
|
37
22
|
* @returns {Prompt} Loaded and rendered prompt object
|
|
38
23
|
*/
|
|
39
|
-
export const loadPrompt = ( name, values ) => {
|
|
24
|
+
export const loadPrompt = ( name, values = {} ) => {
|
|
40
25
|
const promptContent = loadContent( `${name}.prompt` );
|
|
41
26
|
if ( !promptContent ) {
|
|
42
|
-
throw new
|
|
27
|
+
throw new FatalError( `Prompt ${name} not found.` );
|
|
43
28
|
}
|
|
44
29
|
|
|
45
|
-
const
|
|
30
|
+
const renderedContent = renderPrompt( name, promptContent, values );
|
|
31
|
+
|
|
32
|
+
const { config, messages } = parsePrompt( renderedContent );
|
|
46
33
|
|
|
47
34
|
const prompt = {
|
|
48
35
|
name,
|
|
49
36
|
config,
|
|
50
|
-
messages
|
|
51
|
-
renderMessage( role, content, values, name, index )
|
|
52
|
-
)
|
|
37
|
+
messages
|
|
53
38
|
};
|
|
54
39
|
|
|
55
40
|
validatePrompt( prompt );
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { loadPrompt } from './prompt_loader.js';
|
|
3
3
|
|
|
4
|
-
// Mock dependencies
|
|
4
|
+
// Mock dependencies that perform I/O or validation
|
|
5
5
|
vi.mock( './load_content.js', () => ( {
|
|
6
6
|
loadContent: vi.fn()
|
|
7
7
|
} ) );
|
|
8
8
|
|
|
9
|
-
vi.mock( './parser.js', () => ( {
|
|
10
|
-
parsePrompt: vi.fn()
|
|
11
|
-
} ) );
|
|
12
|
-
|
|
13
9
|
vi.mock( './prompt_validations.js', () => ( {
|
|
14
10
|
validatePrompt: vi.fn()
|
|
15
11
|
} ) );
|
|
16
12
|
|
|
17
13
|
import { loadContent } from './load_content.js';
|
|
18
|
-
import { parsePrompt } from './parser.js';
|
|
19
14
|
import { validatePrompt } from './prompt_validations.js';
|
|
20
15
|
|
|
21
16
|
describe( 'loadPrompt', () => {
|
|
@@ -31,10 +26,6 @@ model: claude-3-5-sonnet-20241022
|
|
|
31
26
|
<user>Hello {{ name }}!</user>`;
|
|
32
27
|
|
|
33
28
|
loadContent.mockReturnValue( promptContent );
|
|
34
|
-
parsePrompt.mockReturnValue( {
|
|
35
|
-
data: { provider: 'anthropic', model: 'claude-3-5-sonnet-20241022' },
|
|
36
|
-
messages: [ { role: 'user', content: 'Hello {{ name }}!' } ]
|
|
37
|
-
} );
|
|
38
29
|
|
|
39
30
|
const result = loadPrompt( 'test', { name: 'World' } );
|
|
40
31
|
|
|
@@ -54,3 +45,88 @@ model: claude-3-5-sonnet-20241022
|
|
|
54
45
|
} );
|
|
55
46
|
|
|
56
47
|
} );
|
|
48
|
+
|
|
49
|
+
describe( 'loadPrompt - template hydration in headers (integration tests)', () => {
|
|
50
|
+
beforeEach( () => {
|
|
51
|
+
vi.clearAllMocks();
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
it( 'should hydrate template variables in YAML headers', () => {
|
|
55
|
+
const promptContent = `---
|
|
56
|
+
provider: {{ provider_name }}
|
|
57
|
+
model: {{ model_id }}
|
|
58
|
+
temperature: 0.7
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
<user>Hello</user>`;
|
|
62
|
+
|
|
63
|
+
loadContent.mockReturnValue( promptContent );
|
|
64
|
+
|
|
65
|
+
const result = loadPrompt( 'test', {
|
|
66
|
+
provider_name: 'anthropic',
|
|
67
|
+
model_id: 'claude-sonnet-4'
|
|
68
|
+
} );
|
|
69
|
+
|
|
70
|
+
expect( result.config.provider ).toBe( 'anthropic' );
|
|
71
|
+
expect( result.config.model ).toBe( 'claude-sonnet-4' );
|
|
72
|
+
expect( result.config.temperature ).toBe( 0.7 );
|
|
73
|
+
} );
|
|
74
|
+
|
|
75
|
+
it( 'should hydrate template variables in both headers and messages', () => {
|
|
76
|
+
const promptContent = `---
|
|
77
|
+
provider: {{ provider }}
|
|
78
|
+
model: {{ model }}
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
<user>Tell me about {{ topic }}</user>`;
|
|
82
|
+
|
|
83
|
+
loadContent.mockReturnValue( promptContent );
|
|
84
|
+
|
|
85
|
+
const result = loadPrompt( 'test', {
|
|
86
|
+
provider: 'openai',
|
|
87
|
+
model: 'gpt-4',
|
|
88
|
+
topic: 'testing'
|
|
89
|
+
} );
|
|
90
|
+
|
|
91
|
+
expect( result.config.provider ).toBe( 'openai' );
|
|
92
|
+
expect( result.config.model ).toBe( 'gpt-4' );
|
|
93
|
+
expect( result.messages[0].content ).toBe( 'Tell me about testing' );
|
|
94
|
+
} );
|
|
95
|
+
|
|
96
|
+
it( 'should render undefined template variables as null', () => {
|
|
97
|
+
const promptContent = `---
|
|
98
|
+
provider: {{ undefined_var }}
|
|
99
|
+
model: claude-3-5-sonnet-20241022
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
<user>Hello</user>`;
|
|
103
|
+
|
|
104
|
+
loadContent.mockReturnValue( promptContent );
|
|
105
|
+
|
|
106
|
+
const result = loadPrompt( 'test', {} );
|
|
107
|
+
|
|
108
|
+
// Liquid renders undefined variables as empty, which becomes null in YAML
|
|
109
|
+
expect( result.config.provider ).toBe( null );
|
|
110
|
+
expect( result.config.model ).toBe( 'claude-3-5-sonnet-20241022' );
|
|
111
|
+
} );
|
|
112
|
+
|
|
113
|
+
it( 'should handle complex template expressions in headers', () => {
|
|
114
|
+
const promptContent = `---
|
|
115
|
+
provider: anthropic
|
|
116
|
+
model: {{ base_model }}-{{ version }}
|
|
117
|
+
temperature: 0.7
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
<user>Hello</user>`;
|
|
121
|
+
|
|
122
|
+
loadContent.mockReturnValue( promptContent );
|
|
123
|
+
|
|
124
|
+
const result = loadPrompt( 'test', {
|
|
125
|
+
base_model: 'claude-sonnet',
|
|
126
|
+
version: '4'
|
|
127
|
+
} );
|
|
128
|
+
|
|
129
|
+
expect( result.config.model ).toBe( 'claude-sonnet-4' );
|
|
130
|
+
} );
|
|
131
|
+
|
|
132
|
+
} );
|