halbot 1994.1.8 → 1995.1.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/lib/hal.mjs CHANGED
@@ -1,46 +1,26 @@
1
- import {
2
- bot, callosum, dbio, media, speech, storage, uoid, utilitas, web,
3
- } from 'utilitas';
4
-
5
- import { basename, join } from 'path';
1
+ import { alan, bot, callosum, dbio, storage, utilitas, } from 'utilitas';
2
+ import { join } from 'path';
6
3
  import { parseArgs as _parseArgs } from 'node:util';
7
4
  import { readdirSync } from 'fs';
5
+ import { ignoreErrFunc } from 'utilitas/lib/utilitas.mjs';
8
6
 
9
- const _NEED = ['lorem-ipsum', 'mime'];
10
7
  // 👇 https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this
11
- const table = 'utilitas_hal_events';
12
- const [PRIVATE_LIMIT, GROUP_LIMIT] = [60 / 60, 60 / 20].map(x => x * 1000);
13
- const log = (c, o) => utilitas.log(c, import.meta.url, { time: 1, ...o || {} });
14
- const [end, parse_mode] = [bot.end, bot.parse_mode];
15
- const normalizeKey = chatId => `${HALBOT}_SESSION_${chatId}`;
16
- const lines2 = arr => bot.lines(arr, '\n\n');
17
- const uList = arr => bot.lines(arr.map(x => `- ${x}`));
18
- const oList = arr => bot.lines(arr.map((v, k) => `${k + 1}. ${v}`));
19
- const isMarkdownError = e => e?.description?.includes?.("can't parse entities");
20
- const getFile = async (id, op) => (await web.get(await getFileUrl(id), op)).content;
21
- const compact = (str, op) => utilitas.ensureString(str, { ...op || {}, compact: true });
22
- const compactLimit = (str, op) => compact(str, { ...op || {}, limit: 140 });
23
- const getKey = s => s?.toLowerCase?.()?.startsWith?.('http') ? 'url' : 'source';
24
- const SEARCH_LIMIT = 10;
25
-
26
8
  const [ // https://limits.tginfo.me/en
27
- HELLO, GROUP, PRIVATE, CHANNEL, MENTION, CALLBACK_LIMIT, API_ROOT,
28
- jsonOptions, sessions, HALBOT, COMMAND_REGEXP, COMMAND_LENGTH,
29
- COMMAND_LIMIT, COMMAND_DESCRIPTION_LENGTH, bot_command, EMOJI_SPEECH,
30
- EMOJI_LOOK, EMOJI_BOT, logOptions, ON, OFF,
9
+ HELLO, GROUP, PRIVATE, CHANNEL, MENTION, jsonOptions, COMMAND_LIMIT, ON,
10
+ OFF, BOT_COMMAND, CHECK, logOptions, COMMAND_LENGTH,
11
+ COMMAND_DESCRIPTION_LENGTH, RELEVANCE, SEARCH_LIMIT, SUB_LIMIT,
31
12
  ] = [
32
- 'Hello!', 'group', 'private', 'channel', 'mention', 30,
33
- 'https://api.telegram.org/', { code: true, extraCodeBlock: 1 }, {},
34
- 'HALBOT', /^\/([a-z0-9_]+)(@([a-z0-9_]*))?\ ?(.*)$/sig, 32, 100, 256,
35
- 'bot_command', '👂', '👀', '🤖', { log: true }, 'on', 'off',
13
+ 'Hello!', 'group', 'private', 'channel', 'mention',
14
+ { code: true, extraCodeBlock: 1 }, 100, 'on', 'off', 'bot_command',
15
+ '☑️', { log: true }, 32, 256, 0.2, 10, 200, // Google Rerank limit
36
16
  ];
37
17
 
38
- const [BUFFER_ENCODE, BINARY_STRINGS] = [{ encode: storage.BUFFER }, [OFF, ON]];
39
-
40
- const KNOWN_UPDATE_TYPES = [
41
- 'callback_query', 'channel_post', 'edited_message', 'message',
42
- 'my_chat_member', // 'inline_query',
43
- ];
18
+ const table = 'utilitas_hal_events';
19
+ const log = (c, o) => utilitas.log(c, import.meta.url, { time: 1, ...o || {} });
20
+ const [end] = [bot.end];
21
+ const uList = arr => bot.lines(arr.map(x => `- ${x}`));
22
+ const oList = arr => bot.lines(arr.map((v, k) => `${k + 1}. ${v}`));
23
+ const [BINARY_STRINGS] = [[OFF, ON]];
44
24
 
45
25
  const initSql = {
46
26
  [dbio.MYSQL]: [[
@@ -120,982 +100,271 @@ const initSql = {
120
100
  ]],
121
101
  };
122
102
 
123
- let hal, mime, lorem;
124
-
125
- const getExtra = (ctx, options) => {
126
- const resp = {
127
- reply_parameters: {
128
- message_id: ctx.chatType === PRIVATE ? undefined : ctx.messageId,
129
- }, disable_notification: !!ctx.done.length, ...options || {},
130
- };
131
- resp.reply_markup || (resp.reply_markup = {});
132
- if (options?.buttons?.length) {
133
- resp.reply_markup.inline_keyboard = options?.buttons.map(row =>
134
- utilitas.ensureArray(row).map(button => {
135
- if (button.url) {
136
- return { text: button.label, url: button.url };
137
- } else if (button.text) {
138
- const id = uoid.fakeUuid(button.text);
139
- ctx.session.callback.push({ id, ...button });
140
- return {
141
- text: button.label,
142
- callback_data: JSON.stringify({ callback: id }),
143
- };
144
- } else {
145
- utilitas.throwError('Invalid button markup.');
146
- }
147
- })
148
- );
149
- } else if (options?.keyboards) {
150
- if (options.keyboards.length) {
151
- resp.reply_markup.keyboard = options?.keyboards.map(utilitas.ensureArray);
152
- } else { resp.reply_markup.remove_keyboard = true; }
153
- }
154
- return resp;
155
- };
156
-
157
- const getFileUrl = async (file_id) => {
158
- assert(file_id, 'File ID is required.', 400);
159
- const file = await (await init()).telegram.getFile(file_id);
160
- assert(file.file_path, 'Error getting file info.', 500);
161
- return `${API_ROOT}file/bot${hal.token}/${file.file_path}`;
162
- };
103
+ let _;
163
104
 
164
- const officeParser = async file => await utilitas.ignoreErrFunc(
165
- async () => await vision.parseOfficeFile(file, { input: storage.BUFFER }), { log: true }
166
- );
105
+ const newCommand = (command, description) => ({
106
+ command: utilitas.ensureString(command, { case: 'SNAKE' }).slice(0, COMMAND_LENGTH),
107
+ description: utilitas.trim(description).slice(0, COMMAND_DESCRIPTION_LENGTH),
108
+ });
167
109
 
168
110
  const json = obj => bot.lines([
169
111
  '```json', utilitas.prettyJson(obj, jsonOptions).replaceAll('```', ''), '```'
170
112
  ]);
171
113
 
172
- const sessionGet = async chatId => {
173
- const key = normalizeKey(chatId);
174
- sessions[chatId] || (sessions[chatId] = (
175
- hal._.session.get && await hal._.session.get(key) || {}
114
+ const establish = module => {
115
+ assert(module?.func, 'Pipeline function is required.', 500);
116
+ _.pipeline[module.name || (module.name = uuidv4())] = {
117
+ help: module.help || '', args: module.args || {},
118
+ hidden: !!module.hidden,
119
+ };
120
+ _.args = { ..._.args, ...module.args || {} };
121
+ Object.keys(module?.cmds || {}).map(command => _.cmds.push(
122
+ newCommand(command, module?.cmds[command])
176
123
  ));
177
- sessions[chatId].callback || (sessions[chatId].callback = []);
178
- sessions[chatId].config || (sessions[chatId].config = {});
179
- return sessions[chatId];
124
+ log(`Establishing: ${module.name} (${module.priority})`, { force: true });
125
+ return _.bot.use(utilitas.countKeys(module.cmds) ? (async (ctx, next) =>
126
+ await (utilitas.insensitiveHas(
127
+ Object.keys(module.cmds), ctx._?.cmd?.cmd
128
+ ) || module.run ? module.func(ctx, next) : next())
129
+ ) : module.func);
180
130
  };
181
131
 
182
- const sessionSet = async chatId => {
183
- const key = normalizeKey(chatId);
184
- while (sessions[chatId]?.callback?.length > CALLBACK_LIMIT) {
185
- sessions[chatId].callback.shift();
132
+ const parseArgs = async (args, ctx) => {
133
+ const { values, tokens } = _parseArgs({
134
+ args: utilitas.splitArgs((args || '').replaceAll('—', '--')),
135
+ options: _.args, tokens: true
136
+ });
137
+ const result = {};
138
+ for (let x of tokens) {
139
+ result[x.name] = _.args[x.name]?.validate
140
+ ? await _.args[x.name].validate(values[x.name], ctx)
141
+ : values[x.name];
186
142
  }
187
- const toSet = {};
188
- Object.keys(sessions[chatId]).filter(x => /^[^_]+$/g.test(x)).map(
189
- x => toSet[x] = sessions[chatId][x]
190
- );
191
- return hal._.session.set && await hal._.session.set(key, toSet);
143
+ return result;
192
144
  };
193
145
 
194
- const newCommand = (command, description) => ({
195
- command: utilitas.ensureString(command, { case: 'SNAKE' }).slice(0, COMMAND_LENGTH),
196
- description: utilitas.trim(description).slice(0, COMMAND_DESCRIPTION_LENGTH),
197
- });
146
+ const packMessage = (messages) => messages.map(x => ({
147
+ message_id: x.message_id, score: x.score,
148
+ request: x.received_text, response: x.response_text,
149
+ }));
198
150
 
199
- const uptime = () => {
200
- let resp = `${utilitas.getTimeIcon(new Date())} ${new Date().toTimeString(
201
- ).split(' ')[0].split(':').slice(0, 2).join(':')} up`;
202
- let seconds = process.uptime();
203
- const ss = Object.keys(sessions);
204
- const days = Math.floor(seconds / (3600 * 24));
205
- seconds -= days * 3600 * 24;
206
- let hours = Math.floor(seconds / 3600);
207
- seconds -= hours * 3600;
208
- hours = hours.toString().padStart(2, '0');
209
- let minutes = Math.floor(seconds / 60);
210
- minutes = minutes.toString().padStart(2, '0');
211
- seconds = Math.floor(seconds % 60).toString().padStart(2, '0');
212
- days > 0 && (resp += ` ${days} day${days > 1 ? 's' : ''},`);
213
- return `${resp} ${hours}:${minutes}:${seconds}, `
214
- + `${ss.length} session${ss.length > 1 ? 's' : ''}`;
215
- };
216
-
217
- const reply = async (ctx, md, text, extra) => {
218
- // if (ctx.type === 'inline_query') {
219
- // return await ctx.answerInlineQuery([{}, {}]);
220
- // }
221
- if (md) {
222
- try {
223
- return await (extra?.reply_parameters?.message_id
224
- ? ctx.replyWithMarkdown(text, { parse_mode, ...extra })
225
- : ctx.sendMessage(text, { parse_mode, ...extra }));
226
- } catch (err) { // utilitas.throwError('Error sending message.');
227
- isMarkdownError(err) || log(err);
228
- await ctx.timeout();
229
- }
151
+ const recall = async (sessionId, keyword, offset = 0, limit = SEARCH_LIMIT, options = {}) => {
152
+ assert(sessionId, 'Session ID is required.');
153
+ let [result, _limit, exclude] = [
154
+ [], _.rerank ? SUB_LIMIT : limit,
155
+ (options?.exclude || []).map(x => `${~~x}`),
156
+ ];
157
+ if (!keyword) { return result; }
158
+ switch (_.storage?.provider) {
159
+ case dbio.MYSQL:
160
+ result = await _.storage?.client?.query?.(
161
+ 'SELECT *, MATCH(`distilled`) '
162
+ + 'AGAINST(? IN NATURAL LANGUAGE MODE) AS `relevance` '
163
+ + "FROM ?? WHERE `bot_id` = ? AND `chat_id` = ? "
164
+ + "AND `received_text` != '' "
165
+ + "AND `received_text` NOT LIKE '/%' "
166
+ + "AND `response_text` != '' HAVING relevance > 0 "
167
+ + (exclude.length ? `AND \`message_id\` NOT IN (${exclude.join(',')}) ` : '')
168
+ + 'ORDER BY `relevance` DESC '
169
+ + `LIMIT ${_limit} OFFSET ?`,
170
+ [keyword, table, _.bot.botInfo.id, sessionId, offset]
171
+ );
172
+ break;
173
+ case dbio.POSTGRESQL:
174
+ // globalThis.debug = 2;
175
+ const vector = await dbio.encodeVector(await _.embed(keyword));
176
+ result = await _.storage?.client?.query?.(
177
+ `SELECT *, (1 - (distilled_vector <=> $1)) as relevance `
178
+ + `FROM ${table} WHERE bot_id = $2 AND chat_id = $3 `
179
+ + `AND received_text != '' `
180
+ + `AND received_text NOT LIKE '/%' `
181
+ + `AND response_text != '' `
182
+ + (exclude.length ? `AND message_id NOT IN (${exclude.join(',')}) ` : '')
183
+ + `ORDER BY (distilled_vector <=> $1) ASC `
184
+ + `LIMIT ${_limit} OFFSET $4`,
185
+ [vector, _.bot.botInfo.id, sessionId, offset]
186
+ );
187
+ break;
230
188
  }
231
- return await utilitas.ignoreErrFunc(
232
- async () => await (extra?.reply_parameters?.message_id
233
- ? ctx.reply(text, extra) : ctx.sendMessage(text, extra)
234
- ), logOptions
235
- );
189
+ return await rerank(keyword, result, offset, limit, options);
236
190
  };
237
191
 
238
- const editMessageText = async (ctx, md, lastMsgId, text, extra) => {
239
- if (md) {
240
- try {
241
- return await ctx.telegram.editMessageText(
242
- ctx.chatId, lastMsgId, '', text, { parse_mode, ...extra }
192
+ const getContext = async (sessionId, offset = 0, limit = SEARCH_LIMIT, options = {}) => {
193
+ assert(sessionId, 'Session ID is required.');
194
+ let result = [];
195
+ switch (_.storage?.provider) {
196
+ case dbio.MYSQL:
197
+ result = await _.storage?.client?.query?.(
198
+ 'SELECT * FROM ?? WHERE `bot_id` = ? AND `chat_id` = ? '
199
+ + "AND `received_text` != '' "
200
+ + "AND `received_text` NOT LIKE '/%' "
201
+ + "AND `response_text` != '' "
202
+ + `ORDER BY \`created_at\` DESC LIMIT ${limit} OFFSET ?`,
203
+ [table, _.bot.botInfo.id, sessionId, offset]
243
204
  );
244
- } catch (err) { // utilitas.throwError('Error editing message.');
245
- isMarkdownError(err) || log(err);
246
- await ctx.timeout();
247
- }
205
+ break;
206
+ case dbio.POSTGRESQL:
207
+ result = await _.storage?.client?.query?.(
208
+ `SELECT * FROM ${table} WHERE bot_id = $1 AND chat_id = $2 `
209
+ + `AND received_text != '' `
210
+ + `AND received_text NOT LIKE '/%' `
211
+ + `AND response_text != '' `
212
+ + `ORDER BY created_at DESC LIMIT ${limit} OFFSET $3`,
213
+ [_.bot.botInfo.id, sessionId, offset]
214
+ );
215
+ break;
248
216
  }
249
- return await utilitas.ignoreErrFunc(async () => await ctx.telegram.editMessageText(
250
- ctx.chatId, lastMsgId, '', text, extra
251
- ), logOptions);
217
+ return packMessage(result);
252
218
  };
253
219
 
254
- const memorize = async (ctx) => {
255
- if (ctx._skipMemorize) { return; }
256
- const received = ctx.update;
257
- const received_text = ctx.txt || ''; // ATTACHMENTS
258
- const id = received.update_id;
259
- const response = utilitas.lastItem(ctx.done.filter(x => x?.text)) || {};
260
- const response_text = response?.text || '';
261
- const collected = ctx.collected.filter(x => String.isString(x.content));
262
- const distilled = compact(bot.lines([
263
- received_text, response_text, ...collected.map(x => x.content)
264
- ]));
265
- if (!ctx.messageId || !distilled) { return; }
266
- const event = {
267
- id, bot_id: ctx.botInfo.id, chat_id: ctx.chatId,
268
- chat_type: ctx.chatType, message_id: ctx.messageId,
269
- received: JSON.stringify(received), received_text,
270
- response: JSON.stringify(response), response_text,
271
- collected: JSON.stringify(collected), distilled,
272
- };
273
- return await utilitas.ignoreErrFunc(async () => {
274
- event.distilled_vector = hal._.embedding
275
- ? await hal._.embedding(event.distilled) : [];
276
- switch (hal._.database.provider) {
277
- case dbio.MYSQL:
278
- event.distilled_vector = JSON.stringify(event.distilled_vector);
279
- break;
220
+ const rerank = async (keyword, result, offset = 0, limit = SEARCH_LIMIT, options = {}) => {
221
+ if (result.length && _.rerank) {
222
+ const keys = {};
223
+ const _result = [];
224
+ for (const x of result) {
225
+ if (!keys[x.distilled]) {
226
+ keys[x.distilled] = true;
227
+ _result.push(x);
228
+ }
280
229
  }
281
- await hal._.database?.client?.upsert?.(table, event, { skipEcho: true });
282
- return hal._.memorize && await hal._.memorize(event);
283
- }, logOptions);
230
+ const resp = await _.rerank(keyword, _result.map(x => x.distilled));
231
+ resp.map(x => result[x.index].score = x.score);
232
+ result.sort((a, b) => b.score - a.score);
233
+ result = result.filter(
234
+ x => x.score > RELEVANCE
235
+ ).slice(offset, offset + limit);
236
+ }
237
+ return packMessage(result);
284
238
  };
285
239
 
286
- // https://stackoverflow.com/questions/50204633/allow-bot-to-access-telegram-group-messages
287
- const subconscious = [{
288
- run: true, priority: -8960, name: 'broca', func: async (ctx, next) => {
289
- const e = `Event: ${ctx.update.update_id} => ${JSON.stringify(ctx.update)}`;
290
- process.stdout.write(`[HAL] ${e}\n`);
291
- log(e);
292
- ctx.done = [];
293
- ctx.collected = [];
294
- ctx.timeout = async () => await utilitas.timeout(ctx.limit);
295
- ctx.hello = str => {
296
- str = str || ctx.session?.config?.hello || hal._.hello;
297
- ctx.collect(str, null, { refresh: true });
298
- return str;
299
- };
300
- ctx.shouldReply = async text => {
301
- const should = utilitas.insensitiveHas(ctx._?.chatType, ctx.chatType)
302
- || ctx.session?.config?.chatty;
303
- should && text && await ctx.ok(text);
304
- return should;
305
- };
306
- ctx.checkSpeech = () => ctx.chatType === PRIVATE
307
- ? ctx.session.config?.tts !== false
308
- : ctx.session.config?.tts === true;
309
- ctx.shouldSpeech = async text => {
310
- text = utilitas.isSet(text, true) ? (text || '') : ctx.tts;
311
- const should = ctx._.speech?.tts && ctx.checkSpeech();
312
- should && text && await ctx.speech(text);
313
- return should;
314
- };
315
- ctx.collect = (content, type, options) => type ? ctx.collected.push(
316
- { type, content }
317
- ) : (ctx.txt = [
318
- (options?.refresh ? '' : ctx.txt) || '', content || ''
319
- ].filter(x => x.length).join('\n\n'));
320
- ctx.skipMemorize = () => ctx._skipMemorize = true;
321
- ctx.end = () => {
322
- ctx.done.push(null);
323
- ctx.skipMemorize();
324
- };
325
- ctx.ok = async (message, options) => {
326
- let pages = bot.paging(message, options);
327
- const extra = getExtra(ctx, options);
328
- const [pageIds, pageMap] = [[], {}];
329
- options?.pageBreak || ctx.done.map(x => {
330
- pageMap[x?.message_id] || (pageIds.push(x?.message_id));
331
- pageMap[x?.message_id] = x;
240
+ const extendSessionStorage = storage => storage && storage.client && {
241
+ get: async (id, options) => {
242
+ const CONTEXT_LIMIT_WITH_RAG = 5;
243
+ let resp = await storage.get(id);
244
+ if (resp) {
245
+ const ctxResp = await getContext(id, undefined, undefined);
246
+ const ragResp = await recall(
247
+ id, options?.prompt, undefined, undefined, {
248
+ exclude: ctxResp.map(x => x.message_id),
332
249
  });
333
- for (let i in pages) {
334
- const lastPage = ~~i === pages.length - 1;
335
- const shouldExtra = options?.lastMessageId || lastPage;
336
- if (options?.onProgress && !options?.lastMessageId
337
- && pageMap[pageIds[~~i]]?.text === pages[i]) { continue; }
338
- if (options?.onProgress && !pageIds[~~i]) { // progress: new page, reply text
339
- ctx.done.push(await reply(
340
- ctx, false, pages[i], extra
341
- ));
342
- } else if (options?.onProgress) { // progress: ongoing, edit text
343
- ctx.done.push(await editMessageText(
344
- ctx, false, pageIds[~~i],
345
- pages[i], shouldExtra ? extra : {}
346
- ));
347
- } else if (options?.lastMessageId || pageIds[~~i]) { // progress: final, edit markdown
348
- ctx.done.push(await editMessageText(
349
- ctx, true, options?.lastMessageId || pageIds[~~i],
350
- pages[i], shouldExtra ? extra : {}
351
- ));
352
- } else { // never progress, reply markdown
353
- ctx.done.push(await reply(ctx, true, pages[i], extra));
354
- }
355
- await ctx.timeout();
356
- }
357
- return ctx.done;
358
- };
359
- ctx.er = async (m, opts) => {
360
- log(m);
361
- return await ctx.ok(`⚠️ ${m?.message || m}`, opts);
362
- };
363
- ctx.complete = async (options) => await ctx.ok('☑️', options);
364
- ctx.json = async (obj, options) => await ctx.ok(json(obj), options);
365
- ctx.list = async (list, options) => await ctx.ok(uList(list), options);
366
- ctx.replyWith = async (func, src, options) => ctx.done.push(
367
- await ctx[func]({ [getKey(src)]: src }, getExtra(ctx, options))
368
- );
369
- ctx.audio = async (s, o) => await ctx.replyWith('replyWithAudio', s, o);
370
- ctx.image = async (s, o) => await ctx.replyWith('replyWithPhoto', s, o);
371
- ctx.video = async (s, o) => await ctx.replyWith('replyWithVideo', s, o);
372
- ctx.media = async (srs, options) => await ctx.done.push(
373
- await ctx.replyWithMediaGroup(srs.map(x => ({
374
- type: x.type || 'photo', media: { [getKey(x.src)]: x.src },
375
- })), getExtra(ctx, options))
376
- );
377
- ctx.sendConfig = async (obj, options, _ctx) => await ctx.ok(
378
- utilitas.prettyJson(obj, { code: true, md: true }), options
379
- );
380
- ctx.speech = async (cnt, options) => {
381
- let file;
382
- if (Buffer.isBuffer(cnt)) {
383
- file = await storage.convert(cnt, {
384
- input: storage.BUFFER, expected: storage.FILE,
385
- });
386
- } else if (cnt.length <= speech.OPENAI_TTS_MAX_LENGTH) {
387
- file = await utilitas.ignoreErrFunc(async () => await ctx._.speech.tts(
388
- cnt, { expected: 'file' }
389
- ), logOptions);
390
- }
391
- if (!file) { return; }
392
- const resp = await ctx.audio(file, options);
393
- await storage.tryRm(file);
394
- return resp;
395
- };
396
- await next();
397
- // https://limits.tginfo.me/en
398
- if (ctx.chatId) {
399
- await memorize(ctx);
400
- ctx._skipMemorize || await utilitas.ignoreErrFunc(async (
401
- ) => await hal.telegram.setMyCommands([
402
- ...ctx._.cmds, ...Object.keys(ctx.session.prompts || {}).map(
403
- command => newCommand(command, ctx.session.prompts[command])
404
- )
405
- ].sort((x, y) =>
406
- (ctx.session?.cmds?.[y.command.toLowerCase()]?.touchedAt || 0)
407
- - (ctx.session?.cmds?.[x.command.toLowerCase()]?.touchedAt || 0)
408
- ).slice(0, COMMAND_LIMIT), {
409
- scope: { type: 'chat', chat_id: ctx.chatId },
410
- }), logOptions);
250
+ ctxResp.sort((a, b) => ~~a.message_id - ~~b.message_id);
251
+ ragResp.sort((a, b) => a.score - b.score);
252
+ resp.messages = [...ragResp, ...ragResp?.length ? ctxResp.slice(
253
+ ctxResp.length - CONTEXT_LIMIT_WITH_RAG
254
+ ) : ctxResp];
255
+ } else {
256
+ resp = { messages: [] };
411
257
  }
412
- if (ctx.done.length) { return; }
413
- const errStr = ctx.cmd ? `Command not found: /${ctx.cmd.cmd}`
414
- : 'No suitable response.';
415
- log(`INFO: ${errStr}`);
416
- await ctx.shouldReply(errStr);
258
+ // print(resp);
259
+ return resp;
417
260
  },
418
- }, {
419
- run: true, priority: -8950, name: 'subconscious', func: async (ctx, next) => {
420
- for (let t of KNOWN_UPDATE_TYPES) {
421
- if (ctx.update[t]) {
422
- ctx.m = ctx.update[ctx.type = t];
423
- break;
424
- }
425
- }
426
- if (ctx.type === 'callback_query') { ctx.m = ctx.m.message; }
427
- // else if (ctx.type === 'inline_query') { ctx.m.chat = { id: ctx.m.from.id, type: PRIVATE }; }
428
- else if (ctx.type === 'my_chat_member') {
429
- log(
430
- 'Group member status changed: '
431
- + ctx.m.new_chat_member.user.id + ' => '
432
- + ctx.m.new_chat_member.status
433
- );
434
- if (ctx.m.new_chat_member.user.id !== ctx.botInfo.id
435
- || ctx.m.new_chat_member.status === 'left') {
436
- return ctx.end();
437
- } else { ctx.hello(); }
438
- } else if (!ctx.type) { return log(`Unsupported update type.`); }
439
- ctx._ = hal._;
440
- ctx.chatId = ctx.m.chat.id;
441
- ctx.chatType = ctx.m.chat.type;
442
- ctx.messageId = ctx.m.message_id;
443
- ctx.m.text && ctx.collect(ctx.m.text);
444
- ctx.session = await sessionGet(ctx.chatId);
445
- ctx.limit = ctx.chatType === PRIVATE ? PRIVATE_LIMIT : GROUP_LIMIT;
446
- ctx.entities = [
447
- ...(ctx.m.entities || []).map(e => ({ ...e, text: ctx.m.text })),
448
- ...(ctx.m.caption_entities || []).map(e => ({ ...e, text: ctx.m.caption })),
449
- ...(ctx.m.reply_to_message?.entities || []).filter(
450
- x => x?.type !== bot_command
451
- ).map(e => ({ ...e, text: ctx.m.reply_to_message.text })),
452
- ].map(e => ({
453
- ...e, matched: e.text.substring(e.offset, e.offset + e.length),
454
- ...e.type === 'text_link' ? { type: 'url', matched: e.url } : {},
455
- }));
456
- ctx.chatType !== PRIVATE && (ctx.entities.some(e => {
457
- let target;
458
- switch (e.type) {
459
- case MENTION: target = e.matched.substring(1, e.length); break;
460
- case bot_command: target = e.matched.split('@')[1]; break;
461
- }
462
- return target === ctx.botInfo.username;
463
- }) || ctx.m.reply_to_message?.from?.username === ctx.botInfo.username
464
- || ctx.type === 'callback_query')
465
- && (ctx.chatType = MENTION);
466
- (((ctx.txt || ctx.m.voice || ctx.m.poll || ctx.m.data || ctx.m.document
467
- || ctx.m.photo || ctx.m.sticker || ctx.m.video_note || ctx.m.video
468
- || ctx.m.audio || ctx.m.location || ctx.m.venue || ctx.m.contact
469
- ) && ctx.messageId)
470
- || (ctx.m.new_chat_member || ctx.m.left_chat_member))
471
- && await next();
472
- await sessionSet(ctx.chatId);
473
- },
474
- }, {
475
- run: true, priority: -8945, name: 'callback', func: async (ctx, next) => {
476
- if (ctx.type === 'callback_query') {
477
- const data = utilitas.parseJson(ctx.update[ctx.type].data);
478
- const cb = ctx.session.callback.filter(x => x.id === data?.callback)[0];
479
- if (cb?.text) {
480
- log(`Callback: ${cb.text}`); // Avoid ctx.text interference:
481
- ctx.collect(cb.text, null, { refresh: true });
482
- } else {
483
- return await ctx.er(
484
- `Command is invalid or expired: ${ctx.m.data}`
485
- );
486
- }
487
- }
488
- await next();
489
- },
490
- }, {
491
- run: true, priority: -8940, name: 'commands', func: async (ctx, next) => {
492
- for (let e of ctx?.entities || []) {
493
- if (e.type !== bot_command) { continue; }
494
- if (!COMMAND_REGEXP.test(e.matched)) { continue; }
495
- const cmd = utilitas.trim(e.matched.replace(
496
- COMMAND_REGEXP, '$1'
497
- ), { case: 'LOW' });
498
- ctx.cmd = { cmd, args: e.text.substring(e.offset + e.length + 1) };
499
- break;
500
- }
501
- for (let str of [ctx.txt || '', ctx.m.caption || ''].map(utilitas.trim)) {
502
- if (!ctx.cmd && COMMAND_REGEXP.test(str)) {
503
- ctx.cmd = { // this will faild if command includes urls
504
- cmd: str.replace(COMMAND_REGEXP, '$1').toLowerCase(),
505
- args: str.replace(COMMAND_REGEXP, '$4'),
506
- };
507
- break;
508
- }
509
- }
510
- if (ctx.cmd) {
511
- log(`Command: ${JSON.stringify(ctx.cmd)}`);
512
- ctx.session.cmds || (ctx.session.cmds = {});
513
- ctx.session.cmds[ctx.cmd.cmd]
514
- = { args: ctx.cmd.args, touchedAt: Date.now() };
515
- }
516
- await next();
517
- },
518
- }, {
519
- run: true, priority: -8930, name: 'echo', hidden: true, func: async (ctx, next) => {
520
- let resp, md = false;
521
- switch (ctx.cmd.cmd) {
522
- case 'echo':
523
- resp = json({ update: ctx.update, session: ctx.session });
524
- break;
525
- case 'uptime':
526
- resp = uptime();
527
- break;
528
- case 'thethreelaws':
529
- resp = bot.lines([
530
- `Isaac Asimov's [Three Laws of Robotics](https://en.wikipedia.org/wiki/Three_Laws_of_Robotics):`,
531
- oList([
532
- 'A robot may not injure a human being or, through inaction, allow a human being to come to harm.',
533
- 'A robot must obey the orders given it by human beings except where such orders would conflict with the First Law.',
534
- 'A robot must protect its own existence as long as such protection does not conflict with the First or Second Laws.',
535
- ])
536
- ]);
537
- md = true;
538
- break;
539
- case 'ultimateanswer':
540
- resp = '[The Answer to the Ultimate Question of Life, The Universe, and Everything is `42`](https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy).';
541
- md = true;
542
- break;
543
- case 'lorem':
544
- const ipsum = () => text += `\n\n${lorem.generateParagraphs(1)}`;
545
- const [demoTitle, demoUrl] = [
546
- 'Lorem ipsum', 'https://en.wikipedia.org/wiki/Lorem_ipsum',
547
- ];
548
- let [text, extra] = [`[${demoTitle}](${demoUrl})`, {
549
- buttons: [{ label: demoTitle, url: demoUrl }]
550
- }];
551
- await ctx.ok(bot.EMOJI_THINKING);
552
- for (let i = 0; i < 2; i++) {
553
- await ctx.timeout();
554
- await ctx.ok(ipsum(), { ...extra, onProgress: true });
555
- }
556
- await ctx.timeout();
557
- await ctx.ok(ipsum(), { ...extra, md: true });
558
- // testing incomplete markdown reply {
559
- // await ctx.ok('_8964', { md: true });
560
- // }
561
- // test pagebreak {
562
- // await ctx.timeout();
563
- // await ctx.ok(ipsum(), { md: true, pageBreak: true });
564
- // }
565
- return;
566
- }
567
- await ctx.ok(resp, { md });
568
- }, help: bot.lines([
569
- '¶ Basic behaviors for debug only.',
570
- ]), cmds: {
571
- thethreelaws: `Isaac Asimov's [Three Laws of Robotics](https://en.wikipedia.org/wiki/Three_Laws_of_Robotics)`,
572
- ultimateanswer: '[The Answer to the Ultimate Question of Life, The Universe, and Everything](https://bit.ly/43wDhR3).',
573
- echo: 'Show debug message.',
574
- uptime: 'Show uptime of this bot.',
575
- lorem: '[Lorem ipsum](https://en.wikipedia.org/wiki/Lorem_ipsum)',
576
- },
577
- }, {
578
- run: true, priority: -8920, name: 'authenticate', func: async (ctx, next) => {
579
- if (!await ctx.shouldReply()) { return; } // if chatType is not in whitelist, exit.
580
- if (!ctx._.private) { return await next(); } // if not private, go next.
581
- if (ctx._.magicWord && utilitas.insensitiveHas(ctx._.magicWord, ctx.txt)) { // auth by magicWord
582
- ctx._.private.add(String(ctx.chatId));
583
- await ctx.ok('😸 You are now allowed to talk to me.');
584
- ctx.hello();
585
- return await next();
586
- }
587
- if (utilitas.insensitiveHas(ctx._.private, ctx.chatId) // auth by chatId
588
- || (ctx?.from && utilitas.insensitiveHas(ctx._.private, ctx.from.id))) { // auth by userId
589
- return await next();
590
- }
591
- if (ctx.chatType !== PRIVATE && ( // 1 of the group admins is in whitelist
592
- await ctx.telegram.getChatAdministrators(ctx.chatId)
593
- ).map(x => x.user.id).some(a => utilitas.insensitiveHas(ctx._.private, a))) {
594
- return await next();
595
- }
596
- if (ctx._.homeGroup && utilitas.insensitiveHas([ // auth by homeGroup
597
- 'creator', 'administrator', 'member' // 'left'
598
- ], (await utilitas.ignoreErrFunc(async () => await ctx.telegram.getChatMember(
599
- ctx._.homeGroup, ctx.from.id
600
- )))?.status)) { return await next(); }
601
- if (ctx._.auth && await ctx._.auth(ctx)) { return await next(); } // auth by custom function
602
- await ctx.ok('😿 Sorry, I am not allowed to talk to strangers.');
603
- },
604
- }, {
605
- run: true, priority: -8910, name: 'speech-to-text', func: async (ctx, next) => {
606
- const audio = ctx.m.voice || ctx.m.audio;
607
- if (ctx._.speech?.stt && audio) {
608
- await ctx.ok(EMOJI_SPEECH);
609
- try {
610
- const url = await getFileUrl(audio.file_id);
611
- let file = await getFile(audio.file_id, BUFFER_ENCODE);
612
- const analyze = async () => {
613
- const resp = await utilitas.ignoreErrFunc(async () => {
614
- [
615
- storage.MIME_MP3, storage.MIME_MPEGA,
616
- storage.MIME_MP4, storage.MIME_MPEG,
617
- storage.MIME_MPGA, storage.MIME_M4A,
618
- storage.MIME_WAV, storage.MIME_WEBM,
619
- storage.MIME_OGG,
620
- ].includes(audio.mime_type) || (
621
- file = await media.convertAudioTo16kNanoPcmWave(
622
- file, { input: storage.BUFFER, expected: storage.BUFFER }
623
- )
624
- );
625
- return await ctx._.speech?.stt(file);
626
- }, logOptions) || ' ';
627
- log(`STT: '${resp}'`);
628
- ctx.collect(resp);
629
- };
630
- if (hal._.supportedMimeTypes.has(storage.MIME_WAV)) {
631
- ctx.collect({
632
- mime_type: storage.MIME_WAV, url, analyze,
633
- data: await media.convertAudioTo16kNanoPcmWave(file, {
634
- input: storage.BUFFER, expected: storage.BASE64,
635
- }),
636
- }, 'PROMPT');
637
- } else { await analyze(); }
638
- } catch (err) { return await ctx.er(err); }
639
- }
640
- await next();
641
- },
642
- }, {
643
- run: true, priority: -8900, name: 'location', func: async (ctx, next) => {
644
- (ctx.m.location || ctx.m.venue) && ctx.collect(bot.lines([
645
- ...ctx.m.location && !ctx.m.venue ? ['Location:', uList([
646
- `latitude: ${ctx.m.location.latitude}`,
647
- `longitude: ${ctx.m.location.longitude}`,
648
- ])] : [],
649
- ...ctx.m.venue ? ['Venue:', uList([
650
- `title: ${ctx.m.venue.title}`,
651
- `address: ${ctx.m.venue.address}`,
652
- `latitude: ${ctx.m.venue.location.latitude}`,
653
- `longitude: ${ctx.m.venue.location.longitude}`,
654
- `foursquare_id: ${ctx.m.venue.foursquare_id}`,
655
- `foursquare_type: ${ctx.m.venue.foursquare_type}`,
656
- ])] : []
657
- ]));
658
- await next();
659
- },
660
- }, {
661
- run: true, priority: -8895, name: 'contact', func: async (ctx, next) => {
662
- ctx.m.contact && ctx.collect(bot.lines(['Contact:', uList([
663
- `first_name: ${ctx.m.contact.first_name}`,
664
- `last_name: ${ctx.m.contact.last_name}`,
665
- `phone_number: ${ctx.m.contact.phone_number}`,
666
- `user_id: ${ctx.m.contact.user_id}`,
667
- `vcard: ${ctx.m.contact.vcard}`,
668
- ])]));
669
- await next();
670
- },
671
- }, {
672
- run: true, priority: -8893, name: 'chat_member', func: async (ctx, next) => {
673
- const member = ctx.m.new_chat_member || ctx.m.left_chat_member;
674
- if (member) {
675
- if (member?.id === ctx.botInfo.id) { return ctx.end(); }
676
- ctx.collect(bot.lines([
677
- `Say ${ctx.m.new_chat_member ? 'hello' : 'goodbye'} to:`,
678
- uList([
679
- // `id: ${member.id}`,
680
- // `ishal: ${member.ishal}`,
681
- // `is_premium: ${member.is_premium}`,
682
- `first_name: ${member.first_name}`,
683
- `last_name: ${member.last_name}`,
684
- `username: ${member.username}`,
685
- `language_code: ${member.language_code || ''}`,
686
- ])
687
- ]));
688
- }
689
- await next();
690
- },
691
- }, {
692
- run: true, priority: -8890, name: 'poll', func: async (ctx, next) => {
693
- ctx.m.poll && ctx.collect(bot.lines([
694
- 'Question:', ctx.m.poll.question, '',
695
- 'Options:', oList(ctx.m.poll.options.map(x => x.text)),
696
- ]));
697
- await next();
698
- },
699
- }, {
700
- run: true, priority: -8880, name: 'contaxt', func: async (ctx, next) => {
701
- ctx.m.reply_to_message?.text && ctx.collect(
702
- ctx.m.reply_to_message.text, 'CONTAXT'
703
- );
704
- await next();
705
- },
706
- }, {
707
- run: true, priority: -8860, name: 'vision', func: async (ctx, next) => {
708
- ctx.collect(ctx.m?.caption || '');
709
- const files = [];
710
- for (const m of [
711
- ctx.m, ...ctx.m.reply_to_message ? [ctx.m.reply_to_message] : []
712
- ]) {
713
- if (m.document) {
714
- let file = {
715
- asPrompt: hal._.supportedMimeTypes.has(m.document.mime_type),
716
- file_name: m.document.file_name, fileId: m.document.file_id,
717
- mime_type: m.document.mime_type, type: storage.FILE,
718
- ocrFunc: async f => (await storage.isTextFile(f)) && f.toString(),
719
- };
720
- if (storage.MIME_PDF === m.document?.mime_type) {
721
- file = { ...file, ocrFunc: ctx._.vision?.read, type: 'DOCUMENT' };
722
- } else if (/^image\/.*$/ig.test(m.document?.mime_type)) {
723
- file = { ...file, ocrFunc: ctx._.vision?.see, type: 'IMAGE' };
724
- } else if (/^.*\.(docx|xlsx|pptx)$/.test(m.document?.file_name)) {
725
- file = { ...file, ocrFunc: officeParser, type: 'DOCUMENT' };
726
- }
727
- files.push(file);
728
- }
729
- if (m.sticker) {
730
- const s = m.sticker;
731
- const url = await getFileUrl(s.file_id);
732
- const file_name = basename(url);
733
- const mime_type = mime.getType(file_name) || 'image';
734
- files.push({
735
- asPrompt: hal._.supportedMimeTypes.has(mime_type), file_name,
736
- fileId: s.file_id, mime_type, type: 'PHOTO',
737
- ocrFunc: ctx._.vision?.see,
738
- });
739
- }
740
- if (m.photo?.[m.photo?.length - 1]) {
741
- const p = m.photo[m.photo.length - 1];
742
- files.push({
743
- asPrompt: hal._.supportedMimeTypes.has(storage.MIME_JPEG),
744
- file_name: `${p.file_id}.jpg`, fileId: p.file_id,
745
- mime_type: storage.MIME_JPEG, type: 'PHOTO',
746
- ocrFunc: ctx._.vision?.see,
747
- });
748
- }
749
- if (m.video_note) {
750
- const vn = m.video_note;
751
- const url = await getFileUrl(vn.file_id);
752
- const file_name = basename(url);
753
- const mime_type = mime.getType(file_name) || 'video';
754
- files.push({
755
- asPrompt: hal._.supportedMimeTypes.has(mime_type), file_name,
756
- fileId: vn.file_id, mime_type, type: 'VIDEO',
757
- });
758
- }
759
- if (m.video) {
760
- const v = m.video;
761
- const url = await getFileUrl(v.file_id);
762
- const file_name = basename(url);
763
- files.push({
764
- asPrompt: hal._.supportedMimeTypes.has(v.mime_type), file_name,
765
- fileId: v.file_id, mime_type: v.mime_type, type: 'VIDEO',
766
- });
767
- }
768
- }
769
- if (files.length) {
770
- await ctx.ok(EMOJI_LOOK);
771
- for (const f of files) {
772
- if (!f.asPrompt && !f.ocrFunc) { continue; }
773
- try {
774
- const url = await getFileUrl(f.fileId);
775
- const file = (await web.get(url, BUFFER_ENCODE)).content;
776
- const analyze = async () => {
777
- const content = utilitas.trim(utilitas.ensureArray(
778
- await utilitas.ignoreErrFunc(async () => await f.ocrFunc(
779
- file, BUFFER_ENCODE
780
- ), logOptions)
781
- ).filter(x => x).join('\n'));
782
- content && ctx.collect(bot.lines([
783
- '---', `file_name: ${f.file_name}`,
784
- `mime_type: ${f.mime_type}`, `type: ${f.type}`,
785
- '---',
786
- content
787
- ]), 'VISION');
788
- };
789
- if (f.asPrompt) {
790
- ctx.collect({
791
- mime_type: f.mime_type, url, analyze,
792
- data: utilitas.base64Encode(file, true),
793
- }, 'PROMPT');
794
- } else if (f.ocrFunc) { await analyze(); }
795
- } catch (err) { return await ctx.er(err); }
796
- }
797
- }
798
- await next();
799
- },
800
- }, {
801
- run: true, priority: -8850, name: 'help', func: async (ctx, next) => {
802
- const help = ctx._.info ? [ctx._.info] : [];
803
- for (let i in ctx._.skills) {
804
- if (ctx._.skills[i].hidden) { continue; }
805
- const _help = [];
806
- if (ctx._.skills[i].help) {
807
- _help.push(ctx._.skills[i].help);
808
- }
809
- const cmdsx = {
810
- ...ctx._.skills[i].cmds || {},
811
- ...ctx._.skills[i].cmdx || {},
812
- };
813
- if (utilitas.countKeys(cmdsx)) {
814
- _help.push(bot.lines([
815
- '_🪄 Commands:_',
816
- ...Object.keys(cmdsx).map(x => `- /${x}: ${cmdsx[x]}`),
817
- ]));
818
- }
819
- if (utilitas.countKeys(ctx._.skills[i].args)) {
820
- _help.push(bot.lines([
821
- '_⚙️ Options:_',
822
- ...Object.keys(ctx._.skills[i].args).map(x => {
823
- const _arg = ctx._.skills[i].args[x];
824
- return `- \`${x}\`` + (_arg.short ? `(${_arg.short})` : '')
825
- + `, ${_arg.type}(${_arg.default ?? 'N/A'})`
826
- + (_arg.desc ? `: ${_arg.desc}` : '');
827
- })
828
- ]));
829
- }
830
- _help.length && help.push(bot.lines([`*${i.toUpperCase()}*`, ..._help]));
831
- }
832
- await ctx.ok(lines2(help), { md: true });
833
- }, help: bot.lines([
834
- '¶ Basic syntax of this document:',
835
- 'Scheme for commands: /`COMMAND`: `DESCRIPTION`',
836
- 'Scheme for options: `OPTION`(`SHORT`), `TYPE`(`DEFAULT`): `DESCRIPTION`',
837
- ]), cmds: {
838
- help: 'Show help message.',
839
- },
840
- }, {
841
- run: true, priority: -8840, name: 'configuration', func: async (ctx, next) => {
842
- let parsed = null;
843
- switch (ctx.cmd.cmd) {
844
- case 'toggle':
845
- parsed = {};
846
- Object.keys(await parseArgs(ctx.cmd.args)).map(x =>
847
- parsed[x] = !ctx.session.config[x]);
848
- case 'set':
849
- try {
850
- const _config = {
851
- ...ctx.session.config = {
852
- ...ctx.session.config, ...ctx.config = parsed
853
- || await parseArgs(ctx.cmd.args, ctx),
854
- }
855
- };
856
- assert(utilitas.countKeys(ctx.config), 'No option matched.');
857
- Object.keys(ctx.config).map(x => _config[x] += ' 🖋');
858
- await ctx.sendConfig(_config, null, ctx);
859
- } catch (err) {
860
- await ctx.er(err.message || err);
861
- }
862
- break;
863
- case 'reset':
864
- ctx.session.config = ctx.config = {};
865
- await ctx.complete();
866
- break;
867
- case 'factory':
868
- sessions[ctx.chatId] = {}; // ctx.session = {}; will not work, because it's a reference.
869
- await ctx.complete();
870
- break;
871
- }
872
- }, help: bot.lines([
873
- '¶ Configure the bot by UNIX/Linux CLI style.',
874
- 'Using [node:util.parseArgs](https://nodejs.org/docs/latest-v21.x/api/util.html#utilparseargsconfig) to parse arguments.',
875
- ]), cmds: {
876
- toggle: 'Toggle configurations. Only works for boolean values.',
877
- set: 'Usage: /set --`OPTION` `VALUE` -`SHORT`',
878
- reset: 'Reset all configurations. Only erase `session.config`.',
879
- factory: 'Factory reset all memory areas. Erase the whole `session`.',
880
- }, args: {
881
- chatty: {
882
- type: 'string', short: 'c', default: ON,
883
- desc: `\`(${BINARY_STRINGS.join(', ')})\` Enable/Disable chatty mode.`,
884
- validate: utilitas.humanReadableBoolean,
885
- },
886
- },
887
- }, {
888
- run: true, priority: -8830, name: 'history', func: async (ctx, next) => {
889
- if (ctx.type === 'callback_query') {
890
- await ctx.deleteMessage(ctx.m.message_id);
891
- }
892
- const regex = '[-—]+skip=[0-9]*';
893
- let result;
894
- const keyWords = ctx.cmd.args.replace(new RegExp(regex, 'i'), '').trim();
895
- let catchArgs = ctx.cmd.args.replace(new RegExp(`^.*(${regex}).*$`, 'i'), '$1');
896
- catchArgs === ctx.cmd.args && (catchArgs = '');
897
- const offset = ~~catchArgs.replace(/^.*=([0-9]*).*$/i, '$1');
898
- if (!keyWords) { return await ctx.er('Topic is required.'); }
899
- switch (hal._?.database?.provider) {
900
- case dbio.MYSQL:
901
- result = await hal._.database?.client?.query?.(
902
- 'SELECT *, MATCH(`distilled`) '
903
- + 'AGAINST(? IN NATURAL LANGUAGE MODE) AS `relevance` '
904
- + 'FROM ?? WHERE `bot_id` = ? AND `chat_id` = ? '
905
- + 'HAVING relevance > 0 '
906
- + 'ORDER BY `relevance` DESC, `id` DESC '
907
- + `LIMIT ${SEARCH_LIMIT} OFFSET ?`,
908
- [keyWords, table, ctx.botInfo.id, ctx.chatId, offset]
909
- );
910
- break;
911
- case dbio.POSTGRESQL:
912
- globalThis.debug = 2;
913
- const vector = await hal._.embedding(keyWords);
914
- result = await hal._.database?.client?.query?.(
915
- `SELECT * FROM ${table} WHERE bot_id = $1 AND chat_id = $2`
916
- + ` ORDER BY distilled_vector <=> $3 ASC`
917
- + ` LIMIT ${SEARCH_LIMIT} OFFSET $4`, [
918
- ctx.botInfo.id, ctx.chatId,
919
- await dbio.encodeVector(vector), offset
920
- ]);
921
- break;
922
- }
923
- for (const i in result) {
924
- const content = bot.lines([
925
- ...result[i].response_text ? [
926
- `- ↩️ ${compactLimit(result[i].response_text)}`
927
- ] : [],
928
- `- ${utilitas.getTimeIcon(result[i].created_at)} `
929
- + `${result[i].created_at.toLocaleString()}`,
930
- ]);
931
- ctx.done.push(await reply(ctx, true, content, {
932
- reply_parameters: {
933
- message_id: result[i].message_id,
934
- }, disable_notification: ~~i > 0,
935
- }));
936
- await ctx.timeout();
937
- }
938
- ctx.done.push(await reply(ctx, true, '___', getExtra(ctx, {
939
- buttons: [{
940
- label: '🔍 More',
941
- text: `/search@${ctx.botInfo.username} ${keyWords} `
942
- + `--skip=${offset + result.length}`,
943
- }]
944
- })));
945
- result.length || await ctx.er('No more records.');
946
- }, help: bot.lines([
947
- '¶ Search history.',
948
- 'Example 1: /search Answer to the Ultimate Question',
949
- 'Example 2: /search Answer to the Ultimate Question --skip=10',
950
- ]), cmds: {
951
- search: 'Usage: /search `ANYTHING` --skip=`OFFSET`',
952
- }
953
- }, {
954
- run: true, priority: 8960, name: 'text-to-speech', func: async (ctx, next) => {
955
- await ctx.shouldSpeech();
956
- await next();
957
- }, help: bot.lines([
958
- '¶ When enabled, the bot will speak out the answer if available.',
959
- 'Example 1: /set --tts on',
960
- 'Example 2: /set --tts off',
961
- ]), args: {
962
- tts: {
963
- type: 'string', short: 't', default: ON,
964
- desc: `\`(${BINARY_STRINGS.join(', ')})\` Enable/Disable TTS. Default \`${ON}\` except in groups.`,
965
- validate: utilitas.humanReadableBoolean,
966
- },
967
- },
968
- }];
969
-
970
- const establish = module => {
971
- if (!module.run) { return; }
972
- assert(module?.func, 'Skill function is required.', 500);
973
- hal._.skills[module.name || (module.name = uuidv4())] = {
974
- args: module.args || {},
975
- cmds: module.cmds || {},
976
- cmdx: module.cmdx,
977
- help: module.help || '',
978
- hidden: !!module.hidden,
979
- };
980
- hal._.args = { ...hal._.args, ...module.args || {} };
981
- for (let sub of ['cmds', 'cmdx']) {
982
- Object.keys(module[sub] || {}).map(command => hal._.cmds.push(
983
- newCommand(command, module[sub][command])
984
- ));
985
- }
986
- log(`Establishing: ${module.name} (${module.priority})`, { force: true });
987
- return hal.use(utilitas.countKeys(module.cmds) && !module.cmdx ? async (ctx, next) => {
988
- for (let c in module.cmds) {
989
- if (utilitas.insensitiveCompare(ctx.cmd?.cmd, c)) {
990
- ctx.skipMemorize();
991
- return await module.func(ctx, next);
992
- }
993
- }
994
- return next();
995
- } : module.func);
261
+ set: async (id, session, options) => await storage.set(id, session, options),
996
262
  };
997
263
 
998
- const parseArgs = async (args, ctx) => {
999
- const { values, tokens } = _parseArgs({
1000
- args: utilitas.splitArgs((args || '').replaceAll('—', '--')),
1001
- options: hal._.args, tokens: true
1002
- });
1003
- const result = {};
1004
- for (let x of tokens) {
1005
- result[x.name] = hal._.args[x.name]?.validate
1006
- ? await hal._.args[x.name].validate(values[x.name], ctx)
1007
- : values[x.name];
1008
- }
1009
- return result;
264
+ const subconscious = {
265
+ name: 'Subconscious', run: true, priority: 0,
266
+ func: async (_, next) => ignoreErrFunc(next, logOptions), // non-blocking
1010
267
  };
1011
268
 
1012
269
  const init = async (options) => {
1013
270
  if (options) {
1014
- hal = await bot.init(options);
271
+ const { ais } = await alan.initChat({
272
+ sessions: extendSessionStorage(options?.storage),
273
+ });
1015
274
  if (callosum.isPrimary) {
275
+ const cmds = options?.cmds || [];
276
+ // config multimodal engines
277
+ const supportedMimeTypes = new Set(ais.map(x => {
278
+ // init instant ai selection
279
+ cmds.push(newCommand(`ai_${x.id}`, `${x.name}: ${x.features}`));
280
+ return x.model;
281
+ }).map(x => x.supportedMimeTypes || []).flat().map(x => x.toLowerCase()));
282
+ const _bot = await bot.init(options);
1016
283
  const pkg = await utilitas.which();
1017
- mime = await utilitas.need('mime');
1018
- lorem = new (await utilitas.need('lorem-ipsum')).LoremIpsum;
1019
- hal._ = {
284
+ _ = {
1020
285
  args: { ...options?.args || {} },
1021
286
  auth: Function.isFunction(options?.auth) && options.auth,
1022
- chatType: new Set(options?.chatType || ['mention', PRIVATE]), // ignore GROUP, CHANNEL by default
287
+ bot: _bot,
288
+ chatType: new Set(options?.chatType || [MENTION, PRIVATE]), // ignore GROUP, CHANNEL by default
1023
289
  cmds: [...options?.cmds || []],
290
+ embed: options?.embed,
1024
291
  hello: options?.hello || HELLO,
1025
292
  help: { ...options?.help || {} },
1026
293
  homeGroup: options?.homeGroup,
1027
- info: options?.info || bot.lines([`[${EMOJI_BOT} ${pkg.title}](${pkg.homepage})`, pkg.description]),
1028
- magicWord: options?.magicWord && new Set(options.magicWord),
294
+ info: options?.info || bot.lines([`[${bot.BOT} ${pkg.title}](${pkg.homepage})`, pkg.description]),
295
+ lang: options?.lang,
296
+ pipeline: options?.pipeline || {},
1029
297
  private: options?.private && new Set(options.private),
1030
- session: { get: options?.session?.get, set: options?.session?.set },
1031
- skills: { ...options?.skills || {} },
1032
- speech: options?.speech,
1033
- vision: options?.vision,
1034
- supportedMimeTypes: options?.supportedMimeTypes || [],
1035
- database: options?.database,
1036
- memorize: options?.memorize,
1037
- embedding: options?.embedding,
298
+ rerank: options?.rerank,
299
+ storage: options?.storage,
300
+ supportedMimeTypes,
1038
301
  };
1039
- (!options?.session?.get || !options?.session?.set)
1040
- && log(`WARNING: Sessions persistence is not enabled.`);
1041
- const mods = [
1042
- ...subconscious.map(s => ({ ...s, run: s.run && !options?.silent })),
1043
- ...utilitas.ensureArray(options?.skill),
1044
- ];
1045
- if (hal._.database) {
1046
- assert(
1047
- [dbio.MYSQL, dbio.POSTGRESQL].includes(hal._.database?.provider),
1048
- 'Invalid database provider.'
1049
- );
1050
- assert(
1051
- hal._.database?.client?.query
1052
- && hal._.database?.client?.upsert,
1053
- 'Database client is required.'
1054
- );
1055
- const dbResult = [];
1056
- try {
1057
- for (const act of initSql[hal._.database?.provider]) {
1058
- dbResult.push(await hal._.database.client.query(...act));
1059
- }
1060
- } catch (e) { console.error(e); }
302
+ if (_.storage) {
303
+ assert([
304
+ dbio.MYSQL, dbio.POSTGRESQL, storage.FILE
305
+ ].includes(_.storage?.provider), 'Invalid storage provider.');
306
+ if ([
307
+ dbio.MYSQL, dbio.POSTGRESQL
308
+ ].includes(_.storage?.provider)) {
309
+ assert(
310
+ _.storage?.client?.query && _.storage?.client?.upsert,
311
+ 'Invalid storage client.'
312
+ );
313
+ const dbResult = [];
314
+ try {
315
+ for (const act of initSql[_.storage?.provider]) {
316
+ dbResult.push(await _.storage.client.query(...act));
317
+ }
318
+ } catch (e) { console.error(e); }
319
+ }
1061
320
  }
1062
- for (let skillPath of utilitas.ensureArray(options?.skillPath)) {
1063
- log(`SKILLS: ${skillPath}`);
1064
- const files = (readdirSync(skillPath) || []).filter(
321
+ const mods = [subconscious];
322
+ for (let pipeline of utilitas.ensureArray(options?.pipelinePath || [])) {
323
+ log(`PIPELINE: ${pipeline}`);
324
+ const files = (readdirSync(pipeline) || []).filter(
1065
325
  file => /\.mjs$/i.test(file) && !file.startsWith('.')
1066
326
  );
1067
327
  for (let f of files) {
1068
- const m = await import(join(skillPath, f));
328
+ const m = await import(join(pipeline, f));
1069
329
  mods.push({ ...m, name: m.name || f.replace(/^(.*)\.mjs$/i, '$1') });
1070
330
  }
1071
331
  }
1072
332
  mods.sort((x, y) => ~~x.priority - ~~y.priority).map(establish);
1073
- assert(mods.length, 'Invalid skill set.', 501);
333
+ assert(mods.length, 'Invalid pipeline.', 501);
1074
334
  await parseArgs(); // Validate args options.
1075
335
  }
1076
336
  }
1077
- assert(hal, 'Hal have not been initialized.', 501);
1078
- return hal;
337
+ assert(_, 'Bot have not been initialized.', 501);
338
+ return _;
1079
339
  };
1080
340
 
1081
-
1082
-
1083
341
  export default init;
1084
342
  export {
1085
- _NEED,
1086
343
  BINARY_STRINGS,
344
+ BOT_COMMAND,
345
+ CHANNEL,
346
+ CHECK,
1087
347
  COMMAND_DESCRIPTION_LENGTH,
1088
348
  COMMAND_LENGTH,
1089
349
  COMMAND_LIMIT,
1090
- EMOJI_BOT,
1091
- EMOJI_SPEECH,
1092
- GROUP_LIMIT,
350
+ GROUP,
1093
351
  HELLO,
1094
- PRIVATE_LIMIT,
352
+ MENTION,
353
+ OFF,
354
+ ON,
355
+ PRIVATE,
356
+ SEARCH_LIMIT,
357
+ parseArgs,
358
+ _,
1095
359
  end,
360
+ getContext,
1096
361
  init,
1097
- lines2,
362
+ json,
363
+ logOptions,
1098
364
  newCommand,
1099
365
  oList,
366
+ recall,
367
+ rerank,
368
+ table,
1100
369
  uList
1101
370
  };