@segmentstream/agent 0.6.5 → 0.6.7
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 +17 -12
- package/dist/extension/lib/chat-post-confirmation.mjs +5 -1
- package/dist/extension/lib/telegram/chat-post-card.d.ts +2 -0
- package/dist/extension/lib/telegram/chat-post-card.mjs +7 -4
- package/dist/extension/lib/telegram/confirmations.mjs +3 -3
- package/dist/extension/lib/telegram/customer-text.mjs +14 -11
- package/dist/extension/lib/telegram/files.d.ts +3 -1
- package/dist/extension/lib/telegram/files.mjs +26 -1
- package/dist/extension/lib/telegram/inbound.d.ts +3 -2
- package/dist/extension/lib/telegram/inbound.mjs +14 -4
- package/dist/extension/lib/telegram/router.d.ts +4 -2
- package/dist/extension/lib/telegram/router.mjs +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ Both entry points export the same handle: `@segmentstream/agent` and
|
|
|
82
82
|
| Field | Default | Meaning |
|
|
83
83
|
| ------------------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
84
84
|
| `channels.telegram.connection` | none | The Connect key whose credential is the bot token. Without `channels.telegram`, no Telegram. |
|
|
85
|
-
| `channels.telegram.policy` | `"smart"` |
|
|
85
|
+
| `channels.telegram.policy` | `"smart"` | Unaddressed group text: `smart` asks the decision model, `mention` observes; attachments always trigger a turn |
|
|
86
86
|
| `models.stt` | `"spacexai/grok-stt"` | The AI Gateway transcription model for voice messages; `false` leaves them untranscribed |
|
|
87
87
|
| `models.imageDescription` | automatic | A separate vision model for ingestion descriptions; omitted prefers native images, `false` disables description fallback |
|
|
88
88
|
| `models.decision` | `"typesafe-ai/jev"` | The AI Gateway decision model of the inbound router |
|
|
@@ -140,33 +140,38 @@ share a group chat's session never share a client.
|
|
|
140
140
|
- **Who the agent is.** A paired sender's turn runs with the principal id `telegram:<user>`
|
|
141
141
|
(`telegram:<chat>:<user>` in groups) and the attributes `memberId`, `memberName`, `roles`,
|
|
142
142
|
`chatId`, `chatType` and `username`.
|
|
143
|
-
- **Routing.**
|
|
144
|
-
|
|
143
|
+
- **Routing.** Any file attachment from a paired member starts a turn under both `smart` and
|
|
144
|
+
`mention`, without a routing model call, caption or mention. This includes PDFs, photos,
|
|
145
|
+
spreadsheets and other media, even when replying to or tagging another person: the agent can
|
|
146
|
+
file or reconcile the upload. The decision archive records `attachment`. Eve 0.76.3 does not
|
|
147
|
+
batch inbound albums; each Telegram message starts one turn, regardless of its file count.
|
|
148
|
+
A private chat, a mention of the bot, a reply to it and a command are always answered.
|
|
149
|
+
Under `smart`, any other group message is put to one `decide` question on the decision
|
|
145
150
|
model, with the agent's description and the chat's recent observed messages. A message that is not
|
|
146
151
|
answered joins a ring of the chat's last 30 observed messages. The next answered message carries
|
|
147
152
|
the ring into its turn as context. A failed decision observes.
|
|
148
|
-
|
|
153
|
+
Text-only replies to a known human or another bot, and explicit mentions of another recipient, are
|
|
149
154
|
observed before smart inference unless this bot is addressed. This also takes precedence over
|
|
150
155
|
a pending-question follow-up. Both mention entities and user-ID `text_mention` entities count;
|
|
151
156
|
quoted/code mentions do not. Smart routing receives the original body, reply author/text,
|
|
152
157
|
tagged users and whether this bot was tagged. Decision events retain a short routing audit note
|
|
153
158
|
alongside the reason code; this is an observable summary, not model chain-of-thought.
|
|
154
159
|
- **Voice.** Voice messages, audio files and video notes are downloaded through the connection and
|
|
155
|
-
transcribed with `models.stt` through AI Gateway before
|
|
156
|
-
|
|
157
|
-
context. `stt: false` skips
|
|
160
|
+
transcribed with `models.stt` through AI Gateway before the turn.
|
|
161
|
+
The transcript becomes the message, with a note such as `[voice message, 12 s]` in
|
|
162
|
+
context. `stt: false` skips transcription. Transcription accepts at most 20 MiB
|
|
158
163
|
and ten minutes of declared duration, with a 60-second inference timeout. A skipped or failed
|
|
159
|
-
transcription reaches the agent as a note. Originals are
|
|
164
|
+
transcription reaches the agent as a note. Originals are stored as ontology attachments.
|
|
160
165
|
Eve 0.76.3 only hydrates inbound images and PDFs; it does **not** deliver native audio bytes to
|
|
161
166
|
the chat model. Even an audio-capable chat model therefore uses the configured STT fallback,
|
|
162
167
|
with an explicit host limitation note. Native audio requires a compatible Eve host change.
|
|
163
|
-
- **Attachments.** Photos and
|
|
168
|
+
- **Attachments.** Photos, documents and other Telegram files up to 20 MB stream through the connection proxy (its
|
|
164
169
|
framed responses are not bound by the 2 MiB buffered limit) and are stored, before the turn,
|
|
165
170
|
as the member's temporary attachments in the ontology's file storage
|
|
166
171
|
(`POST /api/ontology/agent/attachments`), for this chat, for 24 hours.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
Uploads start a turn after participant registration; attachment tools can also list the stored
|
|
173
|
+
files after a cold start. Eve supplies native file parts for photos and documents; other media
|
|
174
|
+
reaches the turn through stored attachment IDs in context. The turn's file parts
|
|
170
175
|
point at the stored copies, which eve reads through the channel's `fetchFile` as the turn's
|
|
171
176
|
session, which then owns them, and an `<attachments>` block names each attachment's id. A file
|
|
172
177
|
that is too large or could not be stored becomes a note. The `segmentstream__attachments_*`
|
|
@@ -6,8 +6,10 @@ __eveDirname(__filename);
|
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { object } from "./confirmation-description.mjs";
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
|
+
import { TelegramFormatConverter, escapeMarkdownV2 } from "@chat-adapter/telegram";
|
|
9
10
|
const CHAT_POST_PROTOCOL = "segmentstream/chat-post";
|
|
10
11
|
const CHAT_POST_ENVELOPE = "_segmentstream";
|
|
12
|
+
const converter = new TelegramFormatConverter();
|
|
11
13
|
function isChatPost(action) {
|
|
12
14
|
return action.split("__").at(-1) === "chat_post_message";
|
|
13
15
|
}
|
|
@@ -16,6 +18,7 @@ function supportsChatPost(tool) {
|
|
|
16
18
|
}
|
|
17
19
|
function chatPostDigest(preview) {
|
|
18
20
|
return createHash("sha256").update(JSON.stringify([
|
|
21
|
+
"v2",
|
|
19
22
|
preview.chatId,
|
|
20
23
|
preview.provider,
|
|
21
24
|
preview.externalId,
|
|
@@ -43,6 +46,7 @@ function describeChatPost(input) {
|
|
|
43
46
|
target.external_id,
|
|
44
47
|
caller.attribution
|
|
45
48
|
].every((value) => typeof value === "string" && value.length > 0) || target.provider !== "telegram" || target.id !== values.chat && target.title !== values.chat) throw new Error("An unambiguous destination and a complete exact chat preview are required.");
|
|
49
|
+
const footer = escapeMarkdownV2(caller.attribution);
|
|
46
50
|
return {
|
|
47
51
|
action: "Post message",
|
|
48
52
|
fields: [],
|
|
@@ -53,7 +57,7 @@ function describeChatPost(input) {
|
|
|
53
57
|
title: target.title,
|
|
54
58
|
attribution: caller.attribution,
|
|
55
59
|
text: values.text,
|
|
56
|
-
finalText: `${
|
|
60
|
+
finalText: `${converter.fromMarkdown(values.text)}\n\n_${footer}_`,
|
|
57
61
|
replyTo: typeof values.reply_to === "string" ? values.reply_to : null
|
|
58
62
|
}
|
|
59
63
|
};
|
|
@@ -12,4 +12,6 @@ export declare function chatPostCopy(locale: string): {
|
|
|
12
12
|
expired: string;
|
|
13
13
|
};
|
|
14
14
|
export declare function chatPostCard(preview: ChatPostPreview, locale: string): ConfirmationText;
|
|
15
|
+
/** Status/receipt strings are data outside the already rendered preview. */
|
|
16
|
+
export declare function chatPostCardStatus(text: string, status: string): string;
|
|
15
17
|
export declare function chatPostReceipt(output: unknown, locale: string): string | undefined;
|
|
@@ -4,6 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
|
|
|
4
4
|
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { escapeMarkdownV2 } from "@chat-adapter/telegram";
|
|
7
8
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
9
|
const MAX_PREVIEW_LENGTH = 3584;
|
|
9
10
|
const exactText = new AsyncLocalStorage();
|
|
@@ -37,9 +38,8 @@ function chatPostCard(preview, locale) {
|
|
|
37
38
|
russian ? "Отправить сообщение?" : "Send message?",
|
|
38
39
|
`${russian ? "Чат" : "Chat"}: ${preview.title} (${preview.provider})`,
|
|
39
40
|
...preview.replyTo === null ? [] : [`${russian ? "В ответ на" : "Reply to"}: ${preview.replyTo}`],
|
|
40
|
-
""
|
|
41
|
-
|
|
42
|
-
].join("\n");
|
|
41
|
+
""
|
|
42
|
+
].map(escapeMarkdownV2).join("\n") + `\n${preview.finalText}`;
|
|
43
43
|
if (text.length > MAX_PREVIEW_LENGTH) throw new Error("The full message and receipt cannot fit in one Telegram confirmation. Make a shorter draft.");
|
|
44
44
|
return {
|
|
45
45
|
text,
|
|
@@ -51,6 +51,9 @@ function chatPostCard(preview, locale) {
|
|
|
51
51
|
failed: russian ? "Ошибка отправки сообщения." : "Chat posting failed."
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
function chatPostCardStatus(text, status) {
|
|
55
|
+
return `${text}\n\n${escapeMarkdownV2(status)}`;
|
|
56
|
+
}
|
|
54
57
|
function chatPostReceipt(output, locale) {
|
|
55
58
|
const value = typeof output === "object" && output !== null ? output : {};
|
|
56
59
|
if (value.status !== "sent" && value.status !== "uncertain" || typeof value.history_recorded !== "boolean" || typeof value.duplicate !== "boolean" || value.message_id !== null && typeof value.message_id !== "string" || value.url !== null && typeof value.url !== "string") return;
|
|
@@ -62,4 +65,4 @@ function chatPostReceipt(output, locale) {
|
|
|
62
65
|
...value.history_recorded ? [] : [copy.history]
|
|
63
66
|
].join("\n");
|
|
64
67
|
}
|
|
65
|
-
export { chatPostCard, chatPostCopy, chatPostReceipt, isExactChatPostCard, withChatPostCard };
|
|
68
|
+
export { chatPostCard, chatPostCardStatus, chatPostCopy, chatPostReceipt, isExactChatPostCard, withChatPostCard };
|
|
@@ -6,7 +6,7 @@ __eveDirname(__filename);
|
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { conversationLanguage, memberIdOf, telegramPrincipal } from "./principal.mjs";
|
|
8
8
|
import { chatPostOperationId, isChatPost } from "../chat-post-confirmation.mjs";
|
|
9
|
-
import { chatPostCard, chatPostCopy, chatPostReceipt, withChatPostCard } from "./chat-post-card.mjs";
|
|
9
|
+
import { chatPostCard, chatPostCardStatus, chatPostCopy, chatPostReceipt, withChatPostCard } from "./chat-post-card.mjs";
|
|
10
10
|
import { confirmations } from "../confirmations.mjs";
|
|
11
11
|
import { conversationEvent } from "../conversations.mjs";
|
|
12
12
|
import { currentInboundRequest } from "./inbound-request.mjs";
|
|
@@ -302,7 +302,7 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
302
302
|
const copy = message.copy ?? confirmationCopy(message.locale ?? confirmationLocale(ctx.session.auth.current, currentLocale()));
|
|
303
303
|
const reason = confirmations.get()[callId]?.cancellationReason;
|
|
304
304
|
const status = resolution.outcome === "approved" ? copy.executing : reason === "expired" ? chatPostCopy(message.locale ?? "en").expired : copy.cancelled;
|
|
305
|
-
const text = message.exactPost ?
|
|
305
|
+
const text = message.exactPost ? chatPostCardStatus(message.text, status) : status;
|
|
306
306
|
const edit = () => channel.telegram.editMessageText({
|
|
307
307
|
messageId: message.messageId,
|
|
308
308
|
replyMarkup: { inline_keyboard: [] },
|
|
@@ -390,7 +390,7 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
390
390
|
const receipt = !failed && data.result.kind === "tool-result" ? chatPostReceipt(data.result.output, locale) : void 0;
|
|
391
391
|
const errorId = failed ? reportTelegramError("confirmed_chat_post_failed", data, { callId: data.result.callId }) : void 0;
|
|
392
392
|
const status = receipt ?? (failed ? `${message.copy?.failed ?? "Chat posting failed."} ${errorId}\n${chatPostCopy(locale).uncertain}` : chatPostCopy(locale).uncertain);
|
|
393
|
-
const text =
|
|
393
|
+
const text = chatPostCardStatus(message.text, status);
|
|
394
394
|
await withChatPostCard(text, () => channel.telegram.editMessageText({
|
|
395
395
|
messageId: message.messageId,
|
|
396
396
|
replyMarkup: { inline_keyboard: [] },
|
|
@@ -140,17 +140,20 @@ function safeTelegramFetch(fetchImpl, scope = "telegram") {
|
|
|
140
140
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
141
141
|
if (!methods.has(url.split("/").at(-1) ?? "") || typeof init?.body !== "string") return fetchImpl(input, init);
|
|
142
142
|
const body = JSON.parse(init.body);
|
|
143
|
-
for (const field of ["text", "caption"])
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
143
|
+
for (const field of ["text", "caption"]) {
|
|
144
|
+
if (typeof body[field] === "string" && isExactChatPostCard(body[field])) body.parse_mode = "MarkdownV2";
|
|
145
|
+
if (typeof body[field] === "string" && !isExactChatPostCard(body[field])) {
|
|
146
|
+
const entityField = field === "text" ? "entities" : "caption_entities";
|
|
147
|
+
const existing = body[entityField];
|
|
148
|
+
const message = Array.isArray(existing) ? {
|
|
149
|
+
text: body[field],
|
|
150
|
+
entities: existing
|
|
151
|
+
} : customerMessage(body[field]);
|
|
152
|
+
const guarded = customerText(message.text);
|
|
153
|
+
body[field] = message.entities.some((entity) => typeof entity.url === "string" && isTechnicalText(entity.url)) ? failureText(reportTelegramError("unsafe_entity_link", new Error("Credential-bearing link refused.")), "response") : guarded;
|
|
154
|
+
if (!url.endsWith("/answerCallbackQuery")) body[entityField] = body[field] === message.text ? message.entities : [];
|
|
155
|
+
delete body.parse_mode;
|
|
156
|
+
}
|
|
154
157
|
}
|
|
155
158
|
if (Array.isArray(body.media)) body.media = body.media.map((item) => ({
|
|
156
159
|
...item,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TelegramMessage } from "eve/channels/telegram";
|
|
1
|
+
import type { TelegramAttachment, TelegramMessage } from "eve/channels/telegram";
|
|
2
2
|
import { type AgentAttachments } from "../attachments.js";
|
|
3
3
|
import type { StagedFile } from "./inbound-request.js";
|
|
4
4
|
import { type ImageDescriber } from "./images.js";
|
|
@@ -13,6 +13,8 @@ import type { MediaPlan } from "./media-policy.js";
|
|
|
13
13
|
*/
|
|
14
14
|
/** The chat an attachment belongs to: `telegram:<chat id>`. */
|
|
15
15
|
export declare function telegramConversation(chatId: string): string;
|
|
16
|
+
/** Eve normalizes photos/documents only. Keep other Telegram files available to attachment tools. */
|
|
17
|
+
export declare function inboundAttachments(message: TelegramMessage): ReadonlyArray<TelegramAttachment>;
|
|
16
18
|
/** Stores the message's files one after another, so that a large album holds one file at a time. */
|
|
17
19
|
export declare function stageFiles(input: {
|
|
18
20
|
readonly message: TelegramMessage;
|
|
@@ -11,6 +11,31 @@ import { describableImage, describeReceivedImage } from "./images.mjs";
|
|
|
11
11
|
function telegramConversation(chatId) {
|
|
12
12
|
return `telegram:${chatId}`;
|
|
13
13
|
}
|
|
14
|
+
function inboundAttachments(message) {
|
|
15
|
+
const files = [...message.attachments];
|
|
16
|
+
for (const kind of [
|
|
17
|
+
"voice",
|
|
18
|
+
"audio",
|
|
19
|
+
"video",
|
|
20
|
+
"video_note",
|
|
21
|
+
"sticker",
|
|
22
|
+
"animation"
|
|
23
|
+
]) {
|
|
24
|
+
const value = message.raw[kind];
|
|
25
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) continue;
|
|
26
|
+
const file = value;
|
|
27
|
+
if (typeof file.file_id !== "string" || file.file_id === "" || files.some((existing) => existing.fileId === file.file_id)) continue;
|
|
28
|
+
files.push({
|
|
29
|
+
kind: "document",
|
|
30
|
+
fileId: file.file_id,
|
|
31
|
+
fileName: typeof file.file_name === "string" ? file.file_name : kind,
|
|
32
|
+
...typeof file.file_unique_id === "string" ? { fileUniqueId: file.file_unique_id } : {},
|
|
33
|
+
...typeof file.mime_type === "string" ? { mediaType: file.mime_type } : {},
|
|
34
|
+
...typeof file.file_size === "number" ? { size: file.file_size } : {}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
14
39
|
function described(file, index) {
|
|
15
40
|
return {
|
|
16
41
|
filename: file.fileName ?? (file.kind === "photo" ? `photo-${index}.jpg` : `file-${index}`),
|
|
@@ -106,4 +131,4 @@ function attachmentsContext(input) {
|
|
|
106
131
|
lines.push("</attachments>");
|
|
107
132
|
return [lines.join("\n")];
|
|
108
133
|
}
|
|
109
|
-
export { attachmentsContext, stageFiles, telegramConversation };
|
|
134
|
+
export { attachmentsContext, inboundAttachments, stageFiles, telegramConversation };
|
|
@@ -21,8 +21,9 @@ import { type Transcriber } from "./voice.js";
|
|
|
21
21
|
* and so is the agent's own, which unlinks every member. Neither is the notice, in the old chat
|
|
22
22
|
* or the new one, that a group became a supergroup: the migration is reported.
|
|
23
23
|
* 2. Voice becomes text, so the router and the agent read what was said.
|
|
24
|
-
* 3. The router answers or observes.
|
|
25
|
-
*
|
|
24
|
+
* 3. The router answers or observes. Files always start a turn after the identity gates, even
|
|
25
|
+
* without a caption or when addressed to another person. They are stored after the registry knows the
|
|
26
|
+
* sender. Observing starts no session or typing indication;
|
|
26
27
|
* the ring carries the stored files' references (or storage failures) into the next reply.
|
|
27
28
|
* 4. The session policy runs on the session an answered message continues: an idle one is cleared, the
|
|
28
29
|
* first message after the day's compaction time compacts it.
|
|
@@ -14,7 +14,7 @@ import { formatAddress, messageAddress, scopedAddress } from "./sessions.mjs";
|
|
|
14
14
|
import { questionAddress, questionFromHistory } from "./confirmations.mjs";
|
|
15
15
|
import { MAXIMUM_NATIVE_IMAGE_BYTES } from "./images.mjs";
|
|
16
16
|
import { chatContext } from "./chats.mjs";
|
|
17
|
-
import { attachmentsContext, stageFiles } from "./files.mjs";
|
|
17
|
+
import { attachmentsContext, inboundAttachments, stageFiles } from "./files.mjs";
|
|
18
18
|
import { messageAudience } from "./audience.mjs";
|
|
19
19
|
import { addressingReason, routeMessage } from "./router.mjs";
|
|
20
20
|
import { routingContext } from "./routing-context.mjs";
|
|
@@ -175,7 +175,8 @@ async function handleInboundMessage(input) {
|
|
|
175
175
|
if (message.from === void 0 || message.from.isBot || message.chat.type === "channel") return null;
|
|
176
176
|
const sent = {
|
|
177
177
|
...message,
|
|
178
|
-
from: message.from
|
|
178
|
+
from: message.from,
|
|
179
|
+
attachments: inboundAttachments(message)
|
|
179
180
|
};
|
|
180
181
|
const bot = await dependencies.bot(context.telegram);
|
|
181
182
|
const audience = messageAudience(sent, bot);
|
|
@@ -227,6 +228,7 @@ async function handleInboundMessage(input) {
|
|
|
227
228
|
const caption = sent.text || sent.caption;
|
|
228
229
|
const notes = [];
|
|
229
230
|
let text = caption;
|
|
231
|
+
let voiceBytes;
|
|
230
232
|
const chat = observedChatKey({
|
|
231
233
|
chatId: sent.chat.id,
|
|
232
234
|
...sent.messageThreadId === void 0 ? {} : { messageThreadId: sent.messageThreadId }
|
|
@@ -236,7 +238,10 @@ async function handleInboundMessage(input) {
|
|
|
236
238
|
const spoken = await voiceText({
|
|
237
239
|
voice,
|
|
238
240
|
model: dependencies.sttModel,
|
|
239
|
-
download:
|
|
241
|
+
download: async (fileId) => {
|
|
242
|
+
voiceBytes = await dependencies.download(fileId);
|
|
243
|
+
return voiceBytes;
|
|
244
|
+
},
|
|
240
245
|
transcribe: dependencies.transcribe
|
|
241
246
|
});
|
|
242
247
|
text = [caption, spoken.text].filter((part) => part.trim() !== "").join("\n\n");
|
|
@@ -252,12 +257,16 @@ async function handleInboundMessage(input) {
|
|
|
252
257
|
recordGateDecision("empty_message");
|
|
253
258
|
return null;
|
|
254
259
|
}
|
|
260
|
+
if (text.trim() === "" && sent.attachments.length > 0) {
|
|
261
|
+
text = "[attachment]";
|
|
262
|
+
if (input.request !== void 0) input.request.text = text;
|
|
263
|
+
}
|
|
255
264
|
if (sent.attachments.length > 0) await report;
|
|
256
265
|
const files = sent.attachments.length === 0 || dependencies.attachments === void 0 || input.request === void 0 ? new Map() : await stageFiles({
|
|
257
266
|
message: sent,
|
|
258
267
|
memberId: lookup.member.id,
|
|
259
268
|
attachments: dependencies.attachments,
|
|
260
|
-
download: dependencies.download,
|
|
269
|
+
download: (fileId) => voiceBytes !== void 0 && voice?.fileId === fileId ? Promise.resolve(voiceBytes) : dependencies.download(fileId),
|
|
261
270
|
...mediaPlan === void 0 ? {} : { imagePlan: mediaPlan },
|
|
262
271
|
...dependencies.describeImage === void 0 ? {} : { describeImage: dependencies.describeImage }
|
|
263
272
|
});
|
|
@@ -269,6 +278,7 @@ async function handleInboundMessage(input) {
|
|
|
269
278
|
chatId: sent.chat.id,
|
|
270
279
|
memberId: lookup.member.id,
|
|
271
280
|
policy: dependencies.policy,
|
|
281
|
+
hasAttachments: sent.attachments.length > 0,
|
|
272
282
|
addressed: addressed ?? (answering ? "pending-question" : void 0),
|
|
273
283
|
audience,
|
|
274
284
|
record: (decision) => recordRouteDecision(decision.route, addressed, dependencies.policy, decision),
|
|
@@ -4,8 +4,8 @@ import type { ObservedMessage, RecentMessage } from "./observed.js";
|
|
|
4
4
|
import { type MessageAudience } from "./audience.js";
|
|
5
5
|
/**
|
|
6
6
|
* The inbound router (`agent-design.md`, section 4): every message from a paired member is either
|
|
7
|
-
* answered or observed before eve starts a turn.
|
|
8
|
-
* a command are always answered. Any other group message is observed, or, with the `smart`
|
|
7
|
+
* answered or observed before eve starts a turn. Attachments, a private chat, a mention, a reply
|
|
8
|
+
* to the bot and a command are always answered. Any other group message is observed, or, with the `smart`
|
|
9
9
|
* policy, put to one decision on the decision model.
|
|
10
10
|
*/
|
|
11
11
|
export type Route = "observe" | "reply";
|
|
@@ -78,6 +78,8 @@ export declare function routeMessage(input: {
|
|
|
78
78
|
readonly chatId: string;
|
|
79
79
|
readonly memberId: string;
|
|
80
80
|
readonly policy: TelegramPolicy;
|
|
81
|
+
/** Files always start a turn, including replies to other people and captionless media. */
|
|
82
|
+
readonly hasAttachments?: boolean;
|
|
81
83
|
readonly addressed: AddressingReason | undefined;
|
|
82
84
|
readonly audience?: MessageAudience;
|
|
83
85
|
readonly record?: (decision: RouteDecision) => void;
|
|
@@ -115,6 +115,7 @@ async function routeMessage(input) {
|
|
|
115
115
|
});
|
|
116
116
|
return route;
|
|
117
117
|
};
|
|
118
|
+
if (input.hasAttachments) return complete("reply", "attachment", "Inbound attachment starts an agent turn regardless of caption or recipient.");
|
|
118
119
|
if (input.addressed !== void 0 && input.addressed !== "pending-question") return complete("reply", input.addressed, `Bot addressed directly: ${input.addressed}.`);
|
|
119
120
|
const elsewhere = otherRecipient(input.audience);
|
|
120
121
|
if (elsewhere !== void 0) return complete("observe", "other_recipient", elsewhere);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream/agent",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "Mount a SegmentStream workspace's agent on eve: the ontology as tools, acting as the member who writes, and Telegram through the workspace's connection.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SegmentStream",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"prepack": "npm run build"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
+
"@chat-adapter/telegram": "4.41.0",
|
|
63
64
|
"@vercel/oidc": "^3.8.4",
|
|
64
65
|
"ai": "^7.0.128",
|
|
65
66
|
"csv-parse": "^7.0.3",
|