@llblab/pi-telegram 0.2.10 → 0.4.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 +52 -19
- package/docs/README.md +2 -3
- package/docs/architecture.md +62 -31
- package/docs/locks.md +136 -0
- package/index.ts +323 -1880
- package/lib/api.ts +396 -60
- package/lib/attachments.ts +128 -16
- package/lib/commands.ts +648 -14
- package/lib/config.ts +169 -0
- package/lib/handlers.ts +474 -0
- package/lib/locks.ts +306 -0
- package/lib/media.ts +196 -46
- package/lib/menu.ts +920 -338
- package/lib/model.ts +647 -0
- package/lib/pi.ts +90 -0
- package/lib/polling.ts +240 -14
- package/lib/preview.ts +420 -25
- package/lib/queue.ts +1137 -110
- package/lib/registration.ts +214 -31
- package/lib/rendering.ts +560 -366
- package/lib/replies.ts +198 -8
- package/lib/routing.ts +217 -0
- package/lib/runtime.ts +475 -0
- package/lib/setup.ts +143 -1
- package/lib/status.ts +432 -13
- package/lib/turns.ts +217 -36
- package/lib/updates.ts +340 -109
- package/package.json +18 -3
- package/AGENTS.md +0 -91
- package/BACKLOG.md +0 -5
- package/CHANGELOG.md +0 -34
- package/lib/model-switch.ts +0 -62
- package/lib/types.ts +0 -137
- package/tests/api.test.ts +0 -331
- package/tests/attachments.test.ts +0 -132
- package/tests/commands.test.ts +0 -85
- package/tests/config.test.ts +0 -80
- package/tests/media.test.ts +0 -166
- package/tests/menu.test.ts +0 -676
- package/tests/polling.test.ts +0 -202
- package/tests/preview.test.ts +0 -480
- package/tests/queue.test.ts +0 -3245
- package/tests/registration.test.ts +0 -268
- package/tests/rendering.test.ts +0 -526
- package/tests/replies.test.ts +0 -142
- package/tests/turns.test.ts +0 -247
- package/tests/updates.test.ts +0 -416
package/lib/turns.ts
CHANGED
|
@@ -3,27 +3,34 @@
|
|
|
3
3
|
* Owns prompt-turn summary and content construction so queued Telegram turns are assembled consistently
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import { basename, dirname, join } from "node:path";
|
|
9
8
|
|
|
10
9
|
import {
|
|
11
10
|
collectTelegramMessageIds,
|
|
11
|
+
type DownloadedTelegramMessageFile,
|
|
12
|
+
type DownloadTelegramMessageFilesDeps,
|
|
13
|
+
downloadTelegramMessageFiles,
|
|
14
|
+
extractTelegramMessagesText,
|
|
12
15
|
formatTelegramHistoryText,
|
|
16
|
+
guessMediaType,
|
|
17
|
+
type TelegramMediaMessage,
|
|
13
18
|
} from "./media.ts";
|
|
14
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
PendingTelegramTurn,
|
|
21
|
+
TelegramPromptContent,
|
|
22
|
+
TelegramQueueItem,
|
|
23
|
+
TelegramQueueStore,
|
|
24
|
+
} from "./queue.ts";
|
|
25
|
+
|
|
26
|
+
export const TELEGRAM_PREFIX = "[telegram]";
|
|
15
27
|
|
|
16
|
-
export interface
|
|
28
|
+
export interface TelegramTurnMessage {
|
|
17
29
|
message_id: number;
|
|
18
30
|
chat: { id: number };
|
|
19
31
|
}
|
|
20
32
|
|
|
21
|
-
export
|
|
22
|
-
path: string;
|
|
23
|
-
fileName: string;
|
|
24
|
-
isImage: boolean;
|
|
25
|
-
mimeType?: string;
|
|
26
|
-
}
|
|
33
|
+
export type DownloadedTelegramTurnFile = DownloadedTelegramMessageFile;
|
|
27
34
|
|
|
28
35
|
export function truncateTelegramQueueSummary(
|
|
29
36
|
text: string,
|
|
@@ -45,10 +52,15 @@ export function truncateTelegramQueueSummary(
|
|
|
45
52
|
|
|
46
53
|
export function formatTelegramTurnStatusSummary(
|
|
47
54
|
rawText: string,
|
|
48
|
-
files:
|
|
55
|
+
files: DownloadedTelegramTurnFile[],
|
|
56
|
+
handlerOutputs: string[] = [],
|
|
49
57
|
): string {
|
|
50
58
|
const textSummary = truncateTelegramQueueSummary(rawText);
|
|
51
59
|
if (textSummary) return textSummary;
|
|
60
|
+
const handlerSummary = truncateTelegramQueueSummary(
|
|
61
|
+
handlerOutputs.join(" "),
|
|
62
|
+
);
|
|
63
|
+
if (handlerSummary) return handlerSummary;
|
|
52
64
|
if (files.length === 1) {
|
|
53
65
|
const fileName = basename(
|
|
54
66
|
files[0]?.fileName || files[0]?.path || "attachment",
|
|
@@ -59,10 +71,37 @@ export function formatTelegramTurnStatusSummary(
|
|
|
59
71
|
return "(empty message)";
|
|
60
72
|
}
|
|
61
73
|
|
|
74
|
+
function appendTelegramListSection(
|
|
75
|
+
text: string,
|
|
76
|
+
title: string,
|
|
77
|
+
items: string[],
|
|
78
|
+
): string {
|
|
79
|
+
if (items.length === 0) return text;
|
|
80
|
+
const prefix = text.length > 0 ? `${text}\n\n` : "";
|
|
81
|
+
return `${prefix}[${title}]\n${items.map((item) => `- ${item}`).join("\n")}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function appendTelegramAttachmentSection(
|
|
85
|
+
text: string,
|
|
86
|
+
files: Pick<DownloadedTelegramTurnFile, "path">[],
|
|
87
|
+
): string {
|
|
88
|
+
if (files.length === 0) return text;
|
|
89
|
+
const dirs = [...new Set(files.map((file) => dirname(file.path)))];
|
|
90
|
+
const sameDir = dirs.length === 1;
|
|
91
|
+
const header = sameDir ? `[attachments] ${dirs[0]}` : "[attachments]";
|
|
92
|
+
const items = sameDir
|
|
93
|
+
? files.map((file) => `/${basename(file.path)}`)
|
|
94
|
+
: files.map((file) => file.path);
|
|
95
|
+
const prefix = text.length > 0 ? `${text}\n\n` : "";
|
|
96
|
+
return `${prefix}${header}\n${items.map((item) => `- ${item}`).join("\n")}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
62
99
|
export function buildTelegramTurnPrompt(options: {
|
|
63
100
|
telegramPrefix: string;
|
|
64
101
|
rawText: string;
|
|
65
|
-
files:
|
|
102
|
+
files: DownloadedTelegramTurnFile[];
|
|
103
|
+
promptFiles?: DownloadedTelegramTurnFile[];
|
|
104
|
+
handlerOutputs?: string[];
|
|
66
105
|
historyTurns?: Pick<PendingTelegramTurn, "historyText">[];
|
|
67
106
|
}): string {
|
|
68
107
|
let prompt = options.telegramPrefix;
|
|
@@ -80,21 +119,18 @@ export function buildTelegramTurnPrompt(options: {
|
|
|
80
119
|
? `\n${options.rawText}`
|
|
81
120
|
: ` ${options.rawText}`;
|
|
82
121
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
prompt += `\n- ${file.path}`;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
122
|
+
const promptFiles = options.promptFiles ?? options.files;
|
|
123
|
+
prompt = appendTelegramAttachmentSection(prompt, promptFiles);
|
|
124
|
+
prompt = appendTelegramListSection(prompt, "outputs", options.handlerOutputs ?? []);
|
|
89
125
|
return prompt;
|
|
90
126
|
}
|
|
91
127
|
|
|
92
128
|
function splitTelegramPromptAttachmentSuffix(prompt: string): {
|
|
93
129
|
promptWithoutAttachments: string;
|
|
94
130
|
attachmentSuffix: string;
|
|
95
|
-
attachmentFiles:
|
|
131
|
+
attachmentFiles: DownloadedTelegramTurnFile[];
|
|
96
132
|
} {
|
|
97
|
-
const marker = "\n\
|
|
133
|
+
const marker = "\n\n[attachments]";
|
|
98
134
|
const markerIndex = prompt.indexOf(marker);
|
|
99
135
|
if (markerIndex === -1) {
|
|
100
136
|
return {
|
|
@@ -105,11 +141,30 @@ function splitTelegramPromptAttachmentSuffix(prompt: string): {
|
|
|
105
141
|
}
|
|
106
142
|
const promptWithoutAttachments = prompt.slice(0, markerIndex);
|
|
107
143
|
const attachmentSuffix = prompt.slice(markerIndex);
|
|
108
|
-
const
|
|
109
|
-
|
|
144
|
+
const attachmentLines: string[] = [];
|
|
145
|
+
let readingAttachments = false;
|
|
146
|
+
let attachmentDir: string | undefined;
|
|
147
|
+
for (const line of attachmentSuffix.split("\n")) {
|
|
148
|
+
const trimmed = line.trim();
|
|
149
|
+
const attachmentMatch = trimmed.match(/^\[attachments\](?:\s+(.+))?$/);
|
|
150
|
+
if (attachmentMatch) {
|
|
151
|
+
readingAttachments = true;
|
|
152
|
+
attachmentDir = attachmentMatch[1]?.trim();
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (readingAttachments && /^\[[^\]]+\](?:\s+.*)?$/.test(trimmed)) break;
|
|
156
|
+
if (readingAttachments) attachmentLines.push(line);
|
|
157
|
+
}
|
|
158
|
+
const attachmentFiles = attachmentLines
|
|
110
159
|
.map((line) => line.match(/^- (.+)$/)?.[1]?.trim())
|
|
111
160
|
.filter((path): path is string => !!path)
|
|
112
|
-
.map((path) => (
|
|
161
|
+
.map((path) => (attachmentDir ? join(attachmentDir, path.replace(/^\/+/, "")) : path))
|
|
162
|
+
.map((path) => ({
|
|
163
|
+
path,
|
|
164
|
+
fileName: basename(path),
|
|
165
|
+
isImage: false,
|
|
166
|
+
kind: "document" as const,
|
|
167
|
+
}));
|
|
113
168
|
return { promptWithoutAttachments, attachmentSuffix, attachmentFiles };
|
|
114
169
|
}
|
|
115
170
|
|
|
@@ -117,13 +172,12 @@ function buildEditedTelegramPromptText(options: {
|
|
|
117
172
|
existingPrompt: string;
|
|
118
173
|
telegramPrefix: string;
|
|
119
174
|
rawText: string;
|
|
120
|
-
}): { text: string; attachmentFiles:
|
|
175
|
+
}): { text: string; attachmentFiles: DownloadedTelegramTurnFile[] } {
|
|
121
176
|
const { promptWithoutAttachments, attachmentSuffix, attachmentFiles } =
|
|
122
177
|
splitTelegramPromptAttachmentSuffix(options.existingPrompt);
|
|
123
178
|
const currentMessageMarker = "Current Telegram message:";
|
|
124
|
-
const currentMessageIndex =
|
|
125
|
-
currentMessageMarker
|
|
126
|
-
);
|
|
179
|
+
const currentMessageIndex =
|
|
180
|
+
promptWithoutAttachments.lastIndexOf(currentMessageMarker);
|
|
127
181
|
if (currentMessageIndex !== -1) {
|
|
128
182
|
const prefix = promptWithoutAttachments.slice(
|
|
129
183
|
0,
|
|
@@ -150,7 +204,7 @@ export function updateTelegramPromptTurnText(options: {
|
|
|
150
204
|
telegramPrefix: string;
|
|
151
205
|
rawText: string;
|
|
152
206
|
}): PendingTelegramTurn {
|
|
153
|
-
let attachmentFiles:
|
|
207
|
+
let attachmentFiles: DownloadedTelegramTurnFile[] = [];
|
|
154
208
|
const nextContent = options.turn.content.map((block, index) => {
|
|
155
209
|
if (index !== 0 || block.type !== "text") return block;
|
|
156
210
|
const updated = buildEditedTelegramPromptText({
|
|
@@ -175,27 +229,140 @@ export function updateTelegramPromptTurnText(options: {
|
|
|
175
229
|
};
|
|
176
230
|
}
|
|
177
231
|
|
|
178
|
-
export
|
|
232
|
+
export function updateQueuedTelegramPromptTurnText<
|
|
233
|
+
TContext = unknown,
|
|
234
|
+
>(options: {
|
|
235
|
+
items: TelegramQueueItem<TContext>[];
|
|
236
|
+
sourceMessageId: number | undefined;
|
|
179
237
|
telegramPrefix: string;
|
|
180
|
-
|
|
238
|
+
rawText: string;
|
|
239
|
+
}): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
|
|
240
|
+
if (options.sourceMessageId === undefined) {
|
|
241
|
+
return { items: options.items, changed: false };
|
|
242
|
+
}
|
|
243
|
+
let changed = false;
|
|
244
|
+
const items = options.items.map((item) => {
|
|
245
|
+
if (
|
|
246
|
+
item.kind !== "prompt" ||
|
|
247
|
+
!item.sourceMessageIds.includes(options.sourceMessageId as number)
|
|
248
|
+
) {
|
|
249
|
+
return item;
|
|
250
|
+
}
|
|
251
|
+
changed = true;
|
|
252
|
+
return updateTelegramPromptTurnText({
|
|
253
|
+
turn: item,
|
|
254
|
+
telegramPrefix: options.telegramPrefix,
|
|
255
|
+
rawText: options.rawText,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
return { items, changed };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface TelegramQueuedPromptEditRuntimeDeps<
|
|
262
|
+
TContext = unknown,
|
|
263
|
+
> extends TelegramQueueStore<TContext> {
|
|
264
|
+
updateStatus: (ctx: TContext) => void;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function createTelegramQueuedPromptEditRuntime<
|
|
268
|
+
TMessage extends TelegramMediaMessage,
|
|
269
|
+
TContext = unknown,
|
|
270
|
+
>(deps: TelegramQueuedPromptEditRuntimeDeps<TContext>) {
|
|
271
|
+
return {
|
|
272
|
+
updateFromEditedMessage: (message: TMessage, ctx: TContext): boolean => {
|
|
273
|
+
const { changed, items } = updateQueuedTelegramPromptTurnText({
|
|
274
|
+
items: deps.getQueuedItems(),
|
|
275
|
+
sourceMessageId: message.message_id,
|
|
276
|
+
telegramPrefix: TELEGRAM_PREFIX,
|
|
277
|
+
rawText: extractTelegramMessagesText([message]),
|
|
278
|
+
});
|
|
279
|
+
deps.setQueuedItems(items);
|
|
280
|
+
if (changed) deps.updateStatus(ctx);
|
|
281
|
+
return changed;
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export interface BuildTelegramPromptTurnOptions {
|
|
287
|
+
telegramPrefix: string;
|
|
288
|
+
messages: TelegramTurnMessage[];
|
|
181
289
|
historyTurns?: PendingTelegramTurn[];
|
|
182
290
|
queueOrder: number;
|
|
183
291
|
rawText: string;
|
|
184
|
-
files:
|
|
292
|
+
files: DownloadedTelegramTurnFile[];
|
|
293
|
+
promptFiles?: DownloadedTelegramTurnFile[];
|
|
294
|
+
handlerOutputs?: string[];
|
|
185
295
|
readBinaryFile: (path: string) => Promise<Uint8Array>;
|
|
186
296
|
inferImageMimeType: (path: string) => string | undefined;
|
|
187
|
-
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export type BuildTelegramPromptTurnRuntimeOptions = Omit<
|
|
300
|
+
BuildTelegramPromptTurnOptions,
|
|
301
|
+
"readBinaryFile"
|
|
302
|
+
>;
|
|
303
|
+
|
|
304
|
+
export interface TelegramPromptTurnRuntimeBuilderDeps<TContext = unknown>
|
|
305
|
+
extends DownloadTelegramMessageFilesDeps {
|
|
306
|
+
allocateQueueOrder: () => number;
|
|
307
|
+
processAttachments?: (
|
|
308
|
+
files: DownloadedTelegramTurnFile[],
|
|
309
|
+
rawText: string,
|
|
310
|
+
ctx: TContext,
|
|
311
|
+
) => Promise<{
|
|
312
|
+
rawText: string;
|
|
313
|
+
promptFiles?: DownloadedTelegramTurnFile[];
|
|
314
|
+
handlerOutputs?: string[];
|
|
315
|
+
}>;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function createTelegramPromptTurnRuntimeBuilder<
|
|
319
|
+
TMessage extends TelegramTurnMessage & TelegramMediaMessage,
|
|
320
|
+
TContext = unknown,
|
|
321
|
+
>(
|
|
322
|
+
deps: TelegramPromptTurnRuntimeBuilderDeps<TContext>,
|
|
323
|
+
): (
|
|
324
|
+
messages: TMessage[],
|
|
325
|
+
historyTurns?: PendingTelegramTurn[],
|
|
326
|
+
ctx?: TContext,
|
|
327
|
+
) => Promise<PendingTelegramTurn> {
|
|
328
|
+
return async (messages, historyTurns = [], ctx) => {
|
|
329
|
+
const rawText = extractTelegramMessagesText(messages);
|
|
330
|
+
const files = await downloadTelegramMessageFiles(messages, {
|
|
331
|
+
downloadFile: deps.downloadFile,
|
|
332
|
+
});
|
|
333
|
+
const processed = deps.processAttachments
|
|
334
|
+
? await deps.processAttachments(files, rawText, ctx as TContext)
|
|
335
|
+
: { rawText, promptFiles: files };
|
|
336
|
+
return buildTelegramPromptTurnRuntime({
|
|
337
|
+
telegramPrefix: TELEGRAM_PREFIX,
|
|
338
|
+
messages,
|
|
339
|
+
historyTurns,
|
|
340
|
+
queueOrder: deps.allocateQueueOrder(),
|
|
341
|
+
rawText: processed.rawText,
|
|
342
|
+
files,
|
|
343
|
+
promptFiles: processed.promptFiles,
|
|
344
|
+
handlerOutputs: processed.handlerOutputs,
|
|
345
|
+
inferImageMimeType: guessMediaType,
|
|
346
|
+
});
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function buildTelegramPromptTurn(
|
|
351
|
+
options: BuildTelegramPromptTurnOptions,
|
|
352
|
+
): Promise<PendingTelegramTurn> {
|
|
188
353
|
const firstMessage = options.messages[0];
|
|
189
354
|
if (!firstMessage) {
|
|
190
355
|
throw new Error("Missing Telegram message for turn creation");
|
|
191
356
|
}
|
|
192
|
-
const content:
|
|
357
|
+
const content: TelegramPromptContent[] = [
|
|
193
358
|
{
|
|
194
359
|
type: "text",
|
|
195
360
|
text: buildTelegramTurnPrompt({
|
|
196
361
|
telegramPrefix: options.telegramPrefix,
|
|
197
362
|
rawText: options.rawText,
|
|
198
363
|
files: options.files,
|
|
364
|
+
promptFiles: options.promptFiles,
|
|
365
|
+
handlerOutputs: options.handlerOutputs,
|
|
199
366
|
historyTurns: options.historyTurns,
|
|
200
367
|
}),
|
|
201
368
|
},
|
|
@@ -221,10 +388,24 @@ export async function buildTelegramPromptTurn(options: {
|
|
|
221
388
|
laneOrder: options.queueOrder,
|
|
222
389
|
queuedAttachments: [],
|
|
223
390
|
content,
|
|
224
|
-
historyText: formatTelegramHistoryText(
|
|
391
|
+
historyText: formatTelegramHistoryText(
|
|
392
|
+
options.rawText,
|
|
393
|
+
options.promptFiles ?? options.files,
|
|
394
|
+
options.handlerOutputs,
|
|
395
|
+
),
|
|
225
396
|
statusSummary: formatTelegramTurnStatusSummary(
|
|
226
397
|
options.rawText,
|
|
227
|
-
options.files,
|
|
398
|
+
options.promptFiles ?? options.files,
|
|
399
|
+
options.handlerOutputs,
|
|
228
400
|
),
|
|
229
401
|
};
|
|
230
402
|
}
|
|
403
|
+
|
|
404
|
+
export async function buildTelegramPromptTurnRuntime(
|
|
405
|
+
options: BuildTelegramPromptTurnRuntimeOptions,
|
|
406
|
+
): Promise<PendingTelegramTurn> {
|
|
407
|
+
return buildTelegramPromptTurn({
|
|
408
|
+
...options,
|
|
409
|
+
readBinaryFile: readFile,
|
|
410
|
+
});
|
|
411
|
+
}
|