@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/README.md
CHANGED
|
@@ -198,7 +198,7 @@ Every `SourcedError` carries `source`, `operation`, `status`, `context`, and the
|
|
|
198
198
|
- **XDG**: XDG Base Directory paths — reads env vars set by [xdg-dirs](https://github.com/adriangalilea/xdg-dirs), falls back to spec defaults
|
|
199
199
|
- **Unseen**: Persistent dedup filter — "what's new since last time?" for cron/monitoring workflows
|
|
200
200
|
- **Project Discovery**: Find project/monorepo roots, detect JS/TS projects
|
|
201
|
-
- **Bot plugins (GramIO)**: `kit` (graceful shutdown + admin context), `access-control` (gate + approve/deny menu, backed by sessions), `llm
|
|
201
|
+
- **Bot plugins (GramIO)**: `kit` (graceful shutdown + admin context), `access-control` (gate + approve/deny menu, backed by sessions), `llm` (OpenAI-compat SSE parser `streamChat` + Telegram streaming output `ctx.startStream` + per-thread conversation history `ctx.llm`), `coalesce`, `language`, `menu`
|
|
202
202
|
|
|
203
203
|
### XDG Base Directories
|
|
204
204
|
|
|
@@ -305,7 +305,8 @@ Then `pnpm install`. Every `ctx.send` / `ctx.sendDocument` / `ctx.reply` / etc.
|
|
|
305
305
|
| `@adriangalilea/utils/bot/kit` | `gracefulStart(bot)` — SIGINT/SIGTERM → `bot.stop()` → exit; force-kills if shutdown hangs.<br>`adminContext({ adminId? })` — reads `TELEGRAM_ADMIN_ID` from `kev` (with optional hardcoded fallback), decorates `ctx.adminId` + `ctx.isAdmin`. |
|
|
306
306
|
| `@adriangalilea/utils/bot/access-control` | Personal-bot ACL — gates non-admin/non-default users; admin gets DM with `[✅ Aprobar][❌ Denegar]` on first attempt; `/access` opens a persistent menu (revoke / reapprove / list pending). Backed by `@gramio/session` per-user + a small index. |
|
|
307
307
|
| `@adriangalilea/utils/bot/coalesce` | Joins client-split inbound messages back into one. When a user pastes >4096 chars, Telegram clients fragment it into separate `message` updates with no marker. Middleware detects the burst and emits one combined event. |
|
|
308
|
-
| `@adriangalilea/utils/bot/llm
|
|
308
|
+
| `@adriangalilea/utils/bot/llm` | The full LLM-chatbot pipeline in one module. **Input:** `streamChat(response)` parses OpenAI-compatible SSE (OpenAI, vllm, mlx-lm, llama.cpp, Together, Groq, …) into a typed `AsyncGenerator<{type: 'content' \| 'reasoning', text}>`. **Output:** `ctx.startStream()` (low-level: debounced markdown to Telegram, 4000-char split, exposes `wasPartial` after `.end()`). `ctx.startChatStream(response)` (high-level: consumes the stream, renders reasoning as a Telegram `expandable_blockquote` entity + content as streamed markdown — both go through `markdownToFormattable` with graceful degradation — returns `{ content, reasoning }`). **History:** `llmHistory({...})` returns `.plugin` (decorates `ctx.llm` with `.add() / .get() / .clear() / .all() / .clearAll()`, per-(user, thread) OpenAI `ChatMessage` shape, persisted in the shared session record so the menu's 🗑 Forget wipes it automatically) AND `.menuItem` (drop-in "🧹 Clear this thread" for `botMenu`). |
|
|
309
|
+
| `@adriangalilea/utils/bot/menu` | `botMenu({ command, description, items, privacy?, personalData?, adminContact })` — `/settings` command + InlineKeyboard router. With `personalData: { storage }`, auto-adds 🗑 Forget + 📥 Export buttons. `toggleMenuItem({ id, read, write, label: { off, on }, toast? })` — convenience factory for boolean-toggle items with dynamic label + optional toast, storage-agnostic via `read`/`write` closures. |
|
|
309
310
|
|
|
310
311
|
Standard wiring:
|
|
311
312
|
|
|
@@ -314,18 +315,37 @@ import { Bot } from 'gramio'
|
|
|
314
315
|
import { redisStorage } from '@gramio/storage-redis'
|
|
315
316
|
import { adminContext, gracefulStart } from '@adriangalilea/utils/bot/kit'
|
|
316
317
|
import { accessControl } from '@adriangalilea/utils/bot/access-control'
|
|
317
|
-
import {
|
|
318
|
+
import { session } from '@gramio/session'
|
|
319
|
+
import { llmStream, llmHistory } from '@adriangalilea/utils/bot/llm'
|
|
318
320
|
|
|
319
321
|
const storage = redisStorage() // ONE instance, shared
|
|
322
|
+
const userSession = session({ storage, key: 'session', initial: () => ({}) })
|
|
323
|
+
const chat = llmHistory({ session: userSession, maxTurns: 20, retentionDays: 7 })
|
|
320
324
|
|
|
321
325
|
const bot = new Bot(process.env.BOT_TOKEN!)
|
|
322
326
|
.extend(adminContext({ adminId: 190202471 })) // KEV.TELEGRAM_ADMIN_ID overrides
|
|
323
|
-
.extend(
|
|
327
|
+
.extend(userSession)
|
|
328
|
+
.extend(accessControl({ session: userSession, storage, defaults: [] }))
|
|
324
329
|
.extend(llmStream())
|
|
325
|
-
.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
330
|
+
.extend(chat.plugin)
|
|
331
|
+
.on('message', async (ctx) => {
|
|
332
|
+
if (!ctx.access.allowed) return
|
|
333
|
+
ctx.llm.add({ role: 'user', content: ctx.text ?? '' })
|
|
334
|
+
|
|
335
|
+
// Any OpenAI-compatible endpoint: vllm-mlx, mlx-lm, llama.cpp, Together, Groq, OpenAI, …
|
|
336
|
+
const response = await fetch(process.env.LLM_URL!, {
|
|
337
|
+
method: 'POST',
|
|
338
|
+
headers: { 'Content-Type': 'application/json' },
|
|
339
|
+
body: JSON.stringify({
|
|
340
|
+
model: process.env.LLM_MODEL,
|
|
341
|
+
messages: [{ role: 'system', content: 'You are helpful.' }, ...ctx.llm.get()],
|
|
342
|
+
stream: true,
|
|
343
|
+
}),
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
// High-level: handles reasoning (collapsed blockquote) + content streaming.
|
|
347
|
+
const { content } = await ctx.startChatStream(response)
|
|
348
|
+
ctx.llm.add({ role: 'assistant', content })
|
|
329
349
|
})
|
|
330
350
|
|
|
331
351
|
await gracefulStart(bot)
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* storage[String(userId)] = {
|
|
26
26
|
* access: { status, approvedAt, … }, // ← this plugin
|
|
27
27
|
* language: 'es', // ← bot/language
|
|
28
|
-
*
|
|
28
|
+
* llm: { shards: { 'general': [...] } }, // ← bot/llm (history)
|
|
29
29
|
* }
|
|
30
30
|
*
|
|
31
31
|
* Plus one tiny admin-side index so `/access` can list pending /
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* storage[String(userId)] = {
|
|
26
26
|
* access: { status, approvedAt, … }, // ← this plugin
|
|
27
27
|
* language: 'es', // ← bot/language
|
|
28
|
-
*
|
|
28
|
+
* llm: { shards: { 'general': [...] } }, // ← bot/llm (history)
|
|
29
29
|
* }
|
|
30
30
|
*
|
|
31
31
|
* Plus one tiny admin-side index so `/access` can list pending /
|
package/dist/bot/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* import { adminContext, gracefulStart } from '@adriangalilea/utils/bot/kit'
|
|
8
8
|
* import { accessControl } from '@adriangalilea/utils/bot/access-control'
|
|
9
|
-
* import { llmStream } from '@adriangalilea/utils/bot/llm
|
|
9
|
+
* import { llmStream } from '@adriangalilea/utils/bot/llm'
|
|
10
10
|
*
|
|
11
11
|
* Or all-in-one (pulls every subpath):
|
|
12
12
|
* import { ... } from '@adriangalilea/utils/bot'
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
export * from './kit.js';
|
|
15
15
|
export * from './access-control.js';
|
|
16
16
|
export * from './coalesce.js';
|
|
17
|
-
export * from './llm
|
|
17
|
+
export * from './llm.js';
|
|
18
18
|
export * from './menu.js';
|
|
19
19
|
export * from './language.js';
|
|
20
|
-
export * from './message-history.js';
|
|
21
20
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/bot/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bot/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,cAAc,UAAU,CAAA;AACxB,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bot/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,cAAc,UAAU,CAAA;AACxB,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA"}
|
package/dist/bot/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* import { adminContext, gracefulStart } from '@adriangalilea/utils/bot/kit'
|
|
8
8
|
* import { accessControl } from '@adriangalilea/utils/bot/access-control'
|
|
9
|
-
* import { llmStream } from '@adriangalilea/utils/bot/llm
|
|
9
|
+
* import { llmStream } from '@adriangalilea/utils/bot/llm'
|
|
10
10
|
*
|
|
11
11
|
* Or all-in-one (pulls every subpath):
|
|
12
12
|
* import { ... } from '@adriangalilea/utils/bot'
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
export * from './kit.js';
|
|
15
15
|
export * from './access-control.js';
|
|
16
16
|
export * from './coalesce.js';
|
|
17
|
-
export * from './llm
|
|
17
|
+
export * from './llm.js';
|
|
18
18
|
export * from './menu.js';
|
|
19
19
|
export * from './language.js';
|
|
20
|
-
export * from './message-history.js';
|
|
21
20
|
//# sourceMappingURL=index.js.map
|
package/dist/bot/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/bot/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,cAAc,UAAU,CAAA;AACxB,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/bot/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,cAAc,UAAU,CAAA;AACxB,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA"}
|
|
@@ -0,0 +1,520 @@
|
|
|
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 { type DeriveDefinitions, Plugin } from 'gramio';
|
|
84
|
+
import { session } from '@gramio/session';
|
|
85
|
+
import { type Polyglot } from '../say/index.js';
|
|
86
|
+
import type { MenuItem } from './menu.js';
|
|
87
|
+
/**
|
|
88
|
+
* A single chunk yielded by `streamChat`. Two kinds:
|
|
89
|
+
*
|
|
90
|
+
* - `content` — the visible reply text the user should see
|
|
91
|
+
* - `reasoning` — chain-of-thought / "thinking" text from reasoning
|
|
92
|
+
* models. Empty unless the model emits it.
|
|
93
|
+
*
|
|
94
|
+
* Most callers care about `content` only. Render `reasoning` separately
|
|
95
|
+
* (collapsed, italicized) if you want to surface thinking.
|
|
96
|
+
*/
|
|
97
|
+
export type LLMChunk = {
|
|
98
|
+
type: 'content';
|
|
99
|
+
text: string;
|
|
100
|
+
} | {
|
|
101
|
+
type: 'reasoning';
|
|
102
|
+
text: string;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Parse an OpenAI-compatible chat-completions SSE response into a
|
|
106
|
+
* typed `AsyncGenerator<LLMChunk>`. Reads `response.body` once.
|
|
107
|
+
*
|
|
108
|
+
* Recognised reasoning aliases (as of 2026): `reasoning_content`
|
|
109
|
+
* (vllm, qwen3, DeepSeek-R1, gpt-oss harmony) and `reasoning` (some
|
|
110
|
+
* mlx-lm forks, gemma builds). If a model surfaces a new key, add it
|
|
111
|
+
* here — single source of truth for the field.
|
|
112
|
+
*
|
|
113
|
+
* Constrained-SSE assumption: lines are `\n`-delimited and each event
|
|
114
|
+
* is `data: <json>` or `data: [DONE]`. This matches every OpenAI-compat
|
|
115
|
+
* server in the wild but is NOT the full SSE spec (no comments, no
|
|
116
|
+
* multi-line `data:`, no `retry`/`id` fields). Swap to
|
|
117
|
+
* `eventsource-parser` if you hit a producer that needs them.
|
|
118
|
+
*
|
|
119
|
+
* Malformed JSON lines are silently skipped; the generator ends when
|
|
120
|
+
* the stream closes.
|
|
121
|
+
*
|
|
122
|
+
* @param response the `fetch` `Response` from a `stream: true` chat
|
|
123
|
+
* completion call. Must not be already consumed.
|
|
124
|
+
* @throws if `response.body` is null (non-streaming response).
|
|
125
|
+
*
|
|
126
|
+
* @example framework-agnostic — parser doesn't know about Telegram
|
|
127
|
+
* const res = await fetch(url, { method: 'POST', body })
|
|
128
|
+
* for await (const chunk of streamChat(res)) {
|
|
129
|
+
* if (chunk.type === 'content') process.stdout.write(chunk.text)
|
|
130
|
+
* }
|
|
131
|
+
*/
|
|
132
|
+
export declare function streamChat(response: Response): AsyncGenerator<LLMChunk>;
|
|
133
|
+
export type StreamOptions = {
|
|
134
|
+
/** Debounce window between edits, in ms. Default 800. */
|
|
135
|
+
debounceMs?: number;
|
|
136
|
+
/** Initial placeholder shown until the first chunk arrives. Default "…". */
|
|
137
|
+
placeholder?: string;
|
|
138
|
+
/** Parse buffer as markdown. Default true. Set false for plain text streaming. */
|
|
139
|
+
markdown?: boolean;
|
|
140
|
+
/** Called on edit/send errors after internal recovery (rate limits, etc.). */
|
|
141
|
+
onError?: (err: unknown) => void;
|
|
142
|
+
};
|
|
143
|
+
export declare class MarkdownStreamer {
|
|
144
|
+
private buffer;
|
|
145
|
+
private currentMessageId?;
|
|
146
|
+
private firstSendPromise?;
|
|
147
|
+
private debounceTimer?;
|
|
148
|
+
private inFlight;
|
|
149
|
+
private dirty;
|
|
150
|
+
private ended;
|
|
151
|
+
/**
|
|
152
|
+
* `true` after `.end()` returns if the stream finished with un-flushed
|
|
153
|
+
* buffer or an outstanding error that the streamer couldn't recover
|
|
154
|
+
* from. Useful for partial-response diagnostics in catch blocks:
|
|
155
|
+
*
|
|
156
|
+
* try { for await (...) await stream.append(...) }
|
|
157
|
+
* finally {
|
|
158
|
+
* await stream.end()
|
|
159
|
+
* if (stream.wasPartial) logger.warn('LLM stream cut off')
|
|
160
|
+
* }
|
|
161
|
+
*/
|
|
162
|
+
wasPartial: boolean;
|
|
163
|
+
private chatId;
|
|
164
|
+
private threadId?;
|
|
165
|
+
private bot;
|
|
166
|
+
private opts;
|
|
167
|
+
constructor(ctx: {
|
|
168
|
+
chat: {
|
|
169
|
+
id: number;
|
|
170
|
+
};
|
|
171
|
+
threadId?: number;
|
|
172
|
+
bot: MarkdownStreamer['bot'];
|
|
173
|
+
}, opts: StreamOptions);
|
|
174
|
+
/** Append a chunk. Schedules a debounced edit. */
|
|
175
|
+
append(text: string): Promise<void>;
|
|
176
|
+
/** Flush any pending edit and close the stream. Idempotent. */
|
|
177
|
+
end(): Promise<void>;
|
|
178
|
+
private scheduleFlush;
|
|
179
|
+
private flushNow;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* GramIO plugin. Adds `ctx.startStream(opts?)` on every message context.
|
|
183
|
+
*
|
|
184
|
+
* Defaults set here apply to every stream; per-call options in
|
|
185
|
+
* `ctx.startStream({...})` override them.
|
|
186
|
+
*/
|
|
187
|
+
export declare const llmStream: (defaults?: StreamOptions) => Plugin<{}, DeriveDefinitions & {
|
|
188
|
+
message: {
|
|
189
|
+
startStream: (opts?: StreamOptions) => MarkdownStreamer;
|
|
190
|
+
/**
|
|
191
|
+
* One-call helper for chat completions: consumes a `Response`
|
|
192
|
+
* (or any `AsyncIterable<LLMChunk>`), renders reasoning to an
|
|
193
|
+
* expandable blockquote message + content to a streamed markdown
|
|
194
|
+
* message, returns `{ content, reasoning }` when the stream ends.
|
|
195
|
+
*/
|
|
196
|
+
startChatStream: (source: Response | AsyncIterable<LLMChunk>, opts?: ChatStreamOptions) => Promise<ChatStreamResult>;
|
|
197
|
+
};
|
|
198
|
+
}, {}>;
|
|
199
|
+
/**
|
|
200
|
+
* Return shape of `consumeChatStream` / `ctx.startChatStream`. Both
|
|
201
|
+
* full transcripts are returned so the caller decides what to persist
|
|
202
|
+
* (typically `content` to `ctx.llm`; `reasoning` already lives in the
|
|
203
|
+
* Telegram message and is rarely worth storing).
|
|
204
|
+
*/
|
|
205
|
+
export type ChatStreamResult = {
|
|
206
|
+
/** Full assistant text concatenated from all `content` chunks. */
|
|
207
|
+
content: string;
|
|
208
|
+
/** Full reasoning text concatenated from all `reasoning` chunks. Empty for non-thinking models. */
|
|
209
|
+
reasoning: string;
|
|
210
|
+
};
|
|
211
|
+
export type ChatStreamOptions = StreamOptions;
|
|
212
|
+
/**
|
|
213
|
+
* One-shot helper that consumes an LLM stream and renders BOTH phases
|
|
214
|
+
* (reasoning, content) to Telegram with the canonical pattern:
|
|
215
|
+
*
|
|
216
|
+
* - reasoning chunks → one message containing a single
|
|
217
|
+
* `<blockquote expandable>…</blockquote>` (HTML), debounced edits,
|
|
218
|
+
* stays expanded after the stream ends so the user can review it
|
|
219
|
+
* collapsed/uncollapsed at will
|
|
220
|
+
* - content chunks → a fresh `MarkdownStreamer` message below the
|
|
221
|
+
* reasoning one (auto-threaded via SendMixin)
|
|
222
|
+
*
|
|
223
|
+
* Returns when the source generator ends. Equivalent to (and replaces)
|
|
224
|
+
* the ~50 LOC of bookkeeping every thinking-model bot would otherwise
|
|
225
|
+
* write by hand around `streamChat` + `ctx.startStream`.
|
|
226
|
+
*
|
|
227
|
+
* Accepts either a raw `fetch` `Response` (wrapped with `streamChat`
|
|
228
|
+
* internally) or an `AsyncIterable<LLMChunk>` (when you want to map /
|
|
229
|
+
* filter chunks before the stream hits Telegram).
|
|
230
|
+
*
|
|
231
|
+
* @example via ctx.startChatStream from the llmStream plugin
|
|
232
|
+
* const response = await fetch(LLM_URL, { method: 'POST', body })
|
|
233
|
+
* const { content } = await ctx.startChatStream(response)
|
|
234
|
+
* ctx.llm.add({ role: 'assistant', content })
|
|
235
|
+
*
|
|
236
|
+
* @example standalone, framework-agnostic
|
|
237
|
+
* const { content, reasoning } = await consumeChatStream(ctx, response)
|
|
238
|
+
*/
|
|
239
|
+
export declare const consumeChatStream: (ctx: {
|
|
240
|
+
chat: {
|
|
241
|
+
id: number;
|
|
242
|
+
};
|
|
243
|
+
threadId?: number;
|
|
244
|
+
bot: MarkdownStreamer["bot"];
|
|
245
|
+
}, source: Response | AsyncIterable<LLMChunk>, opts?: ChatStreamOptions) => Promise<ChatStreamResult>;
|
|
246
|
+
/**
|
|
247
|
+
* Multimodal content shape from OpenAI's chat-completions spec. Either
|
|
248
|
+
* a plain string or an ordered array of typed parts. Image URLs cover
|
|
249
|
+
* both http(s) and Telegram `getFile` resolved paths.
|
|
250
|
+
*/
|
|
251
|
+
export type ChatContent = string | Array<{
|
|
252
|
+
type: 'text';
|
|
253
|
+
text: string;
|
|
254
|
+
} | {
|
|
255
|
+
type: 'image_url';
|
|
256
|
+
image_url: {
|
|
257
|
+
url: string;
|
|
258
|
+
};
|
|
259
|
+
}>;
|
|
260
|
+
/**
|
|
261
|
+
* One turn in the conversation. The library does NOT filter by role —
|
|
262
|
+
* if you persist `system` turns, they ride along on every `get()`.
|
|
263
|
+
* Most callers prepend their system prompt fresh each request and only
|
|
264
|
+
* persist `user` / `assistant`.
|
|
265
|
+
*/
|
|
266
|
+
export type ChatMessage = {
|
|
267
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
268
|
+
content: ChatContent;
|
|
269
|
+
/** Unix seconds when added — used for retention pruning. */
|
|
270
|
+
date: number;
|
|
271
|
+
};
|
|
272
|
+
/** Per-thread shards of `ChatMessage`s, persisted in the session. */
|
|
273
|
+
type ChatRecord = {
|
|
274
|
+
shards: {
|
|
275
|
+
[threadKey: string]: ChatMessage[];
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
/** Loose session shape — this plugin only touches the `llm` field. */
|
|
279
|
+
type LLMSessionLike = {
|
|
280
|
+
llm?: ChatRecord;
|
|
281
|
+
};
|
|
282
|
+
/** @internal — kept unexported so it doesn't clash with peers' refs. */
|
|
283
|
+
type LLMSessionPluginRef = ReturnType<typeof session<LLMSessionLike, 'session'>>;
|
|
284
|
+
export type LLMHistoryOptions = {
|
|
285
|
+
/**
|
|
286
|
+
* Shared session plugin. This plugin extends it for type flow;
|
|
287
|
+
* gramio's runtime dedup ensures the session derive runs once.
|
|
288
|
+
*/
|
|
289
|
+
session: LLMSessionPluginRef;
|
|
290
|
+
/** Ring buffer cap **per thread**. Oldest entries dropped past this. */
|
|
291
|
+
maxTurns: number;
|
|
292
|
+
/** Entries older than this (in days) are dropped on read. */
|
|
293
|
+
retentionDays: number;
|
|
294
|
+
/**
|
|
295
|
+
* Override the labels of the `menuItem` (the "🧹 clear this thread"
|
|
296
|
+
* button rendered inside a `botMenu`). Defaults are polyglot
|
|
297
|
+
* literals covering en + es.
|
|
298
|
+
*/
|
|
299
|
+
menuLabels?: {
|
|
300
|
+
/** Button label. Default: `{ en: '🧹 Clear this thread', es: '🧹 Limpiar este hilo' }`. */
|
|
301
|
+
item?: Polyglot<string>;
|
|
302
|
+
/** Toast shown after a successful clear. Default: `{ en: '🧹 Cleared.', es: '🧹 Limpio.' }`. */
|
|
303
|
+
cleared?: Polyglot<string>;
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
export type LLMHistoryFeature = {
|
|
307
|
+
plugin: ReturnType<typeof buildHistoryPlugin>;
|
|
308
|
+
/**
|
|
309
|
+
* Drop-in `MenuItem` for `botMenu({ items: [...] })`: a "clear this
|
|
310
|
+
* thread" button that calls `ctx.llm.clear()` on the current
|
|
311
|
+
* (user, thread) shard and acknowledges with a toast. Sibling
|
|
312
|
+
* threads stay intact.
|
|
313
|
+
*/
|
|
314
|
+
menuItem: MenuItem;
|
|
315
|
+
};
|
|
316
|
+
/**
|
|
317
|
+
* Methods decorated onto `ctx.llm`. All synchronous — reads/writes the
|
|
318
|
+
* session record via `@gramio/session`'s Proxy, which auto-persists.
|
|
319
|
+
*
|
|
320
|
+
* Thread isolation is automatic: every method operates on the shard
|
|
321
|
+
* for `ctx.threadId` (or `'general'` when no thread). Different threads
|
|
322
|
+
* = different conversations, no leakage.
|
|
323
|
+
*/
|
|
324
|
+
export type LLMHistoryApi = {
|
|
325
|
+
/** Append one message to the CURRENT thread's shard. */
|
|
326
|
+
add: (message: Omit<ChatMessage, 'date'> & {
|
|
327
|
+
date?: number;
|
|
328
|
+
}) => void;
|
|
329
|
+
/** Pruned snapshot of the CURRENT thread, oldest-first. */
|
|
330
|
+
get: () => ReadonlyArray<ChatMessage>;
|
|
331
|
+
/** Wipe the CURRENT thread's shard. */
|
|
332
|
+
clear: () => void;
|
|
333
|
+
/**
|
|
334
|
+
* Full sharded map, pruned. Use for /export or admin views. Keys are
|
|
335
|
+
* thread ids (or `'general'`) → ordered messages.
|
|
336
|
+
*/
|
|
337
|
+
all: () => Readonly<{
|
|
338
|
+
[threadKey: string]: ReadonlyArray<ChatMessage>;
|
|
339
|
+
}>;
|
|
340
|
+
/** Wipe ALL threads for this user. */
|
|
341
|
+
clearAll: () => void;
|
|
342
|
+
};
|
|
343
|
+
type LLMHistoryDerives = {
|
|
344
|
+
llm: LLMHistoryApi;
|
|
345
|
+
};
|
|
346
|
+
/**
|
|
347
|
+
* Per-(user, thread) LLM conversation history. Opt-in. Persists in the
|
|
348
|
+
* shared `@gramio/session` record under `llm`, so 🗑 Forget from
|
|
349
|
+
* `botMenu` wipes it together with everything else — one record, one
|
|
350
|
+
* delete, no per-plugin registry.
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* const chat = llmHistory({ session: userSession, maxTurns: 20, retentionDays: 7 })
|
|
354
|
+
* bot.extend(chat.plugin)
|
|
355
|
+
* .on('message', (ctx) => {
|
|
356
|
+
* ctx.llm.add({ role: 'user', content: ctx.text ?? '' })
|
|
357
|
+
* const messages = ctx.llm.get() // ChatMessage[] for current thread
|
|
358
|
+
* // ... call LLM with messages, then:
|
|
359
|
+
* ctx.llm.add({ role: 'assistant', content: reply })
|
|
360
|
+
* })
|
|
361
|
+
*/
|
|
362
|
+
export declare const llmHistory: (opts: LLMHistoryOptions) => LLMHistoryFeature;
|
|
363
|
+
declare const buildHistoryPlugin: (args: {
|
|
364
|
+
sessionPlugin: LLMSessionPluginRef;
|
|
365
|
+
maxTurns: number;
|
|
366
|
+
retentionDays: number;
|
|
367
|
+
}) => Plugin<{}, DeriveDefinitions & {
|
|
368
|
+
global: LLMHistoryDerives;
|
|
369
|
+
} & {
|
|
370
|
+
message: {
|
|
371
|
+
session: LLMSessionLike & {
|
|
372
|
+
$clear: () => Promise<void>;
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
channel_post: {
|
|
376
|
+
session: LLMSessionLike & {
|
|
377
|
+
$clear: () => Promise<void>;
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
inline_query: {
|
|
381
|
+
session: LLMSessionLike & {
|
|
382
|
+
$clear: () => Promise<void>;
|
|
383
|
+
};
|
|
384
|
+
};
|
|
385
|
+
chosen_inline_result: {
|
|
386
|
+
session: LLMSessionLike & {
|
|
387
|
+
$clear: () => Promise<void>;
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
callback_query: {
|
|
391
|
+
session: LLMSessionLike & {
|
|
392
|
+
$clear: () => Promise<void>;
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
shipping_query: {
|
|
396
|
+
session: LLMSessionLike & {
|
|
397
|
+
$clear: () => Promise<void>;
|
|
398
|
+
};
|
|
399
|
+
};
|
|
400
|
+
pre_checkout_query: {
|
|
401
|
+
session: LLMSessionLike & {
|
|
402
|
+
$clear: () => Promise<void>;
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
poll_answer: {
|
|
406
|
+
session: LLMSessionLike & {
|
|
407
|
+
$clear: () => Promise<void>;
|
|
408
|
+
};
|
|
409
|
+
};
|
|
410
|
+
chat_join_request: {
|
|
411
|
+
session: LLMSessionLike & {
|
|
412
|
+
$clear: () => Promise<void>;
|
|
413
|
+
};
|
|
414
|
+
};
|
|
415
|
+
new_chat_members: {
|
|
416
|
+
session: LLMSessionLike & {
|
|
417
|
+
$clear: () => Promise<void>;
|
|
418
|
+
};
|
|
419
|
+
};
|
|
420
|
+
new_chat_title: {
|
|
421
|
+
session: LLMSessionLike & {
|
|
422
|
+
$clear: () => Promise<void>;
|
|
423
|
+
};
|
|
424
|
+
};
|
|
425
|
+
new_chat_photo: {
|
|
426
|
+
session: LLMSessionLike & {
|
|
427
|
+
$clear: () => Promise<void>;
|
|
428
|
+
};
|
|
429
|
+
};
|
|
430
|
+
delete_chat_photo: {
|
|
431
|
+
session: LLMSessionLike & {
|
|
432
|
+
$clear: () => Promise<void>;
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
group_chat_created: {
|
|
436
|
+
session: LLMSessionLike & {
|
|
437
|
+
$clear: () => Promise<void>;
|
|
438
|
+
};
|
|
439
|
+
};
|
|
440
|
+
message_auto_delete_timer_changed: {
|
|
441
|
+
session: LLMSessionLike & {
|
|
442
|
+
$clear: () => Promise<void>;
|
|
443
|
+
};
|
|
444
|
+
};
|
|
445
|
+
migrate_to_chat_id: {
|
|
446
|
+
session: LLMSessionLike & {
|
|
447
|
+
$clear: () => Promise<void>;
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
migrate_from_chat_id: {
|
|
451
|
+
session: LLMSessionLike & {
|
|
452
|
+
$clear: () => Promise<void>;
|
|
453
|
+
};
|
|
454
|
+
};
|
|
455
|
+
pinned_message: {
|
|
456
|
+
session: LLMSessionLike & {
|
|
457
|
+
$clear: () => Promise<void>;
|
|
458
|
+
};
|
|
459
|
+
};
|
|
460
|
+
invoice: {
|
|
461
|
+
session: LLMSessionLike & {
|
|
462
|
+
$clear: () => Promise<void>;
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
successful_payment: {
|
|
466
|
+
session: LLMSessionLike & {
|
|
467
|
+
$clear: () => Promise<void>;
|
|
468
|
+
};
|
|
469
|
+
};
|
|
470
|
+
chat_shared: {
|
|
471
|
+
session: LLMSessionLike & {
|
|
472
|
+
$clear: () => Promise<void>;
|
|
473
|
+
};
|
|
474
|
+
};
|
|
475
|
+
proximity_alert_triggered: {
|
|
476
|
+
session: LLMSessionLike & {
|
|
477
|
+
$clear: () => Promise<void>;
|
|
478
|
+
};
|
|
479
|
+
};
|
|
480
|
+
video_chat_scheduled: {
|
|
481
|
+
session: LLMSessionLike & {
|
|
482
|
+
$clear: () => Promise<void>;
|
|
483
|
+
};
|
|
484
|
+
};
|
|
485
|
+
video_chat_started: {
|
|
486
|
+
session: LLMSessionLike & {
|
|
487
|
+
$clear: () => Promise<void>;
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
video_chat_ended: {
|
|
491
|
+
session: LLMSessionLike & {
|
|
492
|
+
$clear: () => Promise<void>;
|
|
493
|
+
};
|
|
494
|
+
};
|
|
495
|
+
video_chat_participants_invited: {
|
|
496
|
+
session: LLMSessionLike & {
|
|
497
|
+
$clear: () => Promise<void>;
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
web_app_data: {
|
|
501
|
+
session: LLMSessionLike & {
|
|
502
|
+
$clear: () => Promise<void>;
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
location: {
|
|
506
|
+
session: LLMSessionLike & {
|
|
507
|
+
$clear: () => Promise<void>;
|
|
508
|
+
};
|
|
509
|
+
};
|
|
510
|
+
passport_data: {
|
|
511
|
+
session: LLMSessionLike & {
|
|
512
|
+
$clear: () => Promise<void>;
|
|
513
|
+
};
|
|
514
|
+
};
|
|
515
|
+
} & {
|
|
516
|
+
message: LLMHistoryDerives;
|
|
517
|
+
callback_query: LLMHistoryDerives;
|
|
518
|
+
}, {}>;
|
|
519
|
+
export {};
|
|
520
|
+
//# sourceMappingURL=llm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../../src/bot/llm.ts"],"names":[],"mappings":"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"}
|