@llblab/pi-telegram 0.35.1 → 0.36.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/menu-queue.ts CHANGED
@@ -88,7 +88,7 @@ function buildTelegramQueueMenuReplyMarkup(
88
88
  if (items.length === 0) return { inline_keyboard: [backRow, refreshRow] };
89
89
  const rows = items.map((item, index) => {
90
90
  const prefix = item.reactionSuppressionEmoji
91
- ? `${item.reactionSuppressionEmoji} suppressed · `
91
+ ? `${item.reactionSuppressionEmoji} `
92
92
  : item.isPriority
93
93
  ? `${item.priorityEmoji ?? "⚡"} `
94
94
  : item.hasAttachments
@@ -163,17 +163,15 @@ function getTelegramQueueMenuItemText(item: TelegramQueueMenuItem): string {
163
163
  ? ` ${item.priorityEmoji ?? "⚡"}`
164
164
  : "";
165
165
  const heading = `<b>${item.queuePosition}.</b>${badge}`;
166
- const suppression = item.reactionSuppressionEmoji
167
- ? "\n<i>Suppressed by reaction. Remove it to restore this turn.</i>"
168
- : "";
169
166
  const preview = `<pre>${escapeTelegramQueueMenuHtmlPreview(item.promptText)}</pre>`;
170
- return `${heading}${suppression}\n${preview}`;
167
+ return `${heading}\n${preview}`;
171
168
  }
172
169
 
173
170
  function buildTelegramQueueItemSubmenuReplyMarkup(
174
171
  chatId: number,
175
172
  replyToMessageId: number,
176
173
  isPriority: boolean,
174
+ isSkipped: boolean,
177
175
  ): TelegramQueueMenuReplyMarkup {
178
176
  return {
179
177
  inline_keyboard: [
@@ -190,33 +188,18 @@ function buildTelegramQueueItemSubmenuReplyMarkup(
190
188
  ],
191
189
  [
192
190
  {
193
- text: "🗑 Delete",
194
- callback_data: `queue:delete:${chatId}:${replyToMessageId}`,
191
+ text: isSkipped ? "⚫️ Keep" : "🟢 Keep",
192
+ callback_data: `queue:skip-set:${chatId}:${replyToMessageId}:keep`,
195
193
  },
196
- ],
197
- ],
198
- };
199
- }
200
-
201
- function buildTelegramQueueDeleteConfirmationReplyMarkup(
202
- chatId: number,
203
- replyToMessageId: number,
204
- ): TelegramQueueMenuReplyMarkup {
205
- return {
206
- inline_keyboard: [
207
- [
208
194
  {
209
- text: "🗑 Yes, delete",
210
- callback_data: `queue:confirm-delete:${chatId}:${replyToMessageId}`,
211
- },
212
- {
213
- text: "❌ No",
214
- callback_data: `queue:keep:${chatId}:${replyToMessageId}`,
195
+ text: isSkipped ? "🟡 Skip" : "⚫️ Skip",
196
+ callback_data: `queue:skip-set:${chatId}:${replyToMessageId}:skip`,
215
197
  },
216
198
  ],
217
199
  ],
218
200
  };
219
201
  }
202
+
220
203
  interface TelegramQueueMenuCallbackDeps<Context = unknown> {
221
204
  getQueuedItems: () => TelegramQueueMenuItem[];
222
205
  findItem: (
@@ -229,10 +212,10 @@ interface TelegramQueueMenuCallbackDeps<Context = unknown> {
229
212
  replyToMessageId: number,
230
213
  enabled: boolean,
231
214
  ) => boolean;
232
- cancelItem: (
215
+ setSkipped: (
233
216
  chatId: number,
234
217
  replyToMessageId: number,
235
- ctx: Context,
218
+ skipped: boolean,
236
219
  ) => boolean;
237
220
  updateQueueMessage: (
238
221
  chatId: number,
@@ -322,38 +305,17 @@ async function handleTelegramQueueMenuCallback<Context>(
322
305
  );
323
306
  return true;
324
307
  }
325
- const deleteMatch = data.match(/^queue:(?:delete|cancel):(\d+):(\d+)$/);
326
- if (deleteMatch) {
327
- await handleTelegramQueueMenuDeleteRequest(
328
- callbackQueryId,
329
- replyChatId,
330
- replyMessageId,
331
- Number(deleteMatch[1]),
332
- Number(deleteMatch[2]),
333
- deps,
334
- );
335
- return true;
336
- }
337
- const keepMatch = data.match(/^queue:keep:(\d+):(\d+)$/);
338
- if (keepMatch) {
339
- await handleTelegramQueueMenuKeep(
340
- callbackQueryId,
341
- replyChatId,
342
- replyMessageId,
343
- Number(keepMatch[1]),
344
- Number(keepMatch[2]),
345
- deps,
346
- );
347
- return true;
348
- }
349
- const confirmDeleteMatch = data.match(/^queue:confirm-delete:(\d+):(\d+)$/);
350
- if (confirmDeleteMatch) {
351
- await handleTelegramQueueMenuConfirmDelete(
308
+ const skipSetMatch = data.match(
309
+ /^queue:skip-set:(\d+):(\d+):(skip|keep)$/,
310
+ );
311
+ if (skipSetMatch) {
312
+ await handleTelegramQueueMenuSkipSet(
352
313
  callbackQueryId,
353
314
  replyChatId,
354
315
  replyMessageId,
355
- Number(confirmDeleteMatch[1]),
356
- Number(confirmDeleteMatch[2]),
316
+ Number(skipSetMatch[1]),
317
+ Number(skipSetMatch[2]),
318
+ skipSetMatch[3] === "skip",
357
319
  ctx,
358
320
  deps,
359
321
  );
@@ -427,7 +389,12 @@ async function handleTelegramQueueMenuPick<Context>(
427
389
  replyChatId,
428
390
  replyMessageId,
429
391
  getTelegramQueueMenuItemText(item),
430
- buildTelegramQueueItemSubmenuReplyMarkup(chatId, msgId, item.isPriority),
392
+ buildTelegramQueueItemSubmenuReplyMarkup(
393
+ chatId,
394
+ msgId,
395
+ item.isPriority,
396
+ item.reactionSuppressionEmoji !== undefined,
397
+ ),
431
398
  );
432
399
  await deps.answerCallbackQuery(callbackQueryId);
433
400
  }
@@ -518,7 +485,12 @@ async function updateTelegramQueueMenuPriority<Context>(
518
485
  replyChatId,
519
486
  replyMessageId,
520
487
  getTelegramQueueMenuItemText(updated),
521
- buildTelegramQueueItemSubmenuReplyMarkup(chatId, msgId, updated.isPriority),
488
+ buildTelegramQueueItemSubmenuReplyMarkup(
489
+ chatId,
490
+ msgId,
491
+ updated.isPriority,
492
+ updated.reactionSuppressionEmoji !== undefined,
493
+ ),
522
494
  );
523
495
  await deps.answerCallbackQuery(
524
496
  callbackQueryId,
@@ -526,12 +498,14 @@ async function updateTelegramQueueMenuPriority<Context>(
526
498
  );
527
499
  }
528
500
 
529
- async function handleTelegramQueueMenuDeleteRequest<Context>(
501
+ async function handleTelegramQueueMenuSkipSet<Context>(
530
502
  callbackQueryId: string,
531
503
  replyChatId: number,
532
504
  replyMessageId: number,
533
505
  chatId: number,
534
506
  msgId: number,
507
+ skipped: boolean,
508
+ ctx: Context,
535
509
  deps: TelegramQueueMenuCallbackDeps<Context>,
536
510
  ): Promise<void> {
537
511
  const item = deps.findItem(chatId, msgId);
@@ -543,25 +517,10 @@ async function handleTelegramQueueMenuDeleteRequest<Context>(
543
517
  deps,
544
518
  );
545
519
  }
546
- await deps.updateQueueMessage(
547
- replyChatId,
548
- replyMessageId,
549
- "<b>Delete this queued prompt?</b>",
550
- buildTelegramQueueDeleteConfirmationReplyMarkup(chatId, msgId),
551
- );
552
- await deps.answerCallbackQuery(callbackQueryId);
553
- }
554
-
555
- async function handleTelegramQueueMenuKeep<Context>(
556
- callbackQueryId: string,
557
- replyChatId: number,
558
- replyMessageId: number,
559
- chatId: number,
560
- msgId: number,
561
- deps: TelegramQueueMenuCallbackDeps<Context>,
562
- ): Promise<void> {
563
- const item = deps.findItem(chatId, msgId);
564
- if (!item) {
520
+ deps.setSkipped(chatId, msgId, skipped);
521
+ deps.updateStatus(ctx);
522
+ const updated = deps.findItem(chatId, msgId);
523
+ if (!updated) {
565
524
  return refreshStaleTelegramQueueMenuItem(
566
525
  callbackQueryId,
567
526
  replyChatId,
@@ -572,30 +531,15 @@ async function handleTelegramQueueMenuKeep<Context>(
572
531
  await deps.updateQueueMessage(
573
532
  replyChatId,
574
533
  replyMessageId,
575
- getTelegramQueueMenuItemText(item),
576
- buildTelegramQueueItemSubmenuReplyMarkup(chatId, msgId, item.isPriority),
577
- );
578
- await deps.answerCallbackQuery(callbackQueryId, "Kept in queue.");
579
- }
580
-
581
- async function handleTelegramQueueMenuConfirmDelete<Context>(
582
- callbackQueryId: string,
583
- replyChatId: number,
584
- replyMessageId: number,
585
- chatId: number,
586
- msgId: number,
587
- ctx: Context,
588
- deps: TelegramQueueMenuCallbackDeps<Context>,
589
- ): Promise<void> {
590
- const removed = deps.cancelItem(chatId, msgId, ctx);
591
- deps.updateStatus(ctx);
592
- await updateTelegramQueueMenuList(
593
- callbackQueryId,
594
- replyChatId,
595
- replyMessageId,
596
- deps,
597
- removed ? "Deleted from queue." : "Item not found.",
534
+ getTelegramQueueMenuItemText(updated),
535
+ buildTelegramQueueItemSubmenuReplyMarkup(
536
+ chatId,
537
+ msgId,
538
+ updated.isPriority,
539
+ updated.reactionSuppressionEmoji !== undefined,
540
+ ),
598
541
  );
542
+ await deps.answerCallbackQuery(callbackQueryId);
599
543
  }
600
544
 
601
545
  interface TelegramQueueMenuCallbackQuery {
@@ -803,8 +747,8 @@ function createQueueMenuCallbackHandler<
803
747
  queueMutationRuntime: deps.queueMutationRuntime,
804
748
  });
805
749
  },
806
- cancelItem: (cId, rId, c) => {
807
- return cancelQueuedTelegramItem(cId, rId, c, {
750
+ setSkipped: (cId, rId, skipped) => {
751
+ return setQueuedTelegramPromptSkipped(cId, rId, skipped, ctx, {
808
752
  getQueueSnapshot,
809
753
  queueMutationRuntime: deps.queueMutationRuntime,
810
754
  });
@@ -817,6 +761,40 @@ function createQueueMenuCallbackHandler<
817
761
  };
818
762
  }
819
763
 
764
+ function getQueueMenuReactionDisposition<Context>(
765
+ item: Queue.TelegramQueueItem<Context>,
766
+ priority: boolean,
767
+ skipped: boolean,
768
+ ): Queue.TelegramQueueReactionDisposition {
769
+ if (priority && skipped) {
770
+ return {
771
+ kind: "priority-suppressed",
772
+ priorityEmoji:
773
+ item.kind === "prompt" ? item.priorityEmoji ?? "⚡" : "⚡",
774
+ suppressionEmoji:
775
+ item.kind === "prompt"
776
+ ? item.reactionSuppressionEmoji ?? "👎"
777
+ : "👎",
778
+ };
779
+ }
780
+ if (priority) {
781
+ return {
782
+ kind: "priority",
783
+ emoji: item.kind === "prompt" ? item.priorityEmoji ?? "⚡" : "⚡",
784
+ };
785
+ }
786
+ if (skipped) {
787
+ return {
788
+ kind: "suppressed",
789
+ emoji:
790
+ item.kind === "prompt"
791
+ ? item.reactionSuppressionEmoji ?? "👎"
792
+ : "👎",
793
+ };
794
+ }
795
+ return { kind: "default" };
796
+ }
797
+
820
798
  function toggleQueuedTelegramPromptPriority<Context>(
821
799
  chatId: number,
822
800
  replyToMessageId: number,
@@ -834,9 +812,12 @@ function toggleQueuedTelegramPromptPriority<Context>(
834
812
  if (!item) return false;
835
813
  deps.queueMutationRuntime.applyReactionByMessageId(
836
814
  replyToMessageId,
837
- item.queueLane === "priority"
838
- ? { kind: "default" }
839
- : { kind: "priority", emoji: "⚡" },
815
+ getQueueMenuReactionDisposition(
816
+ item,
817
+ item.queueLane !== "priority",
818
+ item.kind === "prompt" &&
819
+ item.reactionSuppressionEmoji !== undefined,
820
+ ),
840
821
  ctx,
841
822
  );
842
823
  return true;
@@ -860,15 +841,21 @@ function setQueuedTelegramPromptPriority<Context>(
860
841
  if (!item) return false;
861
842
  deps.queueMutationRuntime.applyReactionByMessageId(
862
843
  replyToMessageId,
863
- enabled ? { kind: "priority", emoji: "⚡" } : { kind: "default" },
844
+ getQueueMenuReactionDisposition(
845
+ item,
846
+ enabled,
847
+ item.kind === "prompt" &&
848
+ item.reactionSuppressionEmoji !== undefined,
849
+ ),
864
850
  ctx,
865
851
  );
866
852
  return true;
867
853
  }
868
854
 
869
- function cancelQueuedTelegramItem<Context>(
855
+ function setQueuedTelegramPromptSkipped<Context>(
870
856
  chatId: number,
871
857
  replyToMessageId: number,
858
+ skipped: boolean,
872
859
  ctx: Context,
873
860
  deps: {
874
861
  getQueueSnapshot: () => Queue.TelegramQueueItem<Context>[];
@@ -880,11 +867,17 @@ function cancelQueuedTelegramItem<Context>(
880
867
  chatId,
881
868
  replyToMessageId,
882
869
  );
883
- if (!item) return false;
884
- return (
885
- deps.queueMutationRuntime.removeByMessageIds([item.replyToMessageId], ctx) >
886
- 0
870
+ if (!item || item.kind !== "prompt") return false;
871
+ deps.queueMutationRuntime.applyReactionByMessageId(
872
+ replyToMessageId,
873
+ getQueueMenuReactionDisposition(
874
+ item,
875
+ item.queueLane === "priority",
876
+ skipped,
877
+ ),
878
+ ctx,
887
879
  );
880
+ return true;
888
881
  }
889
882
 
890
883
  function createQueueMenuSendMessageAdapter(
@@ -24,9 +24,16 @@ import {
24
24
  const TELEGRAM_BUTTON_CALLBACK_PREFIX = "tgbtn";
25
25
  const TELEGRAM_BUTTON_ACTION_TTL_MS = 24 * 60 * 60 * 1000;
26
26
 
27
+ export interface TelegramOutboundButtonBinding {
28
+ generation: string;
29
+ app: string;
30
+ revision: number;
31
+ }
32
+
27
33
  export interface TelegramOutboundButtonAction {
28
34
  text: string;
29
35
  prompt: string;
36
+ binding?: TelegramOutboundButtonBinding;
30
37
  selectedStyle?: TelegramInlineKeyboardButtonStyle;
31
38
  }
32
39
 
@@ -72,6 +79,11 @@ export interface TelegramButtonCallbackHandlerDeps<TContext = unknown> {
72
79
  action: TelegramOutboundButtonAction,
73
80
  ctx: TContext,
74
81
  ) => boolean | void;
82
+ invokeBoundAction?: (
83
+ query: TelegramButtonCallbackQuery,
84
+ action: TelegramOutboundButtonAction,
85
+ ctx: TContext,
86
+ ) => Promise<false | "new" | "edit">;
75
87
  editMessageReplyMarkup?: (
76
88
  chatId: number,
77
89
  messageId: number,
@@ -146,6 +158,7 @@ export function createTelegramButtonActionStore(
146
158
  return {
147
159
  text: action.text,
148
160
  prompt: action.prompt,
161
+ ...(action.binding ? { binding: action.binding } : {}),
149
162
  ...(action.selectedStyle
150
163
  ? { selectedStyle: action.selectedStyle }
151
164
  : {}),
@@ -159,7 +172,10 @@ const DEFAULT_TELEGRAM_BUTTON_REPLY_MARKDOWN =
159
172
 
160
173
  export function planTelegramButtonReply(
161
174
  markdown: string,
162
- deps: { registerAction: (action: TelegramOutboundButtonAction) => string },
175
+ deps: {
176
+ registerAction: (action: TelegramOutboundButtonAction) => string;
177
+ binding?: TelegramOutboundButtonBinding;
178
+ },
163
179
  ): TelegramButtonReplyPlan {
164
180
  const keyboard: TelegramOutboundButtonMarkup["inline_keyboard"] = [];
165
181
  const stripped = replaceTopLevelHtmlComments(markdown, (comment) => {
@@ -172,7 +188,13 @@ export function planTelegramButtonReply(
172
188
  const row = payloadRow.flatMap((payload) => {
173
189
  const action = parseTelegramButtonAction(payload);
174
190
  return action
175
- ? [{ text: action.text, callback_data: deps.registerAction(action) }]
191
+ ? [{
192
+ text: action.text,
193
+ callback_data: deps.registerAction({
194
+ ...action,
195
+ ...(deps.binding ? { binding: deps.binding } : {}),
196
+ }),
197
+ }]
176
198
  : [];
177
199
  });
178
200
  if (row.length > 0) keyboard.push(row);
@@ -262,6 +284,33 @@ export async function handleTelegramButtonCallbackQuery<TContext = unknown>(
262
284
  return true;
263
285
  }
264
286
 
287
+ if (deps.invokeBoundAction) {
288
+ try {
289
+ const viewMode = await deps.invokeBoundAction(query, action, ctx);
290
+ if (viewMode) {
291
+ if (viewMode === "new" && query.data && query.message?.reply_markup) {
292
+ const selectedMarkup = markTelegramButtonSelected(
293
+ query.message.reply_markup,
294
+ query.data,
295
+ action.selectedStyle,
296
+ );
297
+ if (selectedMarkup && deps.editMessageReplyMarkup) {
298
+ try {
299
+ await deps.editMessageReplyMarkup(chatId, messageId, selectedMarkup);
300
+ } catch {
301
+ // The action already succeeded; old-surface styling is best-effort.
302
+ }
303
+ }
304
+ }
305
+ await deps.answerCallbackQuery(query.id, "Done.");
306
+ return true;
307
+ }
308
+ } catch (error) {
309
+ await deps.answerCallbackQuery(query.id, "Generative App action failed.");
310
+ throw error;
311
+ }
312
+ }
313
+
265
314
  const enqueued = deps.enqueueButtonPrompt(query, action, ctx);
266
315
  if (enqueued === false) {
267
316
  await deps.answerCallbackQuery(query.id, "Already queued.");
@@ -228,9 +228,7 @@ function parseTelegramCompactActionPayloadRows(
228
228
  const parseCell = (): Record<string, unknown> | undefined => {
229
229
  if (source[offset] !== "{") return undefined;
230
230
  offset += 1;
231
- const keySource: string[] = [];
232
- const valueSource: string[] = [];
233
- let hasSeparator = false;
231
+ const atomSources: string[][] = [[]];
234
232
  while (offset < source.length) {
235
233
  const character = source[offset]!;
236
234
  if (character === "\\") {
@@ -238,27 +236,33 @@ function parseTelegramCompactActionPayloadRows(
238
236
  if (escaped !== "|" && escaped !== "}" && escaped !== "\\") {
239
237
  return undefined;
240
238
  }
241
- if (hasSeparator) valueSource.push(escaped);
242
- else keySource.push(escaped);
239
+ atomSources.at(-1)!.push(escaped);
243
240
  offset += 2;
244
241
  continue;
245
242
  }
246
243
  if (character === "|") {
247
- if (hasSeparator) return undefined;
248
- hasSeparator = true;
244
+ if (atomSources.length >= 3) return undefined;
245
+ atomSources.push([]);
249
246
  offset += 1;
250
247
  continue;
251
248
  }
252
249
  if (character === "}") {
253
250
  offset += 1;
254
- const key = normalizeAtom(keySource.join(""));
255
- if (!key) return undefined;
256
- if (!hasSeparator) return { value: key };
257
- const value = normalizeAtom(valueSource.join(""));
258
- return value ? { label: key, prompt: value } : undefined;
251
+ const atoms = atomSources.map((atom) =>
252
+ normalizeAtom(atom.join("")),
253
+ );
254
+ if (atoms.some((atom) => atom === undefined)) return undefined;
255
+ const [label, prompt, selectedStyle] = atoms as string[];
256
+ if (atoms.length === 1) return { value: label };
257
+ if (atoms.length === 2) return { label, prompt };
258
+ if (
259
+ selectedStyle !== "primary" &&
260
+ selectedStyle !== "success" &&
261
+ selectedStyle !== "danger"
262
+ ) return undefined;
263
+ return { label, prompt, selected_style: selectedStyle };
259
264
  }
260
- if (hasSeparator) valueSource.push(character);
261
- else keySource.push(character);
265
+ atomSources.at(-1)!.push(character);
262
266
  offset += 1;
263
267
  }
264
268
  return undefined;
package/lib/outbound.ts CHANGED
@@ -21,6 +21,7 @@ import type {
21
21
  import {
22
22
  planTelegramButtonReply,
23
23
  type TelegramButtonActionStore,
24
+ type TelegramOutboundButtonBinding,
24
25
  type TelegramOutboundButtonMarkup,
25
26
  } from "./outbound-buttons.ts";
26
27
  import {
@@ -824,6 +825,7 @@ export {
824
825
  type TelegramButtonCallbackQuery,
825
826
  type TelegramButtonReplyPlan,
826
827
  type TelegramOutboundButtonAction,
828
+ type TelegramOutboundButtonBinding,
827
829
  type TelegramOutboundButtonMarkup,
828
830
  type TelegramOutboundButtonStoredAction,
829
831
  } from "./outbound-buttons.ts";
@@ -832,10 +834,12 @@ export function createTelegramOutboundReplyPlanner(
832
834
  store: Pick<TelegramButtonActionStore, "register">,
833
835
  ): (
834
836
  markdown: string,
837
+ options?: { binding?: TelegramOutboundButtonBinding },
835
838
  ) => TelegramOutboundReplyPlan<TelegramOutboundButtonMarkup> {
836
- return (markdown) => {
839
+ return (markdown, options) => {
837
840
  const buttonReply = planTelegramButtonReply(markdown, {
838
841
  registerAction: store.register,
842
+ ...(options?.binding ? { binding: options.binding } : {}),
839
843
  });
840
844
 
841
845
  // Button replies can also contain <!-- telegram_voice --> markup
package/lib/prompts.ts CHANGED
@@ -33,6 +33,7 @@ export const TELEGRAM_MESSAGE_PROMPT_GUIDELINES = [
33
33
 
34
34
  const TELEGRAM_MODEL_CONTEXT_TOOL_NAMES = new Set([
35
35
  "telegram_attach",
36
+ "telegram_bind",
36
37
  "telegram_message",
37
38
  ]);
38
39
  const TELEGRAM_MODEL_CONTEXT_MEMORY_KEY = Symbol.for(