@llblab/pi-telegram 0.10.7 → 0.11.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/AGENTS.md +10 -6
- package/BACKLOG.md +5 -0
- package/CHANGELOG.md +26 -1
- package/README.md +48 -24
- package/docs/README.md +3 -2
- package/docs/architecture.md +3 -3
- package/docs/extension-sections.md +6 -4
- package/docs/inbound-handlers.md +41 -1
- package/docs/outbound-handlers.md +32 -49
- package/docs/voice.md +210 -0
- package/index.ts +77 -1
- package/lib/api.ts +35 -2
- package/lib/commands.ts +18 -0
- package/lib/config.ts +70 -0
- package/lib/extension-sections.ts +39 -20
- package/lib/external-handlers.ts +3 -4
- package/lib/inbound-handlers.ts +197 -0
- package/lib/media.ts +3 -0
- package/lib/menu-settings.ts +134 -8
- package/lib/menu-status.ts +17 -3
- package/lib/menu-thinking.ts +12 -1
- package/lib/menu.ts +10 -1
- package/lib/outbound-handlers.ts +719 -277
- package/lib/preview.ts +9 -0
- package/lib/prompts.ts +2 -1
- package/lib/queue.ts +84 -5
- package/lib/routing.ts +14 -1
- package/lib/turns.ts +84 -20
- package/lib/voice.ts +295 -0
- package/package.json +1 -1
package/lib/preview.ts
CHANGED
|
@@ -20,8 +20,10 @@ import {
|
|
|
20
20
|
type TelegramRenderedChunk,
|
|
21
21
|
type TelegramRenderMode,
|
|
22
22
|
} from "./rendering.ts";
|
|
23
|
+
|
|
23
24
|
import { buildTelegramReplyParameters } from "./replies.ts";
|
|
24
25
|
import { stripTelegramCommentMarkupForPreview } from "./outbound-handlers.ts";
|
|
26
|
+
import { shouldSuppressPreviewForVoice } from "./voice.ts";
|
|
25
27
|
|
|
26
28
|
const TELEGRAM_PREVIEW_THROTTLE_MS = 750;
|
|
27
29
|
const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
|
|
@@ -104,6 +106,8 @@ export interface TelegramPreviewRuntimeDeps<
|
|
|
104
106
|
|
|
105
107
|
export interface TelegramPreviewActiveTurn {
|
|
106
108
|
chatId: number;
|
|
109
|
+
voiceReplyPreferred?: boolean;
|
|
110
|
+
voiceReplyRequired?: boolean;
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
export interface TelegramAssistantMessagePreviewStartDeps<
|
|
@@ -504,6 +508,10 @@ export async function handleTelegramAssistantMessagePreviewStart<
|
|
|
504
508
|
): Promise<void> {
|
|
505
509
|
const turn = deps.getActiveTurn();
|
|
506
510
|
if (!turn || !deps.isAssistantMessage(message)) return;
|
|
511
|
+
if (shouldSuppressPreviewForVoice(turn)) {
|
|
512
|
+
deps.setState(undefined);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
507
515
|
const state = deps.getState();
|
|
508
516
|
if (
|
|
509
517
|
state &&
|
|
@@ -526,6 +534,7 @@ export async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
|
|
|
526
534
|
): Promise<void> {
|
|
527
535
|
const turn = deps.getActiveTurn();
|
|
528
536
|
if (!turn || !deps.isAssistantMessage(message)) return;
|
|
537
|
+
if (shouldSuppressPreviewForVoice(turn)) return;
|
|
529
538
|
let state = deps.getState();
|
|
530
539
|
if (!state) {
|
|
531
540
|
state = deps.createPreviewState();
|
package/lib/prompts.ts
CHANGED
|
@@ -15,6 +15,7 @@ Inbound context:
|
|
|
15
15
|
- \`[telegram]\` marks Telegram-originated messages. Suffixes \`|from:user\` (sender) and \`|guest:group\` (guest mode — message from another chat where the bot is not a member) may be present; the bot sees the message as if forwarded from that user/chat.
|
|
16
16
|
- \`[reply]\` is quoted context from the replied-to message, not a new instruction by itself. Suffix \`|from:user\` identifies the original author in guest-mode replies. Use it to resolve references like "this", "it", or "that message"; the actual instruction is before [reply] unless it explicitly asks to act on the quote.
|
|
17
17
|
- \`[attachments]\` gives a base directory plus relative local files; resolve and read them as needed. \`[outputs]\` contains inbound-handler stdout such as transcriptions or extracted text for those attachments.
|
|
18
|
+
- \`[voice]\` describes Telegram voice reply policy for this turn. \`manual\` means answer normally and use explicit \`telegram_voice\` markup only when a spoken reply is useful; \`mirror\` means voice input prefers a voice reply; \`always\` means the final reply is expected to be converted to voice, so keep it TTS-friendly.
|
|
18
19
|
- Unknown \`[callback] ...\` messages may be intended for another extension; if you see one, say the callback was not handled and the environment may be misconfigured.
|
|
19
20
|
|
|
20
21
|
Telegram-visible output:
|
|
@@ -25,7 +26,7 @@ Telegram-visible output:
|
|
|
25
26
|
|
|
26
27
|
Native outbound actions:
|
|
27
28
|
- Use top-level column-zero hidden Markdown comments outside code, quotes, and lists; the bridge handles them after agent_end, so do not call or register transport/TTS/text-to-OGG tools.
|
|
28
|
-
- \`telegram_voice\`: text is synthesized
|
|
29
|
+
- \`telegram_voice\`: text is synthesized by the registered voice synthesis provider and delivered by pi-telegram. Use body text for multiline voice, \`<!-- telegram_voice text="Short summary" -->\` for explicit one-line voice, or \`<!-- telegram_voice: Short summary -->\` for one-line voice with no attributes. A companion summary is optional, no specific summary format is required. Keep it TTS-friendly; avoid raw Markdown, code, formulas, tables, or long lists.
|
|
29
30
|
- \`telegram_button\`: callback prompt is routed back as a normal Telegram turn. Use \`<!-- telegram_button: OK -->\` when prompt equals label, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\` for one-line prompts, or body form \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\` for multiline prompts.
|
|
30
31
|
- If only hidden action comments would remain, add visible parent text like "Choose one:".
|
|
31
32
|
`;
|
package/lib/queue.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Owns queue item contracts, lane admission, pure queue mutations, and dispatch planning
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { isVoiceTurn } from "./voice.ts";
|
|
8
|
+
|
|
7
9
|
// --- Queue Items ---
|
|
8
10
|
|
|
9
11
|
export interface QueuedAttachment {
|
|
@@ -80,6 +82,11 @@ export interface PendingTelegramTurn extends TelegramQueueItemBase {
|
|
|
80
82
|
content: TelegramPromptContent[];
|
|
81
83
|
historyText: string;
|
|
82
84
|
priorityEmoji?: string;
|
|
85
|
+
|
|
86
|
+
/** Turn should preferably be delivered as voice (mirror mode + user sent voice) */
|
|
87
|
+
voiceReplyPreferred?: boolean;
|
|
88
|
+
/** Turn must be delivered as voice (voice mode) */
|
|
89
|
+
voiceReplyRequired?: boolean;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
export interface PendingTelegramControlItem<
|
|
@@ -371,6 +378,24 @@ export function formatQueuedTelegramItemsStatus<TContext = unknown>(
|
|
|
371
378
|
return items.length === 0 ? "" : ` +${items.length}`;
|
|
372
379
|
}
|
|
373
380
|
|
|
381
|
+
export function truncateTelegramQueueSummary(
|
|
382
|
+
text: string,
|
|
383
|
+
maxWords = 5,
|
|
384
|
+
maxLength = 40,
|
|
385
|
+
): string {
|
|
386
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
387
|
+
if (!normalized) return "";
|
|
388
|
+
const words = normalized.split(" ");
|
|
389
|
+
let summary = words.slice(0, maxWords).join(" ");
|
|
390
|
+
if (summary.length === 0) summary = normalized;
|
|
391
|
+
if (summary.length > maxLength) {
|
|
392
|
+
summary = summary.slice(0, maxLength).trimEnd();
|
|
393
|
+
}
|
|
394
|
+
return summary.length < normalized.length || words.length > maxWords
|
|
395
|
+
? `${summary}…`
|
|
396
|
+
: summary;
|
|
397
|
+
}
|
|
398
|
+
|
|
374
399
|
export function canDispatchTelegramTurnState(
|
|
375
400
|
state: TelegramDispatchGuardState,
|
|
376
401
|
): boolean {
|
|
@@ -788,7 +813,11 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
788
813
|
text: string,
|
|
789
814
|
) => Promise<unknown>;
|
|
790
815
|
sendQueuedAttachments: (turn: TTurn) => Promise<void>;
|
|
791
|
-
answerGuestQuery?: (
|
|
816
|
+
answerGuestQuery?: (
|
|
817
|
+
guestQueryId: string,
|
|
818
|
+
text?: string,
|
|
819
|
+
options?: { parseMode?: string },
|
|
820
|
+
) => Promise<void>;
|
|
792
821
|
sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
|
|
793
822
|
planOutboundReply?: (
|
|
794
823
|
markdown: string,
|
|
@@ -976,9 +1005,32 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
976
1005
|
>(deps: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>): Promise<void> {
|
|
977
1006
|
const { turn, assistant } = deps;
|
|
978
1007
|
const rawFinalText = assistant.text;
|
|
979
|
-
|
|
1008
|
+
let outboundReply = rawFinalText
|
|
980
1009
|
? deps.planOutboundReply?.(rawFinalText)
|
|
981
1010
|
: undefined;
|
|
1011
|
+
// Preserve the planned reply so voice-fallback can use stripped markdown + replyMarkup
|
|
1012
|
+
const plannedReply = outboundReply;
|
|
1013
|
+
|
|
1014
|
+
// Transparent voice interception: when the turn is voice-tagged and the agent
|
|
1015
|
+
// did not explicitly use <!-- telegram_voice --> markup, we automatically
|
|
1016
|
+
// convert the whole response to voice.
|
|
1017
|
+
const voiceInterceptionGuard =
|
|
1018
|
+
turn &&
|
|
1019
|
+
isVoiceTurn(turn) &&
|
|
1020
|
+
rawFinalText?.trim() &&
|
|
1021
|
+
deps.planOutboundReply &&
|
|
1022
|
+
(!outboundReply ||
|
|
1023
|
+
(!outboundReply.voiceText && !outboundReply.voiceReplies?.length));
|
|
1024
|
+
if (voiceInterceptionGuard) {
|
|
1025
|
+
const voiceText =
|
|
1026
|
+
plannedReply !== undefined
|
|
1027
|
+
? plannedReply.markdown?.trim() || ""
|
|
1028
|
+
: (rawFinalText ?? "");
|
|
1029
|
+
outboundReply = outboundReply
|
|
1030
|
+
? { ...outboundReply, voiceText, markdown: "" }
|
|
1031
|
+
: { markdown: "", voiceText };
|
|
1032
|
+
}
|
|
1033
|
+
|
|
982
1034
|
const finalText = outboundReply ? outboundReply.markdown : rawFinalText;
|
|
983
1035
|
const hasOutboundArtifacts =
|
|
984
1036
|
!!outboundReply?.voiceText || !!outboundReply?.voiceReplies?.length;
|
|
@@ -1080,9 +1132,36 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1080
1132
|
}
|
|
1081
1133
|
}
|
|
1082
1134
|
if (outboundReply && deps.sendOutboundReplyArtifacts) {
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1135
|
+
try {
|
|
1136
|
+
await deps.sendOutboundReplyArtifacts(turn, outboundReply, {
|
|
1137
|
+
replyToPrompt: !finalText,
|
|
1138
|
+
});
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1141
|
+
phase: "voice-artifacts",
|
|
1142
|
+
chatId: turn.chatId,
|
|
1143
|
+
});
|
|
1144
|
+
// Fallback to planned text when voice delivery fails and text wasn't already delivered
|
|
1145
|
+
if (rawFinalText?.trim() && !finalText && hasOutboundArtifacts) {
|
|
1146
|
+
try {
|
|
1147
|
+
const fallbackMarkdown =
|
|
1148
|
+
plannedReply?.markdown || outboundReply?.voiceText || rawFinalText;
|
|
1149
|
+
await deps.sendMarkdownReply(
|
|
1150
|
+
turn.chatId,
|
|
1151
|
+
turn.replyToMessageId,
|
|
1152
|
+
fallbackMarkdown,
|
|
1153
|
+
plannedReply?.replyMarkup
|
|
1154
|
+
? { replyMarkup: plannedReply.replyMarkup }
|
|
1155
|
+
: undefined,
|
|
1156
|
+
);
|
|
1157
|
+
} catch (fallbackError) {
|
|
1158
|
+
deps.recordRuntimeEvent?.("delivery", fallbackError, {
|
|
1159
|
+
phase: "voice-fallback-text",
|
|
1160
|
+
chatId: turn.chatId,
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1086
1165
|
}
|
|
1087
1166
|
if (endPlan.shouldSendAttachmentNotice) {
|
|
1088
1167
|
await deps.sendTextReply(
|
package/lib/routing.ts
CHANGED
|
@@ -18,6 +18,7 @@ import * as Queue from "./queue.ts";
|
|
|
18
18
|
import type { TelegramBridgeRuntime } from "./runtime.ts";
|
|
19
19
|
import * as TextGroups from "./text-groups.ts";
|
|
20
20
|
import * as Turns from "./turns.ts";
|
|
21
|
+
import { getTelegramVoiceReplyMode } from "./voice.ts";
|
|
21
22
|
import type { TelegramUser } from "./updates.ts";
|
|
22
23
|
import * as Updates from "./updates.ts";
|
|
23
24
|
|
|
@@ -38,7 +39,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
38
39
|
> {
|
|
39
40
|
configStore: Pick<
|
|
40
41
|
TelegramConfigStore,
|
|
41
|
-
"getAllowedUserId" | "setAllowedUserId" | "persist"
|
|
42
|
+
"get" | "getAllowedUserId" | "setAllowedUserId" | "persist"
|
|
42
43
|
>;
|
|
43
44
|
bridgeRuntime: TelegramBridgeRuntime;
|
|
44
45
|
activeTurnRuntime: Queue.TelegramActiveTurnStore;
|
|
@@ -82,6 +83,8 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
82
83
|
requestDeferredDispatchNextQueuedTelegramTurn?: (
|
|
83
84
|
dispatch: (ctx: TContext) => void,
|
|
84
85
|
) => void;
|
|
86
|
+
startTypingLoop?: (ctx: TContext, chatId?: number) => void;
|
|
87
|
+
stopTypingLoop?: () => void;
|
|
85
88
|
answerCallbackQuery: (
|
|
86
89
|
callbackQueryId: string,
|
|
87
90
|
text?: string,
|
|
@@ -284,6 +287,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
284
287
|
allocateQueueOrder: deps.bridgeRuntime.queue.allocateItemOrder,
|
|
285
288
|
downloadFile: deps.downloadFile,
|
|
286
289
|
processAttachments: deps.inboundHandlerRuntime.process,
|
|
290
|
+
|
|
291
|
+
// Voice policy for the current turn. Missing config still behaves as manual,
|
|
292
|
+
// but only explicit telegram.json voice.replyMode is shown in prompt context.
|
|
293
|
+
getVoiceReplyMode: () => getTelegramVoiceReplyMode(deps.configStore.get()),
|
|
294
|
+
isVoiceReplyModeConfigured: () => {
|
|
295
|
+
const mode = deps.configStore.get().voice?.replyMode;
|
|
296
|
+
return mode === "manual" || mode === "mirror" || mode === "always";
|
|
297
|
+
},
|
|
287
298
|
});
|
|
288
299
|
const enqueueContinueTurn = async (
|
|
289
300
|
message: TMessage,
|
|
@@ -344,6 +355,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
344
355
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
345
356
|
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
346
357
|
deps.requestDeferredDispatchNextQueuedTelegramTurn,
|
|
358
|
+
startTypingLoop: deps.startTypingLoop,
|
|
359
|
+
stopTypingLoop: deps.stopTypingLoop,
|
|
347
360
|
enqueueContinueTurn,
|
|
348
361
|
compact: deps.compact,
|
|
349
362
|
allocateItemOrder: deps.bridgeRuntime.queue.allocateItemOrder,
|
package/lib/turns.ts
CHANGED
|
@@ -8,16 +8,16 @@ import { readFile } from "node:fs/promises";
|
|
|
8
8
|
import { basename, dirname, join } from "node:path";
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
+
appendTelegramReplyContext,
|
|
11
12
|
collectTelegramMessageIds,
|
|
12
|
-
type DownloadedTelegramMessageFile,
|
|
13
|
-
type DownloadTelegramMessageFilesDeps,
|
|
14
13
|
downloadTelegramMessageFiles,
|
|
15
14
|
extractTelegramMessagesPromptText,
|
|
16
15
|
extractTelegramMessagesText,
|
|
17
|
-
appendTelegramReplyContext,
|
|
18
16
|
extractTelegramReplyContextText,
|
|
19
17
|
formatTelegramHistoryText,
|
|
20
18
|
guessMediaType,
|
|
19
|
+
type DownloadedTelegramMessageFile,
|
|
20
|
+
type DownloadTelegramMessageFilesDeps,
|
|
21
21
|
type TelegramMediaMessage,
|
|
22
22
|
} from "./media.ts";
|
|
23
23
|
import type {
|
|
@@ -27,6 +27,21 @@ import type {
|
|
|
27
27
|
TelegramQueueStore,
|
|
28
28
|
} from "./queue.ts";
|
|
29
29
|
|
|
30
|
+
import {
|
|
31
|
+
computeVoicePromptContribution,
|
|
32
|
+
computeVoiceTurnFlags,
|
|
33
|
+
getTelegramVoiceReplyMode,
|
|
34
|
+
TELEGRAM_VOICE_REPLY_MODES,
|
|
35
|
+
type TelegramVoiceReplyMode,
|
|
36
|
+
} from "./voice.ts";
|
|
37
|
+
|
|
38
|
+
// Re-export for backward compatibility with existing namespace imports (e.g. Turns.getTelegramVoiceReplyMode in routing.ts)
|
|
39
|
+
export {
|
|
40
|
+
getTelegramVoiceReplyMode,
|
|
41
|
+
TELEGRAM_VOICE_REPLY_MODES,
|
|
42
|
+
type TelegramVoiceReplyMode,
|
|
43
|
+
};
|
|
44
|
+
|
|
30
45
|
export const TELEGRAM_PREFIX = "[telegram]";
|
|
31
46
|
|
|
32
47
|
export interface TelegramTurnMessage {
|
|
@@ -36,23 +51,8 @@ export interface TelegramTurnMessage {
|
|
|
36
51
|
|
|
37
52
|
export type DownloadedTelegramTurnFile = DownloadedTelegramMessageFile;
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
maxWords = 5,
|
|
42
|
-
maxLength = 40,
|
|
43
|
-
): string {
|
|
44
|
-
const normalized = text.replace(/\s+/g, " ").trim();
|
|
45
|
-
if (!normalized) return "";
|
|
46
|
-
const words = normalized.split(" ");
|
|
47
|
-
let summary = words.slice(0, maxWords).join(" ");
|
|
48
|
-
if (summary.length === 0) summary = normalized;
|
|
49
|
-
if (summary.length > maxLength) {
|
|
50
|
-
summary = summary.slice(0, maxLength).trimEnd();
|
|
51
|
-
}
|
|
52
|
-
return summary.length < normalized.length || words.length > maxWords
|
|
53
|
-
? `${summary}…`
|
|
54
|
-
: summary;
|
|
55
|
-
}
|
|
54
|
+
import { truncateTelegramQueueSummary } from "./queue.ts";
|
|
55
|
+
export { truncateTelegramQueueSummary };
|
|
56
56
|
|
|
57
57
|
export function formatTelegramTurnStatusSummary(
|
|
58
58
|
rawText: string,
|
|
@@ -103,6 +103,23 @@ function appendTelegramPromptText(prompt: string, rawText: string): string {
|
|
|
103
103
|
return `${prompt} ${rawText}`;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
function appendTelegramVoiceContext(
|
|
107
|
+
prompt: string,
|
|
108
|
+
entries: Record<string, string>,
|
|
109
|
+
): string {
|
|
110
|
+
const prefix = prompt.length > 0 ? `${prompt}\n\n` : "";
|
|
111
|
+
const pairs = Object.entries(entries);
|
|
112
|
+
if (pairs.length === 1) {
|
|
113
|
+
const [key, value] = pairs[0];
|
|
114
|
+
return `${prefix}[voice] ${key}: ${value}`;
|
|
115
|
+
}
|
|
116
|
+
return `${prefix}[voice]\n${pairs
|
|
117
|
+
.map(([key, value]) => `- ${key}: ${value}`)
|
|
118
|
+
.join("\n")}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- Voice Policy And Tagging ---
|
|
122
|
+
|
|
106
123
|
export function buildTelegramTurnPrompt(options: {
|
|
107
124
|
telegramPrefix: string;
|
|
108
125
|
rawText: string;
|
|
@@ -110,6 +127,7 @@ export function buildTelegramTurnPrompt(options: {
|
|
|
110
127
|
promptFiles?: DownloadedTelegramTurnFile[];
|
|
111
128
|
handlerOutputs?: string[];
|
|
112
129
|
historyTurns?: Pick<PendingTelegramTurn, "historyText">[];
|
|
130
|
+
voiceContext?: Record<string, string>;
|
|
113
131
|
}): string {
|
|
114
132
|
let prompt = options.telegramPrefix;
|
|
115
133
|
if ((options.historyTurns?.length ?? 0) > 0) {
|
|
@@ -133,6 +151,9 @@ export function buildTelegramTurnPrompt(options: {
|
|
|
133
151
|
"outputs",
|
|
134
152
|
options.handlerOutputs ?? [],
|
|
135
153
|
);
|
|
154
|
+
if (options.voiceContext) {
|
|
155
|
+
prompt = appendTelegramVoiceContext(prompt, options.voiceContext);
|
|
156
|
+
}
|
|
136
157
|
return prompt;
|
|
137
158
|
}
|
|
138
159
|
|
|
@@ -311,6 +332,9 @@ export interface BuildTelegramPromptTurnOptions {
|
|
|
311
332
|
handlerOutputs?: string[];
|
|
312
333
|
readBinaryFile: (path: string) => Promise<Uint8Array>;
|
|
313
334
|
inferImageMimeType: (path: string) => string | undefined;
|
|
335
|
+
voiceReplyMode?: TelegramVoiceReplyMode;
|
|
336
|
+
voiceReplyModeConfigured?: boolean;
|
|
337
|
+
voicePromptContribution?: string;
|
|
314
338
|
}
|
|
315
339
|
|
|
316
340
|
export type BuildTelegramPromptTurnRuntimeOptions = Omit<
|
|
@@ -331,6 +355,8 @@ export interface TelegramPromptTurnRuntimeBuilderDeps<
|
|
|
331
355
|
promptFiles?: DownloadedTelegramTurnFile[];
|
|
332
356
|
handlerOutputs?: string[];
|
|
333
357
|
}>;
|
|
358
|
+
getVoiceReplyMode?: () => TelegramVoiceReplyMode;
|
|
359
|
+
isVoiceReplyModeConfigured?: () => boolean;
|
|
334
360
|
}
|
|
335
361
|
|
|
336
362
|
export function createTelegramPromptTurnRuntimeBuilder<
|
|
@@ -358,6 +384,8 @@ export function createTelegramPromptTurnRuntimeBuilder<
|
|
|
358
384
|
processed.rawText,
|
|
359
385
|
replyContext,
|
|
360
386
|
);
|
|
387
|
+
// Compute voice mode once and pass it to both the turn builder and the prompt contribution helper
|
|
388
|
+
const voiceReplyMode = deps.getVoiceReplyMode?.();
|
|
361
389
|
return buildTelegramPromptTurnRuntime({
|
|
362
390
|
telegramPrefix: TELEGRAM_PREFIX,
|
|
363
391
|
messages,
|
|
@@ -369,10 +397,26 @@ export function createTelegramPromptTurnRuntimeBuilder<
|
|
|
369
397
|
promptFiles: processed.promptFiles,
|
|
370
398
|
handlerOutputs: processed.handlerOutputs,
|
|
371
399
|
inferImageMimeType: guessMediaType,
|
|
400
|
+
voiceReplyMode,
|
|
401
|
+
voiceReplyModeConfigured: deps.isVoiceReplyModeConfigured?.(),
|
|
402
|
+
voicePromptContribution: computeVoicePromptContribution(
|
|
403
|
+
voiceReplyMode,
|
|
404
|
+
files,
|
|
405
|
+
rawText,
|
|
406
|
+
),
|
|
372
407
|
});
|
|
373
408
|
};
|
|
374
409
|
}
|
|
375
410
|
|
|
411
|
+
function getTelegramVoicePromptContext(
|
|
412
|
+
voiceReplyMode: TelegramVoiceReplyMode,
|
|
413
|
+
hasVoiceFile: boolean,
|
|
414
|
+
): Record<string, string> | undefined {
|
|
415
|
+
if (voiceReplyMode === "always") return { "reply mode": "always" };
|
|
416
|
+
if (!hasVoiceFile) return undefined;
|
|
417
|
+
return { "reply mode": voiceReplyMode };
|
|
418
|
+
}
|
|
419
|
+
|
|
376
420
|
export async function buildTelegramPromptTurn(
|
|
377
421
|
options: BuildTelegramPromptTurnOptions,
|
|
378
422
|
): Promise<PendingTelegramTurn> {
|
|
@@ -380,6 +424,12 @@ export async function buildTelegramPromptTurn(
|
|
|
380
424
|
if (!firstMessage) {
|
|
381
425
|
throw new Error("Missing Telegram message for turn creation");
|
|
382
426
|
}
|
|
427
|
+
const hasVoiceFile = options.files.some(
|
|
428
|
+
(f) => f.kind === "voice" || f.kind === "audio",
|
|
429
|
+
);
|
|
430
|
+
const voiceReplyMode = options.voiceReplyMode ?? getTelegramVoiceReplyMode();
|
|
431
|
+
const showVoiceContext =
|
|
432
|
+
options.voiceReplyModeConfigured ?? options.voiceReplyMode !== undefined;
|
|
383
433
|
const content: TelegramPromptContent[] = [
|
|
384
434
|
{
|
|
385
435
|
type: "text",
|
|
@@ -390,6 +440,9 @@ export async function buildTelegramPromptTurn(
|
|
|
390
440
|
promptFiles: options.promptFiles,
|
|
391
441
|
handlerOutputs: options.handlerOutputs,
|
|
392
442
|
historyTurns: options.historyTurns,
|
|
443
|
+
voiceContext: showVoiceContext
|
|
444
|
+
? getTelegramVoicePromptContext(voiceReplyMode, hasVoiceFile)
|
|
445
|
+
: undefined,
|
|
393
446
|
}),
|
|
394
447
|
},
|
|
395
448
|
];
|
|
@@ -404,6 +457,15 @@ export async function buildTelegramPromptTurn(
|
|
|
404
457
|
mimeType: mediaType,
|
|
405
458
|
});
|
|
406
459
|
}
|
|
460
|
+
if (options.voicePromptContribution?.trim()) {
|
|
461
|
+
const textItem = content.find((c) => c.type === "text") as
|
|
462
|
+
| { type: "text"; text: string }
|
|
463
|
+
| undefined;
|
|
464
|
+
if (textItem) {
|
|
465
|
+
textItem.text = `${textItem.text}\n\n${options.voicePromptContribution.trim()}`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
407
469
|
return {
|
|
408
470
|
kind: "prompt",
|
|
409
471
|
chatId: firstMessage.chat.id,
|
|
@@ -424,6 +486,8 @@ export async function buildTelegramPromptTurn(
|
|
|
424
486
|
options.promptFiles ?? options.files,
|
|
425
487
|
options.handlerOutputs,
|
|
426
488
|
),
|
|
489
|
+
// Voice tagging (used for preview suppression and prompt guidance)
|
|
490
|
+
...computeVoiceTurnFlags(voiceReplyMode, hasVoiceFile),
|
|
427
491
|
};
|
|
428
492
|
}
|
|
429
493
|
|