@segmentstream/agent 0.0.0-stage → 0.1.1
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/LICENSE +201 -0
- package/NOTICE +2 -0
- package/README.md +178 -2
- package/dist/extension/_manifest.json +13 -0
- package/dist/extension/channels/telegram.d.ts +6 -0
- package/dist/extension/channels/telegram.mjs +15 -0
- package/dist/extension/config.d.ts +99 -0
- package/dist/extension/config.mjs +144 -0
- package/dist/extension/connections/ontology.d.ts +7 -0
- package/dist/extension/connections/ontology.mjs +11 -0
- package/dist/extension/extension.d.ts +19 -0
- package/dist/extension/extension.mjs +15 -0
- package/dist/extension/instructions.md +13 -0
- package/dist/extension/lib/connection-proxy.d.ts +57 -0
- package/dist/extension/lib/connection-proxy.mjs +113 -0
- package/dist/extension/lib/environment.d.ts +30 -0
- package/dist/extension/lib/environment.mjs +46 -0
- package/dist/extension/lib/identities.d.ts +65 -0
- package/dist/extension/lib/identities.mjs +133 -0
- package/dist/extension/lib/ontology-connection.d.ts +36 -0
- package/dist/extension/lib/ontology-connection.mjs +48 -0
- package/dist/extension/lib/runtime-token.d.ts +17 -0
- package/dist/extension/lib/runtime-token.mjs +31 -0
- package/dist/extension/lib/telegram/adapter.d.ts +29 -0
- package/dist/extension/lib/telegram/adapter.mjs +115 -0
- package/dist/extension/lib/telegram/bot.d.ts +4 -0
- package/dist/extension/lib/telegram/bot.mjs +45 -0
- package/dist/extension/lib/telegram/inbound-request.d.ts +22 -0
- package/dist/extension/lib/telegram/inbound-request.mjs +37 -0
- package/dist/extension/lib/telegram/inbound.d.ts +35 -0
- package/dist/extension/lib/telegram/inbound.mjs +117 -0
- package/dist/extension/lib/telegram/observed.d.ts +32 -0
- package/dist/extension/lib/telegram/observed.mjs +47 -0
- package/dist/extension/lib/telegram/principal.d.ts +20 -0
- package/dist/extension/lib/telegram/principal.mjs +40 -0
- package/dist/extension/lib/telegram/proxy-fetch.d.ts +28 -0
- package/dist/extension/lib/telegram/proxy-fetch.mjs +69 -0
- package/dist/extension/lib/telegram/router.d.ts +69 -0
- package/dist/extension/lib/telegram/router.mjs +64 -0
- package/dist/extension/lib/telegram/silent.d.ts +9 -0
- package/dist/extension/lib/telegram/silent.mjs +15 -0
- package/dist/extension/lib/telegram/voice.d.ts +52 -0
- package/dist/extension/lib/telegram/voice.mjs +115 -0
- package/dist/extension/lib/telegram/webhook.d.ts +13 -0
- package/dist/extension/lib/telegram/webhook.mjs +23 -0
- package/dist/extension/model.d.ts +29 -0
- package/dist/extension/model.mjs +23 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.mjs +8 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.mjs +7 -0
- package/package.json +76 -4
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { TelegramMessage } from "eve/channels/telegram";
|
|
2
|
+
import type { TelegramPolicy } from "../../config.js";
|
|
3
|
+
import type { ObservedMessage } from "./observed.js";
|
|
4
|
+
/**
|
|
5
|
+
* The inbound router (`agent-design.md`, section 4): every message from a paired member is either
|
|
6
|
+
* answered or observed before eve starts a turn. A private chat, a mention, a reply to the bot and
|
|
7
|
+
* a command are always answered. Any other group message is observed, or, with the `smart`
|
|
8
|
+
* policy, put to one decision on the decision model.
|
|
9
|
+
*/
|
|
10
|
+
export type Route = "observe" | "reply";
|
|
11
|
+
/** The bot, as Telegram's `getMe` describes it. Unknown when `getMe` failed. */
|
|
12
|
+
export interface BotIdentity {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly username: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
/** Whether the message is for the bot without asking any model. */
|
|
17
|
+
export declare function isAddressed(message: TelegramMessage, bot: BotIdentity | undefined): boolean;
|
|
18
|
+
/** What the decision model sees. */
|
|
19
|
+
export interface RouteQuestion {
|
|
20
|
+
/** The agent's own description, so the model knows what it can be asked for. */
|
|
21
|
+
readonly agent: string;
|
|
22
|
+
readonly observed: ReadonlyArray<ObservedMessage>;
|
|
23
|
+
readonly message: ObservedMessage;
|
|
24
|
+
}
|
|
25
|
+
/** Decides whether a group message not addressed to the bot is still for it. */
|
|
26
|
+
export type RouteDecider = (question: RouteQuestion) => Promise<Route>;
|
|
27
|
+
/**
|
|
28
|
+
* The shape of eve's `decide` this router uses. Tests pass a fake; production passes `decide`
|
|
29
|
+
* from `eve/ai`, which authenticates to AI Gateway like every other model call.
|
|
30
|
+
*/
|
|
31
|
+
export type DecideFunction = (options: {
|
|
32
|
+
readonly model: string;
|
|
33
|
+
readonly state: {
|
|
34
|
+
readonly [key: string]: string | ReadonlyArray<string>;
|
|
35
|
+
};
|
|
36
|
+
readonly questions: {
|
|
37
|
+
readonly route: {
|
|
38
|
+
readonly type: "choice";
|
|
39
|
+
readonly instructions: string;
|
|
40
|
+
readonly criteria: {
|
|
41
|
+
readonly observe: string;
|
|
42
|
+
readonly reply: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
readonly abortSignal?: AbortSignal;
|
|
47
|
+
}) => Promise<{
|
|
48
|
+
readonly answers: {
|
|
49
|
+
readonly route: {
|
|
50
|
+
readonly choice: Route;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
}>;
|
|
54
|
+
/** A {@link RouteDecider} that asks one `decide` question on the configured decision model. */
|
|
55
|
+
export declare function decisionRouter(input: {
|
|
56
|
+
readonly decide: DecideFunction;
|
|
57
|
+
readonly model: string;
|
|
58
|
+
}): RouteDecider;
|
|
59
|
+
/**
|
|
60
|
+
* Routes one message from a paired member. A failed decision observes: staying quiet is the safe
|
|
61
|
+
* mistake in a group chat.
|
|
62
|
+
*/
|
|
63
|
+
export declare function routeMessage(input: {
|
|
64
|
+
readonly policy: TelegramPolicy;
|
|
65
|
+
readonly addressed: boolean;
|
|
66
|
+
/** Builds the question only when a decision is needed: it reads the agent's description. */
|
|
67
|
+
readonly question: () => Promise<RouteQuestion>;
|
|
68
|
+
readonly decide: RouteDecider;
|
|
69
|
+
}): Promise<Route>;
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
function escapeRegExp(value) {
|
|
8
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9
|
+
}
|
|
10
|
+
function isCommand(text, bot) {
|
|
11
|
+
const match = /^\/[A-Za-z0-9_]+(?:@(?<target>[A-Za-z0-9_]+))?(?:\s|$)/u.exec(text);
|
|
12
|
+
if (match === null) return false;
|
|
13
|
+
const target = match.groups?.target;
|
|
14
|
+
return target === void 0 || target.toLowerCase() === bot?.username?.toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
function mentions(text, bot) {
|
|
17
|
+
if (bot?.username === void 0) return false;
|
|
18
|
+
return new RegExp(`(?:^|[^A-Za-z0-9_])@${escapeRegExp(bot.username)}(?=$|[^A-Za-z0-9_])`, "iu").test(text);
|
|
19
|
+
}
|
|
20
|
+
function repliesToBot(message, bot) {
|
|
21
|
+
const author = message.replyToMessage?.from;
|
|
22
|
+
if (author === void 0) return false;
|
|
23
|
+
return bot !== void 0 && author.id === bot.id;
|
|
24
|
+
}
|
|
25
|
+
function isAddressed(message, bot) {
|
|
26
|
+
const text = message.text || message.caption;
|
|
27
|
+
return message.chat.type === "private" || isCommand(text, bot) || mentions(text, bot) || repliesToBot(message, bot);
|
|
28
|
+
}
|
|
29
|
+
const DECISION_TIMEOUT_MS = 15e3;
|
|
30
|
+
function line(message) {
|
|
31
|
+
return `${message.author}: ${message.text}`;
|
|
32
|
+
}
|
|
33
|
+
function decisionRouter(input) {
|
|
34
|
+
return async (question) => {
|
|
35
|
+
return (await input.decide({
|
|
36
|
+
model: input.model,
|
|
37
|
+
state: {
|
|
38
|
+
agent: question.agent,
|
|
39
|
+
recent_messages: question.observed.map(line),
|
|
40
|
+
message: line(question.message)
|
|
41
|
+
},
|
|
42
|
+
questions: { route: {
|
|
43
|
+
type: "choice",
|
|
44
|
+
instructions: "A group chat message arrived that does not mention the agent. Decide whether the agent should answer it, given what the agent does and what the chat was talking about.",
|
|
45
|
+
criteria: {
|
|
46
|
+
observe: "The message is people talking among themselves, or nothing the agent can help with.",
|
|
47
|
+
reply: "The message asks the agent, or asks for something the agent does, even without naming it."
|
|
48
|
+
}
|
|
49
|
+
} },
|
|
50
|
+
abortSignal: AbortSignal.timeout(DECISION_TIMEOUT_MS)
|
|
51
|
+
})).answers.route.choice;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function routeMessage(input) {
|
|
55
|
+
if (input.addressed) return "reply";
|
|
56
|
+
if (input.policy === "mention") return "observe";
|
|
57
|
+
try {
|
|
58
|
+
return await input.decide(await input.question());
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error("[segmentstream] inbound routing decision failed; observing the message", { error });
|
|
61
|
+
return "observe";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export { decisionRouter, isAddressed, routeMessage };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TelegramChannelEvents } from "eve/channels/telegram";
|
|
2
|
+
export declare function isSilentReply(text: string): boolean;
|
|
3
|
+
type MessageCompleted = NonNullable<TelegramChannelEvents["message.completed"]>;
|
|
4
|
+
/**
|
|
5
|
+
* The channel's `message.completed` handler: eve's default delivery (plain text through
|
|
6
|
+
* `sendMessage`, nothing for a tool-call step) unless the reply is a silence marker.
|
|
7
|
+
*/
|
|
8
|
+
export declare const deliverUnlessSilent: MessageCompleted;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
const SILENT_MARKERS = new Set(["[silent]", "no_reply"]);
|
|
8
|
+
function isSilentReply(text) {
|
|
9
|
+
return SILENT_MARKERS.has(text.trim().toLowerCase());
|
|
10
|
+
}
|
|
11
|
+
const deliverUnlessSilent = async (event, channel) => {
|
|
12
|
+
if (event.finishReason === "tool-calls" || !event.message || isSilentReply(event.message)) return;
|
|
13
|
+
await channel.telegram.post(event.message);
|
|
14
|
+
};
|
|
15
|
+
export { deliverUnlessSilent, isSilentReply };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ConnectionProxy } from "../connection-proxy.js";
|
|
2
|
+
/**
|
|
3
|
+
* Voice, audio and video notes. eve's Telegram parser reads only photos and documents, so these
|
|
4
|
+
* arrive as a message with no text; the adapter reads them from the raw update, downloads them
|
|
5
|
+
* through the connection's `file` API and turns them into text with the configured speech-to-text
|
|
6
|
+
* model before the conversation sees them, whatever the main model accepts.
|
|
7
|
+
*/
|
|
8
|
+
export type VoiceKind = "voice" | "audio" | "video_note";
|
|
9
|
+
export interface VoiceMessage {
|
|
10
|
+
readonly kind: VoiceKind;
|
|
11
|
+
readonly fileId: string;
|
|
12
|
+
readonly seconds: number | undefined;
|
|
13
|
+
readonly size: number | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** The voice, audio or video note a raw Telegram message carries, if any. */
|
|
16
|
+
export declare function voiceOf(raw: Readonly<Record<string, unknown>>): VoiceMessage | undefined;
|
|
17
|
+
/** The note that tells the model a message was spoken, such as `[voice message, 12 s]`. */
|
|
18
|
+
export declare function voiceNote(voice: VoiceMessage, detail?: string): string;
|
|
19
|
+
/** Speech to text: the audio bytes in, the transcript out. */
|
|
20
|
+
export type Transcriber = (input: {
|
|
21
|
+
readonly audio: Uint8Array;
|
|
22
|
+
readonly model: string;
|
|
23
|
+
}) => Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Transcribes through AI Gateway. The Gateway provider authenticates with `AI_GATEWAY_API_KEY`
|
|
26
|
+
* when set and otherwise with the deployment's Vercel OIDC token, so a deployment needs no key.
|
|
27
|
+
*/
|
|
28
|
+
export declare const gatewayTranscriber: Transcriber;
|
|
29
|
+
/** A Telegram file could not be downloaded through the connection. */
|
|
30
|
+
export declare class TelegramFileError extends Error {
|
|
31
|
+
readonly name = "TelegramFileError";
|
|
32
|
+
}
|
|
33
|
+
/** Downloads a file by id: `getFile` on the `bot` API, then the bytes from the `file` API. */
|
|
34
|
+
export declare function downloadTelegramFile(input: {
|
|
35
|
+
readonly proxy: ConnectionProxy;
|
|
36
|
+
readonly fileId: string;
|
|
37
|
+
}): Promise<Uint8Array>;
|
|
38
|
+
/** What a voice message becomes: the text of the turn and the note that goes with it. */
|
|
39
|
+
export interface VoiceText {
|
|
40
|
+
readonly text: string;
|
|
41
|
+
readonly note: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Turns a voice message into text. When it cannot (transcription is off, the file exceeds what the
|
|
45
|
+
* proxy forwards, or a call fails), the text says so, so the agent can ask for a typed message.
|
|
46
|
+
*/
|
|
47
|
+
export declare function voiceText(input: {
|
|
48
|
+
readonly voice: VoiceMessage;
|
|
49
|
+
readonly model: string | false;
|
|
50
|
+
readonly download: (fileId: string) => Promise<Uint8Array>;
|
|
51
|
+
readonly transcribe: Transcriber;
|
|
52
|
+
}): Promise<VoiceText>;
|
|
@@ -0,0 +1,115 @@
|
|
|
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 "../connection-proxy.mjs";
|
|
8
|
+
import { gateway, transcribe } from "ai";
|
|
9
|
+
const LABELS = {
|
|
10
|
+
voice: "voice message",
|
|
11
|
+
audio: "audio file",
|
|
12
|
+
video_note: "video message"
|
|
13
|
+
};
|
|
14
|
+
const TRANSCRIPTION_TIMEOUT_MS = 6e4;
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function optionalNumber(value) {
|
|
19
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
20
|
+
}
|
|
21
|
+
function voiceOf(raw) {
|
|
22
|
+
for (const kind of [
|
|
23
|
+
"voice",
|
|
24
|
+
"audio",
|
|
25
|
+
"video_note"
|
|
26
|
+
]) {
|
|
27
|
+
const media = raw[kind];
|
|
28
|
+
if (isRecord(media) && typeof media.file_id === "string" && media.file_id !== "") return {
|
|
29
|
+
kind,
|
|
30
|
+
fileId: media.file_id,
|
|
31
|
+
seconds: optionalNumber(media.duration),
|
|
32
|
+
size: optionalNumber(media.file_size)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function voiceNote(voice, detail) {
|
|
37
|
+
const parts = [LABELS[voice.kind]];
|
|
38
|
+
if (voice.seconds !== void 0) parts.push(`${Math.round(voice.seconds)} s`);
|
|
39
|
+
if (detail !== void 0) parts.push(detail);
|
|
40
|
+
return `[${parts.join(", ")}]`;
|
|
41
|
+
}
|
|
42
|
+
const gatewayTranscriber = async ({ audio, model }) => {
|
|
43
|
+
return (await transcribe({
|
|
44
|
+
model: gateway.transcription(model),
|
|
45
|
+
audio,
|
|
46
|
+
abortSignal: AbortSignal.timeout(TRANSCRIPTION_TIMEOUT_MS)
|
|
47
|
+
})).text.trim();
|
|
48
|
+
};
|
|
49
|
+
var TelegramFileError = class extends Error {
|
|
50
|
+
name = "TelegramFileError";
|
|
51
|
+
};
|
|
52
|
+
async function downloadTelegramFile(input) {
|
|
53
|
+
const metadata = await input.proxy.request({
|
|
54
|
+
api: "bot",
|
|
55
|
+
method: "POST",
|
|
56
|
+
path: "/getFile",
|
|
57
|
+
headers: [["content-type", "application/json"]],
|
|
58
|
+
body: new TextEncoder().encode(JSON.stringify({ file_id: input.fileId }))
|
|
59
|
+
});
|
|
60
|
+
let filePath;
|
|
61
|
+
let description = "";
|
|
62
|
+
try {
|
|
63
|
+
const value = JSON.parse(new TextDecoder().decode(metadata.body));
|
|
64
|
+
filePath = isRecord(value) && isRecord(value.result) ? value.result.file_path : void 0;
|
|
65
|
+
description = isRecord(value) && typeof value.description === "string" ? value.description.slice(0, 200) : "";
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new TelegramFileError("Telegram answered getFile with an invalid body.", { cause: error });
|
|
68
|
+
}
|
|
69
|
+
if (metadata.status !== 200 || typeof filePath !== "string" || filePath === "") throw new TelegramFileError(`Telegram refused getFile with HTTP ${metadata.status}${description ? `: ${description}` : "."}`);
|
|
70
|
+
const file = await input.proxy.request({
|
|
71
|
+
api: "file",
|
|
72
|
+
method: "GET",
|
|
73
|
+
path: `/${filePath}`,
|
|
74
|
+
headers: [],
|
|
75
|
+
body: new Uint8Array()
|
|
76
|
+
});
|
|
77
|
+
if (file.status !== 200) throw new TelegramFileError(`Telegram refused the file download with HTTP ${file.status}.`);
|
|
78
|
+
return file.body;
|
|
79
|
+
}
|
|
80
|
+
async function voiceText(input) {
|
|
81
|
+
const { voice, model } = input;
|
|
82
|
+
const note = voiceNote(voice);
|
|
83
|
+
if (model === false) return {
|
|
84
|
+
text: voiceNote(voice, "voice transcription is off"),
|
|
85
|
+
note
|
|
86
|
+
};
|
|
87
|
+
if (voice.size !== void 0 && voice.size > 2097152) return {
|
|
88
|
+
text: voiceNote(voice, "too long to transcribe"),
|
|
89
|
+
note
|
|
90
|
+
};
|
|
91
|
+
try {
|
|
92
|
+
const audio = await input.download(voice.fileId);
|
|
93
|
+
const transcript = await input.transcribe({
|
|
94
|
+
audio,
|
|
95
|
+
model
|
|
96
|
+
});
|
|
97
|
+
return transcript === "" ? {
|
|
98
|
+
text: voiceNote(voice, "no speech recognized"),
|
|
99
|
+
note
|
|
100
|
+
} : {
|
|
101
|
+
text: transcript,
|
|
102
|
+
note
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error("[segmentstream] voice transcription failed", {
|
|
106
|
+
kind: voice.kind,
|
|
107
|
+
error
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
text: voiceNote(voice, "could not be transcribed"),
|
|
111
|
+
note
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export { TelegramFileError, downloadTelegramFile, gatewayTranscriber, voiceNote, voiceOf, voiceText };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The header Telegram echoes the `secret_token` given to `setWebhook` in. */
|
|
2
|
+
export declare const TELEGRAM_SECRET_HEADER = "x-telegram-bot-api-secret-token";
|
|
3
|
+
/**
|
|
4
|
+
* Compares in constant time. Hashing both sides first gives buffers of equal length, so neither
|
|
5
|
+
* the comparison nor an early length check reveals how long the secret is.
|
|
6
|
+
*/
|
|
7
|
+
export declare function secretsMatch(expected: string, received: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* The Telegram channel's `webhookVerifier`: accepts a webhook whose secret header equals the
|
|
10
|
+
* project's `SEGMENTSTREAM_AGENT_WEBHOOK_SECRET`, which the platform generates once per project and
|
|
11
|
+
* registers with `setWebhook`. Without a configured secret every webhook is refused.
|
|
12
|
+
*/
|
|
13
|
+
export declare function telegramWebhookVerifier(secret: () => string | undefined): (request: Request) => boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
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 { createHash, timingSafeEqual } from "node:crypto";
|
|
8
|
+
const TELEGRAM_SECRET_HEADER = "x-telegram-bot-api-secret-token";
|
|
9
|
+
function digest(value) {
|
|
10
|
+
return createHash("sha256").update(value, "utf8").digest();
|
|
11
|
+
}
|
|
12
|
+
function secretsMatch(expected, received) {
|
|
13
|
+
return timingSafeEqual(digest(expected), digest(received));
|
|
14
|
+
}
|
|
15
|
+
function telegramWebhookVerifier(secret) {
|
|
16
|
+
return (request) => {
|
|
17
|
+
const expected = secret();
|
|
18
|
+
const received = request.headers.get(TELEGRAM_SECRET_HEADER);
|
|
19
|
+
if (expected === void 0 || received === null || received === "") return false;
|
|
20
|
+
return secretsMatch(expected, received);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export { TELEGRAM_SECRET_HEADER, secretsMatch, telegramWebhookVerifier };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type AgentModelDefinition, type AgentStaticModelDefinition } from "eve";
|
|
2
|
+
import type { SessionAuthContext } from "eve/context";
|
|
3
|
+
import { type Environment } from "./lib/environment.js";
|
|
4
|
+
/** The models `segmentstream.model()` chooses between, as AI Gateway model ids. */
|
|
5
|
+
export interface ModelChoice {
|
|
6
|
+
/** The model for every session no other rule picks. */
|
|
7
|
+
readonly default: string;
|
|
8
|
+
/**
|
|
9
|
+
* A model per member role. The first entry whose role the member holds wins, in the order
|
|
10
|
+
* written, so put the more specific roles first.
|
|
11
|
+
*/
|
|
12
|
+
readonly byRole?: Readonly<Record<string, string>>;
|
|
13
|
+
/** The model of preview deployments, so a preview never spends on a large model. */
|
|
14
|
+
readonly preview?: string;
|
|
15
|
+
}
|
|
16
|
+
/** The dynamic form of an agent's `model`, which `defineAgent` accepts as the whole field. */
|
|
17
|
+
export type DynamicModel = Exclude<AgentModelDefinition, AgentStaticModelDefinition>;
|
|
18
|
+
/** The one model for a session started by `principal`. */
|
|
19
|
+
export declare function chooseModel(input: {
|
|
20
|
+
readonly choice: ModelChoice;
|
|
21
|
+
readonly principal: SessionAuthContext | null;
|
|
22
|
+
readonly environment?: Environment;
|
|
23
|
+
}): string;
|
|
24
|
+
/**
|
|
25
|
+
* The agent's model, chosen once when a session starts from the member's roles and `VERCEL_ENV`.
|
|
26
|
+
* Choosing once keeps the provider's prompt cache: a switch mid-session re-reads the history at
|
|
27
|
+
* full price.
|
|
28
|
+
*/
|
|
29
|
+
export declare function model(choice: ModelChoice): DynamicModel;
|
|
@@ -0,0 +1,23 @@
|
|
|
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 { isPreview } from "./lib/environment.mjs";
|
|
8
|
+
import { rolesOf } from "./lib/telegram/principal.mjs";
|
|
9
|
+
import { defineDynamic } from "eve";
|
|
10
|
+
function chooseModel(input) {
|
|
11
|
+
const { choice } = input;
|
|
12
|
+
if (choice.preview !== void 0 && isPreview(input.environment ?? process.env)) return choice.preview;
|
|
13
|
+
const roles = rolesOf(input.principal);
|
|
14
|
+
for (const [role, model] of Object.entries(choice.byRole ?? {})) if (roles.includes(role)) return model;
|
|
15
|
+
return choice.default;
|
|
16
|
+
}
|
|
17
|
+
function model(choice) {
|
|
18
|
+
return defineDynamic({ events: { "session.started": (_event, context) => chooseModel({
|
|
19
|
+
choice,
|
|
20
|
+
principal: context.session.auth.current ?? context.session.auth.initiator
|
|
21
|
+
}) } });
|
|
22
|
+
}
|
|
23
|
+
export { chooseModel, model };
|
package/dist/index.d.ts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
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 segmentstream from "./extension/extension.mjs";
|
|
8
|
+
export { segmentstream as agent, segmentstream as default };
|
|
@@ -0,0 +1,7 @@
|
|
|
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
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream/agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
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
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "SegmentStream",
|
|
7
|
+
"homepage": "https://segmentstream.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/segmentstream/segmentstream.git",
|
|
11
|
+
"directory": "packages/agent-sdk"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"segmentstream",
|
|
15
|
+
"eve",
|
|
16
|
+
"agent",
|
|
17
|
+
"ontology",
|
|
18
|
+
"telegram"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./dist/extension/extension.d.ts",
|
|
25
|
+
"default": "./dist/extension/extension.mjs"
|
|
26
|
+
},
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.mjs"
|
|
29
|
+
},
|
|
30
|
+
"./extension": {
|
|
31
|
+
"types": "./dist/extension/extension.d.ts",
|
|
32
|
+
"default": "./dist/extension/extension.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./tools": {
|
|
35
|
+
"types": "./dist/tools/index.d.ts",
|
|
36
|
+
"default": "./dist/tools/index.mjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"eve": {
|
|
40
|
+
"extension": {
|
|
41
|
+
"source": "./src",
|
|
42
|
+
"dist": "./dist/extension"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"README.md",
|
|
48
|
+
"LICENSE",
|
|
49
|
+
"NOTICE"
|
|
50
|
+
],
|
|
51
|
+
"sideEffects": false,
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=24"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "eve extension build",
|
|
57
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
58
|
+
"lint": "eslint src tests --config ../../eslint.config.mjs",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"prepack": "npm run build"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@vercel/oidc": "^3.8.4",
|
|
64
|
+
"ai": "^7.0.128"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"eve": "0.76.3"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@types/node": "^24",
|
|
71
|
+
"eve": "0.76.3",
|
|
72
|
+
"typescript": "^5.9",
|
|
73
|
+
"vitest": "^4.1.11"
|
|
74
|
+
},
|
|
75
|
+
"publishConfig": {
|
|
76
|
+
"access": "public"
|
|
77
|
+
}
|
|
78
|
+
}
|