@segmentstream/agent 0.5.3 → 0.5.4
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 +84 -12
- package/dist/extension/config.d.ts +4 -0
- package/dist/extension/config.mjs +12 -3
- package/dist/extension/lib/conversation-runtime.mjs +18 -6
- package/dist/extension/lib/conversations.mjs +1 -1
- package/dist/extension/lib/telegram/adapter.d.ts +7 -1
- package/dist/extension/lib/telegram/adapter.mjs +37 -2
- package/dist/extension/lib/telegram/audience.d.ts +26 -0
- package/dist/extension/lib/telegram/audience.mjs +80 -0
- package/dist/extension/lib/telegram/content.d.ts +29 -0
- package/dist/extension/lib/telegram/content.mjs +161 -0
- package/dist/extension/lib/telegram/conversations.d.ts +2 -2
- package/dist/extension/lib/telegram/conversations.mjs +16 -8
- package/dist/extension/lib/telegram/customer-text.mjs +2 -2
- package/dist/extension/lib/telegram/files.d.ts +4 -0
- package/dist/extension/lib/telegram/files.mjs +33 -2
- package/dist/extension/lib/telegram/images.d.ts +21 -0
- package/dist/extension/lib/telegram/images.mjs +47 -0
- package/dist/extension/lib/telegram/inbound-request.d.ts +5 -0
- package/dist/extension/lib/telegram/inbound-request.mjs +11 -1
- package/dist/extension/lib/telegram/inbound.d.ts +10 -5
- package/dist/extension/lib/telegram/inbound.mjs +82 -22
- package/dist/extension/lib/telegram/media-policy.d.ts +15 -0
- package/dist/extension/lib/telegram/media-policy.mjs +72 -0
- package/dist/extension/lib/telegram/observed.d.ts +8 -0
- package/dist/extension/lib/telegram/observed.mjs +10 -5
- package/dist/extension/lib/telegram/router.d.ts +10 -0
- package/dist/extension/lib/telegram/router.mjs +39 -32
- package/dist/extension/lib/telegram/silent.d.ts +2 -0
- package/dist/extension/lib/telegram/silent.mjs +8 -2
- package/dist/extension/lib/telegram/voice.d.ts +1 -0
- package/dist/extension/lib/telegram/voice.mjs +12 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,13 +79,14 @@ Both entry points export the same handle: `@segmentstream/agent` and
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
| Field | Default | Meaning
|
|
83
|
-
| ------------------------------ | --------------------- |
|
|
84
|
-
| `channels.telegram.connection` | none | The Connect key whose credential is the bot token. Without `channels.telegram`, no Telegram.
|
|
85
|
-
| `channels.telegram.policy` | `"smart"` | Group messages that do not address the bot: `smart` asks the decision model, `mention` observes
|
|
86
|
-
| `models.stt` | `"spacexai/grok-stt"` | The AI Gateway transcription model for voice messages; `false` leaves them untranscribed
|
|
87
|
-
| `models.
|
|
88
|
-
| `
|
|
82
|
+
| Field | Default | Meaning |
|
|
83
|
+
| ------------------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
84
|
+
| `channels.telegram.connection` | none | The Connect key whose credential is the bot token. Without `channels.telegram`, no Telegram. |
|
|
85
|
+
| `channels.telegram.policy` | `"smart"` | Group messages that do not address the bot: `smart` asks the decision model, `mention` observes |
|
|
86
|
+
| `models.stt` | `"spacexai/grok-stt"` | The AI Gateway transcription model for voice messages; `false` leaves them untranscribed |
|
|
87
|
+
| `models.imageDescription` | automatic | A separate vision model for ingestion descriptions; omitted prefers native images, `false` disables description fallback |
|
|
88
|
+
| `models.decision` | `"typesafe-ai/jev"` | The AI Gateway decision model of the inbound router |
|
|
89
|
+
| `ontology.connection` | `true` | Whether the agent reaches the ontology over MCP |
|
|
89
90
|
|
|
90
91
|
An unknown field or an invalid value fails the mount with `SegmentStreamAgentConfigError`, naming
|
|
91
92
|
each path. `manifestOf(config)` returns the normalized configuration, defaults applied, for the
|
|
@@ -145,14 +146,28 @@ share a group chat's session never share a client.
|
|
|
145
146
|
model, with the agent's description and the chat's recent observed messages. A message that is not
|
|
146
147
|
answered joins a ring of the chat's last 30 observed messages. The next answered message carries
|
|
147
148
|
the ring into its turn as context. A failed decision observes.
|
|
149
|
+
Replies to a known human or another bot, and explicit mentions of another recipient, are
|
|
150
|
+
observed before smart inference unless this bot is addressed. This also takes precedence over
|
|
151
|
+
a pending-question follow-up. Both mention entities and user-ID `text_mention` entities count;
|
|
152
|
+
quoted/code mentions do not. Smart routing receives the original body, reply author/text,
|
|
153
|
+
tagged users and whether this bot was tagged. Decision events retain a short routing audit note
|
|
154
|
+
alongside the reason code; this is an observable summary, not model chain-of-thought.
|
|
148
155
|
- **Voice.** Voice messages, audio files and video notes are downloaded through the connection and
|
|
149
|
-
transcribed with `models.stt` through AI Gateway
|
|
150
|
-
such as `[voice message, 12 s]` in
|
|
151
|
-
|
|
156
|
+
transcribed with `models.stt` through AI Gateway before routing, including observations under
|
|
157
|
+
`mention`. The transcript becomes the message, with a note such as `[voice message, 12 s]` in
|
|
158
|
+
context. `stt: false` skips download and transcription. Transcription accepts at most 20 MiB
|
|
159
|
+
and ten minutes of declared duration, with a 60-second inference timeout. A skipped or failed
|
|
160
|
+
transcription reaches the agent as a note. Originals are not stored as ontology attachments.
|
|
161
|
+
Eve 0.76.3 only hydrates inbound images and PDFs; it does **not** deliver native audio bytes to
|
|
162
|
+
the chat model. Even an audio-capable chat model therefore uses the configured STT fallback,
|
|
163
|
+
with an explicit host limitation note. Native audio requires a compatible Eve host change.
|
|
152
164
|
- **Attachments.** Photos and documents up to 20 MB stream through the connection proxy (its
|
|
153
165
|
framed responses are not bound by the 2 MiB buffered limit) and are stored, before the turn,
|
|
154
166
|
as the member's temporary attachments in the ontology's file storage
|
|
155
|
-
(`POST /api/ontology/agent/attachments`), for this chat, for 24 hours.
|
|
167
|
+
(`POST /api/ontology/agent/attachments`), for this chat, for 24 hours.
|
|
168
|
+
Observed group uploads are stored too, after participant registration, without starting a turn
|
|
169
|
+
or sending a typing indication. The next reply receives their references; attachment tools can
|
|
170
|
+
also list the stored files after a cold start. The turn's file parts
|
|
156
171
|
point at the stored copies, which eve reads through the channel's `fetchFile` as the turn's
|
|
157
172
|
session, which then owns them, and an `<attachments>` block names each attachment's id. A file
|
|
158
173
|
that is too large or could not be stored becomes a note. The `segmentstream__attachments_*`
|
|
@@ -195,12 +210,69 @@ share a group chat's session never share a client.
|
|
|
195
210
|
production, 404 without `channels.telegram`, 401 for a missing or invalid token and 400 for a
|
|
196
211
|
body that does not match, including a request over 4000 characters.
|
|
197
212
|
- **Silence.** When the model answers a routed message with exactly `[SILENT]` (or `NO_REPLY`), in
|
|
198
|
-
any case, nothing is sent to the chat.
|
|
213
|
+
any case, nothing is sent to the chat. Empty and whitespace-only final text also send nothing.
|
|
214
|
+
The turn stays in the session. Archive decisions distinguish silence markers, empty text and
|
|
215
|
+
whitespace, retaining finish reason and character count without logging raw output. Model reply
|
|
216
|
+
decisions do not prove transport delivery; completed outbound events carry that evidence.
|
|
199
217
|
|
|
200
218
|
The observed ring and chat-report caches live in process memory. On a cold start, the adapter
|
|
201
219
|
restores recent conversation context from the platform archive and reports its chats afresh.
|
|
202
220
|
The archive survives independently of eve session state and these caches.
|
|
203
221
|
|
|
222
|
+
Text and captions retain their Telegram entity annotations for addressing. Rich-message blocks
|
|
223
|
+
are projected to plain text before capture and dispatch, bounded to 16,384 UTF-16 code units,
|
|
224
|
+
2048 traversal steps and 24 nested levels. The archive records content shape and truncation without retaining
|
|
225
|
+
raw webhook payloads. Edits remain separate audit events, but replace the earlier message in
|
|
226
|
+
observed/restored model context. An edit alone never starts an agent turn. Entity offsets cannot
|
|
227
|
+
reconstruct a missing body; unsupported empty content is recorded with shape diagnostics.
|
|
228
|
+
|
|
229
|
+
### Image context and capabilities
|
|
230
|
+
|
|
231
|
+
Before handling media, the adapter reads the selected model's declared input modalities from
|
|
232
|
+
AI Gateway's public model metadata. Existing sessions use the model recorded in their latest
|
|
233
|
+
available `step.started` event, rather than reselecting by the current speaker's roles. A new
|
|
234
|
+
session uses `models.chat` with the normal role and preview rules. An inline model choice in
|
|
235
|
+
`agent.ts` cannot be inspected before its first session step; capabilities then stay unknown.
|
|
236
|
+
|
|
237
|
+
| Main chat input | `models.imageDescription` | Ingestion and later context |
|
|
238
|
+
| ------------------ | ------------------------- | ------------------------------------------------------------------------- |
|
|
239
|
+
| Images supported | omitted or `false` | Store the original and retain native file references; no description call |
|
|
240
|
+
| Any capability | a model id | Store the original and describe with that separate model before routing |
|
|
241
|
+
| Images unsupported | omitted | Describe with `google/gemini-2.5-flash-lite` before routing |
|
|
242
|
+
| Images unsupported | `false` | Store the original and record that analysis is disabled |
|
|
243
|
+
| Unknown | omitted or `false` | Store the original and record that analysis is unavailable |
|
|
244
|
+
|
|
245
|
+
Descriptions cover JPEG, PNG, WebP and GIF, at most one per Telegram message, 5 MiB of input,
|
|
246
|
+
256 output tokens, 512 retained characters, and a 15-second inference timeout. Albums are
|
|
247
|
+
multiple Telegram messages and can incur multiple calls. These ingestion calls are separate
|
|
248
|
+
from the chat model's session spending limit. The descriptor treats image text as untrusted
|
|
249
|
+
evidence. Failure, unsupported format and limit notes retain the original attachment reference.
|
|
250
|
+
Metadata lookups have a five-second timeout, a five-minute success cache and a 30-second failure
|
|
251
|
+
cache, with at most 128 model entries. A lookup failure never asserts that the model saw pixels.
|
|
252
|
+
|
|
253
|
+
Eve's native image input limit is 3 MiB per image. On the next reply, the adapter also appends
|
|
254
|
+
eligible observed images as file parts, up to five images and 3 MiB total after accounting for
|
|
255
|
+
current images. It creates attachment URLs for the **current** requester, and storage checks that
|
|
256
|
+
member's access again. Expired or omitted images get notes. A text-only reply receives generated
|
|
257
|
+
descriptions and references, never hidden image bytes. Other formats remain attachment-tool inputs.
|
|
258
|
+
|
|
259
|
+
The process-local ring retains 30 observations per chat/topic, with separate budgets of 1000
|
|
260
|
+
UTF-16 code units of text and 2000 of media notes per observation, plus at most five image
|
|
261
|
+
references. The recent routing window also retains 30 entries. A process retains at most 500
|
|
262
|
+
chats, evicting the chat least recently written. Reading does not refresh its position. A reply
|
|
263
|
+
drains the observation backlog; an idle reset clears both windows; daily compaction leaves the
|
|
264
|
+
ring intact. Image bytes are not kept in this cache.
|
|
265
|
+
|
|
266
|
+
Archive restoration can recover raw text/captions after restarts and compaction: the webhook
|
|
267
|
+
warms an empty ring from up to 30 inbound events, and each turn refreshes a separate bounded
|
|
268
|
+
archive transcript. The latter reads at most 100 events, 4000 text characters and ten raw
|
|
269
|
+
attachment references per event, within 30,000 serialized characters overall. Generated
|
|
270
|
+
transcripts/descriptions and native-image ring references are currently **not** archived.
|
|
271
|
+
They can be lost on restart or eviction before the next reply; the originals of stored images
|
|
272
|
+
remain discoverable with attachment tools until expiry. Already dispatched native images follow
|
|
273
|
+
Eve's sandbox/session history and may be omitted by compaction. Durable enriched media context
|
|
274
|
+
and native audio are follow-up work, not guarantees of this adapter.
|
|
275
|
+
|
|
204
276
|
The webhook subscribes to `message`, `edited_message`, `callback_query`, `my_chat_member`
|
|
205
277
|
and `chat_member`. A bot join reports the group immediately, including when the inviter is
|
|
206
278
|
unpaired; membership updates do not start turns. Report tasks participate in the wrapper's
|
|
@@ -14,6 +14,7 @@ export declare const TELEGRAM_POLICIES: readonly ["mention", "smart"];
|
|
|
14
14
|
export type TelegramPolicy = (typeof TELEGRAM_POLICIES)[number];
|
|
15
15
|
/** The speech-to-text model voice messages are transcribed with, in the AI Gateway catalog. */
|
|
16
16
|
export declare const DEFAULT_STT_MODEL = "spacexai/grok-stt";
|
|
17
|
+
export declare const DEFAULT_IMAGE_DESCRIPTION_MODEL = "google/gemini-2.5-flash-lite";
|
|
17
18
|
/** The decision model the inbound router asks; eve's `decide` default. */
|
|
18
19
|
export declare const DEFAULT_DECISION_MODEL = "typesafe-ai/jev";
|
|
19
20
|
/**
|
|
@@ -42,6 +43,8 @@ export interface SegmentStreamAgentConfigInput {
|
|
|
42
43
|
};
|
|
43
44
|
/** A Gateway transcription model, or `false` to leave voice messages untranscribed. */
|
|
44
45
|
readonly stt?: string | boolean;
|
|
46
|
+
/** Separate vision model, or false to disable fallback descriptions; omitted prefers native input. */
|
|
47
|
+
readonly imageDescription?: string | boolean;
|
|
45
48
|
/** The Gateway decision model of the inbound router. */
|
|
46
49
|
readonly decision?: string;
|
|
47
50
|
};
|
|
@@ -102,6 +105,7 @@ export interface SegmentStreamAgentConfig {
|
|
|
102
105
|
/** `null` when `agent.ts` passes the choice to `segmentstream.model()` itself. */
|
|
103
106
|
readonly chat: ChatModelChoice | null;
|
|
104
107
|
readonly stt: string | false;
|
|
108
|
+
readonly imageDescription: string | false | null;
|
|
105
109
|
readonly decision: string;
|
|
106
110
|
};
|
|
107
111
|
readonly ontology: {
|
|
@@ -7,6 +7,7 @@ __eveCreateRequire(import.meta.url);
|
|
|
7
7
|
const CONNECTION_KEY = /^[a-z][a-z0-9_]{0,63}$/;
|
|
8
8
|
const TELEGRAM_POLICIES = ["mention", "smart"];
|
|
9
9
|
const DEFAULT_STT_MODEL = "spacexai/grok-stt";
|
|
10
|
+
const DEFAULT_IMAGE_DESCRIPTION_MODEL = "google/gemini-2.5-flash-lite";
|
|
10
11
|
const DEFAULT_DECISION_MODEL = "typesafe-ai/jev";
|
|
11
12
|
const SESSION_SCOPES = ["chat", "thread"];
|
|
12
13
|
var SegmentStreamAgentConfigError = class extends Error {
|
|
@@ -202,7 +203,8 @@ function validateConfig(input) {
|
|
|
202
203
|
allowed: [
|
|
203
204
|
"chat",
|
|
204
205
|
"stt",
|
|
205
|
-
"decision"
|
|
206
|
+
"decision",
|
|
207
|
+
"imageDescription"
|
|
206
208
|
],
|
|
207
209
|
issues
|
|
208
210
|
});
|
|
@@ -226,6 +228,12 @@ function validateConfig(input) {
|
|
|
226
228
|
path: ["models", "decision"],
|
|
227
229
|
issues
|
|
228
230
|
});
|
|
231
|
+
const imageDescription = models?.imageDescription === void 0 ? null : models.imageDescription === false ? false : modelId({
|
|
232
|
+
value: models.imageDescription,
|
|
233
|
+
fallback: DEFAULT_IMAGE_DESCRIPTION_MODEL,
|
|
234
|
+
path: ["models", "imageDescription"],
|
|
235
|
+
issues
|
|
236
|
+
});
|
|
229
237
|
const connection = ontology?.connection ?? true;
|
|
230
238
|
const sessions = sessionSettings(root?.sessions, issues);
|
|
231
239
|
if (typeof connection !== "boolean") issues.push({
|
|
@@ -238,7 +246,8 @@ function validateConfig(input) {
|
|
|
238
246
|
models: {
|
|
239
247
|
chat,
|
|
240
248
|
stt,
|
|
241
|
-
decision
|
|
249
|
+
decision,
|
|
250
|
+
imageDescription
|
|
242
251
|
},
|
|
243
252
|
ontology: { connection: connection === true },
|
|
244
253
|
sessions
|
|
@@ -255,4 +264,4 @@ function manifestOf(config) {
|
|
|
255
264
|
if (validation.issues !== void 0) throw new SegmentStreamAgentConfigError(validation.issues);
|
|
256
265
|
return validation.value;
|
|
257
266
|
}
|
|
258
|
-
export { DEFAULT_DECISION_MODEL, DEFAULT_STT_MODEL, SESSION_SCOPES, SegmentStreamAgentConfigError, TELEGRAM_POLICIES, configSchema, manifestOf, validateConfig };
|
|
267
|
+
export { DEFAULT_DECISION_MODEL, DEFAULT_IMAGE_DESCRIPTION_MODEL, DEFAULT_STT_MODEL, SESSION_SCOPES, SegmentStreamAgentConfigError, TELEGRAM_POLICIES, configSchema, manifestOf, validateConfig };
|
|
@@ -7,21 +7,25 @@ __eveCreateRequire(import.meta.url);
|
|
|
7
7
|
import { memberIdOf } from "./telegram/principal.mjs";
|
|
8
8
|
import { conversationEvent, conversations, summarizeArguments } from "./conversations.mjs";
|
|
9
9
|
import { parseAddress } from "./telegram/sessions.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { completionKind } from "./telegram/silent.mjs";
|
|
11
11
|
function conversationCoordinates(context) {
|
|
12
12
|
const principal = context.session.auth.current;
|
|
13
13
|
const telegram = principal?.authenticator === "segmentstream-telegram" || context.channel.kind?.includes("telegram");
|
|
14
14
|
const address = context.channel.continuationToken;
|
|
15
15
|
const attributes = principal?.attributes;
|
|
16
|
-
const
|
|
16
|
+
const fields = address?.split(":");
|
|
17
|
+
const localAddress = telegram && fields?.length === 4 ? fields.slice(1).join(":") : address;
|
|
18
|
+
const parsed = telegram && localAddress !== void 0 && /^-?\d+:\d*:\d*$/u.test(localAddress) ? parseAddress(localAddress) : void 0;
|
|
19
|
+
const attributeChatId = typeof attributes?.chatId === "string" && (!telegram || /^-?\d+$/u.test(attributes.chatId)) ? attributes.chatId : void 0;
|
|
20
|
+
const chatId = telegram ? parsed?.chatId ?? attributeChatId : attributeChatId !== void 0 ? attributeChatId : address;
|
|
17
21
|
if (!chatId) return;
|
|
18
22
|
const memberId = memberIdOf(principal);
|
|
19
23
|
const chatType = attributes?.chatType;
|
|
20
24
|
return {
|
|
21
25
|
provider: telegram ? "telegram" : context.channel.kind ?? "eve",
|
|
22
26
|
chatId,
|
|
23
|
-
chatType: chatType === "group" || chatType === "supergroup" ? "group" : chatType === "channel" ? "channel" : "private",
|
|
24
|
-
threadId: telegram
|
|
27
|
+
chatType: chatType === "group" || chatType === "supergroup" || chatType === void 0 && telegram && chatId.startsWith("-") ? "group" : chatType === "channel" ? "channel" : "private",
|
|
28
|
+
threadId: !telegram ? null : parsed !== void 0 ? parsed.messageThreadId || null : typeof attributes?.messageThreadId === "string" ? attributes.messageThreadId : null,
|
|
25
29
|
participantIds: memberId === void 0 ? [] : [memberId],
|
|
26
30
|
senderId: principal?.subject ?? null
|
|
27
31
|
};
|
|
@@ -70,13 +74,21 @@ function conversationRecorder(archive = conversations()) {
|
|
|
70
74
|
status: event.type === "task.started" ? "requested" : event.data.status === "cancelled" ? "cancelled" : event.data.status === "failed" ? "failed" : "completed"
|
|
71
75
|
}));
|
|
72
76
|
else if (event.type === "message.completed" && event.data.finishReason !== "tool-calls") {
|
|
73
|
-
const
|
|
77
|
+
const kind = completionKind(event.data.message, event.data.finishReason);
|
|
78
|
+
const silent = kind !== "reply";
|
|
79
|
+
const note = kind === "silent_marker" ? "Model returned an explicit silence marker; no message delivery." : kind === "empty" ? "Model completed without final text; no message delivery." : kind === "whitespace" ? "Model returned only whitespace; no message delivery." : "Model produced a reply; transport delivery is recorded separately.";
|
|
74
80
|
await archive.append(conversationEvent({
|
|
75
81
|
...base,
|
|
76
82
|
eventId,
|
|
77
83
|
kind: "decision",
|
|
78
84
|
decision: silent ? "silent" : "reply",
|
|
79
|
-
reason:
|
|
85
|
+
reason: kind === "reply" ? "model_reply" : `model_${kind}_output`,
|
|
86
|
+
text: note,
|
|
87
|
+
argsSummary: JSON.stringify({
|
|
88
|
+
outputKind: kind,
|
|
89
|
+
outputCharacters: event.data.message?.length ?? 0,
|
|
90
|
+
finishReason: event.data.finishReason
|
|
91
|
+
})
|
|
80
92
|
}));
|
|
81
93
|
if (coordinates.provider !== "telegram" && !silent) await archive.append(conversationEvent({
|
|
82
94
|
...base,
|
|
@@ -9,7 +9,7 @@ import { runtimeToken } from "./runtime-token.mjs";
|
|
|
9
9
|
function collapseConversationEvents(events) {
|
|
10
10
|
const latest = new Map();
|
|
11
11
|
for (const event of events) {
|
|
12
|
-
const key = event.kind === "outbound" && /^telegram:(send|edit):/u.test(event.eventId) ? event.eventId.replace(/:result$/u, "") : event.eventId;
|
|
12
|
+
const key = event.kind === "inbound" && event.provider === "telegram" && event.messageId !== null ? `telegram:${event.chatId}:${event.messageId}:message` : event.kind === "outbound" && /^telegram:(send|edit):/u.test(event.eventId) ? event.eventId.replace(/:result$/u, "") : event.eventId;
|
|
13
13
|
const previous = latest.get(key);
|
|
14
14
|
if (previous === void 0 || (/^\d+$/u.test(event.sequence) && /^\d+$/u.test(previous.sequence) ? BigInt(event.sequence) > BigInt(previous.sequence) : event.storedAt > previous.storedAt)) latest.set(key, {
|
|
15
15
|
...event,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type TelegramChannel } from "eve/channels/telegram";
|
|
2
|
-
import type { SessionSettings, TelegramSettings } from "../../config.js";
|
|
2
|
+
import type { ChatModelChoice, SessionSettings, TelegramSettings } from "../../config.js";
|
|
3
3
|
import { type AgentAttachments } from "../attachments.js";
|
|
4
4
|
import { type Environment } from "../environment.js";
|
|
5
5
|
import { type Conversations } from "../conversations.js";
|
|
6
6
|
import { type RuntimeTokenVerifier } from "../runtime-identity.js";
|
|
7
7
|
import { type RuntimeToken } from "../runtime-token.js";
|
|
8
|
+
import { type ImageDescriber } from "./images.js";
|
|
9
|
+
import { type CapabilityLookup } from "./media-policy.js";
|
|
8
10
|
import { type ObservedMessages } from "./observed.js";
|
|
9
11
|
import { type DecideFunction } from "./router.js";
|
|
10
12
|
import { type HistoryReader } from "./sessions.js";
|
|
@@ -20,6 +22,8 @@ export interface TelegramAdapterOptions {
|
|
|
20
22
|
readonly models: () => {
|
|
21
23
|
readonly stt: string | false;
|
|
22
24
|
readonly decision: string;
|
|
25
|
+
readonly chat?: ChatModelChoice | null;
|
|
26
|
+
readonly imageDescription?: string | false | null;
|
|
23
27
|
};
|
|
24
28
|
/** The mounted `sessions`. */
|
|
25
29
|
readonly sessions: () => SessionSettings;
|
|
@@ -29,6 +33,8 @@ export interface TelegramAdapterOptions {
|
|
|
29
33
|
/** For the proxy and the ontology service; tests pass a fake. */
|
|
30
34
|
readonly fetch?: typeof fetch;
|
|
31
35
|
readonly transcribe?: Transcriber;
|
|
36
|
+
readonly describeImage?: ImageDescriber;
|
|
37
|
+
readonly modelCapabilities?: CapabilityLookup;
|
|
32
38
|
readonly observed?: ObservedMessages;
|
|
33
39
|
/** Verifies callers of the messages route; tests pass one on a local key set. */
|
|
34
40
|
readonly verify?: RuntimeTokenVerifier;
|
|
@@ -5,7 +5,8 @@ const __filename = __eveFileURLToPath(import.meta.url);
|
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { appOrigin, channelsEnabled, proxyEnvironment, runtimeIdentityEnvironment, webhookSecret } from "../environment.mjs";
|
|
8
|
-
import { conversationLanguage } from "./principal.mjs";
|
|
8
|
+
import { conversationLanguage, telegramPrincipal } from "./principal.mjs";
|
|
9
|
+
import { chooseModel } from "../../model.mjs";
|
|
9
10
|
import { MAXIMUM_DOWNLOAD_BYTES, connectionProxy } from "../connection-proxy.mjs";
|
|
10
11
|
import { runtimeToken } from "../runtime-token.mjs";
|
|
11
12
|
import { agentAttachments, parseAttachmentUrl } from "../attachments.mjs";
|
|
@@ -20,6 +21,8 @@ import { botIdentityCache } from "./bot.mjs";
|
|
|
20
21
|
import { confirmationLocale, confirmationRenderer } from "./confirmation-text.mjs";
|
|
21
22
|
import { scopedAddress, sessionPolicy } from "./sessions.mjs";
|
|
22
23
|
import { handleConfirmationCallback, telegramConfirmationEvents } from "./confirmations.mjs";
|
|
24
|
+
import { gatewayImageDescriber } from "./images.mjs";
|
|
25
|
+
import { gatewayMediaCapabilities, planMedia } from "./media-policy.mjs";
|
|
23
26
|
import { agentChats } from "./chats.mjs";
|
|
24
27
|
import { decisionRouter } from "./router.mjs";
|
|
25
28
|
import { replyDelivery } from "./silent.mjs";
|
|
@@ -40,7 +43,7 @@ const eveHistory = async (sessionId) => {
|
|
|
40
43
|
})) events.push({
|
|
41
44
|
type: event.type,
|
|
42
45
|
meta: { at: event.meta.at },
|
|
43
|
-
...event.type === "message.completed" ? { data: {
|
|
46
|
+
...event.type === "step.started" ? { data: { modelId: event.data.modelId } } : event.type === "message.completed" ? { data: {
|
|
44
47
|
message: event.data.message,
|
|
45
48
|
finishReason: event.data.finishReason
|
|
46
49
|
} } : {}
|
|
@@ -63,6 +66,7 @@ function segmentstreamTelegram(options) {
|
|
|
63
66
|
const observed = options.observed ?? observedMessages();
|
|
64
67
|
const readHistory = options.history ?? eveHistory;
|
|
65
68
|
const readInteractions = options.history ?? eveInteractions;
|
|
69
|
+
const capabilities = options.modelCapabilities ?? gatewayMediaCapabilities(options.fetch);
|
|
66
70
|
const archive = options.conversations ?? conversations({
|
|
67
71
|
environment,
|
|
68
72
|
token,
|
|
@@ -147,6 +151,37 @@ function segmentstreamTelegram(options) {
|
|
|
147
151
|
sessions: options.sessions,
|
|
148
152
|
sessionPolicy: policy,
|
|
149
153
|
sttModel: options.models().stt,
|
|
154
|
+
describeImage: options.describeImage ?? gatewayImageDescriber,
|
|
155
|
+
mediaPlan: async (member, sessionId) => {
|
|
156
|
+
let selected;
|
|
157
|
+
if (sessionId !== void 0) try {
|
|
158
|
+
for (const event of await readHistory(sessionId)) {
|
|
159
|
+
const data = event.data;
|
|
160
|
+
if (event.type === "step.started" && typeof data?.modelId === "string") selected = data.modelId;
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
reportTelegramError("media_session_model_lookup_failed", error, { sessionId });
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const choice = options.models().chat;
|
|
167
|
+
if (choice) selected = chooseModel({
|
|
168
|
+
choice,
|
|
169
|
+
principal: telegramPrincipal({
|
|
170
|
+
message: {
|
|
171
|
+
...message,
|
|
172
|
+
from: message.from
|
|
173
|
+
},
|
|
174
|
+
member
|
|
175
|
+
}),
|
|
176
|
+
environment
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return planMedia(selected === void 0 ? {
|
|
180
|
+
known: false,
|
|
181
|
+
images: false,
|
|
182
|
+
audio: false
|
|
183
|
+
} : await capabilities(selected), options.models().imageDescription ?? null);
|
|
184
|
+
},
|
|
150
185
|
identities: conversationIdentities(pairing()),
|
|
151
186
|
bot,
|
|
152
187
|
decide: captureRouteDecider(decisionRouter({
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { TelegramMessage } from "eve/channels/telegram";
|
|
2
|
+
import type { BotIdentity } from "./router.js";
|
|
3
|
+
type TargetKind = "bot" | "human" | "other_bot" | "unknown";
|
|
4
|
+
interface Recipient {
|
|
5
|
+
readonly kind: TargetKind;
|
|
6
|
+
readonly id?: string;
|
|
7
|
+
readonly username?: string;
|
|
8
|
+
readonly name?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface MessageAudience {
|
|
11
|
+
readonly originalText: string;
|
|
12
|
+
readonly botTagged: boolean;
|
|
13
|
+
readonly mentions: ReadonlyArray<Recipient & {
|
|
14
|
+
readonly type: "mention" | "text_mention";
|
|
15
|
+
readonly text: string;
|
|
16
|
+
}>;
|
|
17
|
+
readonly replyTo: (Recipient & {
|
|
18
|
+
readonly messageId: string;
|
|
19
|
+
readonly text: string;
|
|
20
|
+
}) | null;
|
|
21
|
+
}
|
|
22
|
+
/** Current-message entities only; mentions inside a quoted/code range are not addressees. */
|
|
23
|
+
export declare function messageAudience(message: TelegramMessage, bot: BotIdentity | undefined): MessageAudience;
|
|
24
|
+
/** A known human reply or an explicit other recipient takes precedence over smart follow-ups. */
|
|
25
|
+
export declare function otherRecipient(audience: MessageAudience | undefined): string | undefined;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { messageContent, telegramRecord } from "./content.mjs";
|
|
8
|
+
function recipient(user, bot) {
|
|
9
|
+
const id = typeof user.id === "string" || typeof user.id === "number" ? String(user.id) : void 0;
|
|
10
|
+
const username = typeof user.username === "string" ? user.username : void 0;
|
|
11
|
+
return {
|
|
12
|
+
kind: bot !== void 0 && (id === bot.id || username !== void 0 && bot.username !== void 0 && username.toLowerCase() === bot.username.toLowerCase()) ? "bot" : user.is_bot === false ? "human" : user.is_bot === true && bot !== void 0 ? "other_bot" : "unknown",
|
|
13
|
+
...id === void 0 ? {} : { id },
|
|
14
|
+
...username === void 0 ? {} : { username },
|
|
15
|
+
...typeof user.first_name === "string" ? { name: user.first_name } : {}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function messageAudience(message, bot) {
|
|
19
|
+
const content = messageContent(message.raw);
|
|
20
|
+
const text = content.source === "none" ? message.text || message.caption : content.text;
|
|
21
|
+
const mentions = [];
|
|
22
|
+
const quoted = (offset) => content.entities.some((entity) => [
|
|
23
|
+
"code",
|
|
24
|
+
"pre",
|
|
25
|
+
"blockquote",
|
|
26
|
+
"expandable_blockquote"
|
|
27
|
+
].includes(entity.type) && offset >= entity.offset && offset < entity.offset + entity.length);
|
|
28
|
+
for (const entity of content.entities) {
|
|
29
|
+
if (entity.type !== "mention" && entity.type !== "text_mention" || quoted(entity.offset)) continue;
|
|
30
|
+
const label = text.slice(entity.offset, entity.offset + entity.length);
|
|
31
|
+
if (entity.type === "text_mention") mentions.push({
|
|
32
|
+
...recipient(entity.user ?? {}, bot),
|
|
33
|
+
type: "text_mention",
|
|
34
|
+
text: label
|
|
35
|
+
});
|
|
36
|
+
else {
|
|
37
|
+
const username = entity.username ?? /^@([A-Za-z0-9_]+)$/u.exec(label)?.[1];
|
|
38
|
+
if (username !== void 0) mentions.push({
|
|
39
|
+
...recipient({ username }, bot),
|
|
40
|
+
type: "mention",
|
|
41
|
+
text: label
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const match of text.matchAll(/(?<![A-Za-z0-9_@])@([A-Za-z0-9_]{1,32})(?![A-Za-z0-9_])/gu)) {
|
|
46
|
+
if (quoted(match.index)) continue;
|
|
47
|
+
const username = match[1];
|
|
48
|
+
if (mentions.some((item) => item.username?.toLowerCase() === username.toLowerCase())) continue;
|
|
49
|
+
mentions.push({
|
|
50
|
+
...recipient({ username }, bot),
|
|
51
|
+
type: "mention",
|
|
52
|
+
text: match[0]
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const rawReply = telegramRecord(message.raw.reply_to_message);
|
|
56
|
+
const author = message.replyToMessage?.from;
|
|
57
|
+
const replyId = message.replyToMessage?.messageId ?? rawReply.message_id;
|
|
58
|
+
const replyTo = replyId === void 0 ? null : {
|
|
59
|
+
...recipient(Object.keys(telegramRecord(rawReply.from)).length > 0 ? telegramRecord(rawReply.from) : {
|
|
60
|
+
id: author?.id,
|
|
61
|
+
username: author?.username,
|
|
62
|
+
is_bot: author?.isBot,
|
|
63
|
+
first_name: author?.firstName
|
|
64
|
+
}, bot),
|
|
65
|
+
messageId: String(replyId),
|
|
66
|
+
text: messageContent(rawReply).text.slice(0, 1e3)
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
originalText: text,
|
|
70
|
+
mentions: mentions.slice(0, 50),
|
|
71
|
+
botTagged: mentions.some((item) => item.kind === "bot"),
|
|
72
|
+
replyTo
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function otherRecipient(audience) {
|
|
76
|
+
if (audience === void 0 || audience.botTagged || audience.replyTo?.kind === "bot") return;
|
|
77
|
+
if (audience.replyTo?.kind === "human" || audience.replyTo?.kind === "other_bot") return "Reply targets another person or bot; this bot was not addressed.";
|
|
78
|
+
if (audience.mentions.some((item) => item.kind !== "bot")) return "Message tags another recipient; this bot was not addressed.";
|
|
79
|
+
}
|
|
80
|
+
export { messageAudience, otherRecipient };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Telegram entities annotate UTF-16 text ranges; they never contain the missing message body. */
|
|
2
|
+
export interface ContentEntity {
|
|
3
|
+
readonly type: string;
|
|
4
|
+
readonly offset: number;
|
|
5
|
+
readonly length: number;
|
|
6
|
+
readonly user?: Readonly<Record<string, unknown>>;
|
|
7
|
+
readonly username?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface MessageContent {
|
|
10
|
+
readonly text: string;
|
|
11
|
+
readonly entities: ReadonlyArray<ContentEntity>;
|
|
12
|
+
readonly source: "text" | "caption" | "rich_message" | "none";
|
|
13
|
+
readonly truncated: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function telegramRecord(value: unknown): Readonly<Record<string, unknown>>;
|
|
16
|
+
/** Text/caption stay verbatim, including forwarded messages; rich bodies need flattening. */
|
|
17
|
+
export declare function messageContent(raw: Readonly<Record<string, unknown>>): MessageContent;
|
|
18
|
+
/** Only shape/size diagnostics: do not retain/log raw webhook bodies, URLs or credential values. */
|
|
19
|
+
export declare function contentShape(raw: Readonly<Record<string, unknown>>, content?: MessageContent): {
|
|
20
|
+
contentSource: "text" | "caption" | "rich_message" | "none";
|
|
21
|
+
textType: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
|
|
22
|
+
captionType: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
|
|
23
|
+
hasRichMessage: boolean;
|
|
24
|
+
entityCount: number;
|
|
25
|
+
captionEntityCount: number;
|
|
26
|
+
textLength: number;
|
|
27
|
+
truncated: boolean;
|
|
28
|
+
forwarded: boolean;
|
|
29
|
+
};
|