@copilotkit/channels-telegram 0.0.3
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 +21 -0
- package/README.md +250 -0
- package/dist/adapter.d.ts +117 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +584 -0
- package/dist/built-in-context.d.ts +22 -0
- package/dist/built-in-context.d.ts.map +1 -0
- package/dist/built-in-context.js +77 -0
- package/dist/built-in-tools.d.ts +23 -0
- package/dist/built-in-tools.d.ts.map +1 -0
- package/dist/built-in-tools.js +46 -0
- package/dist/chunked-edit-stream.d.ts +70 -0
- package/dist/chunked-edit-stream.d.ts.map +1 -0
- package/dist/chunked-edit-stream.js +197 -0
- package/dist/conversation-store.d.ts +56 -0
- package/dist/conversation-store.d.ts.map +1 -0
- package/dist/conversation-store.js +98 -0
- package/dist/download-files.d.ts +68 -0
- package/dist/download-files.d.ts.map +1 -0
- package/dist/download-files.js +157 -0
- package/dist/event-renderer.d.ts +39 -0
- package/dist/event-renderer.d.ts.map +1 -0
- package/dist/event-renderer.js +295 -0
- package/dist/format-fallback.d.ts +6 -0
- package/dist/format-fallback.d.ts.map +1 -0
- package/dist/format-fallback.js +21 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/interaction.d.ts +65 -0
- package/dist/interaction.d.ts.map +1 -0
- package/dist/interaction.js +159 -0
- package/dist/listener.d.ts +16 -0
- package/dist/listener.d.ts.map +1 -0
- package/dist/listener.js +419 -0
- package/dist/render/budget.d.ts +18 -0
- package/dist/render/budget.d.ts.map +1 -0
- package/dist/render/budget.js +24 -0
- package/dist/render/telegram.d.ts +11 -0
- package/dist/render/telegram.d.ts.map +1 -0
- package/dist/render/telegram.js +429 -0
- package/dist/telegram-html.d.ts +22 -0
- package/dist/telegram-html.d.ts.map +1 -0
- package/dist/telegram-html.js +98 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +57 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram-platform-universal frontend tools — tools every Telegram bot
|
|
3
|
+
* benefits from, regardless of what the bot does. Apps spread
|
|
4
|
+
* `defaultTelegramTools` into the `tools:` config they pass to
|
|
5
|
+
* `createBot`.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import type { BotTool } from "@copilotkit/channels";
|
|
9
|
+
export declare const lookupTelegramUserTool: BotTool<z.ZodObject<{
|
|
10
|
+
query: z.ZodString;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
query: string;
|
|
13
|
+
}, {
|
|
14
|
+
query: string;
|
|
15
|
+
}>>;
|
|
16
|
+
/**
|
|
17
|
+
* The flat list of tools the SDK ships. Spread into your
|
|
18
|
+
* `createBot({tools: …})`:
|
|
19
|
+
*
|
|
20
|
+
* tools: [...defaultTelegramTools, ...myAppTools],
|
|
21
|
+
*/
|
|
22
|
+
export declare const defaultTelegramTools: ReadonlyArray<BotTool>;
|
|
23
|
+
//# sourceMappingURL=built-in-tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"built-in-tools.d.ts","sourceRoot":"","sources":["../src/built-in-tools.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AASpD,eAAO,MAAM,sBAAsB;;;;;;GAuBjC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,OAAO,CAEvD,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram-platform-universal frontend tools — tools every Telegram bot
|
|
3
|
+
* benefits from, regardless of what the bot does. Apps spread
|
|
4
|
+
* `defaultTelegramTools` into the `tools:` config they pass to
|
|
5
|
+
* `createBot`.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { defineBotTool } from "@copilotkit/channels";
|
|
9
|
+
const lookupSchema = z.object({
|
|
10
|
+
query: z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.describe("Handle, display name, or first name of the person to look up."),
|
|
14
|
+
});
|
|
15
|
+
export const lookupTelegramUserTool = defineBotTool({
|
|
16
|
+
name: "lookup_telegram_user",
|
|
17
|
+
description: "Resolve a person to a Telegram user ID so you can mention them. " +
|
|
18
|
+
"Accepts a handle (`ada`), display name (`Ada Lovelace`), or first name. " +
|
|
19
|
+
"Returns an object with `found`, and on success a " +
|
|
20
|
+
"`mention` string (e.g. `@ada` or `tg://user?id=7`) — put that string verbatim " +
|
|
21
|
+
"in your reply to ping them. If `found` is false, write the plain " +
|
|
22
|
+
"name instead.",
|
|
23
|
+
parameters: lookupSchema,
|
|
24
|
+
async handler({ query }, { thread }) {
|
|
25
|
+
const u = await thread.lookupUser(query);
|
|
26
|
+
return u
|
|
27
|
+
? {
|
|
28
|
+
found: true,
|
|
29
|
+
query,
|
|
30
|
+
userId: u.id,
|
|
31
|
+
name: u.name,
|
|
32
|
+
handle: u.handle,
|
|
33
|
+
mention: u.handle ? "@" + u.handle : "tg://user?id=" + u.id,
|
|
34
|
+
}
|
|
35
|
+
: { found: false, query };
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* The flat list of tools the SDK ships. Spread into your
|
|
40
|
+
* `createBot({tools: …})`:
|
|
41
|
+
*
|
|
42
|
+
* tools: [...defaultTelegramTools, ...myAppTools],
|
|
43
|
+
*/
|
|
44
|
+
export const defaultTelegramTools = [
|
|
45
|
+
lookupTelegramUserTool,
|
|
46
|
+
];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-Telegram-message streaming text state machine.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the Slack MessageStream + ChunkedMessageStream pattern but uses
|
|
5
|
+
* numeric Telegram message IDs instead of Slack `ts` strings.
|
|
6
|
+
*
|
|
7
|
+
* ChunkedEditStream owns N underlying EditStream instances (one per Telegram
|
|
8
|
+
* message). It accepts the full accumulated text on `append()`, decides where
|
|
9
|
+
* the chunk boundaries are (frozen once a message has been posted — no reflow),
|
|
10
|
+
* mints new Telegram messages via `postPlaceholder` as needed, and dispatches
|
|
11
|
+
* the right slice to each chunk's stream.
|
|
12
|
+
*
|
|
13
|
+
* `transform` (e.g. telegramHtml) is applied just before every `editAt` call.
|
|
14
|
+
*/
|
|
15
|
+
export interface ChunkedEditStreamConfig {
|
|
16
|
+
/**
|
|
17
|
+
* Soft per-message raw char limit. Defaults to Math.floor(TELEGRAM_LIMITS.messageText / 2).
|
|
18
|
+
* The headroom below the 4096 hard cap exists because `transform` (telegramHtml) can
|
|
19
|
+
* expand raw chars significantly: `&`→`&` (5×), `<`/`>`→`<`/`>` (4×),
|
|
20
|
+
* bold/link markdown → HTML tags. Halving the limit is a risk-reduction heuristic that
|
|
21
|
+
* keeps the HTML-transformed slice under 4096 for typical prose/markdown content (which
|
|
22
|
+
* expands modestly); it is NOT a hard guarantee against adversarial all-entity input
|
|
23
|
+
* (e.g. 2048 raw `&` chars → ~10 240 transformed chars).
|
|
24
|
+
*/
|
|
25
|
+
limit?: number;
|
|
26
|
+
/** Throttle floor (ms) between consecutive edits per message. Defaults to 1000. */
|
|
27
|
+
minIntervalMs?: number;
|
|
28
|
+
/** Posts a new Telegram message with placeholder text; resolves with the message id. */
|
|
29
|
+
postPlaceholder: (text: string) => Promise<number>;
|
|
30
|
+
/** Edits the Telegram message with the given id to contain `text`. */
|
|
31
|
+
editAt: (messageId: number, text: string) => Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Optional transformer applied to each chunk's text before `editAt` /
|
|
34
|
+
* `postPlaceholder` — e.g. telegramHtml markdown→HTML conversion.
|
|
35
|
+
*/
|
|
36
|
+
transform?: (text: string) => string;
|
|
37
|
+
}
|
|
38
|
+
export declare class ChunkedEditStream {
|
|
39
|
+
private buffer;
|
|
40
|
+
/** Sorted character positions where a chunk ends (= where the next begins). */
|
|
41
|
+
private boundaries;
|
|
42
|
+
private streams;
|
|
43
|
+
/** Serialises new-chunk creation so async postPlaceholder calls don't race. */
|
|
44
|
+
private setupPromise;
|
|
45
|
+
private finished;
|
|
46
|
+
private readonly limit;
|
|
47
|
+
private readonly minIntervalMs;
|
|
48
|
+
private readonly postPlaceholder;
|
|
49
|
+
private readonly editAt;
|
|
50
|
+
private readonly transform;
|
|
51
|
+
constructor(config: ChunkedEditStreamConfig);
|
|
52
|
+
/**
|
|
53
|
+
* Feed the FULL accumulated text so far. The stream will work out which
|
|
54
|
+
* slice belongs to which Telegram message and throttle edits accordingly.
|
|
55
|
+
*/
|
|
56
|
+
append(fullText: string): void;
|
|
57
|
+
/** Flush all pending edits and wait until every message reflects its final text. */
|
|
58
|
+
finish(): Promise<void>;
|
|
59
|
+
/** Number of Telegram messages posted so far. */
|
|
60
|
+
get chunkCount(): number;
|
|
61
|
+
/**
|
|
62
|
+
* Walk forward from the last frozen boundary, freezing new ones whenever
|
|
63
|
+
* the active chunk's length exceeds the soft limit. Choose the break point
|
|
64
|
+
* at the last newline (or last space) within the window; fall back to a hard
|
|
65
|
+
* cut if neither is found. Boundaries never move once frozen.
|
|
66
|
+
*/
|
|
67
|
+
private refreezeBoundaries;
|
|
68
|
+
private ensureStreamsAndDispatch;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=chunked-edit-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunked-edit-stream.d.ts","sourceRoot":"","sources":["../src/chunked-edit-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,MAAM,WAAW,uBAAuB;IACtC;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wFAAwF;IACxF,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,sEAAsE;IACtE,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC;AA+FD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAM;IACpB,+EAA+E;IAC/E,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,OAAO,CAAoB;IACnC,+EAA+E;IAC/E,OAAO,CAAC,YAAY,CAAoC;IACxD,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoC;IACpE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqD;IAC5E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2B;gBAEzC,MAAM,EAAE,uBAAuB;IAQ3C;;;OAGG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAU9B,oFAAoF;IAC9E,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAS7B,iDAAiD;IACjD,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;YAgBZ,wBAAwB;CAsCvC"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-Telegram-message streaming text state machine.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the Slack MessageStream + ChunkedMessageStream pattern but uses
|
|
5
|
+
* numeric Telegram message IDs instead of Slack `ts` strings.
|
|
6
|
+
*
|
|
7
|
+
* ChunkedEditStream owns N underlying EditStream instances (one per Telegram
|
|
8
|
+
* message). It accepts the full accumulated text on `append()`, decides where
|
|
9
|
+
* the chunk boundaries are (frozen once a message has been posted — no reflow),
|
|
10
|
+
* mints new Telegram messages via `postPlaceholder` as needed, and dispatches
|
|
11
|
+
* the right slice to each chunk's stream.
|
|
12
|
+
*
|
|
13
|
+
* `transform` (e.g. telegramHtml) is applied just before every `editAt` call.
|
|
14
|
+
*/
|
|
15
|
+
import { TELEGRAM_LIMITS } from "./render/budget.js";
|
|
16
|
+
/**
|
|
17
|
+
* Default raw-char limit per chunk. Set to half of Telegram's 4096 hard cap as a
|
|
18
|
+
* generous headroom heuristic: typical prose/markdown content expands modestly under
|
|
19
|
+
* telegramHtml, so this keeps the rendered output well under 4096 in practice.
|
|
20
|
+
* It is NOT a hard guarantee — adversarial all-entity input can still exceed the cap.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_LIMIT = Math.floor(TELEGRAM_LIMITS.messageText / 2); // 2048
|
|
23
|
+
const DEFAULT_MIN_INTERVAL_MS = 1000;
|
|
24
|
+
class EditStream {
|
|
25
|
+
buffer = "";
|
|
26
|
+
posted = "";
|
|
27
|
+
queue = Promise.resolve();
|
|
28
|
+
lastFlushedAt = 0;
|
|
29
|
+
flushTimer;
|
|
30
|
+
minIntervalMs;
|
|
31
|
+
update;
|
|
32
|
+
/** Non-null when the terminal flush (from finish()) failed. */
|
|
33
|
+
terminalError = undefined;
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.update = config.update;
|
|
36
|
+
this.minIntervalMs = config.minIntervalMs;
|
|
37
|
+
}
|
|
38
|
+
append(text) {
|
|
39
|
+
if (text === this.buffer)
|
|
40
|
+
return;
|
|
41
|
+
this.buffer = text;
|
|
42
|
+
this.scheduleFlush();
|
|
43
|
+
}
|
|
44
|
+
async finish() {
|
|
45
|
+
if (this.flushTimer) {
|
|
46
|
+
clearTimeout(this.flushTimer);
|
|
47
|
+
this.flushTimer = undefined;
|
|
48
|
+
}
|
|
49
|
+
this.enqueueFlush(/* terminal */ true);
|
|
50
|
+
await this.queue;
|
|
51
|
+
if (this.terminalError !== undefined) {
|
|
52
|
+
throw this.terminalError;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
scheduleFlush() {
|
|
56
|
+
if (this.flushTimer)
|
|
57
|
+
return;
|
|
58
|
+
const elapsed = Date.now() - this.lastFlushedAt;
|
|
59
|
+
const delay = Math.max(0, this.minIntervalMs - elapsed);
|
|
60
|
+
this.flushTimer = setTimeout(() => {
|
|
61
|
+
this.flushTimer = undefined;
|
|
62
|
+
this.enqueueFlush(false);
|
|
63
|
+
}, delay);
|
|
64
|
+
}
|
|
65
|
+
enqueueFlush(terminal) {
|
|
66
|
+
this.queue = this.queue.then(() => this.flushNow(terminal));
|
|
67
|
+
}
|
|
68
|
+
async flushNow(terminal) {
|
|
69
|
+
if (this.buffer === this.posted)
|
|
70
|
+
return;
|
|
71
|
+
const text = this.buffer;
|
|
72
|
+
try {
|
|
73
|
+
await this.update(text);
|
|
74
|
+
// Only advance posted after a successful edit so a transient failure
|
|
75
|
+
// leaves the text eligible for retry on the next flush.
|
|
76
|
+
this.posted = text;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (terminal) {
|
|
80
|
+
// Surface terminal-flush failures to finish() so the caller can react.
|
|
81
|
+
this.terminalError = err;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Intermediate flush: log but leave posted unchanged so the next
|
|
85
|
+
// flush will re-attempt the current buffer.
|
|
86
|
+
console.error("[chunked-edit-stream] editAt failed:", err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
this.lastFlushedAt = Date.now();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// ChunkedEditStream
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
export class ChunkedEditStream {
|
|
98
|
+
buffer = "";
|
|
99
|
+
/** Sorted character positions where a chunk ends (= where the next begins). */
|
|
100
|
+
boundaries = [];
|
|
101
|
+
streams = [];
|
|
102
|
+
/** Serialises new-chunk creation so async postPlaceholder calls don't race. */
|
|
103
|
+
setupPromise = Promise.resolve();
|
|
104
|
+
finished = false;
|
|
105
|
+
limit;
|
|
106
|
+
minIntervalMs;
|
|
107
|
+
postPlaceholder;
|
|
108
|
+
editAt;
|
|
109
|
+
transform;
|
|
110
|
+
constructor(config) {
|
|
111
|
+
this.limit = config.limit ?? DEFAULT_LIMIT;
|
|
112
|
+
this.minIntervalMs = config.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
113
|
+
this.postPlaceholder = config.postPlaceholder;
|
|
114
|
+
this.editAt = config.editAt;
|
|
115
|
+
this.transform = config.transform ?? ((t) => t);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Feed the FULL accumulated text so far. The stream will work out which
|
|
119
|
+
* slice belongs to which Telegram message and throttle edits accordingly.
|
|
120
|
+
*/
|
|
121
|
+
append(fullText) {
|
|
122
|
+
if (this.finished)
|
|
123
|
+
return;
|
|
124
|
+
if (fullText === this.buffer)
|
|
125
|
+
return;
|
|
126
|
+
this.buffer = fullText;
|
|
127
|
+
this.refreezeBoundaries();
|
|
128
|
+
this.setupPromise = this.setupPromise.then(() => this.ensureStreamsAndDispatch());
|
|
129
|
+
}
|
|
130
|
+
/** Flush all pending edits and wait until every message reflects its final text. */
|
|
131
|
+
async finish() {
|
|
132
|
+
this.finished = true;
|
|
133
|
+
this.setupPromise = this.setupPromise.then(() => this.ensureStreamsAndDispatch());
|
|
134
|
+
await this.setupPromise;
|
|
135
|
+
for (const s of this.streams)
|
|
136
|
+
await s.finish();
|
|
137
|
+
}
|
|
138
|
+
/** Number of Telegram messages posted so far. */
|
|
139
|
+
get chunkCount() {
|
|
140
|
+
return this.streams.length;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Walk forward from the last frozen boundary, freezing new ones whenever
|
|
144
|
+
* the active chunk's length exceeds the soft limit. Choose the break point
|
|
145
|
+
* at the last newline (or last space) within the window; fall back to a hard
|
|
146
|
+
* cut if neither is found. Boundaries never move once frozen.
|
|
147
|
+
*/
|
|
148
|
+
refreezeBoundaries() {
|
|
149
|
+
const minAdvance = Math.max(1, Math.floor(this.limit / 2));
|
|
150
|
+
let lastFrozen = this.boundaries.at(-1) ?? 0;
|
|
151
|
+
while (this.buffer.length - lastFrozen > this.limit) {
|
|
152
|
+
const window = this.buffer.slice(lastFrozen, lastFrozen + this.limit);
|
|
153
|
+
let breakAt = window.lastIndexOf("\n");
|
|
154
|
+
if (breakAt < this.limit / 4)
|
|
155
|
+
breakAt = window.lastIndexOf(" ");
|
|
156
|
+
// Bug 1 fix: enforce a minimum advance floor so adversarial input
|
|
157
|
+
// (e.g. leading spaces / early newlines) doesn't produce ~1-char chunks.
|
|
158
|
+
if (breakAt < minAdvance)
|
|
159
|
+
breakAt = this.limit - 1;
|
|
160
|
+
const candidate = lastFrozen + breakAt + 1;
|
|
161
|
+
this.boundaries.push(candidate);
|
|
162
|
+
lastFrozen = candidate;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async ensureStreamsAndDispatch() {
|
|
166
|
+
if (this.buffer.length === 0)
|
|
167
|
+
return;
|
|
168
|
+
// Bug 2 fix: if the last boundary lands exactly at buffer.length the final
|
|
169
|
+
// "chunk" would be an empty string — drop that phantom boundary so we don't
|
|
170
|
+
// post a blank placeholder message.
|
|
171
|
+
while (this.boundaries.length > 0 &&
|
|
172
|
+
this.boundaries[this.boundaries.length - 1] >= this.buffer.length) {
|
|
173
|
+
this.boundaries.pop();
|
|
174
|
+
}
|
|
175
|
+
const chunkCount = this.boundaries.length + 1;
|
|
176
|
+
while (this.streams.length < chunkCount) {
|
|
177
|
+
// Plain-text placeholder (no Markdown): postPlaceholder sends with
|
|
178
|
+
// parse_mode:"HTML", so Markdown italics would render literal underscores.
|
|
179
|
+
const placeholder = "…";
|
|
180
|
+
const messageId = await this.postPlaceholder(placeholder);
|
|
181
|
+
this.streams.push(new EditStream({
|
|
182
|
+
update: (text) => this.editAt(messageId, this.transform(text) || " "),
|
|
183
|
+
minIntervalMs: this.minIntervalMs,
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
// Dispatch slices to each message's stream.
|
|
187
|
+
for (let i = 0; i < chunkCount; i++) {
|
|
188
|
+
const start = i === 0 ? 0 : this.boundaries[i - 1];
|
|
189
|
+
const end = i < this.boundaries.length ? this.boundaries[i] : this.buffer.length;
|
|
190
|
+
const slice = this.buffer.slice(start, end);
|
|
191
|
+
// Bug 2 fix: never dispatch an empty slice.
|
|
192
|
+
if (slice.length > 0) {
|
|
193
|
+
this.streams[i].append(slice);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { AbstractAgent } from "@ag-ui/client";
|
|
2
|
+
import type { ThreadMessage } from "@copilotkit/channels-ui";
|
|
3
|
+
import type { ConversationStore, AgentSession } from "@copilotkit/channels";
|
|
4
|
+
import type { ReplyTarget } from "./types.js";
|
|
5
|
+
import type { AgentContentPart } from "./download-files.js";
|
|
6
|
+
/**
|
|
7
|
+
* Telegram conversation store with stable threadIds.
|
|
8
|
+
*
|
|
9
|
+
* Unlike Slack (which rebuilds history from the platform API and mints a fresh
|
|
10
|
+
* threadId per turn), Telegram's Bot API cannot read chat history. This store
|
|
11
|
+
* therefore:
|
|
12
|
+
* - assigns each conversation a **stable** threadId (`tg-thread-<key>`) so the
|
|
13
|
+
* agent's own persistence layer can accumulate state across turns; and
|
|
14
|
+
* - maintains an in-memory message log (lost on restart, capped at 200 entries)
|
|
15
|
+
* that callers can use for lightweight within-session context.
|
|
16
|
+
*/
|
|
17
|
+
export declare class TelegramConversationStore implements ConversationStore {
|
|
18
|
+
private readonly sessions;
|
|
19
|
+
private readonly history;
|
|
20
|
+
/**
|
|
21
|
+
* Per-conversation queue of user messages awaiting delivery to the agent.
|
|
22
|
+
*
|
|
23
|
+
* The listener enqueues each turn's user message (text, or text + file
|
|
24
|
+
* parts) BEFORE invoking the bot handler that calls `runAgent` →
|
|
25
|
+
* {@link getOrCreate}, which drains the queue into the cached agent. This is
|
|
26
|
+
* how Telegram delivers the user's turn to the agent: unlike Slack, the Bot
|
|
27
|
+
* API cannot read chat history, so `Thread.run()` (which only injects a
|
|
28
|
+
* message when `extra.prompt` is set) would otherwise never see the input.
|
|
29
|
+
*/
|
|
30
|
+
private readonly pending;
|
|
31
|
+
private threadIdFor;
|
|
32
|
+
/**
|
|
33
|
+
* Enqueue a user message to be delivered to the agent on the next
|
|
34
|
+
* {@link getOrCreate} for `conversationKey`. Content is either a plain text
|
|
35
|
+
* string or an array of AG-UI content parts (text + media).
|
|
36
|
+
*/
|
|
37
|
+
enqueueUserMessage(conversationKey: string, content: string | AgentContentPart[]): void;
|
|
38
|
+
/**
|
|
39
|
+
* Return the cached AgentSession for `conversationKey`, or create one.
|
|
40
|
+
*
|
|
41
|
+
* On first call for a key `makeAgent` is invoked exactly once with the
|
|
42
|
+
* stable threadId; subsequent calls return the same cached session without
|
|
43
|
+
* invoking `makeAgent` again.
|
|
44
|
+
*/
|
|
45
|
+
getOrCreate(conversationKey: string, _replyTarget: ReplyTarget, makeAgent: (threadId: string) => AbstractAgent): Promise<AgentSession>;
|
|
46
|
+
/**
|
|
47
|
+
* Append `msg` to the in-memory history for `conversationKey`.
|
|
48
|
+
* Drops the oldest entry when the list exceeds {@link MAX_HISTORY}.
|
|
49
|
+
*/
|
|
50
|
+
recordMessage(conversationKey: string, msg: ThreadMessage): void;
|
|
51
|
+
/** Return the recorded message history for `conversationKey` (or `[]`). */
|
|
52
|
+
getMessages(conversationKey: string): ThreadMessage[];
|
|
53
|
+
/** Whether a session exists for `conversationKey`. */
|
|
54
|
+
has(conversationKey: string): boolean;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=conversation-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-store.d.ts","sourceRoot":"","sources":["../src/conversation-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAK5D;;;;;;;;;;GAUG;AACH,qBAAa,yBAA0B,YAAW,iBAAiB;IACjE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAGpB;IAEJ,OAAO,CAAC,WAAW;IAInB;;;;OAIG;IACH,kBAAkB,CAChB,eAAe,EAAE,MAAM,EACvB,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,GACnC,IAAI;IASP;;;;;;OAMG;IACG,WAAW,CACf,eAAe,EAAE,MAAM,EACvB,YAAY,EAAE,WAAW,EACzB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,aAAa,GAC7C,OAAO,CAAC,YAAY,CAAC;IA2BxB;;;OAGG;IACH,aAAa,CAAC,eAAe,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,GAAG,IAAI;IAYhE,2EAA2E;IAC3E,WAAW,CAAC,eAAe,EAAE,MAAM,GAAG,aAAa,EAAE;IAIrD,sDAAsD;IACtD,GAAG,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO;CAGtC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Maximum number of messages retained per conversation (oldest dropped). */
|
|
2
|
+
const MAX_HISTORY = 200;
|
|
3
|
+
/**
|
|
4
|
+
* Telegram conversation store with stable threadIds.
|
|
5
|
+
*
|
|
6
|
+
* Unlike Slack (which rebuilds history from the platform API and mints a fresh
|
|
7
|
+
* threadId per turn), Telegram's Bot API cannot read chat history. This store
|
|
8
|
+
* therefore:
|
|
9
|
+
* - assigns each conversation a **stable** threadId (`tg-thread-<key>`) so the
|
|
10
|
+
* agent's own persistence layer can accumulate state across turns; and
|
|
11
|
+
* - maintains an in-memory message log (lost on restart, capped at 200 entries)
|
|
12
|
+
* that callers can use for lightweight within-session context.
|
|
13
|
+
*/
|
|
14
|
+
export class TelegramConversationStore {
|
|
15
|
+
sessions = new Map();
|
|
16
|
+
history = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* Per-conversation queue of user messages awaiting delivery to the agent.
|
|
19
|
+
*
|
|
20
|
+
* The listener enqueues each turn's user message (text, or text + file
|
|
21
|
+
* parts) BEFORE invoking the bot handler that calls `runAgent` →
|
|
22
|
+
* {@link getOrCreate}, which drains the queue into the cached agent. This is
|
|
23
|
+
* how Telegram delivers the user's turn to the agent: unlike Slack, the Bot
|
|
24
|
+
* API cannot read chat history, so `Thread.run()` (which only injects a
|
|
25
|
+
* message when `extra.prompt` is set) would otherwise never see the input.
|
|
26
|
+
*/
|
|
27
|
+
pending = new Map();
|
|
28
|
+
threadIdFor(conversationKey) {
|
|
29
|
+
return `tg-thread-${conversationKey}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Enqueue a user message to be delivered to the agent on the next
|
|
33
|
+
* {@link getOrCreate} for `conversationKey`. Content is either a plain text
|
|
34
|
+
* string or an array of AG-UI content parts (text + media).
|
|
35
|
+
*/
|
|
36
|
+
enqueueUserMessage(conversationKey, content) {
|
|
37
|
+
let list = this.pending.get(conversationKey);
|
|
38
|
+
if (!list) {
|
|
39
|
+
list = [];
|
|
40
|
+
this.pending.set(conversationKey, list);
|
|
41
|
+
}
|
|
42
|
+
list.push({ content });
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Return the cached AgentSession for `conversationKey`, or create one.
|
|
46
|
+
*
|
|
47
|
+
* On first call for a key `makeAgent` is invoked exactly once with the
|
|
48
|
+
* stable threadId; subsequent calls return the same cached session without
|
|
49
|
+
* invoking `makeAgent` again.
|
|
50
|
+
*/
|
|
51
|
+
async getOrCreate(conversationKey, _replyTarget, makeAgent) {
|
|
52
|
+
let session = this.sessions.get(conversationKey);
|
|
53
|
+
if (!session) {
|
|
54
|
+
const threadId = this.threadIdFor(conversationKey);
|
|
55
|
+
const agent = makeAgent(threadId);
|
|
56
|
+
session = { agent };
|
|
57
|
+
this.sessions.set(conversationKey, session);
|
|
58
|
+
}
|
|
59
|
+
// Drain any user messages the listener enqueued for this turn into the
|
|
60
|
+
// agent so they actually reach the model. Clears the queue so a later
|
|
61
|
+
// getOrCreate (e.g. a follow-up run) does not re-deliver them.
|
|
62
|
+
const queued = this.pending.get(conversationKey);
|
|
63
|
+
if (queued && queued.length > 0) {
|
|
64
|
+
for (const { content } of queued) {
|
|
65
|
+
session.agent.addMessage({
|
|
66
|
+
id: globalThis.crypto.randomUUID(),
|
|
67
|
+
role: "user",
|
|
68
|
+
content,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
this.pending.delete(conversationKey);
|
|
72
|
+
}
|
|
73
|
+
return session;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Append `msg` to the in-memory history for `conversationKey`.
|
|
77
|
+
* Drops the oldest entry when the list exceeds {@link MAX_HISTORY}.
|
|
78
|
+
*/
|
|
79
|
+
recordMessage(conversationKey, msg) {
|
|
80
|
+
let list = this.history.get(conversationKey);
|
|
81
|
+
if (!list) {
|
|
82
|
+
list = [];
|
|
83
|
+
this.history.set(conversationKey, list);
|
|
84
|
+
}
|
|
85
|
+
list.push(msg);
|
|
86
|
+
if (list.length > MAX_HISTORY) {
|
|
87
|
+
list.shift();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Return the recorded message history for `conversationKey` (or `[]`). */
|
|
91
|
+
getMessages(conversationKey) {
|
|
92
|
+
return this.history.get(conversationKey) ?? [];
|
|
93
|
+
}
|
|
94
|
+
/** Whether a session exists for `conversationKey`. */
|
|
95
|
+
has(conversationKey) {
|
|
96
|
+
return this.sessions.has(conversationKey);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound file transport. Telegram messages can carry file attachments; this
|
|
3
|
+
* turns them into AG-UI multimodal message content the agent's model can
|
|
4
|
+
* read — images, audio, video, and documents as their respective binary parts —
|
|
5
|
+
* downloading each from the Telegram Bot API file endpoint.
|
|
6
|
+
*
|
|
7
|
+
* The bridge is transport-only: it delivers the bytes to the agent and lets
|
|
8
|
+
* the app decide what to do with them. Anything it can't represent is skipped
|
|
9
|
+
* with a short note so the agent knows a file was dropped and why.
|
|
10
|
+
*/
|
|
11
|
+
/** The subset of a Telegram file object we use. */
|
|
12
|
+
export interface TelegramFileRef {
|
|
13
|
+
fileId: string;
|
|
14
|
+
fileName?: string;
|
|
15
|
+
mimeType?: string;
|
|
16
|
+
size?: number;
|
|
17
|
+
}
|
|
18
|
+
/** A base64 data source, shared by every binary media part. */
|
|
19
|
+
type MediaDataSource = {
|
|
20
|
+
type: "data";
|
|
21
|
+
value: string;
|
|
22
|
+
mimeType: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* AG-UI multimodal content parts (shape the runtime's converter expects).
|
|
26
|
+
*
|
|
27
|
+
* Binary media (image/audio/video/document) is passed straight through as a
|
|
28
|
+
* data part; the agent's model decides what it can actually consume.
|
|
29
|
+
*/
|
|
30
|
+
export type AgentContentPart = {
|
|
31
|
+
type: "text";
|
|
32
|
+
text: string;
|
|
33
|
+
} | {
|
|
34
|
+
type: "image";
|
|
35
|
+
source: MediaDataSource;
|
|
36
|
+
} | {
|
|
37
|
+
type: "audio";
|
|
38
|
+
source: MediaDataSource;
|
|
39
|
+
} | {
|
|
40
|
+
type: "video";
|
|
41
|
+
source: MediaDataSource;
|
|
42
|
+
} | {
|
|
43
|
+
type: "document";
|
|
44
|
+
source: MediaDataSource;
|
|
45
|
+
};
|
|
46
|
+
/** Tunables for inbound file handling (all optional; sane defaults). */
|
|
47
|
+
export interface FileDeliveryConfig {
|
|
48
|
+
/** Skip a single file larger than this many bytes. Default 8 MiB. */
|
|
49
|
+
maxBytesPerFile?: number;
|
|
50
|
+
/** Process at most this many files per message. Default 5. */
|
|
51
|
+
maxFiles?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Download a message's files and turn them into AG-UI content parts. Returns
|
|
55
|
+
* the parts plus human-readable `notes` for anything skipped (appended to the
|
|
56
|
+
* message as a text part by the caller).
|
|
57
|
+
*
|
|
58
|
+
* @param files List of Telegram file references to process.
|
|
59
|
+
* @param token Telegram bot token used to build the download URL.
|
|
60
|
+
* @param getFilePath Resolves a fileId to a Telegram file path (e.g. via getFile API).
|
|
61
|
+
* @param config Optional delivery configuration.
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildFileContentParts(files: TelegramFileRef[], token: string, getFilePath: (fileId: string) => Promise<string>, config?: FileDeliveryConfig): Promise<{
|
|
64
|
+
parts: AgentContentPart[];
|
|
65
|
+
notes: string[];
|
|
66
|
+
}>;
|
|
67
|
+
export {};
|
|
68
|
+
//# sourceMappingURL=download-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download-files.d.ts","sourceRoot":"","sources":["../src/download-files.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,+DAA+D;AAC/D,KAAK,eAAe,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,CAAC;AAElD,wEAAwE;AACxE,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA0DD;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,eAAe,EAAE,EACxB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,EAChD,MAAM,GAAE,kBAAuB,GAC9B,OAAO,CAAC;IAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAoGzD"}
|