@adriangalilea/utils 0.12.0 → 0.14.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/README.md +28 -8
- package/dist/bot/access-control.d.ts +1 -1
- package/dist/bot/access-control.js +1 -1
- package/dist/bot/index.d.ts +2 -3
- package/dist/bot/index.d.ts.map +1 -1
- package/dist/bot/index.js +2 -3
- package/dist/bot/index.js.map +1 -1
- package/dist/bot/llm.d.ts +520 -0
- package/dist/bot/llm.d.ts.map +1 -0
- package/dist/bot/llm.js +589 -0
- package/dist/bot/llm.js.map +1 -0
- package/dist/bot/menu.d.ts +99 -6
- package/dist/bot/menu.d.ts.map +1 -1
- package/dist/bot/menu.js +54 -6
- package/dist/bot/menu.js.map +1 -1
- package/package.json +4 -8
- package/dist/bot/llm-stream.d.ts +0 -142
- package/dist/bot/llm-stream.d.ts.map +0 -1
- package/dist/bot/llm-stream.js +0 -285
- package/dist/bot/llm-stream.js.map +0 -1
- package/dist/bot/message-history.d.ts +0 -279
- package/dist/bot/message-history.d.ts.map +0 -1
- package/dist/bot/message-history.js +0 -142
- package/dist/bot/message-history.js.map +0 -1
package/dist/bot/llm.js
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GramIO LLM toolkit — three primitives that together form a complete
|
|
3
|
+
* Telegram LLM-chatbot pipeline:
|
|
4
|
+
*
|
|
5
|
+
* `streamChat(response)` — INPUT. Parses OpenAI-compatible SSE
|
|
6
|
+
* (OpenAI, vllm, mlx-lm, llama.cpp,
|
|
7
|
+
* Together, Groq, …) into a typed
|
|
8
|
+
* `AsyncGenerator` of `{type, text}` with
|
|
9
|
+
* `content` / `reasoning` separation.
|
|
10
|
+
*
|
|
11
|
+
* `ctx.startStream()` — OUTPUT (low-level). Debounced
|
|
12
|
+
* `editMessageText` to Telegram, local
|
|
13
|
+
* Markdown parse via `@gramio/format`,
|
|
14
|
+
* 4000-char split on paragraph / line /
|
|
15
|
+
* word boundary.
|
|
16
|
+
*
|
|
17
|
+
* `ctx.startChatStream()` — OUTPUT (high-level). Consumes a fetch
|
|
18
|
+
* `Response` (or any `AsyncIterable<LLMChunk>`)
|
|
19
|
+
* and renders BOTH phases of a thinking
|
|
20
|
+
* model: `reasoning` chunks go into one
|
|
21
|
+
* `<blockquote expandable>` message;
|
|
22
|
+
* `content` chunks go into a streamed
|
|
23
|
+
* markdown message below it. Returns
|
|
24
|
+
* `{ content, reasoning }` once the
|
|
25
|
+
* source generator ends.
|
|
26
|
+
*
|
|
27
|
+
* `ctx.llm.add / .get / …` — HISTORY. Per-(user, thread) conversation
|
|
28
|
+
* buffer in OpenAI `ChatMessage` shape.
|
|
29
|
+
* Persisted in the shared `@gramio/session`
|
|
30
|
+
* record under the `llm` field, so the
|
|
31
|
+
* `botMenu` 🗑 Forget button wipes it
|
|
32
|
+
* together with everything else (one
|
|
33
|
+
* record, one delete, no per-plugin
|
|
34
|
+
* registry).
|
|
35
|
+
*
|
|
36
|
+
* The trio composes:
|
|
37
|
+
*
|
|
38
|
+
* fetch(...) ──streamChat──> chunks ──ctx.startStream──> Telegram
|
|
39
|
+
* ▲ │
|
|
40
|
+
* └──── ctx.llm.get() ◀──── ctx.llm.add(assistant) ◀──┘
|
|
41
|
+
*
|
|
42
|
+
* Peer deps: `gramio`, `@gramio/session`, `@gramio/format`, `marked`.
|
|
43
|
+
*
|
|
44
|
+
* @example high-level — thinking model in ~10 LOC
|
|
45
|
+
* import { Bot } from 'gramio'
|
|
46
|
+
* import { session } from '@gramio/session'
|
|
47
|
+
* import { llmStream, llmHistory } from '@adriangalilea/utils/bot/llm'
|
|
48
|
+
*
|
|
49
|
+
* const userSession = session({ storage, key: 'session', initial: () => ({}) })
|
|
50
|
+
* const chat = llmHistory({ session: userSession, maxTurns: 20, retentionDays: 7 })
|
|
51
|
+
*
|
|
52
|
+
* const bot = new Bot(process.env.BOT_TOKEN!)
|
|
53
|
+
* .extend(userSession)
|
|
54
|
+
* .extend(llmStream())
|
|
55
|
+
* .extend(chat.plugin)
|
|
56
|
+
* .on('message', async (ctx) => {
|
|
57
|
+
* ctx.llm.add({ role: 'user', content: ctx.text ?? '' })
|
|
58
|
+
* const response = await fetch(process.env.LLM_URL!, {
|
|
59
|
+
* method: 'POST',
|
|
60
|
+
* headers: { 'Content-Type': 'application/json' },
|
|
61
|
+
* body: JSON.stringify({
|
|
62
|
+
* model: process.env.LLM_MODEL,
|
|
63
|
+
* messages: [{ role: 'system', content: 'You are helpful.' }, ...ctx.llm.get()],
|
|
64
|
+
* stream: true,
|
|
65
|
+
* }),
|
|
66
|
+
* })
|
|
67
|
+
* const { content } = await ctx.startChatStream(response)
|
|
68
|
+
* ctx.llm.add({ role: 'assistant', content })
|
|
69
|
+
* })
|
|
70
|
+
*
|
|
71
|
+
* bot.start()
|
|
72
|
+
*
|
|
73
|
+
* @example low-level — manual loop when you need to map/filter chunks
|
|
74
|
+
* import { streamChat } from '@adriangalilea/utils/bot/llm'
|
|
75
|
+
*
|
|
76
|
+
* const stream = ctx.startStream()
|
|
77
|
+
* for await (const chunk of streamChat(response)) {
|
|
78
|
+
* if (chunk.type === 'content') await stream.append(chunk.text)
|
|
79
|
+
* // reasoning chunks dropped here
|
|
80
|
+
* }
|
|
81
|
+
* await stream.end()
|
|
82
|
+
*/
|
|
83
|
+
import { Plugin } from 'gramio';
|
|
84
|
+
import { expandableBlockquote } from '@gramio/format';
|
|
85
|
+
import { markdownToFormattable } from '@gramio/format/markdown';
|
|
86
|
+
import { say } from '../say/index.js';
|
|
87
|
+
/**
|
|
88
|
+
* Parse an OpenAI-compatible chat-completions SSE response into a
|
|
89
|
+
* typed `AsyncGenerator<LLMChunk>`. Reads `response.body` once.
|
|
90
|
+
*
|
|
91
|
+
* Recognised reasoning aliases (as of 2026): `reasoning_content`
|
|
92
|
+
* (vllm, qwen3, DeepSeek-R1, gpt-oss harmony) and `reasoning` (some
|
|
93
|
+
* mlx-lm forks, gemma builds). If a model surfaces a new key, add it
|
|
94
|
+
* here — single source of truth for the field.
|
|
95
|
+
*
|
|
96
|
+
* Constrained-SSE assumption: lines are `\n`-delimited and each event
|
|
97
|
+
* is `data: <json>` or `data: [DONE]`. This matches every OpenAI-compat
|
|
98
|
+
* server in the wild but is NOT the full SSE spec (no comments, no
|
|
99
|
+
* multi-line `data:`, no `retry`/`id` fields). Swap to
|
|
100
|
+
* `eventsource-parser` if you hit a producer that needs them.
|
|
101
|
+
*
|
|
102
|
+
* Malformed JSON lines are silently skipped; the generator ends when
|
|
103
|
+
* the stream closes.
|
|
104
|
+
*
|
|
105
|
+
* @param response the `fetch` `Response` from a `stream: true` chat
|
|
106
|
+
* completion call. Must not be already consumed.
|
|
107
|
+
* @throws if `response.body` is null (non-streaming response).
|
|
108
|
+
*
|
|
109
|
+
* @example framework-agnostic — parser doesn't know about Telegram
|
|
110
|
+
* const res = await fetch(url, { method: 'POST', body })
|
|
111
|
+
* for await (const chunk of streamChat(res)) {
|
|
112
|
+
* if (chunk.type === 'content') process.stdout.write(chunk.text)
|
|
113
|
+
* }
|
|
114
|
+
*/
|
|
115
|
+
export async function* streamChat(response) {
|
|
116
|
+
if (!response.body)
|
|
117
|
+
throw new Error('streamChat: response.body is null');
|
|
118
|
+
const reader = response.body.getReader();
|
|
119
|
+
const decoder = new TextDecoder();
|
|
120
|
+
let buffer = '';
|
|
121
|
+
while (true) {
|
|
122
|
+
const { done, value } = await reader.read();
|
|
123
|
+
if (done)
|
|
124
|
+
break;
|
|
125
|
+
buffer += decoder.decode(value, { stream: true });
|
|
126
|
+
const lines = buffer.split('\n');
|
|
127
|
+
buffer = lines.pop() ?? '';
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
if (!line.startsWith('data: '))
|
|
130
|
+
continue;
|
|
131
|
+
const data = line.slice(6).trim();
|
|
132
|
+
if (data === '[DONE]')
|
|
133
|
+
continue;
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(data);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
142
|
+
if (!delta)
|
|
143
|
+
continue;
|
|
144
|
+
const reasoning = delta.reasoning_content ?? delta.reasoning;
|
|
145
|
+
if (typeof reasoning === 'string' && reasoning.length > 0) {
|
|
146
|
+
yield { type: 'reasoning', text: reasoning };
|
|
147
|
+
}
|
|
148
|
+
if (typeof delta.content === 'string' && delta.content.length > 0) {
|
|
149
|
+
yield { type: 'content', text: delta.content };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// ─── OUTPUT: Telegram streamer ─────────────────────────────────────
|
|
155
|
+
const MAX_LEN = 4000; // Telegram caps at 4096; leave headroom for entity offsets
|
|
156
|
+
const DEFAULT_DEBOUNCE_MS = 800;
|
|
157
|
+
export class MarkdownStreamer {
|
|
158
|
+
buffer = '';
|
|
159
|
+
currentMessageId;
|
|
160
|
+
firstSendPromise;
|
|
161
|
+
debounceTimer;
|
|
162
|
+
inFlight = false;
|
|
163
|
+
dirty = false;
|
|
164
|
+
ended = false;
|
|
165
|
+
/**
|
|
166
|
+
* `true` after `.end()` returns if the stream finished with un-flushed
|
|
167
|
+
* buffer or an outstanding error that the streamer couldn't recover
|
|
168
|
+
* from. Useful for partial-response diagnostics in catch blocks:
|
|
169
|
+
*
|
|
170
|
+
* try { for await (...) await stream.append(...) }
|
|
171
|
+
* finally {
|
|
172
|
+
* await stream.end()
|
|
173
|
+
* if (stream.wasPartial) logger.warn('LLM stream cut off')
|
|
174
|
+
* }
|
|
175
|
+
*/
|
|
176
|
+
wasPartial = false;
|
|
177
|
+
chatId;
|
|
178
|
+
threadId;
|
|
179
|
+
// Match gramio's `bot.api` shape structurally. `text` accepts string or any
|
|
180
|
+
// `Formattable` (from `@gramio/format`) — both stringify safely. We don't
|
|
181
|
+
// import gramio's full Bot type to keep the streamer testable in isolation.
|
|
182
|
+
bot;
|
|
183
|
+
opts;
|
|
184
|
+
constructor(ctx, opts) {
|
|
185
|
+
this.chatId = ctx.chat.id;
|
|
186
|
+
// Captured so the streamed reply stays in the same thread.
|
|
187
|
+
// We call bot.api.sendMessage directly (no ctx.send), so the
|
|
188
|
+
// SendMixin's auto-thread doesn't help us here.
|
|
189
|
+
this.threadId = ctx.threadId;
|
|
190
|
+
this.bot = ctx.bot;
|
|
191
|
+
this.opts = {
|
|
192
|
+
debounceMs: opts.debounceMs ?? DEFAULT_DEBOUNCE_MS,
|
|
193
|
+
placeholder: opts.placeholder ?? '…',
|
|
194
|
+
markdown: opts.markdown ?? true,
|
|
195
|
+
onError: opts.onError ?? ((e) => console.error('[bot/llm:stream]', e)),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** Append a chunk. Schedules a debounced edit. */
|
|
199
|
+
async append(text) {
|
|
200
|
+
if (this.ended)
|
|
201
|
+
throw new Error('stream already ended');
|
|
202
|
+
if (!text)
|
|
203
|
+
return;
|
|
204
|
+
// first chunk: send the placeholder so we have a message_id to edit.
|
|
205
|
+
// Serialized via firstSendPromise so concurrent appends don't double-send.
|
|
206
|
+
if (this.currentMessageId === undefined && !this.firstSendPromise) {
|
|
207
|
+
this.firstSendPromise = (async () => {
|
|
208
|
+
const sent = await this.bot.api.sendMessage({
|
|
209
|
+
chat_id: this.chatId,
|
|
210
|
+
...(this.threadId !== undefined && { message_thread_id: this.threadId }),
|
|
211
|
+
text: this.opts.placeholder,
|
|
212
|
+
});
|
|
213
|
+
this.currentMessageId = sent.message_id;
|
|
214
|
+
})();
|
|
215
|
+
}
|
|
216
|
+
if (this.firstSendPromise)
|
|
217
|
+
await this.firstSendPromise;
|
|
218
|
+
// overflow: freeze current message at last good split, start a new one.
|
|
219
|
+
const next = this.buffer + text;
|
|
220
|
+
if (next.length > MAX_LEN) {
|
|
221
|
+
const splitAt = findSplit(next, MAX_LEN);
|
|
222
|
+
const head = next.slice(0, splitAt);
|
|
223
|
+
const tail = next.slice(splitAt).trimStart();
|
|
224
|
+
this.buffer = head;
|
|
225
|
+
this.dirty = true;
|
|
226
|
+
await this.flushNow(); // commits head into current message
|
|
227
|
+
this.buffer = '';
|
|
228
|
+
this.currentMessageId = undefined;
|
|
229
|
+
this.firstSendPromise = undefined;
|
|
230
|
+
this.dirty = false;
|
|
231
|
+
if (this.debounceTimer) {
|
|
232
|
+
clearTimeout(this.debounceTimer);
|
|
233
|
+
this.debounceTimer = undefined;
|
|
234
|
+
}
|
|
235
|
+
if (tail)
|
|
236
|
+
await this.append(tail);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.buffer = next;
|
|
240
|
+
this.dirty = true;
|
|
241
|
+
this.scheduleFlush();
|
|
242
|
+
}
|
|
243
|
+
/** Flush any pending edit and close the stream. Idempotent. */
|
|
244
|
+
async end() {
|
|
245
|
+
if (this.ended)
|
|
246
|
+
return;
|
|
247
|
+
this.ended = true;
|
|
248
|
+
if (this.debounceTimer) {
|
|
249
|
+
clearTimeout(this.debounceTimer);
|
|
250
|
+
this.debounceTimer = undefined;
|
|
251
|
+
}
|
|
252
|
+
while (this.inFlight)
|
|
253
|
+
await sleep(50);
|
|
254
|
+
if (this.dirty) {
|
|
255
|
+
await this.flushNow();
|
|
256
|
+
// If `dirty` is still set after the final flush, an edit error
|
|
257
|
+
// re-armed it and there's nothing more we'll do. Mark partial
|
|
258
|
+
// so the caller can log / fall back.
|
|
259
|
+
if (this.dirty)
|
|
260
|
+
this.wasPartial = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
scheduleFlush() {
|
|
264
|
+
if (this.debounceTimer)
|
|
265
|
+
return;
|
|
266
|
+
this.debounceTimer = setTimeout(async () => {
|
|
267
|
+
this.debounceTimer = undefined;
|
|
268
|
+
if (this.inFlight) {
|
|
269
|
+
this.scheduleFlush();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
await this.flushNow();
|
|
273
|
+
if (this.dirty && !this.ended)
|
|
274
|
+
this.scheduleFlush();
|
|
275
|
+
}, this.opts.debounceMs);
|
|
276
|
+
}
|
|
277
|
+
async flushNow() {
|
|
278
|
+
if (!this.dirty)
|
|
279
|
+
return;
|
|
280
|
+
if (this.currentMessageId === undefined) {
|
|
281
|
+
// No active message yet — likely the first send is still in flight
|
|
282
|
+
// or the recovery path cleared it. Caller's next append() will
|
|
283
|
+
// re-create one.
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
this.inFlight = true;
|
|
287
|
+
this.dirty = false;
|
|
288
|
+
const snapshot = this.buffer;
|
|
289
|
+
try {
|
|
290
|
+
const payload = this.opts.markdown ? markdownToFormattable(snapshot) : snapshot;
|
|
291
|
+
await this.bot.api.editMessageText({
|
|
292
|
+
chat_id: this.chatId,
|
|
293
|
+
message_id: this.currentMessageId,
|
|
294
|
+
text: payload,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
const msg = String(e?.message ?? e);
|
|
299
|
+
if (msg.includes('message is not modified')) {
|
|
300
|
+
// identical content — fine
|
|
301
|
+
}
|
|
302
|
+
else if (msg.includes('message to edit not found')) {
|
|
303
|
+
// message gone (deleted by user, etc.) — restart with a fresh send
|
|
304
|
+
this.currentMessageId = undefined;
|
|
305
|
+
this.firstSendPromise = undefined;
|
|
306
|
+
this.dirty = true;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
this.dirty = true;
|
|
310
|
+
this.opts.onError(e);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
this.inFlight = false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* GramIO plugin. Adds `ctx.startStream(opts?)` on every message context.
|
|
320
|
+
*
|
|
321
|
+
* Defaults set here apply to every stream; per-call options in
|
|
322
|
+
* `ctx.startStream({...})` override them.
|
|
323
|
+
*/
|
|
324
|
+
export const llmStream = (defaults = {}) => new Plugin('@adriangalilea/utils/bot/llm/stream').derive('message', (ctx) => ({
|
|
325
|
+
// gramio's `message` scope guarantees `ctx.chat`. `ctx.bot.api` is on
|
|
326
|
+
// every Context. Structural compat → no cast needed.
|
|
327
|
+
startStream: (opts = {}) => new MarkdownStreamer(ctx, { ...defaults, ...opts }),
|
|
328
|
+
/**
|
|
329
|
+
* One-call helper for chat completions: consumes a `Response`
|
|
330
|
+
* (or any `AsyncIterable<LLMChunk>`), renders reasoning to an
|
|
331
|
+
* expandable blockquote message + content to a streamed markdown
|
|
332
|
+
* message, returns `{ content, reasoning }` when the stream ends.
|
|
333
|
+
*/
|
|
334
|
+
startChatStream: (source, opts = {}) => consumeChatStream(ctx, source, { ...defaults, ...opts }),
|
|
335
|
+
}));
|
|
336
|
+
function findSplit(text, maxLen) {
|
|
337
|
+
// Prefer paragraph break, then line, then space. Reject splits in the
|
|
338
|
+
// first half — better to truncate at maxLen than to leave a stub.
|
|
339
|
+
for (const sep of ['\n\n', '\n', ' ']) {
|
|
340
|
+
const idx = text.lastIndexOf(sep, maxLen);
|
|
341
|
+
if (idx > maxLen / 2)
|
|
342
|
+
return idx;
|
|
343
|
+
}
|
|
344
|
+
return maxLen;
|
|
345
|
+
}
|
|
346
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
347
|
+
/**
|
|
348
|
+
* One-shot helper that consumes an LLM stream and renders BOTH phases
|
|
349
|
+
* (reasoning, content) to Telegram with the canonical pattern:
|
|
350
|
+
*
|
|
351
|
+
* - reasoning chunks → one message containing a single
|
|
352
|
+
* `<blockquote expandable>…</blockquote>` (HTML), debounced edits,
|
|
353
|
+
* stays expanded after the stream ends so the user can review it
|
|
354
|
+
* collapsed/uncollapsed at will
|
|
355
|
+
* - content chunks → a fresh `MarkdownStreamer` message below the
|
|
356
|
+
* reasoning one (auto-threaded via SendMixin)
|
|
357
|
+
*
|
|
358
|
+
* Returns when the source generator ends. Equivalent to (and replaces)
|
|
359
|
+
* the ~50 LOC of bookkeeping every thinking-model bot would otherwise
|
|
360
|
+
* write by hand around `streamChat` + `ctx.startStream`.
|
|
361
|
+
*
|
|
362
|
+
* Accepts either a raw `fetch` `Response` (wrapped with `streamChat`
|
|
363
|
+
* internally) or an `AsyncIterable<LLMChunk>` (when you want to map /
|
|
364
|
+
* filter chunks before the stream hits Telegram).
|
|
365
|
+
*
|
|
366
|
+
* @example via ctx.startChatStream from the llmStream plugin
|
|
367
|
+
* const response = await fetch(LLM_URL, { method: 'POST', body })
|
|
368
|
+
* const { content } = await ctx.startChatStream(response)
|
|
369
|
+
* ctx.llm.add({ role: 'assistant', content })
|
|
370
|
+
*
|
|
371
|
+
* @example standalone, framework-agnostic
|
|
372
|
+
* const { content, reasoning } = await consumeChatStream(ctx, response)
|
|
373
|
+
*/
|
|
374
|
+
export const consumeChatStream = async (ctx, source, opts = {}) => {
|
|
375
|
+
const chatId = ctx.chat.id;
|
|
376
|
+
const threadId = ctx.threadId;
|
|
377
|
+
const bot = ctx.bot;
|
|
378
|
+
const debounceMs = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
379
|
+
const gen = source instanceof Response ? streamChat(source) : source;
|
|
380
|
+
// ─── reasoning state ────────────────────────────────────────────
|
|
381
|
+
let reasoning = '';
|
|
382
|
+
let reasoningMessageId;
|
|
383
|
+
let reasoningTimer;
|
|
384
|
+
let reasoningDirty = false;
|
|
385
|
+
let reasoningInFlight = false;
|
|
386
|
+
// Same graceful-degradation pipeline as the content phase:
|
|
387
|
+
// markdownToFormattable parses the reasoning (lists, code blocks,
|
|
388
|
+
// bold — thinking models do emit markdown) into a FormattableString
|
|
389
|
+
// with native Telegram entities, and `expandableBlockquote` wraps
|
|
390
|
+
// the whole thing in a collapsed-by-default block quotation by
|
|
391
|
+
// adding the `expandable_blockquote` entity over the full text.
|
|
392
|
+
// Malformed mid-stream markdown degrades to plain text inside the
|
|
393
|
+
// blockquote instead of failing the message.
|
|
394
|
+
const renderReasoning = () => expandableBlockquote(markdownToFormattable(reasoning));
|
|
395
|
+
const sendReasoningMessage = async () => {
|
|
396
|
+
const params = {
|
|
397
|
+
chat_id: chatId,
|
|
398
|
+
...(threadId !== undefined && { message_thread_id: threadId }),
|
|
399
|
+
text: renderReasoning(),
|
|
400
|
+
};
|
|
401
|
+
const sent = await bot.api.sendMessage(params);
|
|
402
|
+
reasoningMessageId = sent.message_id;
|
|
403
|
+
};
|
|
404
|
+
const flushReasoning = async () => {
|
|
405
|
+
if (!reasoningDirty || reasoningMessageId === undefined)
|
|
406
|
+
return;
|
|
407
|
+
reasoningInFlight = true;
|
|
408
|
+
reasoningDirty = false;
|
|
409
|
+
try {
|
|
410
|
+
await bot.api.editMessageText({
|
|
411
|
+
chat_id: chatId,
|
|
412
|
+
message_id: reasoningMessageId,
|
|
413
|
+
text: renderReasoning(),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
catch (e) {
|
|
417
|
+
const msg = String(e?.message ?? e);
|
|
418
|
+
// "message is not modified" is benign when reasoning chunks
|
|
419
|
+
// arrive faster than the debounce can render.
|
|
420
|
+
if (!msg.includes('message is not modified')) {
|
|
421
|
+
reasoningDirty = true;
|
|
422
|
+
opts.onError?.(e);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
reasoningInFlight = false;
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
const scheduleReasoningFlush = () => {
|
|
430
|
+
if (reasoningTimer)
|
|
431
|
+
return;
|
|
432
|
+
reasoningTimer = setTimeout(async () => {
|
|
433
|
+
reasoningTimer = undefined;
|
|
434
|
+
if (reasoningInFlight) {
|
|
435
|
+
scheduleReasoningFlush();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
await flushReasoning();
|
|
439
|
+
if (reasoningDirty)
|
|
440
|
+
scheduleReasoningFlush();
|
|
441
|
+
}, debounceMs);
|
|
442
|
+
};
|
|
443
|
+
// ─── content state ──────────────────────────────────────────────
|
|
444
|
+
let contentStreamer;
|
|
445
|
+
let content = '';
|
|
446
|
+
// ─── main loop ──────────────────────────────────────────────────
|
|
447
|
+
for await (const chunk of gen) {
|
|
448
|
+
if (chunk.type === 'reasoning') {
|
|
449
|
+
reasoning += chunk.text;
|
|
450
|
+
if (reasoningMessageId === undefined) {
|
|
451
|
+
await sendReasoningMessage();
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
reasoningDirty = true;
|
|
455
|
+
scheduleReasoningFlush();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
else if (chunk.type === 'content') {
|
|
459
|
+
if (!contentStreamer) {
|
|
460
|
+
// Close out reasoning cleanly before content starts: flush any
|
|
461
|
+
// pending edit, drop the debounce. The blockquote stays in
|
|
462
|
+
// the chat as a finished message.
|
|
463
|
+
if (reasoningTimer) {
|
|
464
|
+
clearTimeout(reasoningTimer);
|
|
465
|
+
reasoningTimer = undefined;
|
|
466
|
+
}
|
|
467
|
+
if (reasoningDirty)
|
|
468
|
+
await flushReasoning();
|
|
469
|
+
contentStreamer = new MarkdownStreamer({ chat: { id: chatId }, threadId, bot }, opts);
|
|
470
|
+
}
|
|
471
|
+
content += chunk.text;
|
|
472
|
+
await contentStreamer.append(chunk.text);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Trailing reasoning (model emitted reasoning but no content yet).
|
|
476
|
+
if (reasoningTimer) {
|
|
477
|
+
clearTimeout(reasoningTimer);
|
|
478
|
+
reasoningTimer = undefined;
|
|
479
|
+
}
|
|
480
|
+
if (reasoningDirty)
|
|
481
|
+
await flushReasoning();
|
|
482
|
+
await contentStreamer?.end();
|
|
483
|
+
return { content, reasoning };
|
|
484
|
+
};
|
|
485
|
+
const GENERAL_THREAD = 'general';
|
|
486
|
+
const threadKey = (ctx) => {
|
|
487
|
+
// Message events: ctx.threadId. Callback events: ctx.message.threadId.
|
|
488
|
+
const tid = ctx.threadId ?? ctx.message?.threadId;
|
|
489
|
+
return tid !== undefined ? String(tid) : GENERAL_THREAD;
|
|
490
|
+
};
|
|
491
|
+
const prune = (items, maxTurns, retentionDays) => {
|
|
492
|
+
const cutoffSec = Math.floor(Date.now() / 1000) - retentionDays * 86400;
|
|
493
|
+
const fresh = items.filter((m) => m.date >= cutoffSec);
|
|
494
|
+
return fresh.slice(-maxTurns);
|
|
495
|
+
};
|
|
496
|
+
const pruneAll = (shards, maxTurns, retentionDays) => {
|
|
497
|
+
const out = {};
|
|
498
|
+
for (const k of Object.keys(shards)) {
|
|
499
|
+
const pruned = prune(shards[k], maxTurns, retentionDays);
|
|
500
|
+
if (pruned.length > 0)
|
|
501
|
+
out[k] = pruned;
|
|
502
|
+
}
|
|
503
|
+
return out;
|
|
504
|
+
};
|
|
505
|
+
/**
|
|
506
|
+
* Per-(user, thread) LLM conversation history. Opt-in. Persists in the
|
|
507
|
+
* shared `@gramio/session` record under `llm`, so 🗑 Forget from
|
|
508
|
+
* `botMenu` wipes it together with everything else — one record, one
|
|
509
|
+
* delete, no per-plugin registry.
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* const chat = llmHistory({ session: userSession, maxTurns: 20, retentionDays: 7 })
|
|
513
|
+
* bot.extend(chat.plugin)
|
|
514
|
+
* .on('message', (ctx) => {
|
|
515
|
+
* ctx.llm.add({ role: 'user', content: ctx.text ?? '' })
|
|
516
|
+
* const messages = ctx.llm.get() // ChatMessage[] for current thread
|
|
517
|
+
* // ... call LLM with messages, then:
|
|
518
|
+
* ctx.llm.add({ role: 'assistant', content: reply })
|
|
519
|
+
* })
|
|
520
|
+
*/
|
|
521
|
+
export const llmHistory = (opts) => {
|
|
522
|
+
if (opts.maxTurns <= 0)
|
|
523
|
+
throw new Error('llmHistory: maxTurns must be > 0');
|
|
524
|
+
if (opts.retentionDays <= 0)
|
|
525
|
+
throw new Error('llmHistory: retentionDays must be > 0');
|
|
526
|
+
const plugin = buildHistoryPlugin({
|
|
527
|
+
sessionPlugin: opts.session,
|
|
528
|
+
maxTurns: opts.maxTurns,
|
|
529
|
+
retentionDays: opts.retentionDays,
|
|
530
|
+
});
|
|
531
|
+
const itemLabel = opts.menuLabels?.item ?? {
|
|
532
|
+
en: '🧹 Clear this thread',
|
|
533
|
+
es: '🧹 Limpiar este hilo',
|
|
534
|
+
};
|
|
535
|
+
const clearedToast = opts.menuLabels?.cleared ?? {
|
|
536
|
+
en: '🧹 Cleared.',
|
|
537
|
+
es: '🧹 Limpio.',
|
|
538
|
+
};
|
|
539
|
+
const menuItem = {
|
|
540
|
+
id: 'llmClear',
|
|
541
|
+
label: itemLabel,
|
|
542
|
+
action: async (ctx) => {
|
|
543
|
+
const c = ctx;
|
|
544
|
+
c.llm?.clear();
|
|
545
|
+
await c.answer?.({ text: say(clearedToast, c.lang ?? 'en') });
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
return { plugin, menuItem };
|
|
549
|
+
};
|
|
550
|
+
const buildHistoryPlugin = (args) => {
|
|
551
|
+
const { sessionPlugin, maxTurns, retentionDays } = args;
|
|
552
|
+
return new Plugin('@adriangalilea/utils/bot/llm/history')
|
|
553
|
+
.extend(sessionPlugin)
|
|
554
|
+
.derive(['message', 'callback_query'], (ctx) => {
|
|
555
|
+
const key = threadKey(ctx);
|
|
556
|
+
// Always operate against a freshly-pruned view. Writes go to the
|
|
557
|
+
// pruned base so stale entries never resurface after a read.
|
|
558
|
+
const readShards = () => pruneAll(ctx.session.llm?.shards ?? {}, maxTurns, retentionDays);
|
|
559
|
+
const writeShards = (shards) => {
|
|
560
|
+
ctx.session.llm = { shards };
|
|
561
|
+
};
|
|
562
|
+
return {
|
|
563
|
+
llm: {
|
|
564
|
+
add: (message) => {
|
|
565
|
+
const shards = readShards();
|
|
566
|
+
const cur = shards[key] ?? [];
|
|
567
|
+
const entry = {
|
|
568
|
+
role: message.role,
|
|
569
|
+
content: message.content,
|
|
570
|
+
date: message.date ?? Math.floor(Date.now() / 1000),
|
|
571
|
+
};
|
|
572
|
+
shards[key] = [...cur, entry].slice(-maxTurns);
|
|
573
|
+
writeShards(shards);
|
|
574
|
+
},
|
|
575
|
+
get: () => (readShards()[key] ?? []),
|
|
576
|
+
clear: () => {
|
|
577
|
+
const shards = readShards();
|
|
578
|
+
delete shards[key];
|
|
579
|
+
writeShards(shards);
|
|
580
|
+
},
|
|
581
|
+
all: () => readShards(),
|
|
582
|
+
clearAll: () => {
|
|
583
|
+
writeShards({});
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
});
|
|
588
|
+
};
|
|
589
|
+
//# sourceMappingURL=llm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/bot/llm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AACH,OAAO,EAA0B,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEvD,OAAO,EAAE,oBAAoB,EAA0B,MAAM,gBAAgB,CAAA;AAC7E,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAE/D,OAAO,EAAE,GAAG,EAAiB,MAAM,iBAAiB,CAAA;AA4BpD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,UAAU,CAC/B,QAAkB;IAElB,IAAI,CAAC,QAAQ,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAA;IAExE,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAA;IACxC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;IACjC,IAAI,MAAM,GAAG,EAAE,CAAA;IAEf,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;QAC3C,IAAI,IAAI;YAAE,MAAK;QACf,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QACjD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAChC,MAAM,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,CAAA;QAC1B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;gBAAE,SAAQ;YACxC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;YACjC,IAAI,IAAI,KAAK,QAAQ;gBAAE,SAAQ;YAC/B,IAAI,MAAmB,CAAA;YACvB,IAAI,CAAC;gBACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAgB,CAAA;YAC1C,CAAC;YAAC,MAAM,CAAC;gBACP,SAAQ;YACV,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAA;YACxC,IAAI,CAAC,KAAK;gBAAE,SAAQ;YACpB,MAAM,SAAS,GAAG,KAAK,CAAC,iBAAiB,IAAI,KAAK,CAAC,SAAS,CAAA;YAC5D,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC1D,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;YAC9C,CAAC;YACD,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAClE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,CAAA;YAChD,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,sEAAsE;AAEtE,MAAM,OAAO,GAAG,IAAI,CAAA,CAAC,2DAA2D;AAChF,MAAM,mBAAmB,GAAG,GAAG,CAAA;AAa/B,MAAM,OAAO,gBAAgB;IACnB,MAAM,GAAG,EAAE,CAAA;IACX,gBAAgB,CAAS;IACzB,gBAAgB,CAAgB;IAChC,aAAa,CAAgC;IAC7C,QAAQ,GAAG,KAAK,CAAA;IAChB,KAAK,GAAG,KAAK,CAAA;IACb,KAAK,GAAG,KAAK,CAAA;IACrB;;;;;;;;;;OAUG;IACH,UAAU,GAAG,KAAK,CAAA;IAEV,MAAM,CAAQ;IACd,QAAQ,CAAS;IACzB,4EAA4E;IAC5E,0EAA0E;IAC1E,4EAA4E;IACpE,GAAG,CAaV;IACO,IAAI,CAAyB;IAErC,YACE,GAIC,EACD,IAAmB;QAEnB,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,CAAA;QACzB,2DAA2D;QAC3D,6DAA6D;QAC7D,gDAAgD;QAChD,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAA;QAC5B,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAA;QAClB,IAAI,CAAC,IAAI,GAAG;YACV,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,mBAAmB;YAClD,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,GAAG;YACpC,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,IAAI;YAC/B,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC;SACvE,CAAA;IACH,CAAC;IAED,kDAAkD;IAClD,KAAK,CAAC,MAAM,CAAC,IAAY;QACvB,IAAI,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAA;QACvD,IAAI,CAAC,IAAI;YAAE,OAAM;QAEjB,qEAAqE;QACrE,2EAA2E;QAC3E,IAAI,IAAI,CAAC,gBAAgB,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAClE,IAAI,CAAC,gBAAgB,GAAG,CAAC,KAAK,IAAI,EAAE;gBAClC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC;oBAC1C,OAAO,EAAE,IAAI,CAAC,MAAM;oBACpB,GAAG,CAAC,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,iBAAiB,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACxE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW;iBAC5B,CAAC,CAAA;gBACF,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,UAAU,CAAA;YACzC,CAAC,CAAC,EAAE,CAAA;QACN,CAAC;QACD,IAAI,IAAI,CAAC,gBAAgB;YAAE,MAAM,IAAI,CAAC,gBAAgB,CAAA;QAEtD,wEAAwE;QACxE,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QAC/B,IAAI,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE,CAAC;YAC1B,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;YACxC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAA;YACnC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,CAAA;YAE5C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;YAClB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;YACjB,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAA,CAAC,oCAAoC;YAE1D,IAAI,CAAC,MAAM,GAAG,EAAE,CAAA;YAChB,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAA;YACjC,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAA;YACjC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;YAClB,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;gBACvB,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;gBAChC,IAAI,CAAC,aAAa,GAAG,SAAS,CAAA;YAChC,CAAC;YAED,IAAI,IAAI;gBAAE,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;YACjC,OAAM;QACR,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QAClB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;QACjB,IAAI,CAAC,aAAa,EAAE,CAAA;IACtB,CAAC;IAED,+DAA+D;IAC/D,KAAK,CAAC,GAAG;QACP,IAAI,IAAI,CAAC,KAAK;YAAE,OAAM;QACtB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;QACjB,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;YAChC,IAAI,CAAC,aAAa,GAAG,SAAS,CAAA;QAChC,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ;YAAE,MAAM,KAAK,CAAC,EAAE,CAAC,CAAA;QACrC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAA;YACrB,+DAA+D;YAC/D,8DAA8D;YAC9D,qCAAqC;YACrC,IAAI,IAAI,CAAC,KAAK;gBAAE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;QACxC,CAAC;IACH,CAAC;IAEO,aAAa;QACnB,IAAI,IAAI,CAAC,aAAa;YAAE,OAAM;QAC9B,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,KAAK,IAAI,EAAE;YACzC,IAAI,CAAC,aAAa,GAAG,SAAS,CAAA;YAC9B,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAClB,IAAI,CAAC,aAAa,EAAE,CAAA;gBACpB,OAAM;YACR,CAAC;YACD,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAA;YACrB,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK;gBAAE,IAAI,CAAC,aAAa,EAAE,CAAA;QACrD,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IAC1B,CAAC;IAEO,KAAK,CAAC,QAAQ;QACpB,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAM;QACvB,IAAI,IAAI,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACxC,mEAAmE;YACnE,+DAA+D;YAC/D,iBAAiB;YACjB,OAAM;QACR,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAA;QACpB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAA;QAC5B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;YAC/E,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC;gBACjC,OAAO,EAAE,IAAI,CAAC,MAAM;gBACpB,UAAU,EAAE,IAAI,CAAC,gBAAgB;gBACjC,IAAI,EAAE,OAAO;aACd,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,GAAG,GAAG,MAAM,CAAE,CAAsC,EAAE,OAAO,IAAI,CAAC,CAAC,CAAA;YACzE,IAAI,GAAG,CAAC,QAAQ,CAAC,yBAAyB,CAAC,EAAE,CAAC;gBAC5C,2BAA2B;YAC7B,CAAC;iBAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,2BAA2B,CAAC,EAAE,CAAC;gBACrD,mEAAmE;gBACnE,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAA;gBACjC,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAA;gBACjC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;YACnB,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;gBACjB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;YACtB,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAA;QACvB,CAAC;IACH,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,WAA0B,EAAE,EAAE,EAAE,CACxD,IAAI,MAAM,CAAC,qCAAqC,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC5E,sEAAsE;IACtE,qDAAqD;IACrD,WAAW,EAAE,CAAC,OAAsB,EAAE,EAAE,EAAE,CACxC,IAAI,gBAAgB,CAAC,GAAG,EAAE,EAAE,GAAG,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC;IACrD;;;;;OAKG;IACH,eAAe,EAAE,CACf,MAA0C,EAC1C,OAA0B,EAAE,EAC5B,EAAE,CAAC,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,GAAG,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC;CAC9D,CAAC,CAAC,CAAA;AAEL,SAAS,SAAS,CAAC,IAAY,EAAE,MAAc;IAC7C,sEAAsE;IACtE,kEAAkE;IAClE,KAAK,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;QACzC,IAAI,GAAG,GAAG,MAAM,GAAG,CAAC;YAAE,OAAO,GAAG,CAAA;IAClC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;AAmBzE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EACpC,GAA8E,EAC9E,MAA0C,EAC1C,OAA0B,EAAE,EACD,EAAE;IAC7B,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,CAAA;IAC1B,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAA;IAC7B,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAA;IACnB,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,mBAAmB,CAAA;IAEzD,MAAM,GAAG,GACP,MAAM,YAAY,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;IAE1D,mEAAmE;IACnE,IAAI,SAAS,GAAG,EAAE,CAAA;IAClB,IAAI,kBAAsC,CAAA;IAC1C,IAAI,cAAyD,CAAA;IAC7D,IAAI,cAAc,GAAG,KAAK,CAAA;IAC1B,IAAI,iBAAiB,GAAG,KAAK,CAAA;IAE7B,2DAA2D;IAC3D,kEAAkE;IAClE,oEAAoE;IACpE,kEAAkE;IAClE,+DAA+D;IAC/D,gEAAgE;IAChE,kEAAkE;IAClE,6CAA6C;IAC7C,MAAM,eAAe,GAAG,GAAsB,EAAE,CAC9C,oBAAoB,CAAC,qBAAqB,CAAC,SAAS,CAAC,CAAC,CAAA;IAExD,MAAM,oBAAoB,GAAG,KAAK,IAAmB,EAAE;QAMrD,MAAM,MAAM,GAAe;YACzB,OAAO,EAAE,MAAM;YACf,GAAG,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,iBAAiB,EAAE,QAAQ,EAAE,CAAC;YAC9D,IAAI,EAAE,eAAe,EAAE;SACxB,CAAA;QACD,MAAM,IAAI,GAAG,MACX,GAAG,CAAC,GAAG,CAAC,WAGT,CAAC,MAAM,CAAC,CAAA;QACT,kBAAkB,GAAG,IAAI,CAAC,UAAU,CAAA;IACtC,CAAC,CAAA;IAED,MAAM,cAAc,GAAG,KAAK,IAAmB,EAAE;QAC/C,IAAI,CAAC,cAAc,IAAI,kBAAkB,KAAK,SAAS;YAAE,OAAM;QAC/D,iBAAiB,GAAG,IAAI,CAAA;QACxB,cAAc,GAAG,KAAK,CAAA;QACtB,IAAI,CAAC;YAMH,MACE,GAAG,CAAC,GAAG,CAAC,eAGT,CAAC;gBACA,OAAO,EAAE,MAAM;gBACf,UAAU,EAAE,kBAAkB;gBAC9B,IAAI,EAAE,eAAe,EAAE;aACxB,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,GAAG,GAAG,MAAM,CAAE,CAAsC,EAAE,OAAO,IAAI,CAAC,CAAC,CAAA;YACzE,4DAA4D;YAC5D,8CAA8C;YAC9C,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,yBAAyB,CAAC,EAAE,CAAC;gBAC7C,cAAc,GAAG,IAAI,CAAA;gBACrB,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAA;YACnB,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,iBAAiB,GAAG,KAAK,CAAA;QAC3B,CAAC;IACH,CAAC,CAAA;IAED,MAAM,sBAAsB,GAAG,GAAS,EAAE;QACxC,IAAI,cAAc;YAAE,OAAM;QAC1B,cAAc,GAAG,UAAU,CAAC,KAAK,IAAI,EAAE;YACrC,cAAc,GAAG,SAAS,CAAA;YAC1B,IAAI,iBAAiB,EAAE,CAAC;gBACtB,sBAAsB,EAAE,CAAA;gBACxB,OAAM;YACR,CAAC;YACD,MAAM,cAAc,EAAE,CAAA;YACtB,IAAI,cAAc;gBAAE,sBAAsB,EAAE,CAAA;QAC9C,CAAC,EAAE,UAAU,CAAC,CAAA;IAChB,CAAC,CAAA;IAED,mEAAmE;IACnE,IAAI,eAA6C,CAAA;IACjD,IAAI,OAAO,GAAG,EAAE,CAAA;IAEhB,mEAAmE;IACnE,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QAC9B,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC/B,SAAS,IAAI,KAAK,CAAC,IAAI,CAAA;YACvB,IAAI,kBAAkB,KAAK,SAAS,EAAE,CAAC;gBACrC,MAAM,oBAAoB,EAAE,CAAA;YAC9B,CAAC;iBAAM,CAAC;gBACN,cAAc,GAAG,IAAI,CAAA;gBACrB,sBAAsB,EAAE,CAAA;YAC1B,CAAC;QACH,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YACpC,IAAI,CAAC,eAAe,EAAE,CAAC;gBACrB,+DAA+D;gBAC/D,2DAA2D;gBAC3D,kCAAkC;gBAClC,IAAI,cAAc,EAAE,CAAC;oBACnB,YAAY,CAAC,cAAc,CAAC,CAAA;oBAC5B,cAAc,GAAG,SAAS,CAAA;gBAC5B,CAAC;gBACD,IAAI,cAAc;oBAAE,MAAM,cAAc,EAAE,CAAA;gBAC1C,eAAe,GAAG,IAAI,gBAAgB,CACpC,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,EACvC,IAAI,CACL,CAAA;YACH,CAAC;YACD,OAAO,IAAI,KAAK,CAAC,IAAI,CAAA;YACrB,MAAM,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC1C,CAAC;IACH,CAAC;IAED,mEAAmE;IACnE,IAAI,cAAc,EAAE,CAAC;QACnB,YAAY,CAAC,cAAc,CAAC,CAAA;QAC5B,cAAc,GAAG,SAAS,CAAA;IAC5B,CAAC;IACD,IAAI,cAAc;QAAE,MAAM,cAAc,EAAE,CAAA;IAC1C,MAAM,eAAe,EAAE,GAAG,EAAE,CAAA;IAE5B,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAA;AAC/B,CAAC,CAAA;AAoGD,MAAM,cAAc,GAAG,SAAS,CAAA;AAEhC,MAAM,SAAS,GAAG,CAAC,GAGlB,EAAU,EAAE;IACX,uEAAuE;IACvE,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAA;IACjD,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,cAAc,CAAA;AACzD,CAAC,CAAA;AAED,MAAM,KAAK,GAAG,CACZ,KAAoB,EACpB,QAAgB,EAChB,aAAqB,EACN,EAAE;IACjB,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,aAAa,GAAG,KAAK,CAAA;IACvE,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,SAAS,CAAC,CAAA;IACtD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAA;AAC/B,CAAC,CAAA;AAED,MAAM,QAAQ,GAAG,CACf,MAA4B,EAC5B,QAAgB,EAChB,aAAqB,EACC,EAAE;IACxB,MAAM,GAAG,GAAyB,EAAE,CAAA;IACpC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACpC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAA;QACxD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,CAAA;IACxC,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAuB,EAAqB,EAAE;IACvE,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAA;IAC3E,IAAI,IAAI,CAAC,aAAa,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAA;IAErF,MAAM,MAAM,GAAG,kBAAkB,CAAC;QAChC,aAAa,EAAE,IAAI,CAAC,OAAO;QAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,aAAa,EAAE,IAAI,CAAC,aAAa;KAClC,CAAC,CAAA;IAEF,MAAM,SAAS,GAAqB,IAAI,CAAC,UAAU,EAAE,IAAI,IAAI;QAC3D,EAAE,EAAE,sBAAsB;QAC1B,EAAE,EAAE,sBAAsB;KAC3B,CAAA;IACD,MAAM,YAAY,GAAqB,IAAI,CAAC,UAAU,EAAE,OAAO,IAAI;QACjE,EAAE,EAAE,aAAa;QACjB,EAAE,EAAE,YAAY;KACjB,CAAA;IAED,MAAM,QAAQ,GAAa;QACzB,EAAE,EAAE,UAAU;QACd,KAAK,EAAE,SAAS;QAChB,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAUpB,MAAM,CAAC,GAAG,GAAyB,CAAA;YACnC,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,CAAA;YACd,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC,CAAA;QAC/D,CAAC;KACF,CAAA;IAED,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;AAC7B,CAAC,CAAA;AAED,MAAM,kBAAkB,GAAG,CAAC,IAI3B,EAAE,EAAE;IACH,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI,CAAA;IAEvD,OAAO,IAAI,MAAM,CACf,sCAAsC,CACvC;SACE,MAAM,CAAC,aAAa,CAAC;SACrB,MAAM,CAAC,CAAC,SAAS,EAAE,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAqB,EAAE;QAChE,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,CAAA;QAE1B,iEAAiE;QACjE,6DAA6D;QAC7D,MAAM,UAAU,GAAG,GAAyB,EAAE,CAC5C,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,IAAI,EAAE,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAA;QAElE,MAAM,WAAW,GAAG,CAAC,MAA4B,EAAQ,EAAE;YACzD,GAAG,CAAC,OAAO,CAAC,GAAG,GAAG,EAAE,MAAM,EAAE,CAAA;QAC9B,CAAC,CAAA;QAED,OAAO;YACL,GAAG,EAAE;gBACH,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;oBACf,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;oBAC3B,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAA;oBAC7B,MAAM,KAAK,GAAgB;wBACzB,IAAI,EAAE,OAAO,CAAC,IAAI;wBAClB,OAAO,EAAE,OAAO,CAAC,OAAO;wBACxB,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;qBACpD,CAAA;oBACD,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAA;oBAC9C,WAAW,CAAC,MAAM,CAAC,CAAA;gBACrB,CAAC;gBACD,GAAG,EAAE,GAAG,EAAE,CACR,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,CAA+B;gBACzD,KAAK,EAAE,GAAG,EAAE;oBACV,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;oBAC3B,OAAO,MAAM,CAAC,GAAG,CAAC,CAAA;oBAClB,WAAW,CAAC,MAAM,CAAC,CAAA;gBACrB,CAAC;gBACD,GAAG,EAAE,GAAG,EAAE,CACR,UAAU,EAER;gBACJ,QAAQ,EAAE,GAAG,EAAE;oBACb,WAAW,CAAC,EAAE,CAAC,CAAA;gBACjB,CAAC;aACF;SACF,CAAA;IACH,CAAC,CAAC,CAAA;AACN,CAAC,CAAA"}
|