@ender672/minja-js 0.1.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/LICENSE +19 -0
- package/README.md +56 -0
- package/package.json +20 -0
- package/src/chat-template.js +535 -0
- package/src/minja.js +2486 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright 2024 Google LLC
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @ender672/minja-js
|
|
2
|
+
|
|
3
|
+
Unofficial JavaScript port of [minja](https://github.com/ochafik/minja), a minimalistic Jinja2 template engine for LLM chat templates.
|
|
4
|
+
|
|
5
|
+
This is an independent port — not affiliated with or endorsed by the original minja project. The original C++ library is copyright 2024 Google LLC, licensed under MIT.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
### Low-level: parse and render a template
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { Parser, Context, parseTemplate } from '@ender672/minja-js/minja';
|
|
13
|
+
|
|
14
|
+
// parseTemplate() caches the AST so repeated calls with the same
|
|
15
|
+
// template string skip parsing. The AST is stateless — safe to
|
|
16
|
+
// render concurrently with different contexts.
|
|
17
|
+
const root = parseTemplate('Hello {{ name }}!');
|
|
18
|
+
const ctx = Context.make({ name: 'world' });
|
|
19
|
+
console.log(root.render(ctx)); // "Hello world!"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
If you are generating template strings dynamically and don't want to
|
|
23
|
+
fill the cache, call `Parser.parse()` directly instead.
|
|
24
|
+
|
|
25
|
+
### High-level: ChatTemplate
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import { ChatTemplate } from '@ender672/minja-js/chat-template';
|
|
29
|
+
|
|
30
|
+
const tmpl = new ChatTemplate(templateSource, bosToken, eosToken);
|
|
31
|
+
const output = tmpl.apply({
|
|
32
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
33
|
+
addGenerationPrompt: true,
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`ChatTemplate` automatically caches its parsed AST via `parseTemplate()`,
|
|
38
|
+
so constructing multiple `ChatTemplate` instances with the same source
|
|
39
|
+
string only parses the template once.
|
|
40
|
+
|
|
41
|
+
## Running tests
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npm test
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Differential fuzzing against C++ minja
|
|
48
|
+
|
|
49
|
+
To compare JS output against the C++ implementation:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
npm run build:diff-fuzz # one-time: clones C++ minja + nlohmann/json, compiles harness
|
|
53
|
+
npm run diff-fuzz # runs 10k iterations comparing C++ vs JS output
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Requires a C++ compiler with C++17 support, `curl`, and `git`. Dependencies are cached in `.deps/`.
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ender672/minja-js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "JavaScript port of the Minja Jinja2 template engine for LLM chat templates",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./minja": "./src/minja.js",
|
|
9
|
+
"./chat-template": "./src/chat-template.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test test/*.js",
|
|
17
|
+
"build:diff-fuzz": "bash scripts/build-diff-fuzz-render.sh",
|
|
18
|
+
"diff-fuzz": "node scripts/diff-fuzz.js --cpp-bin scripts/diff-fuzz-render"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2024 Google LLC
|
|
3
|
+
|
|
4
|
+
Use of this source code is governed by an MIT-style
|
|
5
|
+
license that can be found in the LICENSE file or at
|
|
6
|
+
https://opensource.org/licenses/MIT.
|
|
7
|
+
*/
|
|
8
|
+
// SPDX-License-Identifier: MIT
|
|
9
|
+
|
|
10
|
+
import { Parser, Context, Value, parseTemplate } from './minja.js';
|
|
11
|
+
|
|
12
|
+
export class ChatTemplate {
|
|
13
|
+
constructor(source, bosToken = '', eosToken = '') {
|
|
14
|
+
this._source = source;
|
|
15
|
+
this._bosToken = bosToken;
|
|
16
|
+
this._eosToken = eosToken;
|
|
17
|
+
this._templateRoot = parseTemplate(source, {
|
|
18
|
+
trimBlocks: true,
|
|
19
|
+
lstripBlocks: true,
|
|
20
|
+
keepTrailingNewline: false,
|
|
21
|
+
});
|
|
22
|
+
this._toolCallExample = '';
|
|
23
|
+
this._caps = {
|
|
24
|
+
supportsTools: false,
|
|
25
|
+
supportsToolCalls: false,
|
|
26
|
+
supportsToolResponses: false,
|
|
27
|
+
supportsSystemRole: false,
|
|
28
|
+
supportsParallelToolCalls: false,
|
|
29
|
+
supportsToolCallId: false,
|
|
30
|
+
requiresObjectArguments: false,
|
|
31
|
+
requiresNonNullContent: false,
|
|
32
|
+
requiresTypedContent: false,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
this._detectCapabilities();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_tryRawRender(messages, tools, addGenerationPrompt, extraContext) {
|
|
39
|
+
try {
|
|
40
|
+
const inputs = {
|
|
41
|
+
messages,
|
|
42
|
+
tools,
|
|
43
|
+
addGenerationPrompt,
|
|
44
|
+
extraContext,
|
|
45
|
+
now: new Date(0), // epoch for tests
|
|
46
|
+
};
|
|
47
|
+
const opts = {
|
|
48
|
+
applyPolyfills: false,
|
|
49
|
+
};
|
|
50
|
+
return this.apply(inputs, opts);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_detectCapabilities() {
|
|
57
|
+
const contains = (haystack, needle) => haystack.includes(needle);
|
|
58
|
+
|
|
59
|
+
const userNeedle = '<User Needle>';
|
|
60
|
+
const sysNeedle = '<System Needle>';
|
|
61
|
+
const dummyStrUserMsg = { role: 'user', content: userNeedle };
|
|
62
|
+
const dummyTypedUserMsg = {
|
|
63
|
+
role: 'user',
|
|
64
|
+
content: [{ type: 'text', text: userNeedle }],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
this._caps.requiresTypedContent =
|
|
68
|
+
!contains(this._tryRawRender([dummyStrUserMsg], undefined, false), userNeedle) &&
|
|
69
|
+
contains(this._tryRawRender([dummyTypedUserMsg], undefined, false), userNeedle);
|
|
70
|
+
|
|
71
|
+
const dummyUserMsg = this._caps.requiresTypedContent
|
|
72
|
+
? dummyTypedUserMsg
|
|
73
|
+
: dummyStrUserMsg;
|
|
74
|
+
const needleSystemMsg = {
|
|
75
|
+
role: 'system',
|
|
76
|
+
content: this._caps.requiresTypedContent
|
|
77
|
+
? [{ type: 'text', text: sysNeedle }]
|
|
78
|
+
: sysNeedle,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
this._caps.supportsSystemRole = contains(
|
|
82
|
+
this._tryRawRender([needleSystemMsg, dummyUserMsg], undefined, false),
|
|
83
|
+
sysNeedle
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
let out = this._tryRawRender(
|
|
87
|
+
[dummyUserMsg],
|
|
88
|
+
[
|
|
89
|
+
{
|
|
90
|
+
name: 'some_tool',
|
|
91
|
+
type: 'function',
|
|
92
|
+
function: {
|
|
93
|
+
name: 'some_tool',
|
|
94
|
+
description: 'Some tool.',
|
|
95
|
+
parameters: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
arg: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: 'Some argument.',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
required: ['arg'],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
false
|
|
109
|
+
);
|
|
110
|
+
this._caps.supportsTools = contains(out, 'some_tool');
|
|
111
|
+
|
|
112
|
+
const renderWithContent = (content) => {
|
|
113
|
+
const assistantMsg = { role: 'assistant', content };
|
|
114
|
+
return this._tryRawRender(
|
|
115
|
+
[dummyUserMsg, assistantMsg, dummyUserMsg, assistantMsg],
|
|
116
|
+
undefined,
|
|
117
|
+
false
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
const outEmpty = renderWithContent('');
|
|
121
|
+
const outNull = renderWithContent(null);
|
|
122
|
+
this._caps.requiresNonNullContent =
|
|
123
|
+
contains(outEmpty, userNeedle) && !contains(outNull, userNeedle);
|
|
124
|
+
|
|
125
|
+
const makeToolCallsMsg = (toolCalls) => ({
|
|
126
|
+
role: 'assistant',
|
|
127
|
+
content: this._caps.requiresNonNullContent ? '' : null,
|
|
128
|
+
tool_calls: toolCalls,
|
|
129
|
+
});
|
|
130
|
+
const makeToolCall = (toolName, args) => ({
|
|
131
|
+
id: 'call_1___',
|
|
132
|
+
type: 'function',
|
|
133
|
+
function: {
|
|
134
|
+
arguments: args,
|
|
135
|
+
name: toolName,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const dummyArgsObj = { argument_needle: "print('Hello, World!')" };
|
|
139
|
+
const containsArgNeedle = (s) =>
|
|
140
|
+
contains(s, '<parameter=argument_needle>') ||
|
|
141
|
+
contains(s, '"argument_needle"') ||
|
|
142
|
+
contains(s, "'argument_needle':") ||
|
|
143
|
+
contains(s, '>argument_needle<');
|
|
144
|
+
|
|
145
|
+
// Test with string arguments
|
|
146
|
+
out = this._tryRawRender(
|
|
147
|
+
[
|
|
148
|
+
dummyUserMsg,
|
|
149
|
+
makeToolCallsMsg([makeToolCall('ipython', JSON.stringify(dummyArgsObj))]),
|
|
150
|
+
],
|
|
151
|
+
undefined,
|
|
152
|
+
false
|
|
153
|
+
);
|
|
154
|
+
const toolCallRendersStrArguments = containsArgNeedle(out);
|
|
155
|
+
|
|
156
|
+
// Test with object arguments
|
|
157
|
+
out = this._tryRawRender(
|
|
158
|
+
[
|
|
159
|
+
dummyUserMsg,
|
|
160
|
+
makeToolCallsMsg([makeToolCall('ipython', dummyArgsObj)]),
|
|
161
|
+
],
|
|
162
|
+
undefined,
|
|
163
|
+
false
|
|
164
|
+
);
|
|
165
|
+
const toolCallRendersObjArguments = containsArgNeedle(out);
|
|
166
|
+
|
|
167
|
+
this._caps.supportsToolCalls = toolCallRendersStrArguments || toolCallRendersObjArguments;
|
|
168
|
+
this._caps.requiresObjectArguments = !toolCallRendersStrArguments && toolCallRendersObjArguments;
|
|
169
|
+
|
|
170
|
+
if (this._caps.supportsToolCalls) {
|
|
171
|
+
const dummyArgs = this._caps.requiresObjectArguments
|
|
172
|
+
? dummyArgsObj
|
|
173
|
+
: JSON.stringify(dummyArgsObj);
|
|
174
|
+
const tc1 = makeToolCall('test_tool1', dummyArgs);
|
|
175
|
+
const tc2 = makeToolCall('test_tool2', dummyArgs);
|
|
176
|
+
out = this._tryRawRender(
|
|
177
|
+
[dummyUserMsg, makeToolCallsMsg([tc1, tc2])],
|
|
178
|
+
undefined,
|
|
179
|
+
false
|
|
180
|
+
);
|
|
181
|
+
this._caps.supportsParallelToolCalls =
|
|
182
|
+
contains(out, 'test_tool1') && contains(out, 'test_tool2');
|
|
183
|
+
|
|
184
|
+
out = this._tryRawRender(
|
|
185
|
+
[
|
|
186
|
+
dummyUserMsg,
|
|
187
|
+
makeToolCallsMsg([tc1]),
|
|
188
|
+
{
|
|
189
|
+
role: 'tool',
|
|
190
|
+
name: 'test_tool1',
|
|
191
|
+
content: 'Some response!',
|
|
192
|
+
tool_call_id: 'call_911_',
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
undefined,
|
|
196
|
+
false
|
|
197
|
+
);
|
|
198
|
+
this._caps.supportsToolResponses = contains(out, 'Some response!');
|
|
199
|
+
this._caps.supportsToolCallId = contains(out, 'call_911_');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Generate tool call example for polyfill
|
|
203
|
+
try {
|
|
204
|
+
if (!this._caps.supportsTools) {
|
|
205
|
+
const userMsg = { role: 'user', content: 'Hey' };
|
|
206
|
+
const args = { arg1: 'some_value' };
|
|
207
|
+
const toolCallMsg = {
|
|
208
|
+
role: 'assistant',
|
|
209
|
+
content: this._caps.requiresNonNullContent ? '' : null,
|
|
210
|
+
tool_calls: [
|
|
211
|
+
{
|
|
212
|
+
id: 'call_1___',
|
|
213
|
+
type: 'function',
|
|
214
|
+
function: {
|
|
215
|
+
name: 'tool_name',
|
|
216
|
+
arguments: this._caps.requiresObjectArguments
|
|
217
|
+
? args
|
|
218
|
+
: Value.fromJS(args).dump(-1, true),
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
let prefix, full;
|
|
225
|
+
prefix = this.apply({ messages: [userMsg], addGenerationPrompt: true });
|
|
226
|
+
full = this.apply({
|
|
227
|
+
messages: [userMsg, toolCallMsg],
|
|
228
|
+
addGenerationPrompt: false,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
let eosPos = full.lastIndexOf(this._eosToken);
|
|
232
|
+
if (
|
|
233
|
+
this._eosToken.length > 0 &&
|
|
234
|
+
(eosPos === prefix.length - this._eosToken.length ||
|
|
235
|
+
(full[full.length - 1] === '\n' &&
|
|
236
|
+
eosPos === full.length - this._eosToken.length - 1))
|
|
237
|
+
) {
|
|
238
|
+
full = full.substring(0, eosPos);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let commonPrefixLength = 0;
|
|
242
|
+
for (let i = 0; i < prefix.length && i < full.length; i++) {
|
|
243
|
+
if (prefix[i] !== full[i]) break;
|
|
244
|
+
if (prefix[i] === '<') continue;
|
|
245
|
+
commonPrefixLength = i + 1;
|
|
246
|
+
}
|
|
247
|
+
const example = full.substring(commonPrefixLength);
|
|
248
|
+
if (example.includes('tool_name') || example.includes('some_value')) {
|
|
249
|
+
this._toolCallExample = example;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {
|
|
253
|
+
// Failed to generate tool call example
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
originalCaps() {
|
|
258
|
+
return { ...this._caps };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
apply(inputs, options = {}) {
|
|
262
|
+
const {
|
|
263
|
+
messages = [],
|
|
264
|
+
tools,
|
|
265
|
+
addGenerationPrompt = true,
|
|
266
|
+
extraContext,
|
|
267
|
+
now = new Date(),
|
|
268
|
+
} = inputs;
|
|
269
|
+
|
|
270
|
+
const opts = {
|
|
271
|
+
applyPolyfills: options.applyPolyfills !== undefined ? options.applyPolyfills : true,
|
|
272
|
+
useBosToken: options.useBosToken !== undefined ? options.useBosToken : true,
|
|
273
|
+
useEosToken: options.useEosToken !== undefined ? options.useEosToken : true,
|
|
274
|
+
defineStrftimeNow: options.defineStrftimeNow !== undefined ? options.defineStrftimeNow : true,
|
|
275
|
+
polyfillTools: options.polyfillTools !== undefined ? options.polyfillTools : true,
|
|
276
|
+
polyfillToolCallExamples: options.polyfillToolCallExamples !== undefined ? options.polyfillToolCallExamples : true,
|
|
277
|
+
polyfillToolCalls: options.polyfillToolCalls !== undefined ? options.polyfillToolCalls : true,
|
|
278
|
+
polyfillToolResponses: options.polyfillToolResponses !== undefined ? options.polyfillToolResponses : true,
|
|
279
|
+
polyfillSystemRole: options.polyfillSystemRole !== undefined ? options.polyfillSystemRole : true,
|
|
280
|
+
polyfillObjectArguments: options.polyfillObjectArguments !== undefined ? options.polyfillObjectArguments : true,
|
|
281
|
+
polyfillTypedContent: options.polyfillTypedContent !== undefined ? options.polyfillTypedContent : true,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const hasTools = Array.isArray(tools) && tools.length > 0;
|
|
285
|
+
let hasToolCalls = false;
|
|
286
|
+
let hasToolResponses = false;
|
|
287
|
+
let hasStringContent = false;
|
|
288
|
+
for (const message of messages) {
|
|
289
|
+
if (message.tool_calls != null) hasToolCalls = true;
|
|
290
|
+
if (message.role === 'tool') hasToolResponses = true;
|
|
291
|
+
if (typeof message.content === 'string') hasStringContent = true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const polyfillSystemRole = opts.polyfillSystemRole && !this._caps.supportsSystemRole;
|
|
295
|
+
const polyfillTools = opts.polyfillTools && hasTools && !this._caps.supportsTools;
|
|
296
|
+
const polyfillToolCallExample = polyfillTools && opts.polyfillToolCallExamples;
|
|
297
|
+
const polyfillToolCalls = opts.polyfillToolCalls && hasToolCalls && !this._caps.supportsToolCalls;
|
|
298
|
+
const polyfillToolResponses = opts.polyfillToolResponses && hasToolResponses && !this._caps.supportsToolResponses;
|
|
299
|
+
const polyfillObjectArguments = opts.polyfillObjectArguments && hasToolCalls && this._caps.requiresObjectArguments;
|
|
300
|
+
const polyfillTypedContent = opts.polyfillTypedContent && hasStringContent && this._caps.requiresTypedContent;
|
|
301
|
+
|
|
302
|
+
const needsPolyfills = opts.applyPolyfills && (
|
|
303
|
+
polyfillSystemRole ||
|
|
304
|
+
polyfillTools ||
|
|
305
|
+
polyfillToolCalls ||
|
|
306
|
+
polyfillToolResponses ||
|
|
307
|
+
polyfillObjectArguments ||
|
|
308
|
+
polyfillTypedContent
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
let actualMessages;
|
|
312
|
+
|
|
313
|
+
if (needsPolyfills) {
|
|
314
|
+
actualMessages = [];
|
|
315
|
+
|
|
316
|
+
const addMessage = (msg) => {
|
|
317
|
+
if (polyfillTypedContent && msg.content != null && typeof msg.content === 'string') {
|
|
318
|
+
actualMessages.push({
|
|
319
|
+
role: msg.role,
|
|
320
|
+
content: [{ type: 'text', text: msg.content }],
|
|
321
|
+
});
|
|
322
|
+
} else {
|
|
323
|
+
actualMessages.push(msg);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
let pendingSystem = '';
|
|
328
|
+
const flushSys = () => {
|
|
329
|
+
if (pendingSystem.length > 0) {
|
|
330
|
+
addMessage({ role: 'user', content: pendingSystem });
|
|
331
|
+
pendingSystem = '';
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
let adjustedMessages;
|
|
336
|
+
if (polyfillTools) {
|
|
337
|
+
const toolsStr = Value.fromJS(tools).dump(2, true);
|
|
338
|
+
const exampleStr = polyfillToolCallExample && this._toolCallExample
|
|
339
|
+
? '\n\nExample tool call syntax:\n\n' + this._toolCallExample + '\n\n'
|
|
340
|
+
: '';
|
|
341
|
+
adjustedMessages = ChatTemplate.addSystem(
|
|
342
|
+
messages,
|
|
343
|
+
"You can call any of the following tools to satisfy the user's requests: " +
|
|
344
|
+
toolsStr + exampleStr
|
|
345
|
+
);
|
|
346
|
+
} else {
|
|
347
|
+
adjustedMessages = messages;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (let message of adjustedMessages) {
|
|
351
|
+
message = { ...message }; // shallow clone
|
|
352
|
+
|
|
353
|
+
if (message.tool_calls != null) {
|
|
354
|
+
if (polyfillObjectArguments || polyfillToolCalls) {
|
|
355
|
+
message.tool_calls = message.tool_calls.map((tc) => {
|
|
356
|
+
tc = { ...tc };
|
|
357
|
+
if (tc.type === 'function') {
|
|
358
|
+
tc.function = { ...tc.function };
|
|
359
|
+
if (typeof tc.function.arguments === 'string') {
|
|
360
|
+
try {
|
|
361
|
+
tc.function.arguments = JSON.parse(tc.function.arguments);
|
|
362
|
+
} catch (e) {
|
|
363
|
+
// Failed to parse arguments
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return tc;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
if (polyfillToolCalls) {
|
|
371
|
+
const toolCallsArr = [];
|
|
372
|
+
for (const tc of message.tool_calls) {
|
|
373
|
+
if (tc.type !== 'function') continue;
|
|
374
|
+
const entry = {
|
|
375
|
+
name: tc.function.name,
|
|
376
|
+
arguments: tc.function.arguments,
|
|
377
|
+
};
|
|
378
|
+
if (tc.id !== undefined) {
|
|
379
|
+
entry.id = tc.id;
|
|
380
|
+
}
|
|
381
|
+
toolCallsArr.push(entry);
|
|
382
|
+
}
|
|
383
|
+
const obj = { tool_calls: toolCallsArr };
|
|
384
|
+
if (message.content != null && message.content !== '') {
|
|
385
|
+
obj.content = message.content;
|
|
386
|
+
}
|
|
387
|
+
message.content = JSON.stringify(obj, null, 2);
|
|
388
|
+
delete message.tool_calls;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (polyfillToolResponses && message.role === 'tool') {
|
|
393
|
+
message.role = 'user';
|
|
394
|
+
const toolResponse = {};
|
|
395
|
+
if (message.name !== undefined) {
|
|
396
|
+
toolResponse.tool = message.name;
|
|
397
|
+
}
|
|
398
|
+
toolResponse.content = message.content;
|
|
399
|
+
if (message.tool_call_id !== undefined) {
|
|
400
|
+
toolResponse.tool_call_id = message.tool_call_id;
|
|
401
|
+
}
|
|
402
|
+
message.content = JSON.stringify({ tool_response: toolResponse }, null, 2);
|
|
403
|
+
delete message.name;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (message.content != null && polyfillSystemRole) {
|
|
407
|
+
const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
|
|
408
|
+
if (message.role === 'system') {
|
|
409
|
+
if (pendingSystem.length > 0) pendingSystem += '\n';
|
|
410
|
+
pendingSystem += content;
|
|
411
|
+
continue;
|
|
412
|
+
} else {
|
|
413
|
+
if (message.role === 'user') {
|
|
414
|
+
if (pendingSystem.length > 0) {
|
|
415
|
+
message = { ...message };
|
|
416
|
+
message.content = pendingSystem + (content.length === 0 ? '' : '\n' + content);
|
|
417
|
+
pendingSystem = '';
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
flushSys();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
addMessage(message);
|
|
425
|
+
}
|
|
426
|
+
flushSys();
|
|
427
|
+
} else {
|
|
428
|
+
actualMessages = messages;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const contextData = {
|
|
432
|
+
messages: actualMessages,
|
|
433
|
+
add_generation_prompt: addGenerationPrompt,
|
|
434
|
+
};
|
|
435
|
+
const context = Context.make(contextData);
|
|
436
|
+
context.set('bos_token', opts.useBosToken ? this._bosToken : '');
|
|
437
|
+
context.set('eos_token', opts.useEosToken ? this._eosToken : '');
|
|
438
|
+
|
|
439
|
+
if (opts.defineStrftimeNow) {
|
|
440
|
+
const nowDate = now;
|
|
441
|
+
context.set(
|
|
442
|
+
'strftime_now',
|
|
443
|
+
Value.callable((ctx, args) => {
|
|
444
|
+
args.expectArgs('strftime_now', [1, 1], [0, 0]);
|
|
445
|
+
const format = args.args[0].value;
|
|
446
|
+
return Value.fromJS(ChatTemplate._strftime(format, nowDate));
|
|
447
|
+
})
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (tools !== undefined && tools !== null) {
|
|
452
|
+
context.set('tools', Value.fromJS(tools));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (extraContext != null) {
|
|
456
|
+
for (const [key, val] of Object.entries(extraContext)) {
|
|
457
|
+
context.set(key, Value.fromJS(val));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return this._templateRoot.render(context);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
static addSystem(messages, systemPrompt) {
|
|
465
|
+
const result = [...messages];
|
|
466
|
+
if (result.length > 0 && result[0].role === 'system') {
|
|
467
|
+
result[0] = {
|
|
468
|
+
role: 'system',
|
|
469
|
+
content: result[0].content + '\n\n' + systemPrompt,
|
|
470
|
+
};
|
|
471
|
+
} else {
|
|
472
|
+
result.unshift({ role: 'system', content: systemPrompt });
|
|
473
|
+
}
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
static _strftime(format, date) {
|
|
478
|
+
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
479
|
+
let result = '';
|
|
480
|
+
for (let i = 0; i < format.length; i++) {
|
|
481
|
+
if (format[i] === '%' && i + 1 < format.length) {
|
|
482
|
+
i++;
|
|
483
|
+
switch (format[i]) {
|
|
484
|
+
case 'Y': result += String(date.getFullYear()); break;
|
|
485
|
+
case 'm': result += pad(date.getMonth() + 1); break;
|
|
486
|
+
case 'd': result += pad(date.getDate()); break;
|
|
487
|
+
case 'H': result += pad(date.getHours()); break;
|
|
488
|
+
case 'M': result += pad(date.getMinutes()); break;
|
|
489
|
+
case 'S': result += pad(date.getSeconds()); break;
|
|
490
|
+
case 'j': {
|
|
491
|
+
const start = new Date(date.getFullYear(), 0, 0);
|
|
492
|
+
const diff = date - start;
|
|
493
|
+
const oneDay = 1000 * 60 * 60 * 24;
|
|
494
|
+
result += pad(Math.floor(diff / oneDay), 3);
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
case 'p': result += date.getHours() >= 12 ? 'PM' : 'AM'; break;
|
|
498
|
+
case 'I': {
|
|
499
|
+
let h = date.getHours() % 12;
|
|
500
|
+
if (h === 0) h = 12;
|
|
501
|
+
result += pad(h);
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
case 'A': {
|
|
505
|
+
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
506
|
+
result += days[date.getDay()];
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case 'a': {
|
|
510
|
+
const daysShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
511
|
+
result += daysShort[date.getDay()];
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
case 'B': {
|
|
515
|
+
const months = ['January', 'February', 'March', 'April', 'May', 'June',
|
|
516
|
+
'July', 'August', 'September', 'October', 'November', 'December'];
|
|
517
|
+
result += months[date.getMonth()];
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
case 'b': {
|
|
521
|
+
const monthsShort = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
522
|
+
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
523
|
+
result += monthsShort[date.getMonth()];
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
case '%': result += '%'; break;
|
|
527
|
+
default: result += '%' + format[i]; break;
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
result += format[i];
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return result;
|
|
534
|
+
}
|
|
535
|
+
}
|