@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/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 { PendingTelegramTurn } from "./queue.ts";
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 TelegramTurnMessageLike {
28
+ export interface TelegramTurnMessage {
17
29
  message_id: number;
18
30
  chat: { id: number };
19
31
  }
20
32
 
21
- export interface DownloadedTelegramTurnFileLike {
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: DownloadedTelegramTurnFileLike[],
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: DownloadedTelegramTurnFileLike[];
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
- export async function buildTelegramPromptTurn(options: {
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
- messages: TelegramTurnMessageLike[];
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: DownloadedTelegramTurnFileLike[];
244
+ files: DownloadedTelegramTurnFile[];
99
245
  readBinaryFile: (path: string) => Promise<Uint8Array>;
100
246
  inferImageMimeType: (path: string) => string | undefined;
101
- }): Promise<PendingTelegramTurn> {
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: Array<TextContent | ImageContent> = [
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
+ }