@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
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [api documentation](https://docs.x.ai/docs/overview)
|
|
3
|
+
*/
|
|
4
|
+
import { GLOBAL } from '../../fafs.js'
|
|
5
|
+
import { request as doRequest } from '@j-o-r/apiserver';
|
|
6
|
+
import { sleep } from '@j-o-r/sh';
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import('../../Prompt.js').default} Prompt
|
|
9
|
+
* @typedef {import('../../ToolSet.js').default} ToolSet
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {"grok-4"|"grok-3"|"grok-3-mini"|"grok-3-fast"|"grok-3-mini-fast"} grokModels
|
|
13
|
+
* @typedef {"low"|"high"} grokReason
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} SearchParameters
|
|
17
|
+
* @property {"on"|"auto"} mode
|
|
18
|
+
* @property {string} [from_date] ISO-8601 date (YYYY-MM-DD)
|
|
19
|
+
* @property {string} [to_date] ISO-8601 date (YYYY-MM-DD)
|
|
20
|
+
* @property {(RssSource|WebSource|NewsSource|XSource)[]} [sources]
|
|
21
|
+
*
|
|
22
|
+
* @typedef {Object} RssSource
|
|
23
|
+
* @property {"rss"} type
|
|
24
|
+
* @property {string[]} links
|
|
25
|
+
*
|
|
26
|
+
* @typedef {Object} WebSource
|
|
27
|
+
* @property {"web"} type
|
|
28
|
+
* @property {string} [country] ISO-3166-1 alpha-2
|
|
29
|
+
* @property {boolean} [safe_search=true]
|
|
30
|
+
* @property {string[]} [allowed_websites] // cannot coexist with excluded_websites
|
|
31
|
+
* @property {string[]} [excluded_websites]
|
|
32
|
+
*
|
|
33
|
+
* @typedef {Object} NewsSource
|
|
34
|
+
* @property {"news"} type
|
|
35
|
+
* @property {string} [country]
|
|
36
|
+
* @property {boolean} [safe_search=true]
|
|
37
|
+
* @property {string[]} [allowed_websites]
|
|
38
|
+
* @property {string[]} [excluded_websites]
|
|
39
|
+
*
|
|
40
|
+
* @typedef {Object} XSource
|
|
41
|
+
* @property {"x"} type
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {object} XRequest
|
|
45
|
+
* @property {XOptions} body
|
|
46
|
+
* @property {Headers} headers
|
|
47
|
+
* @property {string} url -
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} XFunctionCall
|
|
51
|
+
* @property {string} name - The name of the function to call.
|
|
52
|
+
* @property {string} arguments - The arguments for the function call in JSON string format.
|
|
53
|
+
* @typedef {Object} XToolCall
|
|
54
|
+
* @property {string} id - The unique identifier for the tool call.
|
|
55
|
+
* @property {string} type - The type of the tool call, e.g., "function".
|
|
56
|
+
* @property {XFunctionCall} function - The function call details.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {Object} XFunctionResponse
|
|
60
|
+
* @property {string} role - must be 'tool'
|
|
61
|
+
* @property {string} content - The arguments for the function call in JSON string format.
|
|
62
|
+
* @property {string} tool_call_id - The unique identifier for the tool call.
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* @typedef {Object} XMessageResponse
|
|
66
|
+
* @property {number} index - The index of the message.
|
|
67
|
+
* @property {Object} message - The message details.
|
|
68
|
+
* @property {string} message.role - The role of the sender (e.g., "assistant").
|
|
69
|
+
* @property {string} message.content - The content of the message.
|
|
70
|
+
* @property {string} [message.reasoning_content] - The reasoning behind the response.
|
|
71
|
+
* @property {?string} message.refusal - Any refusal content, if applicable.
|
|
72
|
+
* @property {string} finish_reason - The reason for finishing (e.g., "stop").
|
|
73
|
+
* @property {XToolCall[]} tools_calls - The reason for finishing (e.g., "stop").
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* https://docs.x.ai/api/endpoints#chat-completions
|
|
78
|
+
* @typedef {object} XOptions
|
|
79
|
+
* @property {grokModels} [model] - What model to use
|
|
80
|
+
* @property {Array<*>} [messages] - the actual prompt
|
|
81
|
+
* @property {XToolCall[]} [tools], callable functions
|
|
82
|
+
* @property {string} [tool_choice] - Default 'none' when no function, 'auto' when present, or required
|
|
83
|
+
* @property {Object} [response_format] - Response format
|
|
84
|
+
* @property {string} [response_format.type] - The role of the sender (e.g., "assistant").
|
|
85
|
+
* @property {number} [temperature] - What sampling temperature to use, between 0 and 2.
|
|
86
|
+
* @property {number} [max_completion_tokens] - The maximum number of tokens allowed for the generated answer.
|
|
87
|
+
* @property {number} [presence_penalty] - Number between -2.0 and 2.0.
|
|
88
|
+
* @property {number} [frequency_penalty] - Number between -2.0 and 2.0.
|
|
89
|
+
* @property {number} [top_p] - Number between -2.0 and 2.0.
|
|
90
|
+
* @property {number} [n] - How many chat completion choices to generate for each input message. default 1
|
|
91
|
+
* @property {boolean} [stream] - Chunk/stream request default null
|
|
92
|
+
* @property {boolean} [parallel_tool_calls] - Multiple calls at once
|
|
93
|
+
* @property {grokReason} [reasoning_effort] - Who i am
|
|
94
|
+
* @property {string} [user] - Who i am
|
|
95
|
+
* @property {number} [seed] - see options
|
|
96
|
+
* @property {SearchParameters} [search_parameters] - live search
|
|
97
|
+
* @property {string[]} [stop] - Up to 4 sequences where the API will stop generating further tokens.
|
|
98
|
+
*/
|
|
99
|
+
const API_URL = 'https://api.x.ai/v1/chat/completions';
|
|
100
|
+
/** @type {XOptions} */
|
|
101
|
+
const defaultSettings = {
|
|
102
|
+
model: 'grok-4',
|
|
103
|
+
messages: [
|
|
104
|
+
{
|
|
105
|
+
role: 'system',
|
|
106
|
+
content: 'Be precise and concise'
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
// temperature: 0.2,
|
|
110
|
+
// response_format: { type: 'text' },
|
|
111
|
+
parallel_tool_calls: true,
|
|
112
|
+
// reasoning_effort: null,
|
|
113
|
+
// max_completion_tokens: 2000,
|
|
114
|
+
// presence_penalty: 0,
|
|
115
|
+
// frequency_penalty: 0,
|
|
116
|
+
// top_p: 1,
|
|
117
|
+
// stop: []
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get the default headers
|
|
123
|
+
* @returns {object}
|
|
124
|
+
*/
|
|
125
|
+
const getHeaders = () => {
|
|
126
|
+
if (!process.env['XAIKEY']) {
|
|
127
|
+
throw new Error('Missing XAIKEY! export XAIKEY=<XAIKEY>')
|
|
128
|
+
}
|
|
129
|
+
const KEY = process.env['XAIKEY'];
|
|
130
|
+
return {
|
|
131
|
+
'Content-Type': 'application/json',
|
|
132
|
+
'Authorization': `Bearer ${KEY}`
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get the text from content
|
|
137
|
+
* @param {import('../../Prompt.js').Content[]} content
|
|
138
|
+
* @returns {string}
|
|
139
|
+
*/
|
|
140
|
+
function getTextFromContent(content) {
|
|
141
|
+
let res = '';
|
|
142
|
+
let i = 0;
|
|
143
|
+
const len = content.length;
|
|
144
|
+
for (; i < len; i++) {
|
|
145
|
+
if (content[i].type === 'text') {
|
|
146
|
+
// @ts-ignore
|
|
147
|
+
res = `${res}\n${content[i].text} `
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return res.trim();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Convert messages to XAI
|
|
154
|
+
* @param {Prompt} prompt
|
|
155
|
+
* @returns {Array<*>}
|
|
156
|
+
*/
|
|
157
|
+
const generateHistory = (prompt) => {
|
|
158
|
+
const messages = prompt.messages
|
|
159
|
+
const result = [];
|
|
160
|
+
let i = 0;
|
|
161
|
+
const len = messages.length;
|
|
162
|
+
for (; i < len; i++) {
|
|
163
|
+
const msg = messages[i];
|
|
164
|
+
const role = msg.role;
|
|
165
|
+
if (['system', 'assistant', 'user'].includes(role)) {
|
|
166
|
+
// Get the text
|
|
167
|
+
const text = getTextFromContent(msg.content);
|
|
168
|
+
const newMesg = {
|
|
169
|
+
role,
|
|
170
|
+
content: text,
|
|
171
|
+
refusal: null
|
|
172
|
+
};
|
|
173
|
+
// Add initial toolcalls
|
|
174
|
+
const toolCalls = msg.content
|
|
175
|
+
.filter(item => item.type === 'function_request')
|
|
176
|
+
.map(req => ({
|
|
177
|
+
// @ts-ignore
|
|
178
|
+
id: req.function_request.id,
|
|
179
|
+
type: "function",
|
|
180
|
+
function: {
|
|
181
|
+
// @ts-ignore
|
|
182
|
+
arguments: req.function_request.parameters,
|
|
183
|
+
// @ts-ignore
|
|
184
|
+
name: req.function_request.name,
|
|
185
|
+
}
|
|
186
|
+
}));
|
|
187
|
+
if (toolCalls.length > 0) {
|
|
188
|
+
newMesg.tool_calls = toolCalls;
|
|
189
|
+
}
|
|
190
|
+
result.push(newMesg);
|
|
191
|
+
} else if (role === 'tool') {
|
|
192
|
+
const content = msg.content;
|
|
193
|
+
let it = 0;
|
|
194
|
+
const lent = content.length;
|
|
195
|
+
for (; it < lent; it++) {
|
|
196
|
+
/** @type {import('../..//Prompt.js').FunctionResponse} */
|
|
197
|
+
// @ts-ignore
|
|
198
|
+
const item = content[it];
|
|
199
|
+
if (item.type === 'function_response') {
|
|
200
|
+
// Add tool function response
|
|
201
|
+
result.push({
|
|
202
|
+
role: 'tool',
|
|
203
|
+
name: item.function_response.name,
|
|
204
|
+
call_id: item.function_response.call_id,
|
|
205
|
+
content: item.function_response.response
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Convert a toolset to something openai understands
|
|
216
|
+
* @param {ToolSet} toolset
|
|
217
|
+
* @returns {XToolCall[]}
|
|
218
|
+
*/
|
|
219
|
+
const generateXAIToolCalls = (toolset) => {
|
|
220
|
+
const list = toolset.list();
|
|
221
|
+
/** @type {XToolCall[]} */
|
|
222
|
+
const result = [];
|
|
223
|
+
list.forEach((item) => {
|
|
224
|
+
result.push({
|
|
225
|
+
type: 'function', function: {
|
|
226
|
+
name: item.name,
|
|
227
|
+
// @ts-ignore
|
|
228
|
+
description: item.description,
|
|
229
|
+
parameters: item.parameters
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
});
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Create an anthropic request
|
|
238
|
+
* @param {Prompt} prompt
|
|
239
|
+
* @param {ToolSet|void} [tools]
|
|
240
|
+
* @param {XOptions} [opts] overwrite default request settings
|
|
241
|
+
* @param {object} [hdrs] - optional headers to pass
|
|
242
|
+
* @returns {XRequest}
|
|
243
|
+
* @throws {Error}
|
|
244
|
+
*/
|
|
245
|
+
const generateRequest = (prompt, tools, opts = {}, hdrs = {}) => {
|
|
246
|
+
/** @type {XOptions} */
|
|
247
|
+
const body = { ...defaultSettings, ...opts };
|
|
248
|
+
const headers = { ...getHeaders(), ...hdrs };
|
|
249
|
+
// Sanity check
|
|
250
|
+
// basePromptCheck(prompt);
|
|
251
|
+
// body.messages = generateXAIMessages(prompt) || [];
|
|
252
|
+
body.messages = generateHistory(prompt);
|
|
253
|
+
if (body.messages.length == 0) {
|
|
254
|
+
throw new Error('No messages found');
|
|
255
|
+
}
|
|
256
|
+
body.tools = (tools) ? generateXAIToolCalls(tools) : [];
|
|
257
|
+
body.tool_choice = (tools) ? tools.toolChoice : '';
|
|
258
|
+
if (body.tools.length === 0) {
|
|
259
|
+
delete body.tools;
|
|
260
|
+
delete body.tool_choice;
|
|
261
|
+
}
|
|
262
|
+
return { body, headers, url: API_URL };
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// RESPONSE PARSER
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @typedef {Object} XResponse
|
|
269
|
+
* @property {string} id - The unique identifier for the response.
|
|
270
|
+
* @property {string} object - The type of object returned, typically "chat.completion".
|
|
271
|
+
* @property {number} created - The timestamp of when the response was created.
|
|
272
|
+
* @property {string} model - The model used to generate the response.
|
|
273
|
+
* @property {Array<XChoice>} choices - An array of choice objects containing the response details.
|
|
274
|
+
* @property {XUsage} usage - An object containing token usage information.
|
|
275
|
+
* @property {string} system_fingerprint - The system fingerprint for the response.
|
|
276
|
+
* @property {Object} http_headers - HTTP headers associated with the response.
|
|
277
|
+
*/
|
|
278
|
+
/**
|
|
279
|
+
* @typedef {Object} XChoice
|
|
280
|
+
* @property {number} index - The index of the choice in the response.
|
|
281
|
+
* @property {XMessageResponse} message - The message object containing the role and content.
|
|
282
|
+
* @property {null} logprobs - Log probabilities, typically null.
|
|
283
|
+
* @property {string} finish_reason - The reason why the response finished.
|
|
284
|
+
*/
|
|
285
|
+
/**
|
|
286
|
+
* @typedef {Object} XUsage
|
|
287
|
+
* @property {number} prompt_tokens - The number of tokens in the prompt.
|
|
288
|
+
* @property {number} completion_tokens - The number of tokens in the completion.
|
|
289
|
+
* @property {number} total_tokens - The total number of tokens used.
|
|
290
|
+
*/
|
|
291
|
+
|
|
292
|
+
const parseMessages = (response, prompt) => {
|
|
293
|
+
/** @type {import('../../Prompt.js').FunctionRequest[]} */
|
|
294
|
+
const function_requests = [];
|
|
295
|
+
const messages = response.choices;
|
|
296
|
+
if (response.citations && response.citations.length > 0) {
|
|
297
|
+
const cits = response.citations.map((item, index) => `${index + 1}. ${item}`).join('\n');
|
|
298
|
+
prompt.add('log', cits);
|
|
299
|
+
}
|
|
300
|
+
let i = 0;
|
|
301
|
+
const len = messages.length;
|
|
302
|
+
for (; i < len; i++) {
|
|
303
|
+
const msg = messages[i];
|
|
304
|
+
if (msg.message.reasoning_content) {
|
|
305
|
+
// Add reasoning content
|
|
306
|
+
// Handy for debugging prompts
|
|
307
|
+
prompt.add('reasoning', msg.message.reasoning_content);
|
|
308
|
+
}
|
|
309
|
+
if (msg.refusal) {
|
|
310
|
+
// Wrong question in relation to policies
|
|
311
|
+
prompt.add('assistant', msg.refusal);
|
|
312
|
+
// console.log(msg);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// Add a text message
|
|
316
|
+
if (
|
|
317
|
+
msg.message.role === 'assistant' &&
|
|
318
|
+
typeof msg.message.content === 'string') {
|
|
319
|
+
prompt.add('assistant', msg.message.content);
|
|
320
|
+
}
|
|
321
|
+
// Get function request
|
|
322
|
+
if (msg.message.tool_calls) {
|
|
323
|
+
for (const toolCall of msg.message.tool_calls) {
|
|
324
|
+
function_requests.push(
|
|
325
|
+
{
|
|
326
|
+
type: 'function_request',
|
|
327
|
+
function_request: {
|
|
328
|
+
name: toolCall.function.name,
|
|
329
|
+
id: toolCall.id,
|
|
330
|
+
call_id: toolCall.id,
|
|
331
|
+
parameters: toolCall.function.arguments
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (function_requests.length > 0) {
|
|
339
|
+
prompt.addMultiModal('assistant', function_requests);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Process an openai response
|
|
345
|
+
* @param {number} duration - the time is MS before and after the request
|
|
346
|
+
* @param {import('lib/request.js').FetchResponse} res
|
|
347
|
+
* @param {Prompt} prompt
|
|
348
|
+
* @param {ToolSet} [toolset]
|
|
349
|
+
* @returns {Promise<void>}
|
|
350
|
+
* @throws {Error}
|
|
351
|
+
*/
|
|
352
|
+
const parseResponse = async (duration, prompt, res, toolset) => {
|
|
353
|
+
// @todo - Put some effort in the parsing, this is quick and dirty
|
|
354
|
+
if (res.status !== 200) {
|
|
355
|
+
new Error(`${res.status}: res.response`)
|
|
356
|
+
}
|
|
357
|
+
const record = prompt.templateRecord();
|
|
358
|
+
record.model = res.response.model;
|
|
359
|
+
record.id = res.response.id;
|
|
360
|
+
record.environment = 'openai';
|
|
361
|
+
record.isoDate = new Date(res.response.created * 1000).toISOString();
|
|
362
|
+
record.tokensIn = res.response.usage.prompt_tokens;
|
|
363
|
+
record.tokensOut = res.response.usage.completion_tokens;
|
|
364
|
+
record.duration = duration;
|
|
365
|
+
prompt.addRecord(record);
|
|
366
|
+
parseMessages(res.response, prompt);
|
|
367
|
+
if (toolset) {
|
|
368
|
+
// Inspect for function_request and execute if so
|
|
369
|
+
await toolset.execute(prompt);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Do a request
|
|
375
|
+
* @param {Prompt} prompt
|
|
376
|
+
* @param {ToolSet|null} toolset
|
|
377
|
+
* @param {XOptions} [options]:
|
|
378
|
+
* @param {number} [counter] - leave empty this counts the number of requests when doing recursive request calls
|
|
379
|
+
* @return {Promise<import('../../Session.js').Message>}
|
|
380
|
+
*/
|
|
381
|
+
async function request(prompt, toolset = null, options, counter = 0) {
|
|
382
|
+
// Max number of recurusive calls
|
|
383
|
+
const counterMax = GLOBAL.max_recursive_requests;
|
|
384
|
+
const start = new Date().getTime();
|
|
385
|
+
// Generate the request
|
|
386
|
+
const { url, headers, body } = generateRequest(prompt, toolset, options);
|
|
387
|
+
prompt.emit(prompt.EVENTS.http_request, {url, counter, body});
|
|
388
|
+
const res = await doRequest(url, 'POST', headers, body);
|
|
389
|
+
prompt.emit(prompt.EVENTS.http_response, res);
|
|
390
|
+
if (res.status !== 200) {
|
|
391
|
+
throw new Error(`${res.status}: ${JSON.stringify(res.response, null, ' ')}`)
|
|
392
|
+
}
|
|
393
|
+
const duration = new Date().getTime() - start;
|
|
394
|
+
await parseResponse(duration, prompt, res, toolset);
|
|
395
|
+
counter = 1 + counter;
|
|
396
|
+
if (counter >= counterMax) {
|
|
397
|
+
throw new Error('Max number of recursive calls reached');
|
|
398
|
+
}
|
|
399
|
+
const lastMessage = prompt.getLastMessage();
|
|
400
|
+
if (!lastMessage) throw new Error('No message found');
|
|
401
|
+
const role = lastMessage.role;
|
|
402
|
+
if (role === 'tool') {
|
|
403
|
+
// need to do another roundtrip to include the function_responses
|
|
404
|
+
// Go easy on the speed, do not hit limits to soon
|
|
405
|
+
await sleep('30s');
|
|
406
|
+
return request(prompt, toolset, options, counter)
|
|
407
|
+
} else {
|
|
408
|
+
return lastMessage
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
export {
|
|
412
|
+
generateRequest,
|
|
413
|
+
parseResponse,
|
|
414
|
+
request
|
|
415
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Websocket client for AI sessions
|
|
3
|
+
*/
|
|
4
|
+
import { WebSocketClient } from "@j-o-r/apiserver";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {import('./API/openai.com/reponses/text.js').request} OARequest
|
|
8
|
+
* @typedef {import('./API/x.ai/text.js').request} XRequest
|
|
9
|
+
* @typedef {import('./API/anthropic.com/text.js').request} ANTHRequest
|
|
10
|
+
*
|
|
11
|
+
* @typedef {import('./API/x.ai/text.js').XOptions} XOptions
|
|
12
|
+
* @typedef {import('./API/openai.com/reponses/text.js').OAOptions} OAOptions
|
|
13
|
+
* @typedef {import('./API/anthropic.com/text.js').ANTHOptions} ANTHOptions
|
|
14
|
+
*
|
|
15
|
+
* @typedef {import('./Prompt.js').default} Prompt
|
|
16
|
+
* @typedef {import('./ToolSet.js').default} ToolSet
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} WSOptions
|
|
20
|
+
* @property {Prompt} prompt - The prompt session
|
|
21
|
+
* @property {ToolSet} [toolset] - The toolset
|
|
22
|
+
* @property {string} description - Custom introduction message.
|
|
23
|
+
* @property {string} name - Logical name: e.g. search, code, os, support.
|
|
24
|
+
* @property {string} [url='ws://127.0.0.1:8000/ws?params=1234']
|
|
25
|
+
* @property {number} [intervalMs=250] - How often to pull one message from the queue.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Websocket Client
|
|
29
|
+
* Wrapper combining a session and a websocket
|
|
30
|
+
*/
|
|
31
|
+
class AgentClient {
|
|
32
|
+
/** @type {Prompt} */
|
|
33
|
+
#prompt;
|
|
34
|
+
#description = '';
|
|
35
|
+
#name;
|
|
36
|
+
/** @type {WebSocketClient} */
|
|
37
|
+
#ws;
|
|
38
|
+
#url = 'ws://127.0.0.1:8000/ws';
|
|
39
|
+
|
|
40
|
+
// Message queue and processing flag to ensure one-at-a-time handling
|
|
41
|
+
/** @type {Array<any>} */
|
|
42
|
+
#queue = [];
|
|
43
|
+
#processing = false;
|
|
44
|
+
// Bump this to invalidate in-flight work (hard reset)
|
|
45
|
+
#epoch = 0;
|
|
46
|
+
|
|
47
|
+
// Interval-based scheduler (no tight while-loop)
|
|
48
|
+
#intervalId = null;
|
|
49
|
+
#intervalMs = 2000;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates an instance of CLILoader.
|
|
53
|
+
* @param {WSOptions} options - The options to configure the CLILoader.
|
|
54
|
+
*/
|
|
55
|
+
constructor(options) {
|
|
56
|
+
if (options.url) this.#url = options.url;
|
|
57
|
+
this.#prompt = options.prompt;
|
|
58
|
+
this.#description = options.description;
|
|
59
|
+
this.#name = options.name;
|
|
60
|
+
if (typeof options.intervalMs === 'number') this.#intervalMs = Math.max(1, options.intervalMs);
|
|
61
|
+
this.#registerPromptEvents();
|
|
62
|
+
this._start();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Informative.
|
|
66
|
+
* See what is happening
|
|
67
|
+
*/
|
|
68
|
+
#registerPromptEvents () {
|
|
69
|
+
const events = Object.keys(this.#prompt.EVENTS);
|
|
70
|
+
events.forEach((evt) => {
|
|
71
|
+
this.#prompt.on(evt, (_msg) => {
|
|
72
|
+
if (evt === 'tool_error') {
|
|
73
|
+
console.log(JSON.stringify(_msg, null, ' '));
|
|
74
|
+
}
|
|
75
|
+
console.log(evt);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
_start() {
|
|
80
|
+
this.#ws = new WebSocketClient(this.#url); // Match server port/path
|
|
81
|
+
this.#ws.onopen = () => {
|
|
82
|
+
// Send my introduction to the server
|
|
83
|
+
this.#ws.send(JSON.stringify({
|
|
84
|
+
action: 'agent_introduction',
|
|
85
|
+
name: this.#name,
|
|
86
|
+
content: this.#description,
|
|
87
|
+
id: new Date().getTime()
|
|
88
|
+
}));
|
|
89
|
+
console.log('INIT:>>')
|
|
90
|
+
console.log(this.#name);
|
|
91
|
+
console.log(this.#description);
|
|
92
|
+
};
|
|
93
|
+
this.#ws.onmessage = (m) => {
|
|
94
|
+
// Enqueue messages; processing happens sequentially
|
|
95
|
+
this.incoming(m);
|
|
96
|
+
};
|
|
97
|
+
this.#ws.onclose = () => {
|
|
98
|
+
console.error('CLOSE: Lost Connection, retry in 5 seconds');
|
|
99
|
+
// Stop ticking while disconnected
|
|
100
|
+
this.#stopInterval();
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
this._start();
|
|
103
|
+
}, 5000)
|
|
104
|
+
};
|
|
105
|
+
this.#ws.onerror = (e) => {
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Enqueue an incoming websocket message and trigger the processor.
|
|
111
|
+
*/
|
|
112
|
+
incoming(e) {
|
|
113
|
+
const message = JSON.parse(e.data);
|
|
114
|
+
// Handle resets immediately: do not enqueue, just clear the queue and reset the prompt
|
|
115
|
+
if (message?.action === "reset") {
|
|
116
|
+
console.log("<RESET:incoming>");
|
|
117
|
+
// Invalidate in-flight work
|
|
118
|
+
this.#epoch++;
|
|
119
|
+
// Empty the queue immediately
|
|
120
|
+
this.#queue = [];
|
|
121
|
+
// Stop the scheduler; any in-flight handler will finish, but no further items will run
|
|
122
|
+
this.#stopInterval();
|
|
123
|
+
// Reset the prompt/session state
|
|
124
|
+
this.#prompt.reset();
|
|
125
|
+
return; // Do not enqueue this message
|
|
126
|
+
}
|
|
127
|
+
this.#queue.push(message);
|
|
128
|
+
// Start interval-based processing (no while-loop)
|
|
129
|
+
this.#startInterval();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Start the interval that pulls one message at each tick.
|
|
134
|
+
*/
|
|
135
|
+
#startInterval() {
|
|
136
|
+
if (this.#intervalId) return;
|
|
137
|
+
this.#intervalId = setInterval(() => {
|
|
138
|
+
// We intentionally do not await here; #processOne guards reentrancy
|
|
139
|
+
void this.#processOne();
|
|
140
|
+
}, this.#intervalMs);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Stop the interval if it is running. */
|
|
144
|
+
#stopInterval() {
|
|
145
|
+
if (this.#intervalId) {
|
|
146
|
+
clearInterval(this.#intervalId);
|
|
147
|
+
this.#intervalId = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Internal: process at most ONE message. Called on an interval tick.
|
|
153
|
+
*/
|
|
154
|
+
async #processOne() {
|
|
155
|
+
if (this.#processing) return;
|
|
156
|
+
const message = this.#queue.shift();
|
|
157
|
+
if (!message) {
|
|
158
|
+
// Nothing to do; stop ticking until a new message arrives
|
|
159
|
+
this.#stopInterval();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.#processing = true;
|
|
163
|
+
try {
|
|
164
|
+
const epochAtStart = this.#epoch;
|
|
165
|
+
await this.#handleMessage(message, epochAtStart);
|
|
166
|
+
} finally {
|
|
167
|
+
this.#processing = false;
|
|
168
|
+
// If the queue drained, we can stop; else the next tick will pick the next one
|
|
169
|
+
if (this.#queue.length === 0) this.#stopInterval();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Internal: handle a single message
|
|
175
|
+
*/
|
|
176
|
+
async #handleMessage(message, epochAtStart) {
|
|
177
|
+
if (message.action === 'agent_query') {
|
|
178
|
+
const id = message.id;
|
|
179
|
+
try {
|
|
180
|
+
console.log('IN:>>\n' + message.content);
|
|
181
|
+
const content = await this.#prompt.call(message.content);
|
|
182
|
+
console.log('OUT:>>\n' + content);
|
|
183
|
+
this.#ws.send(JSON.stringify({ action: 'agent_response', content, id }));
|
|
184
|
+
} catch (err) {
|
|
185
|
+
this.#ws.send(JSON.stringify({ action: 'agent_error', content: String(err?.message || err), id }));
|
|
186
|
+
}
|
|
187
|
+
} else if (message.action === 'reset') {
|
|
188
|
+
console.log('<RESET>');
|
|
189
|
+
// Empty the que;
|
|
190
|
+
this.#queue = [];
|
|
191
|
+
this.#epoch++;
|
|
192
|
+
this.#prompt.reset();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export default AgentClient;
|