halbot 1995.1.26 → 1995.1.28

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,4 +1,4 @@
1
- import { alan, bot, callosum, dbio, storage, utilitas, } from 'utilitas';
1
+ import { alan, bot, callosum, dbio, storage, utilitas } from 'utilitas';
2
2
  import { join } from 'path';
3
3
  import { parseArgs as _parseArgs } from 'node:util';
4
4
  import { readdirSync } from 'fs';
@@ -8,11 +8,11 @@ import { ignoreErrFunc } from 'utilitas/lib/utilitas.mjs';
8
8
  const [ // https://limits.tginfo.me/en
9
9
  HELLO, GROUP, PRIVATE, CHANNEL, MENTION, jsonOptions, COMMAND_LIMIT, ON,
10
10
  OFF, BOT_COMMAND, CHECK, logOptions, COMMAND_LENGTH,
11
- COMMAND_DESCRIPTION_LENGTH, RELEVANCE, SEARCH_LIMIT, SUB_LIMIT,
11
+ COMMAND_DESCRIPTION_LENGTH,
12
12
  ] = [
13
13
  'Hello!', 'group', 'private', 'channel', 'mention',
14
14
  { code: true, extraCodeBlock: 1 }, 100, 'on', 'off', 'bot_command',
15
- '☑️', { log: true }, 32, 256, 0.2, 10, 200, // Google Rerank limit
15
+ '☑️', { log: true }, 32, 256,
16
16
  ];
17
17
 
18
18
  const table = 'utilitas_hal_events';
@@ -21,6 +21,10 @@ const [end] = [bot.end];
21
21
  const uList = arr => bot.lines(arr.map(x => `- ${x}`));
22
22
  const oList = arr => bot.lines(arr.map((v, k) => `${k + 1}. ${v}`));
23
23
  const [BINARY_STRINGS] = [[OFF, ON]];
24
+ const assembleKey = (c, t, k) => `HAL/SESSIONS/${c}/${t}${k ? `/${k}` : ''}`;
25
+ const assembleSettingsKey = (c, k) => assembleKey(c, 'SETTINGS', k);
26
+ const assembleCommandsKey = (c, k) => assembleKey(c, 'COMMANDS', k);
27
+ const assembleCallbacksKey = (c, k) => assembleKey(c, 'CALLBACKS', k);
24
28
 
25
29
  const initSql = {
26
30
  [dbio.MYSQL]: [[
@@ -143,124 +147,6 @@ const parseArgs = async (args, ctx) => {
143
147
  return result;
144
148
  };
145
149
 
146
- const packMessage = (messages) => messages.map(x => ({
147
- message_id: x.message_id, score: x.score, created_at: x.created_at,
148
- request: x.received_text, response: x.response_text,
149
- }));
150
-
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;
188
- }
189
- return await rerank(keyword, result, offset, limit, options);
190
- };
191
-
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]
204
- );
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;
216
- }
217
- return packMessage(result);
218
- };
219
-
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
- }
229
- }
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);
238
- };
239
-
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),
249
- });
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: [] };
257
- }
258
- // print(resp);
259
- return resp;
260
- },
261
- set: async (id, session, options) => await storage.set(id, session, options),
262
- };
263
-
264
150
  const subconscious = {
265
151
  name: 'Subconscious', run: true, priority: 0,
266
152
  func: async (_, next) => { ignoreErrFunc(next, logOptions) }, // non-blocking
@@ -268,9 +154,7 @@ const subconscious = {
268
154
 
269
155
  const init = async (options) => {
270
156
  if (options) {
271
- const { ais } = await alan.initChat({
272
- sessions: extendSessionStorage(options?.storage),
273
- });
157
+ const { ais } = await alan.initChat({ sessions: null });
274
158
  if (callosum.isPrimary) {
275
159
  const cmds = options?.cmds || [];
276
160
  // config multimodal engines
@@ -353,18 +237,18 @@ export {
353
237
  OFF,
354
238
  ON,
355
239
  PRIVATE,
356
- SEARCH_LIMIT,
357
- parseArgs,
358
240
  _,
241
+ assembleKey,
242
+ assembleCommandsKey,
243
+ assembleSettingsKey,
244
+ assembleCallbacksKey,
359
245
  end,
360
- getContext,
361
246
  init,
362
247
  json,
363
248
  logOptions,
364
249
  newCommand,
365
250
  oList,
366
- recall,
367
- rerank,
251
+ parseArgs,
368
252
  table,
369
- uList
253
+ uList,
370
254
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "halbot",
3
3
  "description": "Just another AI powered Telegram bot, which is simple design, easy to use, extendable and fun.",
4
- "version": "1995.1.26",
4
+ "version": "1995.1.28",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/Leask/halbot",
7
7
  "type": "module",
@@ -37,18 +37,18 @@
37
37
  "fluent-ffmpeg": "^2.1.3",
38
38
  "google-gax": "^5.0.6",
39
39
  "ioredis": "^5.8.2",
40
- "jsdom": "^27.3.0",
40
+ "jsdom": "^27.4.0",
41
41
  "lorem-ipsum": "^2.0.8",
42
42
  "mime": "^4.1.0",
43
43
  "mysql2": "^3.16.0",
44
- "office-text-extractor": "^3.0.3",
44
+ "office-text-extractor": "^4.0.0",
45
45
  "openai": "^6.15.0",
46
46
  "pg": "^8.16.3",
47
47
  "pgvector": "^0.2.1",
48
48
  "telegraf": "^4.16.3",
49
49
  "tellegram": "^1.1.3",
50
50
  "tesseract.js": "^7.0.0",
51
- "utilitas": "^2001.1.100",
51
+ "utilitas": "^2001.1.108",
52
52
  "youtube-transcript": "^1.2.1"
53
53
  }
54
54
  }
@@ -1,12 +1,12 @@
1
- import { alan, bot, hal, uoid, utilitas } from '../index.mjs';
1
+ import { bot, hal, uoid, utilitas } from '../index.mjs';
2
2
  import { convert, paginate } from 'tellegram';
3
3
 
4
4
  const _name = 'Broca';
5
5
  const [PRIVATE_LIMIT, GROUP_LIMIT] = [60 / 60, 60 / 20].map(x => x * 1000);
6
6
  const log = (c, o) => utilitas.log(c, _name, { time: 1, ...o || {} });
7
7
  const getKey = s => s?.toLowerCase?.()?.startsWith?.('http') ? 'url' : 'source';
8
- const isMarkdownError = e => e?.description?.includes?.("can't parse entities");
9
- const [CALLBACK_LIMIT, parse_mode] = [30, bot.PARSE_MODE_MD_V2];
8
+ const parse_mode = bot.PARSE_MODE_MD_V2;
9
+ // const isMarkdownError = e => e?.description?.includes?.("can't parse entities");
10
10
 
11
11
  const KNOWN_UPDATE_TYPES = [
12
12
  'callback_query', 'channel_post', 'edited_message', 'message',
@@ -26,7 +26,7 @@ const getExtra = (ctx, options) => {
26
26
  return { text: button.label, url: button.url };
27
27
  } else if (button.text) {
28
28
  const id = uoid.fakeUuid(button.text);
29
- ctx._.session.callback.push({ id, ...button });
29
+ ctx._.callbacks_updated[id] = button;
30
30
  return {
31
31
  text: button.label,
32
32
  callback_data: JSON.stringify({ callback: id }),
@@ -96,29 +96,47 @@ const edit = async (ctx, lastMsgId, text, extra) => {
96
96
  return resp;
97
97
  };
98
98
 
99
- const sessionGet = async chatId => {
100
- const session = await alan.getSession(chatId) || {};
101
- session.callback || (session.callback = []);
102
- session.config || (session.config = {});
103
- return session;
99
+ const loadSettings = async chatId => await hal._.storage.get(
100
+ hal.assembleSettingsKey(chatId), { asPrefix: true }
101
+ ) || {};
102
+
103
+ const saveSettings = async (chatId, settings = {}) => await Promise.all(
104
+ Object.entries(settings).map(([key, value]) =>
105
+ hal._.storage.set(hal.assembleSettingsKey(chatId, key), value)
106
+ )
107
+ );
108
+
109
+ const saveCallbacks = async (chatId, callbacks = {}) => await Promise.all(
110
+ Object.entries(callbacks).map(([key, value]) =>
111
+ hal._.storage.set(hal.assembleCallbacksKey(chatId, key), value)
112
+ )
113
+ );
114
+
115
+ const loadSession = async ctx => {
116
+ ctx._.settings = await loadSettings(ctx._.chatId);
117
+ ctx._.settings_updated = {};
118
+ ctx._.callbacks_updated = {};
119
+ // print('Loaded settings: ', ctx._.settings);
104
120
  };
105
121
 
106
- const sessionSet = async (chatId, session) => {
107
- while (session?.callback?.length > CALLBACK_LIMIT) {
108
- session.callback.shift();
109
- }
110
- return await alan.setSession(chatId, session);
122
+ const saveSession = async ctx => {
123
+ // print('Saving settings: ', ctx._.settings_updated);
124
+ // print('Saving callbacks: ', ctx._.callbacks_updated);
125
+ await Promise.all([
126
+ saveSettings(ctx._.chatId, ctx._.settings_updated),
127
+ saveCallbacks(ctx._.chatId, ctx._.callbacks_updated),
128
+ ]);
111
129
  };
112
130
 
113
131
  const ctxExt = ctx => {
114
- ctx.timeout = async () => await utilitas.timeout(ctx._.limit);
132
+ ctx.timeout = async (t) => await utilitas.timeout(t ?? ctx._.limit);
115
133
  ctx.collect = (content, type, options) => type ? ctx._.collected.push(
116
134
  { type, content }
117
135
  ) : (ctx._.text = [
118
136
  (options?.refresh ? '' : ctx._.text) || '', content || ''
119
137
  ].filter(x => x.length).join('\n\n'));
120
138
  ctx.hello = str => {
121
- str = str || ctx._.session?.config?.hello || hal._.hello;
139
+ str = str || ctx._.settings?.hello || hal._.hello;
122
140
  ctx.collect(str, null, { refresh: true });
123
141
  return str;
124
142
  };
@@ -160,7 +178,7 @@ const ctxExt = ctx => {
160
178
  };
161
179
  ctx.shouldReply = async (text, options) => {
162
180
  const should = utilitas.insensitiveHas(hal._?.chatType, ctx._.chatType)
163
- || ctx._.session?.config?.chatty;
181
+ || ctx._.settings?.chatty;
164
182
  text = utilitas.isSet(text, true) ? (text || '') : '';
165
183
  if (!should || !text) { return should; }
166
184
  return await (options?.error
@@ -174,10 +192,6 @@ const ctxExt = ctx => {
174
192
  ctx.image = async (s, o) => await replyWith(ctx, 'replyWithPhoto', s, o);
175
193
  ctx.video = async (s, o) => await replyWith(ctx, 'replyWithVideo', s, o);
176
194
  ctx.media = async (s, o) => await replyWith(ctx, 'replyWithMediaGroup', s, o);
177
- ctx.sessionSet = async () => {
178
- ctx._.saved || await sessionSet(ctx._.chatId, ctx._.session);
179
- ctx._.saved = true;
180
- };
181
195
  };
182
196
 
183
197
  const action = async (ctx, next) => {
@@ -220,8 +234,8 @@ const action = async (ctx, next) => {
220
234
  ctx._.message.text && ctx.collect(ctx._.message.text);
221
235
  ctx._.message.caption && ctx.collect(ctx._.message.caption);
222
236
  // get session
223
- ctx._.session = await sessionGet(ctx._.chatId);
224
- ctx._.limit = ctx.chatType === hal.PRIVATE ? PRIVATE_LIMIT : GROUP_LIMIT;
237
+ await loadSession(ctx);
238
+ ctx._.limit = ctx._.chatType === hal.PRIVATE ? PRIVATE_LIMIT : GROUP_LIMIT;
225
239
  ctx._.entities = [
226
240
  ...(ctx._.message.entities || []).map(e => ({ ...e, text: ctx._.message.text })),
227
241
  ...(ctx._.message.caption_entities || []).map(e => ({ ...e, text: ctx._.message.caption })),
@@ -255,7 +269,7 @@ const action = async (ctx, next) => {
255
269
  || (ctx._.message.new_chat_member || ctx._.message.left_chat_member))
256
270
  && await next();
257
271
  // persistence
258
- await ctx.sessionSet();
272
+ await saveSession(ctx);
259
273
  // fallback response and log
260
274
  if (ctx._.done.length) { return; }
261
275
  const errStr = ctx._.cmd?.cmd
@@ -265,6 +279,5 @@ const action = async (ctx, next) => {
265
279
  };
266
280
 
267
281
  export const { _NEED, name, run, priority, func } = {
268
- _NEED: ['telegramifyMarkdown'], name: _name,
269
- run: true, priority: 10, func: action,
282
+ _NEED: ['tellegram'], name: _name, run: true, priority: 10, func: action,
270
283
  };
@@ -19,13 +19,28 @@ const ctxExt = ctx => {
19
19
  ctx.getKeyboard = () => getKeyboard(ctx);
20
20
  };
21
21
 
22
+ const loadCommands = async chatId => await hal._.storage.get(
23
+ hal.assembleCommandsKey(chatId), {
24
+ asPrefix: true, order: { 'updated_at': '-' }, limit: hal.COMMAND_LIMIT
25
+ }) || {};
26
+
27
+ const saveCommands = async (chatId, commands = {}) => await Promise.all(
28
+ Object.entries(commands).map(([key, value]) =>
29
+ hal._.storage.set(hal.assembleCommandsKey(chatId, key), value)
30
+ )
31
+ );
32
+
33
+ const queryCallback = async (chatId, id) => await hal._.storage.get(
34
+ hal.assembleCallbacksKey(chatId, id)
35
+ );
36
+
22
37
  const action = async (ctx, next) => {
23
38
  // extend ctx
24
39
  ctxExt(ctx);
25
40
  // handle callback query
26
41
  if (ctx._.type === 'callback_query') {
27
42
  const data = utilitas.parseJson(ctx.update.callback_query.data);
28
- const cb = ctx._.session?.callback?.filter?.(x => x.id === data?.callback)?.[0];
43
+ const cb = await queryCallback(ctx._.chatId, data?.callback);
29
44
  if (cb?.text) {
30
45
  log(`Callback: ${cb.text}`); // Avoid ctx._.text interference:
31
46
  ctx.collect(cb.text, null, { refresh: true });
@@ -57,9 +72,7 @@ const action = async (ctx, next) => {
57
72
  // update last touched command
58
73
  if (ctx._.cmd) {
59
74
  log(`Command: ${JSON.stringify(ctx._.cmd)}`);
60
- ctx._.session.cmds || (ctx._.session.cmds = {});
61
- ctx._.session.cmds[ctx._.cmd.cmd]
62
- = { args: ctx._.cmd.args, touchedAt: Date.now() };
75
+ await saveCommands(ctx._.chatId, { [ctx._.cmd.cmd]: ctx._.cmd.args });
63
76
  }
64
77
  // handle commands
65
78
  switch (ctx._.cmd?.cmd) {
@@ -67,14 +80,17 @@ const action = async (ctx, next) => {
67
80
  return await ctx.complete({ keyboards: [] });
68
81
  }
69
82
  // update commands
70
- await utilitas.ignoreErrFunc(async () =>
71
- await hal._.bot.telegram.setMyCommands(hal._.cmds.sort((x, y) =>
72
- (ctx._.session?.cmds?.[y.command.toLowerCase()]?.touchedAt || 0)
73
- - (ctx._.session?.cmds?.[x.command.toLowerCase()]?.touchedAt || 0)
74
- ).slice(0, hal.COMMAND_LIMIT), {
83
+ await utilitas.ignoreErrFunc(async () => {
84
+ const cmds = Object.keys(await loadCommands(ctx._.chatId));
85
+ await hal._.bot.telegram.setMyCommands(hal._.cmds.sort((x, y) => {
86
+ const [_x, _y] = [x, y].map(
87
+ i => cmds.indexOf(i.command.toLowerCase())
88
+ ).map(x => x === -1 ? cmds.length : x);
89
+ return _x - _y;
90
+ }).slice(0, hal.COMMAND_LIMIT), {
75
91
  scope: { type: 'chat', chat_id: ctx._.chatId },
76
92
  }), hal.logOptions
77
- );
93
+ });
78
94
  // next middleware
79
95
  await next();
80
96
  };
@@ -6,9 +6,7 @@ const action = async (ctx, next) => {
6
6
  let resp;
7
7
  switch (ctx._.cmd.cmd) {
8
8
  case 'echo':
9
- const carry = { ...ctx._ };
10
- delete carry.session.messages;
11
- resp = hal.json({ update: ctx.update, _: carry });
9
+ resp = hal.json({ update: ctx.update, _: ctx._ });
12
10
  break;
13
11
  case 'uptime':
14
12
  resp = utilitas.uptime();
@@ -4,6 +4,10 @@ const sendConfig = async (ctx, obj, options) => await ctx.ok(
4
4
  utilitas.prettyJson(obj, { code: true, md: true }), options
5
5
  );
6
6
 
7
+ const deleteSettings = async chatId => await hal._.storage.del(
8
+ hal.assembleSettingsKey(chatId), { asPrefix: true }
9
+ );
10
+
7
11
  const ctxExt = ctx => {
8
12
  ctx.sendConfig = async (obj, options) => await sendConfig(ctx, obj, options);
9
13
  };
@@ -17,40 +21,37 @@ const action = async (ctx, next) => {
17
21
  return await ctx.ok('Please specify a language.');
18
22
  }
19
23
  const _config = {
20
- ...ctx._.session.config = {
21
- ...ctx._.session.config || {},
22
- ...ctx._.config = {
24
+ ...ctx._.settings = {
25
+ ...ctx._.settings,
26
+ ...ctx._.settings_updated = {
23
27
  lang: ctx._.cmd.args,
24
28
  hello: `Please reply in ${ctx._.cmd.args}. Hello!`,
25
29
  },
26
30
  }
27
31
  };
28
- Object.keys(ctx._.config).map(x => _config[x] += ` ${hal.CHECK}`);
32
+ Object.keys(ctx._.settings_updated).map(x => _config[x] += ` ${hal.CHECK}`);
29
33
  ctx.hello();
30
- await ctx.sessionSet(); // save config before break
31
34
  break;
32
35
  case 'toggle':
33
36
  parsed = {};
34
37
  Object.keys(await hal.parseArgs(ctx._.cmd.args)).map(x =>
35
- parsed[x] = !ctx._.session?.config?.[x]);
38
+ parsed[x] = !ctx._.settings?.[x]);
36
39
  case 'set':
37
40
  try {
38
41
  const _config = {
39
- ...ctx._.session.config = {
40
- ...ctx._.session?.config || {},
41
- ...ctx._.config = parsed || await hal.parseArgs(ctx._.cmd.args, ctx),
42
+ ...ctx._.settings = {
43
+ ...ctx._.settings,
44
+ ...ctx._.settings_updated = parsed || await hal.parseArgs(ctx._.cmd.args, ctx),
42
45
  }
43
46
  };
44
- assert(utilitas.countKeys(ctx._.config), 'No option matched.');
45
- Object.keys(ctx._.config).map(x => _config[x] += ` ${hal.CHECK}`);
46
- await ctx.sessionSet(); // save config before return
47
+ assert(utilitas.countKeys(ctx._.settings_updated), 'No option matched.');
48
+ Object.keys(ctx._.settings_updated).map(x => _config[x] += ` ${hal.CHECK}`);
47
49
  return await ctx.sendConfig(_config);
48
50
  } catch (err) {
49
51
  return await ctx.err(err.message || err);
50
52
  }
51
53
  case 'reset':
52
- ctx._.session.config = ctx._.config = {};
53
- await ctx.sessionSet(); // save config before return
54
+ await deleteSettings(ctx._.chatId);
54
55
  return await ctx.complete({ keyboards: ctx.getKeyboard() });
55
56
  }
56
57
  await next();
@@ -1,8 +1,103 @@
1
1
  import { bot, dbio, hal, utilitas } from '../index.mjs';
2
2
 
3
+ const [RELEVANCE, SEARCH_LIMIT, SUB_LIMIT] = [0.2, 10, 200]; // Google Rerank limit
3
4
  const compact = (str, op) => utilitas.ensureString(str, { ...op || {}, compact: true });
4
5
  const compactLimit = (str, op) => compact(str, { ...op || {}, limit: 140 });
5
6
 
7
+ const packMessage = (messages) => messages.map(x => ({
8
+ message_id: x.message_id, score: x.score, created_at: x.created_at,
9
+ request: x.received_text, response: x.response_text,
10
+ }));
11
+
12
+ const recall = async (sessionId, keyword, offset = 0, limit = SEARCH_LIMIT, options = {}) => {
13
+ assert(sessionId, 'Session ID is required.');
14
+ let [result, _limit, exclude] = [
15
+ [], hal._.rerank ? SUB_LIMIT : limit,
16
+ (options?.exclude || []).map(x => `${~~x}`),
17
+ ];
18
+ if (!keyword) { return result; }
19
+ switch (hal._.storage?.provider) {
20
+ case dbio.MYSQL:
21
+ result = await hal._.storage?.client?.query?.(
22
+ 'SELECT *, MATCH(`distilled`) '
23
+ + 'AGAINST(? IN NATURAL LANGUAGE MODE) AS `relevance` '
24
+ + "FROM ?? WHERE `bot_id` = ? AND `chat_id` = ? "
25
+ + "AND `received_text` != '' "
26
+ + "AND `received_text` NOT LIKE '/%' "
27
+ + "AND `response_text` != '' HAVING relevance > 0 "
28
+ + (exclude.length ? `AND \`message_id\` NOT IN (${exclude.join(',')}) ` : '')
29
+ + 'ORDER BY `relevance` DESC '
30
+ + `LIMIT ${_limit} OFFSET ?`,
31
+ [keyword, hal.table, hal._.bot.botInfo.id, sessionId, offset]
32
+ );
33
+ break;
34
+ case dbio.POSTGRESQL:
35
+ // globalThis.debug = 2;
36
+ const vector = await dbio.encodeVector(await hal._.embed(keyword));
37
+ result = await hal._.storage?.client?.query?.(
38
+ `SELECT *, (1 - (distilled_vector <=> $1)) as relevance `
39
+ + `FROM ${hal.table} WHERE bot_id = $2 AND chat_id = $3 `
40
+ + `AND received_text != '' `
41
+ + `AND received_text NOT LIKE '/%' `
42
+ + `AND response_text != '' `
43
+ + (exclude.length ? `AND message_id NOT IN (${exclude.join(',')}) ` : '')
44
+ + `ORDER BY (distilled_vector <=> $1) ASC `
45
+ + `LIMIT ${_limit} OFFSET $4`,
46
+ [vector, hal._.bot.botInfo.id, sessionId, offset]
47
+ );
48
+ break;
49
+ }
50
+ return await rerank(keyword, result, offset, limit, options);
51
+ };
52
+
53
+ const getContext = async (sessionId, offset = 0, limit = SEARCH_LIMIT, options = {}) => {
54
+ assert(sessionId, 'Session ID is required.');
55
+ let result = [];
56
+ switch (hal._.storage?.provider) {
57
+ case dbio.MYSQL:
58
+ result = await hal._.storage?.client?.query?.(
59
+ 'SELECT * FROM ?? WHERE `bot_id` = ? AND `chat_id` = ? '
60
+ + "AND `received_text` != '' "
61
+ + "AND `received_text` NOT LIKE '/%' "
62
+ + "AND `response_text` != '' "
63
+ + `ORDER BY \`created_at\` DESC LIMIT ${limit} OFFSET ?`,
64
+ [hal.table, hal._.bot.botInfo.id, sessionId, offset]
65
+ );
66
+ break;
67
+ case dbio.POSTGRESQL:
68
+ result = await hal._.storage?.client?.query?.(
69
+ `SELECT * FROM ${hal.table} WHERE bot_id = $1 AND chat_id = $2 `
70
+ + `AND received_text != '' `
71
+ + `AND received_text NOT LIKE '/%' `
72
+ + `AND response_text != '' `
73
+ + `ORDER BY created_at DESC LIMIT ${limit} OFFSET $3`,
74
+ [hal._.bot.botInfo.id, sessionId, offset]
75
+ );
76
+ break;
77
+ }
78
+ return packMessage(result);
79
+ };
80
+
81
+ const rerank = async (keyword, result, offset = 0, limit = SEARCH_LIMIT, options = {}) => {
82
+ if (result.length && hal._.rerank) {
83
+ const keys = {};
84
+ const _result = [];
85
+ for (const x of result) {
86
+ if (!keys[x.distilled]) {
87
+ keys[x.distilled] = true;
88
+ _result.push(x);
89
+ }
90
+ }
91
+ const resp = await hal._.rerank(keyword, _result.map(x => x.distilled));
92
+ resp.map(x => result[x.index].score = x.score);
93
+ result.sort((a, b) => b.score - a.score);
94
+ result = result.filter(
95
+ x => x.score > RELEVANCE
96
+ ).slice(offset, offset + limit);
97
+ }
98
+ return packMessage(result);
99
+ };
100
+
6
101
  const memorize = async (ctx) => {
7
102
  // https://limits.tginfo.me/en
8
103
  if (!ctx._.chatId || ctx._.cmd?.cmd) { return; }
@@ -39,10 +134,10 @@ const memorize = async (ctx) => {
39
134
 
40
135
  const ctxExt = ctx => {
41
136
  ctx.memorize = async () => await memorize(ctx);
42
- ctx.recall = async (keyword, offset = 0, limit = hal.SEARCH_LIMIT, options = {}) =>
43
- await hal.recall(ctx._.chatId, keyword, offset, limit, options);
44
- // ctx.getContext = async (offset = 0, limit = hal.SEARCH_LIMIT, options = {}) =>
45
- // await hal.getContext(ctx._.chatId, offset, limit, options);
137
+ ctx.recall = async (keyword, offset = 0, limit = SEARCH_LIMIT, options = {}) =>
138
+ await recall(ctx._.chatId, keyword, offset, limit, options);
139
+ ctx.getContext = async (offset = 0, limit = hal.SEARCH_LIMIT, options = {}) =>
140
+ await getContext(ctx._.chatId, offset, limit, options);
46
141
  };
47
142
 
48
143
  const action = async (ctx, next) => {
@@ -12,8 +12,8 @@ const listAIs = async ctx => {
12
12
  x => `${x[1]} \`${x[0]}\``
13
13
  )) + `\n\n*AI${ais.length > 0 ? 's' : ''}:*\n`;
14
14
  const buttons = ais.map((x, i) => ({
15
- label: `${ctx._.session.config?.ai === x.id
16
- || (!ctx._.session.config?.ai && i === 0) ? `${hal.CHECK} `
15
+ label: `${ctx._.settings?.ai === x.id
16
+ || (!ctx._.settings?.ai && i === 0) ? `${hal.CHECK} `
17
17
  : ''}${x.label}`,
18
18
  text: `/set --ai=${x.id}`,
19
19
  }));
@@ -30,7 +30,7 @@ const action = async (ctx, next) => {
30
30
  ctx._.aiId = TOP;
31
31
  break;
32
32
  default:
33
- ctx._.aiId = ctx._.session.config?.ai;
33
+ ctx._.aiId = ctx._.settings?.ai;
34
34
  }
35
35
  await next();
36
36
  };
@@ -3,8 +3,22 @@ import { alan } from '../index.mjs';
3
3
  const _name = 'Chat';
4
4
  const log = (c, o) => utilitas.log(c, _name, { time: 1, ...o || {} });
5
5
 
6
+ const assembleContext = async (ctx) => {
7
+ const CONTEXT_LIMIT_WITH_RAG = 5;
8
+ const ctxResp = await ctx.getContext();
9
+ const ragResp = await ctx.recall(ctx._.prompt, undefined, undefined, {
10
+ exclude: ctxResp.map(x => x.message_id),
11
+ });
12
+ ctxResp.sort((a, b) => ~~a.message_id - ~~b.message_id);
13
+ ragResp.sort((a, b) => a.score - b.score);
14
+ ctx._.messages = [...ragResp, ...ragResp?.length ? ctxResp.slice(
15
+ ctxResp.length - CONTEXT_LIMIT_WITH_RAG
16
+ ) : ctxResp];
17
+ };
18
+
6
19
  const action = async (ctx, next) => {
7
20
  if (!ctx._.text && !ctx._.collected.length) { return await next(); }
21
+ await assembleContext(ctx);
8
22
  let [resp, extra, lastLen, status] = [null, { buttons: [] }, 0, 0];
9
23
  const ok = async options => {
10
24
  const newLen = ~~resp?.text?.length;
@@ -24,7 +38,7 @@ const action = async (ctx, next) => {
24
38
  while (!status) {
25
39
  // console.log('Processing...');
26
40
  await ok({ processing: true });
27
- await ctx.timeout(1000 * ctx._.limit);
41
+ await ctx.timeout(0);
28
42
  }
29
43
  status++;
30
44
  })();
@@ -35,16 +49,16 @@ const action = async (ctx, next) => {
35
49
  status++;
36
50
  // console.log('Done');
37
51
  for (let image of resp?.images || []) {
38
- await ctx.timeout();
39
52
  await ctx.image(image.data, { caption: image.caption });
53
+ await ctx.timeout();
40
54
  }
41
55
  for (let video of resp?.videos || []) {
42
- await ctx.timeout();
43
56
  await ctx.video(video.data, { caption: video.caption });
57
+ await ctx.timeout();
44
58
  }
45
59
  for (let audio of resp?.audios || []) {
46
- await ctx.timeout();
47
60
  await ctx.audio(audio.data, { caption: audio.caption });
61
+ await ctx.timeout();
48
62
  }
49
63
  // print(resp);
50
64
  while (status < 2) {