@llblab/pi-telegram 0.2.9 → 0.3.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 +40 -26
- package/docs/architecture.md +62 -35
- package/index.ts +388 -1936
- package/lib/api.ts +647 -76
- package/lib/attachments.ts +128 -16
- package/lib/commands.ts +721 -0
- package/lib/config.ts +157 -0
- package/lib/media.ts +211 -36
- package/lib/menu.ts +920 -338
- package/lib/model.ts +647 -0
- package/lib/pi.ts +80 -0
- package/lib/polling.ts +264 -18
- package/lib/preview.ts +451 -29
- package/lib/queue.ts +1134 -110
- package/lib/registration.ts +127 -28
- package/lib/rendering.ts +575 -281
- package/lib/replies.ts +198 -8
- package/lib/runtime.ts +475 -0
- package/lib/setup.ts +129 -1
- package/lib/status.ts +428 -13
- package/lib/turns.ts +207 -17
- package/lib/updates.ts +392 -99
- package/package.json +18 -3
- package/AGENTS.md +0 -91
- package/BACKLOG.md +0 -5
- package/CHANGELOG.md +0 -23
- package/lib/model-switch.ts +0 -62
- package/tests/api.test.ts +0 -89
- package/tests/attachments.test.ts +0 -132
- package/tests/config.test.ts +0 -80
- package/tests/media.test.ts +0 -77
- package/tests/menu.test.ts +0 -676
- package/tests/polling.test.ts +0 -129
- package/tests/preview.test.ts +0 -441
- package/tests/queue.test.ts +0 -3245
- package/tests/registration.test.ts +0 -268
- package/tests/rendering.test.ts +0 -475
- package/tests/replies.test.ts +0 -142
- package/tests/turns.test.ts +0 -132
- package/tests/updates.test.ts +0 -357
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 { readFile } from "node:fs/promises";
|
|
6
7
|
import { basename } from "node:path";
|
|
7
8
|
|
|
8
|
-
import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
|
|
9
|
-
|
|
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,7 +52,7 @@ export function truncateTelegramQueueSummary(
|
|
|
45
52
|
|
|
46
53
|
export function formatTelegramTurnStatusSummary(
|
|
47
54
|
rawText: string,
|
|
48
|
-
files:
|
|
55
|
+
files: DownloadedTelegramTurnFile[],
|
|
49
56
|
): string {
|
|
50
57
|
const textSummary = truncateTelegramQueueSummary(rawText);
|
|
51
58
|
if (textSummary) return textSummary;
|
|
@@ -62,7 +69,7 @@ export function formatTelegramTurnStatusSummary(
|
|
|
62
69
|
export function buildTelegramTurnPrompt(options: {
|
|
63
70
|
telegramPrefix: string;
|
|
64
71
|
rawText: string;
|
|
65
|
-
files:
|
|
72
|
+
files: DownloadedTelegramTurnFile[];
|
|
66
73
|
historyTurns?: Pick<PendingTelegramTurn, "historyText">[];
|
|
67
74
|
}): string {
|
|
68
75
|
let prompt = options.telegramPrefix;
|
|
@@ -89,21 +96,195 @@ export function buildTelegramTurnPrompt(options: {
|
|
|
89
96
|
return prompt;
|
|
90
97
|
}
|
|
91
98
|
|
|
92
|
-
|
|
99
|
+
function splitTelegramPromptAttachmentSuffix(prompt: string): {
|
|
100
|
+
promptWithoutAttachments: string;
|
|
101
|
+
attachmentSuffix: string;
|
|
102
|
+
attachmentFiles: DownloadedTelegramTurnFile[];
|
|
103
|
+
} {
|
|
104
|
+
const marker = "\n\nTelegram attachments were saved locally:";
|
|
105
|
+
const markerIndex = prompt.indexOf(marker);
|
|
106
|
+
if (markerIndex === -1) {
|
|
107
|
+
return {
|
|
108
|
+
promptWithoutAttachments: prompt,
|
|
109
|
+
attachmentSuffix: "",
|
|
110
|
+
attachmentFiles: [],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const promptWithoutAttachments = prompt.slice(0, markerIndex);
|
|
114
|
+
const attachmentSuffix = prompt.slice(markerIndex);
|
|
115
|
+
const attachmentFiles = attachmentSuffix
|
|
116
|
+
.split("\n")
|
|
117
|
+
.map((line) => line.match(/^- (.+)$/)?.[1]?.trim())
|
|
118
|
+
.filter((path): path is string => !!path)
|
|
119
|
+
.map((path) => ({ path, fileName: basename(path), isImage: false }));
|
|
120
|
+
return { promptWithoutAttachments, attachmentSuffix, attachmentFiles };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildEditedTelegramPromptText(options: {
|
|
124
|
+
existingPrompt: string;
|
|
93
125
|
telegramPrefix: string;
|
|
94
|
-
|
|
126
|
+
rawText: string;
|
|
127
|
+
}): { text: string; attachmentFiles: DownloadedTelegramTurnFile[] } {
|
|
128
|
+
const { promptWithoutAttachments, attachmentSuffix, attachmentFiles } =
|
|
129
|
+
splitTelegramPromptAttachmentSuffix(options.existingPrompt);
|
|
130
|
+
const currentMessageMarker = "Current Telegram message:";
|
|
131
|
+
const currentMessageIndex =
|
|
132
|
+
promptWithoutAttachments.lastIndexOf(currentMessageMarker);
|
|
133
|
+
if (currentMessageIndex !== -1) {
|
|
134
|
+
const prefix = promptWithoutAttachments.slice(
|
|
135
|
+
0,
|
|
136
|
+
currentMessageIndex + currentMessageMarker.length,
|
|
137
|
+
);
|
|
138
|
+
const separator = options.rawText.length > 0 ? "\n" : "";
|
|
139
|
+
return {
|
|
140
|
+
text: `${prefix}${separator}${options.rawText}${attachmentSuffix}`,
|
|
141
|
+
attachmentFiles,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const promptText =
|
|
145
|
+
options.rawText.length > 0
|
|
146
|
+
? `${options.telegramPrefix} ${options.rawText}`
|
|
147
|
+
: options.telegramPrefix;
|
|
148
|
+
return {
|
|
149
|
+
text: `${promptText}${attachmentSuffix}`,
|
|
150
|
+
attachmentFiles,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function updateTelegramPromptTurnText(options: {
|
|
155
|
+
turn: PendingTelegramTurn;
|
|
156
|
+
telegramPrefix: string;
|
|
157
|
+
rawText: string;
|
|
158
|
+
}): PendingTelegramTurn {
|
|
159
|
+
let attachmentFiles: DownloadedTelegramTurnFile[] = [];
|
|
160
|
+
const nextContent = options.turn.content.map((block, index) => {
|
|
161
|
+
if (index !== 0 || block.type !== "text") return block;
|
|
162
|
+
const updated = buildEditedTelegramPromptText({
|
|
163
|
+
existingPrompt: block.text,
|
|
164
|
+
telegramPrefix: options.telegramPrefix,
|
|
165
|
+
rawText: options.rawText,
|
|
166
|
+
});
|
|
167
|
+
attachmentFiles = updated.attachmentFiles;
|
|
168
|
+
return {
|
|
169
|
+
...block,
|
|
170
|
+
text: updated.text,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
...options.turn,
|
|
175
|
+
content: nextContent,
|
|
176
|
+
historyText: formatTelegramHistoryText(options.rawText, attachmentFiles),
|
|
177
|
+
statusSummary: formatTelegramTurnStatusSummary(
|
|
178
|
+
options.rawText,
|
|
179
|
+
attachmentFiles,
|
|
180
|
+
),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function updateQueuedTelegramPromptTurnText<
|
|
185
|
+
TContext = unknown,
|
|
186
|
+
>(options: {
|
|
187
|
+
items: TelegramQueueItem<TContext>[];
|
|
188
|
+
sourceMessageId: number | undefined;
|
|
189
|
+
telegramPrefix: string;
|
|
190
|
+
rawText: string;
|
|
191
|
+
}): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
|
|
192
|
+
if (options.sourceMessageId === undefined) {
|
|
193
|
+
return { items: options.items, changed: false };
|
|
194
|
+
}
|
|
195
|
+
let changed = false;
|
|
196
|
+
const items = options.items.map((item) => {
|
|
197
|
+
if (
|
|
198
|
+
item.kind !== "prompt" ||
|
|
199
|
+
!item.sourceMessageIds.includes(options.sourceMessageId as number)
|
|
200
|
+
) {
|
|
201
|
+
return item;
|
|
202
|
+
}
|
|
203
|
+
changed = true;
|
|
204
|
+
return updateTelegramPromptTurnText({
|
|
205
|
+
turn: item,
|
|
206
|
+
telegramPrefix: options.telegramPrefix,
|
|
207
|
+
rawText: options.rawText,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
return { items, changed };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface TelegramQueuedPromptEditRuntimeDeps<
|
|
214
|
+
TContext = unknown,
|
|
215
|
+
> extends TelegramQueueStore<TContext> {
|
|
216
|
+
updateStatus: (ctx: TContext) => void;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function createTelegramQueuedPromptEditRuntime<
|
|
220
|
+
TMessage extends TelegramMediaMessage,
|
|
221
|
+
TContext = unknown,
|
|
222
|
+
>(deps: TelegramQueuedPromptEditRuntimeDeps<TContext>) {
|
|
223
|
+
return {
|
|
224
|
+
updateFromEditedMessage: (message: TMessage, ctx: TContext): boolean => {
|
|
225
|
+
const { changed, items } = updateQueuedTelegramPromptTurnText({
|
|
226
|
+
items: deps.getQueuedItems(),
|
|
227
|
+
sourceMessageId: message.message_id,
|
|
228
|
+
telegramPrefix: TELEGRAM_PREFIX,
|
|
229
|
+
rawText: extractTelegramMessagesText([message]),
|
|
230
|
+
});
|
|
231
|
+
deps.setQueuedItems(items);
|
|
232
|
+
if (changed) deps.updateStatus(ctx);
|
|
233
|
+
return changed;
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface BuildTelegramPromptTurnOptions {
|
|
239
|
+
telegramPrefix: string;
|
|
240
|
+
messages: TelegramTurnMessage[];
|
|
95
241
|
historyTurns?: PendingTelegramTurn[];
|
|
96
242
|
queueOrder: number;
|
|
97
243
|
rawText: string;
|
|
98
|
-
files:
|
|
244
|
+
files: DownloadedTelegramTurnFile[];
|
|
99
245
|
readBinaryFile: (path: string) => Promise<Uint8Array>;
|
|
100
246
|
inferImageMimeType: (path: string) => string | undefined;
|
|
101
|
-
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export type BuildTelegramPromptTurnRuntimeOptions = Omit<
|
|
250
|
+
BuildTelegramPromptTurnOptions,
|
|
251
|
+
"readBinaryFile"
|
|
252
|
+
>;
|
|
253
|
+
|
|
254
|
+
export interface TelegramPromptTurnRuntimeBuilderDeps extends DownloadTelegramMessageFilesDeps {
|
|
255
|
+
allocateQueueOrder: () => number;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function createTelegramPromptTurnRuntimeBuilder<
|
|
259
|
+
TMessage extends TelegramTurnMessage & TelegramMediaMessage,
|
|
260
|
+
>(
|
|
261
|
+
deps: TelegramPromptTurnRuntimeBuilderDeps,
|
|
262
|
+
): (
|
|
263
|
+
messages: TMessage[],
|
|
264
|
+
historyTurns?: PendingTelegramTurn[],
|
|
265
|
+
) => Promise<PendingTelegramTurn> {
|
|
266
|
+
return async (messages, historyTurns = []) =>
|
|
267
|
+
buildTelegramPromptTurnRuntime({
|
|
268
|
+
telegramPrefix: TELEGRAM_PREFIX,
|
|
269
|
+
messages,
|
|
270
|
+
historyTurns,
|
|
271
|
+
queueOrder: deps.allocateQueueOrder(),
|
|
272
|
+
rawText: extractTelegramMessagesText(messages),
|
|
273
|
+
files: await downloadTelegramMessageFiles(messages, {
|
|
274
|
+
downloadFile: deps.downloadFile,
|
|
275
|
+
}),
|
|
276
|
+
inferImageMimeType: guessMediaType,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function buildTelegramPromptTurn(
|
|
281
|
+
options: BuildTelegramPromptTurnOptions,
|
|
282
|
+
): Promise<PendingTelegramTurn> {
|
|
102
283
|
const firstMessage = options.messages[0];
|
|
103
284
|
if (!firstMessage) {
|
|
104
285
|
throw new Error("Missing Telegram message for turn creation");
|
|
105
286
|
}
|
|
106
|
-
const content:
|
|
287
|
+
const content: TelegramPromptContent[] = [
|
|
107
288
|
{
|
|
108
289
|
type: "text",
|
|
109
290
|
text: buildTelegramTurnPrompt({
|
|
@@ -142,3 +323,12 @@ export async function buildTelegramPromptTurn(options: {
|
|
|
142
323
|
),
|
|
143
324
|
};
|
|
144
325
|
}
|
|
326
|
+
|
|
327
|
+
export async function buildTelegramPromptTurnRuntime(
|
|
328
|
+
options: BuildTelegramPromptTurnRuntimeOptions,
|
|
329
|
+
): Promise<PendingTelegramTurn> {
|
|
330
|
+
return buildTelegramPromptTurn({
|
|
331
|
+
...options,
|
|
332
|
+
readBinaryFile: readFile,
|
|
333
|
+
});
|
|
334
|
+
}
|