@j-o-r/hello-dave 0.0.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 +73 -0
- package/README.md +207 -0
- package/bin/hdAsk.js +103 -0
- package/bin/hdClear.js +13 -0
- package/bin/hdCode.js +110 -0
- package/bin/hdConnect.js +230 -0
- package/bin/hdInspect.js +28 -0
- package/bin/hdNpm.js +114 -0
- package/bin/hdPrompt.js +108 -0
- package/examples/claude-test.js +89 -0
- package/examples/claude.js +143 -0
- package/examples/gpt.js +127 -0
- package/examples/gpt_code.js +125 -0
- package/examples/gpt_note_keeping.js +117 -0
- package/examples/grok.js +119 -0
- package/examples/grok_code.js +114 -0
- package/examples/grok_note_keeping.js +111 -0
- package/lib/API/anthropic.com/text.js +402 -0
- package/lib/API/brave.com/search.js +239 -0
- package/lib/API/openai.com/README.md +1 -0
- package/lib/API/openai.com/reponses/MESSAGES.md +69 -0
- package/lib/API/openai.com/reponses/text.js +416 -0
- package/lib/API/x.ai/text.js +415 -0
- package/lib/AgentClient.js +197 -0
- package/lib/AgentManager.js +144 -0
- package/lib/AgentServer.js +336 -0
- package/lib/Cli.js +256 -0
- package/lib/Prompt.js +728 -0
- package/lib/Session.js +231 -0
- package/lib/ToolSet.js +186 -0
- package/lib/fafs.js +93 -0
- package/lib/genericToolset.js +170 -0
- package/lib/index.js +34 -0
- package/lib/promptHelpers.js +132 -0
- package/lib/testToolset.js +42 -0
- package/module.md +189 -0
- package/package.json +49 -0
- package/types/API/anthropic.com/text.d.ts +207 -0
- package/types/API/brave.com/search.d.ts +156 -0
- package/types/API/openai.com/reponses/text.d.ts +225 -0
- package/types/API/x.ai/text.d.ts +286 -0
- package/types/AgentClient.d.ts +70 -0
- package/types/AgentManager.d.ts +112 -0
- package/types/AgentServer.d.ts +38 -0
- package/types/Cli.d.ts +52 -0
- package/types/Prompt.d.ts +298 -0
- package/types/Session.d.ts +31 -0
- package/types/ToolSet.d.ts +95 -0
- package/types/fafs.d.ts +47 -0
- package/types/genericToolset.d.ts +3 -0
- package/types/index.d.ts +23 -0
- package/types/promptHelpers.d.ts +1 -0
- package/types/testToolset.d.ts +3 -0
package/lib/Prompt.js
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a uniform prompt/message AI format
|
|
3
|
+
* An attempt to make working with messages/conversations easier and interchangeable
|
|
4
|
+
* across AI platforms/LLMs.
|
|
5
|
+
* The initial format/structure is inspired by the OPENAI messages format (but not compatible)
|
|
6
|
+
*/
|
|
7
|
+
import tokens from 'gpt-3-encoder';
|
|
8
|
+
import { EventEmitter } from 'node:events';
|
|
9
|
+
import { pruneResolvedToolIOByCallIdExceptLast as pruneResolvedToolIOByCallIdExceptLastHelper } from './promptHelpers.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('./API/openai.com/reponses/text.js').request} OARequest
|
|
13
|
+
* @typedef {import('./API/x.ai/text.js').request} XRequest
|
|
14
|
+
* @typedef {import('./API/anthropic.com/text.js').request} ANTHRequest
|
|
15
|
+
*
|
|
16
|
+
* @typedef {import('./API/x.ai/text.js').XOptions} XOptions
|
|
17
|
+
* @typedef {import('./API/openai.com/reponses/text.js').OAOptions} OAOptions
|
|
18
|
+
* @typedef {import('./API/anthropic.com/text.js').ANTHOptions} ANTHOptions
|
|
19
|
+
*
|
|
20
|
+
* @typedef {import('./ToolSet.js').default} ToolSet
|
|
21
|
+
* @typedef {OARequest|XRequest|ANTHRequest} request - The AI model to use for chat functionality.
|
|
22
|
+
* @typedef {OAOptions|XOptions|ANTHOptions} options - Model options
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {'system'|'assistant'|'user'|'tool'|'log'|'reasoning'} Roles
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} Record
|
|
30
|
+
* @property {string} id - remote request id or generated requestId
|
|
31
|
+
* @property {string} isoDate - Date in ISO string format
|
|
32
|
+
* @property {number} duration - Execution time in MS of the model / method
|
|
33
|
+
* @property {string} environment - Execution context endpoint_name / server_name / interpreter_name
|
|
34
|
+
* @property {string} model - LLM model, method , funtion name
|
|
35
|
+
* @property {number} tokensIn - Number of tokens to process
|
|
36
|
+
* @property {number} tokensOut - Number of tokens in the response
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {Object} TextMessage
|
|
40
|
+
* @property {string} type="text" - The type of the message, should be "text".
|
|
41
|
+
* @property {string} text - The content of the text message.
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {Object} ImageMessage
|
|
45
|
+
* @property {string} type="image_url" - The type of the message, should be "image_url".
|
|
46
|
+
* @property {string} url - URL or "data:image/png;base64,{base64_image}"
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} AudioMessage
|
|
50
|
+
* @property {string} type="url" - The type of the message, should be "image_url".
|
|
51
|
+
* @property {string} url - URL or "data:image/png;base64,{base64_image}"
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {Object} FunctionRequest
|
|
55
|
+
* The `PRMessagerole.role` must be 'assistant'
|
|
56
|
+
* @property {'function_request'} type - The type of the message, should be "function_request".
|
|
57
|
+
* @property {object} function_request - Function object with properties
|
|
58
|
+
* @property {string} function_request.name - function "name" to call
|
|
59
|
+
* @property {string} function_request.id - The given ID of the function call
|
|
60
|
+
* @property {string} function_request.call_id - function call id
|
|
61
|
+
* @property {string} function_request.parameters - JSON string of the parameters e.g. "{\"unit\":\"celsius\"}"
|
|
62
|
+
*/
|
|
63
|
+
/**
|
|
64
|
+
* @typedef {Object} FunctionResponse
|
|
65
|
+
* The `PRMessage.role` must 'tool'
|
|
66
|
+
* @property {'function_response'} type - The type of the message, should be "function_response".
|
|
67
|
+
* @property {object} function_response - Function object with properties
|
|
68
|
+
* @property {string} function_response.name - function "name" called
|
|
69
|
+
* @property {string} function_response.id - The given ID of the function call
|
|
70
|
+
* @property {string} function_response.call_id - the function call id
|
|
71
|
+
* @property {string} function_response.response - JSON string of the reponse e.g. "{\"temperature\":24}"
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {TextMessage|ImageMessage|AudioMessage|FunctionRequest|FunctionResponse} Content
|
|
76
|
+
*/
|
|
77
|
+
/**
|
|
78
|
+
* @typedef {object} Message
|
|
79
|
+
* @property {Roles} role - Message role
|
|
80
|
+
* @property {Content[]} content -
|
|
81
|
+
* @property {string} [name] - optional author name
|
|
82
|
+
* @property {boolean} [sticky] - This message object is part of the base prompt (reuse after reset) and is not truncated
|
|
83
|
+
* @property {number} [ts] - A timestamp in MS for reconstruction truncated prompts (for creating large context embeddings)
|
|
84
|
+
*/
|
|
85
|
+
const MESSAGE_ROLES = {
|
|
86
|
+
SYSTEM: 'system', // system prompt
|
|
87
|
+
ASSISTANT: 'assistant', // assistent response
|
|
88
|
+
REASONING: 'reasoning', // assistant reasoning: https://docs.x.ai/docs/guides/reasoning
|
|
89
|
+
USER: 'user', // user input
|
|
90
|
+
TOOL: 'tool', // function calls
|
|
91
|
+
LOG: 'log' // Additional log info
|
|
92
|
+
}
|
|
93
|
+
const MESSAGE_TYPES = {
|
|
94
|
+
TEXT: 'text',
|
|
95
|
+
IMAGE_URL: 'image_url',
|
|
96
|
+
AUDIO_URL: 'audio_url',
|
|
97
|
+
FUNCTION_REQUEST: 'function_request',
|
|
98
|
+
FUNCTION_RESPONSE: 'function_response'
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const EVENTS = {
|
|
102
|
+
start: 'start', // starts a request
|
|
103
|
+
retrigger: 'retrigger', // retriggers start AFTER an import
|
|
104
|
+
finished: 'finished', // finished requests
|
|
105
|
+
message: 'message', // received a message
|
|
106
|
+
record: 'record', // received a record
|
|
107
|
+
truncated: 'truncated', // Prompt messages are truncated
|
|
108
|
+
reset: 'reset', // Prompt reset
|
|
109
|
+
ready: 'ready', // Call or trigger has finished
|
|
110
|
+
error: 'error', // An error of some kind
|
|
111
|
+
warning: 'warning', // An warning of some kind
|
|
112
|
+
tool_request: 'tool_request', // Start a function call
|
|
113
|
+
tool_response: 'tool_response', // Exit a function call
|
|
114
|
+
tool_error: 'tool_error', // Error in function call
|
|
115
|
+
http_request: 'http_request', // Start a function call
|
|
116
|
+
http_response: 'http_response', // Exit a function call
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Converts an array of text messages to a string.
|
|
120
|
+
* @param {Content[]} content - An array of messages.
|
|
121
|
+
* @returns {string} - The concatenated text messages.
|
|
122
|
+
*/
|
|
123
|
+
const messagesToString = (content) => {
|
|
124
|
+
let result = '';
|
|
125
|
+
content.forEach((mesg) => {
|
|
126
|
+
if (mesg.type === MESSAGE_TYPES.TEXT) {
|
|
127
|
+
result += mesg[MESSAGE_TYPES.TEXT] + '\n';
|
|
128
|
+
}
|
|
129
|
+
if (mesg.type === MESSAGE_TYPES.IMAGE_URL) {
|
|
130
|
+
result += mesg.url + '\n';
|
|
131
|
+
}
|
|
132
|
+
if (mesg.type === MESSAGE_TYPES.AUDIO_URL) {
|
|
133
|
+
result += mesg.url + '\n';
|
|
134
|
+
}
|
|
135
|
+
if (mesg.type === MESSAGE_TYPES.FUNCTION_REQUEST) {
|
|
136
|
+
result += `function_request: ${JSON.stringify(mesg[MESSAGE_TYPES.FUNCTION_REQUEST])}\n`;
|
|
137
|
+
}
|
|
138
|
+
if (mesg.type === MESSAGE_TYPES.FUNCTION_RESPONSE) {
|
|
139
|
+
result += `function_response: ${JSON.stringify(mesg[MESSAGE_TYPES.FUNCTION_RESPONSE])}\n`;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
return result.trim();
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if a record is valid
|
|
147
|
+
* @param {Record} record
|
|
148
|
+
* @throws {Error} If the record is invalid.
|
|
149
|
+
*/
|
|
150
|
+
const isValidRecord = (record) => {
|
|
151
|
+
if (
|
|
152
|
+
typeof record['id'] === 'string' &&
|
|
153
|
+
record.id.length > 0 &&
|
|
154
|
+
typeof record['isoDate'] === 'string' &&
|
|
155
|
+
record.isoDate.length > 9 &&
|
|
156
|
+
typeof record['environment'] === 'string' &&
|
|
157
|
+
record.environment.length > 0 &&
|
|
158
|
+
typeof record['model'] === 'string' &&
|
|
159
|
+
record.model.length > 0 &&
|
|
160
|
+
typeof record['duration'] === 'number' &&
|
|
161
|
+
typeof record['tokensIn'] === 'number' &&
|
|
162
|
+
typeof record['tokensOut'] === 'number'
|
|
163
|
+
) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
throw new Error('Invalid record');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Checks if a message is valid based on its role and type.
|
|
171
|
+
* @param {Roles} role - The role of the message (e.g., 'system', 'assistant', 'user', 'tool').
|
|
172
|
+
* @param {Content} msg - The message object.
|
|
173
|
+
* @throws {Error} If the message is invalid.
|
|
174
|
+
*/
|
|
175
|
+
const isValidMessage = (role, msg) => {
|
|
176
|
+
if (!Object.values(MESSAGE_ROLES).includes(role)) {
|
|
177
|
+
throw new Error('Unkown role detected');
|
|
178
|
+
}
|
|
179
|
+
switch (msg.type) {
|
|
180
|
+
case MESSAGE_TYPES.TEXT:
|
|
181
|
+
if (!isValidTextMessage(msg)) throw new Error('Invalid text message');
|
|
182
|
+
if (role === 'tool') throw new Error('Invalid role for text message: role tool is not valid');
|
|
183
|
+
break;
|
|
184
|
+
case MESSAGE_TYPES.IMAGE_URL:
|
|
185
|
+
if (!isValidUrlMessage(msg)) throw new Error('Invalid image_url message');
|
|
186
|
+
if (role !== 'user') throw new Error('Invalid role for image_url: use user');
|
|
187
|
+
break;
|
|
188
|
+
case MESSAGE_TYPES.AUDIO_URL:
|
|
189
|
+
if (!isValidUrlMessage(msg)) throw new Error('Invalid audio_url message');
|
|
190
|
+
if (role !== 'user') throw new Error('Invalid role for audio_url: use user');
|
|
191
|
+
break;
|
|
192
|
+
case MESSAGE_TYPES.FUNCTION_REQUEST:
|
|
193
|
+
if (!isValidFunctionRequest(msg)) throw new Error('Invalid function_request message');
|
|
194
|
+
if (role !== 'assistant') throw new Error('Invalid role for function_request: use assistant');
|
|
195
|
+
break;
|
|
196
|
+
case MESSAGE_TYPES.FUNCTION_RESPONSE:
|
|
197
|
+
if (!isValidFunctionResponse(msg)) throw new Error('Invalid function_reponse message');
|
|
198
|
+
if (role !== 'tool') throw new Error('Invalid role for function_response: use tool');
|
|
199
|
+
break;
|
|
200
|
+
default:
|
|
201
|
+
throw new Error('Invalid message type detected');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Checks if a URL or data URL is valid.
|
|
207
|
+
* @param {string} str - The URL or data URL string.
|
|
208
|
+
* @returns {boolean} - True if the URL or data URL is valid, false otherwise.
|
|
209
|
+
*/
|
|
210
|
+
const isValidUrlOrDataUrl = (str) => {
|
|
211
|
+
try {
|
|
212
|
+
// Check if it's a valid URL
|
|
213
|
+
new URL(str);
|
|
214
|
+
return true;
|
|
215
|
+
} catch (_) {
|
|
216
|
+
// Check if it's a valid data URL
|
|
217
|
+
const dataUrlPattern = /^data:[a-z]+\/[a-z0-9-+.]+;base64,[a-z0-9+/]+={0,2}$/i;
|
|
218
|
+
return dataUrlPattern.test(str);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Checks if this a text message
|
|
223
|
+
* However, the text may be empty, this can happen with empty assistant reponses
|
|
224
|
+
* @param {Content} msg - The object to check.
|
|
225
|
+
* @returns {boolean} - True if valid, false otherwise.
|
|
226
|
+
*/
|
|
227
|
+
const isValidTextMessage = (msg) => {
|
|
228
|
+
if (
|
|
229
|
+
MESSAGE_TYPES.TEXT === msg['type'] &&
|
|
230
|
+
typeof msg['text'] === 'string'
|
|
231
|
+
) {
|
|
232
|
+
return true
|
|
233
|
+
}
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Checks if this a img_url message
|
|
239
|
+
* @param {Content} msg - The object to check.
|
|
240
|
+
* @returns {boolean} - True if valid, false otherwise.
|
|
241
|
+
*/
|
|
242
|
+
const isValidUrlMessage = (msg) => {
|
|
243
|
+
if (
|
|
244
|
+
[MESSAGE_TYPES.IMAGE_URL, MESSAGE_TYPES.AUDIO_URL].includes(msg['type']) &&
|
|
245
|
+
isValidUrlOrDataUrl(msg['url'])
|
|
246
|
+
) {
|
|
247
|
+
return true
|
|
248
|
+
}
|
|
249
|
+
return false
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Checks if this a function_request message
|
|
254
|
+
* @param {Content} msg - The object to check.
|
|
255
|
+
* @returns {boolean} - True if valid, false otherwise.
|
|
256
|
+
*/
|
|
257
|
+
const isValidFunctionRequest = (msg) => {
|
|
258
|
+
if (
|
|
259
|
+
MESSAGE_TYPES.FUNCTION_REQUEST === msg['type'] &&
|
|
260
|
+
typeof msg['function_request'] === 'object' &&
|
|
261
|
+
typeof msg['function_request']['name'] === 'string' &&
|
|
262
|
+
typeof msg['function_request']['id'] === 'string' &&
|
|
263
|
+
typeof msg['function_request']['parameters'] === 'string' &&
|
|
264
|
+
msg['function_request']['name'].trim().length > 0 &&
|
|
265
|
+
msg['function_request']['id'].trim().length > 0
|
|
266
|
+
) {
|
|
267
|
+
return true
|
|
268
|
+
}
|
|
269
|
+
return false
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Checks if this a function_response message
|
|
274
|
+
* @param {Content} msg - The object to check.
|
|
275
|
+
* @returns {boolean} - True if valid, false otherwise.
|
|
276
|
+
*/
|
|
277
|
+
const isValidFunctionResponse = (msg) => {
|
|
278
|
+
if (
|
|
279
|
+
MESSAGE_TYPES.FUNCTION_RESPONSE === msg['type'] &&
|
|
280
|
+
typeof msg['function_response'] === 'object' &&
|
|
281
|
+
typeof msg['function_response']['name'] === 'string' &&
|
|
282
|
+
typeof msg['function_response']['id'] === 'string' &&
|
|
283
|
+
typeof msg['function_response']['response'] === 'string' &&
|
|
284
|
+
msg['function_response']['name'].trim().length > 0 &&
|
|
285
|
+
msg['function_response']['id'].trim().length > 0
|
|
286
|
+
) {
|
|
287
|
+
return true
|
|
288
|
+
}
|
|
289
|
+
return false
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
*
|
|
293
|
+
* @fires Prompt#add
|
|
294
|
+
* Event: message added
|
|
295
|
+
*
|
|
296
|
+
* @event Prompt#add
|
|
297
|
+
* @type {Message}
|
|
298
|
+
*/
|
|
299
|
+
class Prompt extends EventEmitter {
|
|
300
|
+
#tokenCount = 0;
|
|
301
|
+
/** Max prompt size in tokens to post */
|
|
302
|
+
#contextWindow = 0;
|
|
303
|
+
/** @type {Array<Message>} */
|
|
304
|
+
#messages = [];
|
|
305
|
+
#adapter = {
|
|
306
|
+
/** @type {request} */
|
|
307
|
+
request: async () => {
|
|
308
|
+
throw new Error('Adapapter not set');
|
|
309
|
+
},
|
|
310
|
+
/** @type {ToolSet|void} */
|
|
311
|
+
toolset: null,
|
|
312
|
+
/** @type {options|void} */
|
|
313
|
+
options: null
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* push a message to the messages array
|
|
317
|
+
* @param {Message} msg
|
|
318
|
+
* @fires {Prompt#message}
|
|
319
|
+
*/
|
|
320
|
+
#add(msg) {
|
|
321
|
+
msg.ts = new Date().getTime();
|
|
322
|
+
this.#messages.push(msg);
|
|
323
|
+
this.emit(EVENTS.message, msg);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Basic check for the order of messages.
|
|
328
|
+
* @param {Roles} role - The role of the message.
|
|
329
|
+
* @param {boolean} [sticky] - Whether the message is part of the base prompt.
|
|
330
|
+
* @throws {Error} If the message order is invalid.
|
|
331
|
+
*/
|
|
332
|
+
#basicMessageOrderCheck(role, sticky) {
|
|
333
|
+
if (!Object.values(MESSAGE_ROLES).includes(role)) {
|
|
334
|
+
throw new Error('Unknown role detected');
|
|
335
|
+
}
|
|
336
|
+
if (role === MESSAGE_ROLES.SYSTEM && !sticky) {
|
|
337
|
+
throw new Error('System prompt needs to be sticky');
|
|
338
|
+
}
|
|
339
|
+
if (role === MESSAGE_ROLES.SYSTEM && this.length > 0) {
|
|
340
|
+
throw new Error('System prompt needs to be the first message');
|
|
341
|
+
}
|
|
342
|
+
if (this.length > 0 && sticky) {
|
|
343
|
+
if (this.#messages[this.length - 1].sticky === false) {
|
|
344
|
+
throw new Error('Sticky messages can only be added after other sticky messages');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Constructs a new Prompt instance.
|
|
350
|
+
* If contextWindow = 0 (defaut) the prompt will have no context building up (ONE_SHOT)
|
|
351
|
+
* @param {number} [contextWindow] - The max size of a total prompt in tokens. (context)
|
|
352
|
+
*/
|
|
353
|
+
constructor(contextWindow) {
|
|
354
|
+
super();
|
|
355
|
+
if (typeof contextWindow === 'number') {
|
|
356
|
+
this.#contextWindow = contextWindow;
|
|
357
|
+
}
|
|
358
|
+
this.EVENTS = Object.freeze(EVENTS);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* @param {request} handler
|
|
362
|
+
* @param {ToolSet} toolset
|
|
363
|
+
* @param {options} options
|
|
364
|
+
*/
|
|
365
|
+
setAdaptor(handler, toolset, options) {
|
|
366
|
+
this.#adapter.request = handler
|
|
367
|
+
this.#adapter.toolset = toolset
|
|
368
|
+
this.#adapter.options = options
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Get the names of the available funtion calls
|
|
372
|
+
* @returns {string[]}
|
|
373
|
+
*/
|
|
374
|
+
toolsetFunctions() {
|
|
375
|
+
if (!this.#adapter.toolset) return [];
|
|
376
|
+
return this.#adapter.toolset.list().map(t => t.name);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* @returns {ToolSet|void}
|
|
380
|
+
*/
|
|
381
|
+
get toolset() {
|
|
382
|
+
return this.#adapter.toolset;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Start a requests
|
|
387
|
+
* @param {string|Content} content
|
|
388
|
+
* @returns {Promise<string>}
|
|
389
|
+
*/
|
|
390
|
+
async call(content) {
|
|
391
|
+
if (typeof content === 'string') {
|
|
392
|
+
if (content.trim() === '') {
|
|
393
|
+
throw 'Input is empty';
|
|
394
|
+
}
|
|
395
|
+
this.add('user', content, false);
|
|
396
|
+
} else if (Array.isArray(content)) {
|
|
397
|
+
this.addMultiModal('user', content)
|
|
398
|
+
} else {
|
|
399
|
+
throw 'Not a valid input';
|
|
400
|
+
}
|
|
401
|
+
this.emit(EVENTS.start, true);
|
|
402
|
+
// @ts-ignore
|
|
403
|
+
const msg = await this.#adapter.request(this, this.#adapter.toolset, this.#adapter.options);
|
|
404
|
+
if (this.#tokenCount >= this.#contextWindow) {
|
|
405
|
+
this.truncate();
|
|
406
|
+
}
|
|
407
|
+
this.emit(EVENTS.finished, true);
|
|
408
|
+
return this.contentToString(msg.content)
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Trigger a request when the last added message is a user or tool message
|
|
412
|
+
* Exit on error
|
|
413
|
+
*/
|
|
414
|
+
async triggerRequest() {
|
|
415
|
+
if (this.#adapter.toolset) {
|
|
416
|
+
// See if we need to execute something first
|
|
417
|
+
await this.#adapter.toolset.execute(this);
|
|
418
|
+
}
|
|
419
|
+
const last = this.getLastMessage();
|
|
420
|
+
if (!last) return;
|
|
421
|
+
if (last.role === 'tool' || last.role === 'user') {
|
|
422
|
+
this.emit(EVENTS.retrigger, true);
|
|
423
|
+
// @ts-ignore
|
|
424
|
+
await this.#adapter.request(this, this.#adapter.toolset, this.#adapter.options)
|
|
425
|
+
}
|
|
426
|
+
if (this.#tokenCount >= this.#contextWindow) {
|
|
427
|
+
this.truncate();
|
|
428
|
+
}
|
|
429
|
+
this.emit(EVENTS.finished, true);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Get the maximum token length for this prompt
|
|
433
|
+
* @returns {number}
|
|
434
|
+
*/
|
|
435
|
+
get contextLength() {
|
|
436
|
+
return this.#contextWindow;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Returns a copy of the messages array.
|
|
440
|
+
* @returns {Message[]} - A copy of the messages array.
|
|
441
|
+
*/
|
|
442
|
+
get messages() {
|
|
443
|
+
return JSON.parse(JSON.stringify(this.#messages));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Imports messages.
|
|
448
|
+
* Bypasses the event emitter
|
|
449
|
+
* @param {Message[]} messages
|
|
450
|
+
* @throws {Error} If the input string is invalid.
|
|
451
|
+
*/
|
|
452
|
+
set messages(messages) {
|
|
453
|
+
// Copy
|
|
454
|
+
messages = JSON.parse(JSON.stringify(messages));
|
|
455
|
+
messages.forEach((ob) => {
|
|
456
|
+
if (typeof ob.sticky !== 'boolean') {
|
|
457
|
+
throw new Error(`Missing 'sticky' property`);
|
|
458
|
+
}
|
|
459
|
+
if (typeof ob.role !== 'string') {
|
|
460
|
+
throw new Error(`Missing 'role' property`);
|
|
461
|
+
}
|
|
462
|
+
if (Array.isArray(ob.content) === false) {
|
|
463
|
+
throw new Error(`Missing 'content' property`);
|
|
464
|
+
}
|
|
465
|
+
const role = ob.role;
|
|
466
|
+
const list = ob.content;
|
|
467
|
+
list.forEach((msg) => {
|
|
468
|
+
isValidMessage(role, msg);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
// Refresh token count
|
|
472
|
+
this.#tokenCount = 0;
|
|
473
|
+
this.#messages = messages;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Return the most recent, the LAST message added
|
|
477
|
+
* @returns {Message|void}
|
|
478
|
+
*/
|
|
479
|
+
getLastMessage() {
|
|
480
|
+
if (this.#messages.length > 0) {
|
|
481
|
+
return this.#messages[this.length - 1];
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Return the readable content (type == text)
|
|
486
|
+
* @param {Content[]} content
|
|
487
|
+
* @return {string}
|
|
488
|
+
*/
|
|
489
|
+
contentToString(content) {
|
|
490
|
+
return messagesToString(content);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Count the number of tokens on a given string
|
|
494
|
+
* Or the recorded tokencount of the last roundtrip
|
|
495
|
+
* @param {string} [str] - the string to count
|
|
496
|
+
* @param returns {number}
|
|
497
|
+
*/
|
|
498
|
+
countTokens(str) {
|
|
499
|
+
if (str && str !== '') {
|
|
500
|
+
return tokens.encode(str).length;
|
|
501
|
+
}
|
|
502
|
+
return this.#tokenCount;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* reduce the prompt length to fit in the contextWindow
|
|
508
|
+
* or simply reset when there is no contextWindow
|
|
509
|
+
* @returns {boolean} true when the prompt has been altered/reduced
|
|
510
|
+
*/
|
|
511
|
+
truncate() {
|
|
512
|
+
const length = this.length;
|
|
513
|
+
if (length === 0) {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
// just reset when there is no room for context
|
|
517
|
+
if (this.#contextWindow === 0) {
|
|
518
|
+
this.reset();
|
|
519
|
+
this.emit(EVENTS.truncated, EVENTS.truncated)
|
|
520
|
+
return (length !== this.length);
|
|
521
|
+
}
|
|
522
|
+
pruneResolvedToolIOByCallIdExceptLastHelper(this.#messages, 'lastTurn');
|
|
523
|
+
let totalTokens = 0;
|
|
524
|
+
let messagesToRemove = [];
|
|
525
|
+
let block = false;
|
|
526
|
+
let blockScope = [];
|
|
527
|
+
let blockTokens = 0;
|
|
528
|
+
|
|
529
|
+
for (let i = 0; i < this.#messages.length; i++) {
|
|
530
|
+
const message = this.#messages[i];
|
|
531
|
+
if (message.sticky) {
|
|
532
|
+
totalTokens += this.countTokens(messagesToString(message.content));
|
|
533
|
+
} else {
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Count backwards and start deleting when a block takes to much space
|
|
538
|
+
for (let i = this.#messages.length - 1; i >= 0; i--) {
|
|
539
|
+
const message = this.#messages[i];
|
|
540
|
+
if (!message.sticky) {
|
|
541
|
+
// a block starts with a user message
|
|
542
|
+
// ends with an assistant message
|
|
543
|
+
// in reverse
|
|
544
|
+
if (message.role === MESSAGE_ROLES.ASSISTANT) {
|
|
545
|
+
// start record
|
|
546
|
+
block = true;
|
|
547
|
+
} else if (message.role === MESSAGE_ROLES.USER) {
|
|
548
|
+
// end record
|
|
549
|
+
// finish it of
|
|
550
|
+
if (block === true) {
|
|
551
|
+
blockScope.push(i);
|
|
552
|
+
blockTokens += this.countTokens(messagesToString(message.content));
|
|
553
|
+
totalTokens += blockTokens;
|
|
554
|
+
if (totalTokens > this.#contextWindow) {
|
|
555
|
+
messagesToRemove = [...messagesToRemove, ...blockScope];
|
|
556
|
+
};
|
|
557
|
+
blockTokens = 0;
|
|
558
|
+
blockScope = [];
|
|
559
|
+
}
|
|
560
|
+
block = false;
|
|
561
|
+
}
|
|
562
|
+
if (block) {
|
|
563
|
+
blockScope.push(i);
|
|
564
|
+
blockTokens += this.countTokens(messagesToString(message.content));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
messagesToRemove.forEach((index) => {
|
|
569
|
+
this.#messages.splice(index, 1);
|
|
570
|
+
});
|
|
571
|
+
if (length !== this.length) this.emit(EVENTS.truncated, EVENTS.truncated);
|
|
572
|
+
return (length !== this.length);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Gets the length of the messages array.
|
|
576
|
+
* @returns {number} - The length of the messages array.
|
|
577
|
+
*/
|
|
578
|
+
get length() {
|
|
579
|
+
return this.#messages.length;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Gets the system prompt from the messages.
|
|
584
|
+
* @returns {string} - The system prompt.
|
|
585
|
+
* @throws {Error} If no system prompt is available.
|
|
586
|
+
*/
|
|
587
|
+
get system_prompt() {
|
|
588
|
+
if (!this.hasSystemprompt) {
|
|
589
|
+
throw new Error('No system prompt available');
|
|
590
|
+
}
|
|
591
|
+
const sys = this.#messages[0];
|
|
592
|
+
return messagesToString(sys.content);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Checks if a system prompt is available.
|
|
597
|
+
* @returns {boolean} - True if a system prompt is available, false otherwise.
|
|
598
|
+
*/
|
|
599
|
+
get hasSystemprompt() {
|
|
600
|
+
if (this.#messages.length === 0) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
if (this.#messages[0].role === MESSAGE_ROLES.SYSTEM) {
|
|
604
|
+
const test = messagesToString(this.#messages[0].content);
|
|
605
|
+
if (test.trim().length > 0) {
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Adds a text message to the messages array.
|
|
614
|
+
* @param {Roles} role - The role of the message.
|
|
615
|
+
* @param {string} message - The text message.
|
|
616
|
+
* @param {boolean} [sticky] - Whether the message is part of the base prompt.
|
|
617
|
+
* @fires Prompt#message
|
|
618
|
+
* @throws {Error} If the message order is invalid or the message is invalid.
|
|
619
|
+
*/
|
|
620
|
+
add(role, message, sticky = false) {
|
|
621
|
+
const content = [];
|
|
622
|
+
const txt = {
|
|
623
|
+
type: 'text',
|
|
624
|
+
text: message
|
|
625
|
+
};
|
|
626
|
+
content.push(txt);
|
|
627
|
+
/**
|
|
628
|
+
* @event Prompt#message
|
|
629
|
+
* @type {Message}
|
|
630
|
+
*/
|
|
631
|
+
this.addMultiModal(role, content, sticky);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Adds a multi-modal message to the messages array.
|
|
636
|
+
* @param {Roles} role - The role of the message.
|
|
637
|
+
* @param {Content[]} messages - The actual messages with multi-modal content.
|
|
638
|
+
* @param {boolean} [sticky] - Whether the message is part of the base prompt.
|
|
639
|
+
* @fires Prompt#add
|
|
640
|
+
* @throws {Error} If the message order is invalid or the messages are invalid.
|
|
641
|
+
*/
|
|
642
|
+
addMultiModal(role, messages, sticky = false) {
|
|
643
|
+
this.#basicMessageOrderCheck(role, sticky);
|
|
644
|
+
const content = [];
|
|
645
|
+
messages.forEach((msg) => {
|
|
646
|
+
isValidMessage(role, msg);
|
|
647
|
+
content.push(msg);
|
|
648
|
+
});
|
|
649
|
+
if (content.length === 0) {
|
|
650
|
+
throw new Error('No messages found');
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* @event Prompt#add
|
|
654
|
+
* @type {Message}
|
|
655
|
+
*/
|
|
656
|
+
this.#add({ role, content, sticky });
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Removes all non-sticky messages and returns them
|
|
661
|
+
* Keeps the 'base' (sticky) prompt/messages.
|
|
662
|
+
* @returns {Message[]}
|
|
663
|
+
*/
|
|
664
|
+
reset() {
|
|
665
|
+
this.#tokenCount = 0;
|
|
666
|
+
const systemArray = this.#messages.filter(item => item.sticky === true);
|
|
667
|
+
const conversationArray = this.#messages.filter(item => item.sticky === false);
|
|
668
|
+
this.#messages = systemArray;
|
|
669
|
+
this.emit(EVENTS.reset, EVENTS.reset);
|
|
670
|
+
return conversationArray;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Register a record from the adapter
|
|
675
|
+
* @param {Record} record - Records for billing and optimizing
|
|
676
|
+
* @throws {Error} If the record is invalid.
|
|
677
|
+
*/
|
|
678
|
+
addRecord(record) {
|
|
679
|
+
isValidRecord(record);
|
|
680
|
+
this.#tokenCount = record.tokensIn + record.tokensOut
|
|
681
|
+
this.emit(EVENTS.record, record);
|
|
682
|
+
if (this.#tokenCount >= this.#contextWindow) {
|
|
683
|
+
this.emit(EVENTS.warning, `The number of tokens: ${this.#tokenCount} is exceeding the contextWindow: ${this.#contextWindow}`);
|
|
684
|
+
// Need to truncate
|
|
685
|
+
// 1. delete all satisfied function requests
|
|
686
|
+
// 2. run truncate
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Add a list of records (with multiple prompts to maintain a full history of events)
|
|
692
|
+
* @param {Record[]} records - Records for billing and optimizing
|
|
693
|
+
* @throws {Error} If the record is invalid.
|
|
694
|
+
*/
|
|
695
|
+
addRecords(records) {
|
|
696
|
+
records.forEach((record) => {
|
|
697
|
+
this.addRecord(record)
|
|
698
|
+
})
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Template record
|
|
702
|
+
* @returns {Record}
|
|
703
|
+
*/
|
|
704
|
+
templateRecord() {
|
|
705
|
+
const record = {
|
|
706
|
+
id: '',
|
|
707
|
+
isoDate: '',
|
|
708
|
+
duration: 0,
|
|
709
|
+
model: '',
|
|
710
|
+
environment: '',
|
|
711
|
+
tokensIn: 0,
|
|
712
|
+
tokensOut: 0
|
|
713
|
+
}
|
|
714
|
+
return record
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Gather metrics as info
|
|
718
|
+
*/
|
|
719
|
+
info() {
|
|
720
|
+
const cwd = process.cwd();
|
|
721
|
+
const tools = this.toolsetFunctions().join(', ');
|
|
722
|
+
const context = this.contextLength;
|
|
723
|
+
const tokens = this.countTokens();
|
|
724
|
+
return `Cwd: ${cwd}\nTools: ${tools}\nMax context: ${context}\nUsed Tokens: ${tokens}`
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
}
|
|
728
|
+
export default Prompt;
|