@fickydev/pigent 0.1.7 → 0.1.8
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/CHANGELOG.md +2 -0
- package/PLAN.md +21 -1
- package/TODO.md +12 -0
- package/package.json +1 -1
- package/pigent.yaml +9 -0
- package/src/agents/BotCommandHandler.ts +124 -27
- package/src/channels/telegram/TelegramApi.ts +47 -3
- package/src/channels/telegram/TelegramPollingAdapter.ts +45 -2
- package/src/channels/telegram/types.ts +20 -0
- package/src/channels/types.ts +6 -0
- package/src/config/loadConfig.ts +1 -0
- package/src/config/schemas.ts +8 -0
- package/src/daemon/AgentDaemon.ts +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
- Planned model selection support across profile, agent, and Telegram chat override levels.
|
|
44
44
|
- Added profile and agent config-based Pi model and thinking level selection.
|
|
45
45
|
- Added session-scoped model and thinking level overrides with `/model` and `/thinking` Telegram commands.
|
|
46
|
+
- Planned Telegram inline-button model and thinking level pickers backed by configured model choices.
|
|
47
|
+
- Added Telegram inline-button pickers for `/model` and `/thinking`, callback handling, and bot command menu registration.
|
|
46
48
|
- Kept daemon process alive after startup so CLI runs do not exit after `pigent ready`.
|
|
47
49
|
|
|
48
50
|
### Changed
|
package/PLAN.md
CHANGED
|
@@ -206,7 +206,27 @@ Telegram command support should persist session-scoped overrides after config su
|
|
|
206
206
|
- `/thinking <level>` sets thinking level for the default agent session in the current chat/thread
|
|
207
207
|
- `/thinking default` clears thinking override for the default agent session in the current chat/thread
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
Telegram should also provide inline-button pickers so non-technical users can choose from configured choices:
|
|
210
|
+
|
|
211
|
+
- `/model` can reply with inline keyboard buttons for configured model choices plus `Use default`
|
|
212
|
+
- `/thinking` can reply with inline keyboard buttons for `Off`, `Low`, `Medium`, `High`, and `Use default`
|
|
213
|
+
- callback payloads should be short and action-scoped, such as `model:set:<choiceId>`, `model:default`, `thinking:set:medium`, and `thinking:default`
|
|
214
|
+
- after selection, bot should answer the callback and update or send a confirmation message
|
|
215
|
+
- daemon startup should register Telegram bot commands through `setMyCommands` so users see Pigent commands in the Telegram command menu
|
|
216
|
+
|
|
217
|
+
Model choices should be explicitly configured instead of exposing every Pi model blindly:
|
|
218
|
+
|
|
219
|
+
```yaml
|
|
220
|
+
modelChoices:
|
|
221
|
+
- id: anthropic/claude-sonnet-4-5
|
|
222
|
+
label: Claude Sonnet
|
|
223
|
+
- id: anthropic/claude-opus-4-5
|
|
224
|
+
label: Claude Opus
|
|
225
|
+
- id: openai/gpt-4.1
|
|
226
|
+
label: GPT-4.1
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The command and callback handlers should validate agent access and model availability through a Pi model resolver. They must not reveal API keys, auth values, raw environment variables, or provider secrets.
|
|
210
230
|
|
|
211
231
|
## Prompt Composition
|
|
212
232
|
|
package/TODO.md
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
## Config
|
|
41
41
|
|
|
42
|
+
- [x] Add root `modelChoices` config for Telegram model picker buttons
|
|
42
43
|
- [x] Add profile-level `thinkingLevel` config
|
|
43
44
|
- [x] Add agent-level `model` override config
|
|
44
45
|
- [x] Add agent-level `thinkingLevel` override config
|
|
@@ -84,11 +85,15 @@
|
|
|
84
85
|
- [x] Define `OutboundMessage`
|
|
85
86
|
- [x] Define `ChannelAdapter`
|
|
86
87
|
- [x] Define Telegram normalized types
|
|
88
|
+
- [x] Normalize Telegram callback query updates
|
|
87
89
|
- [ ] Implement `TelegramApi`
|
|
88
90
|
- [x] `getUpdates`
|
|
89
91
|
- [x] `sendMessage`
|
|
90
92
|
- [x] error handling
|
|
91
93
|
- [x] retry/backoff
|
|
94
|
+
- [x] `answerCallbackQuery`
|
|
95
|
+
- [x] inline keyboard `reply_markup` support
|
|
96
|
+
- [x] `setMyCommands` Telegram command menu registration
|
|
92
97
|
- [x] Implement `TelegramPollingAdapter`
|
|
93
98
|
- [x] polling loop
|
|
94
99
|
- [x] offset tracking
|
|
@@ -179,6 +184,11 @@
|
|
|
179
184
|
- [x] `/thinking` show thinking level for default chat agent session
|
|
180
185
|
- [x] `/thinking <level>` set thinking level for default chat agent session
|
|
181
186
|
- [x] `/thinking default` clear thinking level for default chat agent session
|
|
187
|
+
- [x] `/model` inline button picker for configured model choices
|
|
188
|
+
- [x] `/thinking` inline button picker
|
|
189
|
+
- [x] Handle model picker callback and persist selected session model
|
|
190
|
+
- [x] Handle thinking picker callback and persist selected session thinking level
|
|
191
|
+
- [x] Add `Use default` model/thinking buttons
|
|
182
192
|
- [ ] `/model <agentId> <provider/modelId>` set model for an explicit agent session
|
|
183
193
|
- [ ] `/thinking <agentId> <level>` set thinking level for an explicit agent session
|
|
184
194
|
- [x] `/help` show bot commands
|
|
@@ -192,6 +202,8 @@
|
|
|
192
202
|
- [ ] Unit test heartbeat `NOOP` behavior
|
|
193
203
|
- [ ] Unit test model selection priority
|
|
194
204
|
- [ ] Unit test Telegram `/model` command parsing
|
|
205
|
+
- [ ] Unit test Telegram model picker callback parsing
|
|
206
|
+
- [ ] Unit test Telegram thinking picker callback parsing
|
|
195
207
|
- [ ] Repository tests against temp SQLite DB
|
|
196
208
|
- [ ] Integration test fake Telegram update to fake Pi runner
|
|
197
209
|
|
package/package.json
CHANGED
package/pigent.yaml
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
telegramChats: []
|
|
2
|
+
modelChoices: []
|
|
2
3
|
|
|
4
|
+
# Add choices to show buttons for /model.
|
|
5
|
+
#
|
|
6
|
+
# modelChoices:
|
|
7
|
+
# - id: anthropic/claude-sonnet-4-5
|
|
8
|
+
# label: Claude Sonnet
|
|
9
|
+
# - id: anthropic/claude-opus-4-5
|
|
10
|
+
# label: Claude Opus
|
|
11
|
+
#
|
|
3
12
|
# Replace chatId with a real Telegram chat id to enable routing.
|
|
4
13
|
#
|
|
5
14
|
# telegramChats:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { InboundMessage } from "../channels/types";
|
|
1
|
+
import type { InboundMessage, InlineKeyboardButton } from "../channels/types";
|
|
2
|
+
import type { ModelChoiceConfig } from "../config/schemas";
|
|
2
3
|
import type { Repositories } from "../db/repositories";
|
|
3
4
|
import { isValidModelRef } from "../pi/PiModelResolver";
|
|
4
5
|
import type { AgentRegistry } from "./AgentRegistry";
|
|
@@ -7,6 +8,7 @@ export type BotCommandResult =
|
|
|
7
8
|
| {
|
|
8
9
|
handled: true;
|
|
9
10
|
text: string;
|
|
11
|
+
inlineKeyboard?: InlineKeyboardButton[][];
|
|
10
12
|
}
|
|
11
13
|
| {
|
|
12
14
|
handled: false;
|
|
@@ -19,6 +21,7 @@ export class BotCommandHandler {
|
|
|
19
21
|
constructor(
|
|
20
22
|
private readonly registry: AgentRegistry,
|
|
21
23
|
private readonly repositories: Repositories,
|
|
24
|
+
private readonly modelChoices: ModelChoiceConfig[] = [],
|
|
22
25
|
) {}
|
|
23
26
|
|
|
24
27
|
async handle(message: InboundMessage): Promise<BotCommandResult> {
|
|
@@ -31,9 +34,17 @@ export class BotCommandHandler {
|
|
|
31
34
|
case "/agents":
|
|
32
35
|
return { handled: true, text: await this.agentsText(message) };
|
|
33
36
|
case "/model":
|
|
34
|
-
return
|
|
37
|
+
return await this.modelResult(message);
|
|
35
38
|
case "/thinking":
|
|
36
|
-
return
|
|
39
|
+
return await this.thinkingResult(message);
|
|
40
|
+
case "/model:set":
|
|
41
|
+
return { handled: true, text: await this.setModelFromChoice(message) };
|
|
42
|
+
case "/model:default":
|
|
43
|
+
return { handled: true, text: await this.clearModel(message) };
|
|
44
|
+
case "/thinking:set":
|
|
45
|
+
return { handled: true, text: await this.setThinkingFromCallback(message) };
|
|
46
|
+
case "/thinking:default":
|
|
47
|
+
return { handled: true, text: await this.clearThinking(message) };
|
|
37
48
|
default:
|
|
38
49
|
return { handled: false };
|
|
39
50
|
}
|
|
@@ -44,10 +55,10 @@ export class BotCommandHandler {
|
|
|
44
55
|
"Pigent commands:",
|
|
45
56
|
"/help - show this help",
|
|
46
57
|
"/agents - list agents for this chat",
|
|
47
|
-
"/model - show model for this chat session",
|
|
58
|
+
"/model - show model picker for this chat session",
|
|
48
59
|
"/model <provider/modelId> - set model for this chat session",
|
|
49
60
|
"/model default - clear model override for this chat session",
|
|
50
|
-
"/thinking - show thinking
|
|
61
|
+
"/thinking - show thinking picker for this chat session",
|
|
51
62
|
"/thinking <off|low|medium|high> - set thinking level for this chat session",
|
|
52
63
|
"/thinking default - clear thinking override for this chat session",
|
|
53
64
|
"/agent <agentId> <message> - send message to specific agent",
|
|
@@ -77,56 +88,97 @@ export class BotCommandHandler {
|
|
|
77
88
|
].join("\n");
|
|
78
89
|
}
|
|
79
90
|
|
|
80
|
-
private async
|
|
91
|
+
private async modelResult(message: InboundMessage): Promise<BotCommandResult> {
|
|
81
92
|
const sessionResult = await this.getDefaultSession(message);
|
|
82
|
-
if (!sessionResult.ok) return sessionResult.message;
|
|
93
|
+
if (!sessionResult.ok) return { handled: true, text: sessionResult.message };
|
|
83
94
|
|
|
84
95
|
const [, ...args] = message.text.trim().split(/\s+/);
|
|
85
96
|
const value = args.join(" ").trim();
|
|
86
97
|
|
|
87
98
|
if (!value) {
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
return {
|
|
100
|
+
handled: true,
|
|
101
|
+
text: [
|
|
102
|
+
`Agent: ${sessionResult.agentId}`,
|
|
103
|
+
`Session model: ${sessionResult.session.model ?? "default"}`,
|
|
104
|
+
this.modelChoices.length > 0
|
|
105
|
+
? "Choose a model:"
|
|
106
|
+
: "No model choices configured. Use /model <provider/modelId> to set manually.",
|
|
107
|
+
].join("\n"),
|
|
108
|
+
inlineKeyboard: this.modelChoices.length > 0 ? this.modelKeyboard() : undefined,
|
|
109
|
+
};
|
|
94
110
|
}
|
|
95
111
|
|
|
96
112
|
if (value === "default") {
|
|
97
|
-
|
|
98
|
-
return `Session model cleared for ${session.agentId}. Current: ${session.model ?? "default"}`;
|
|
113
|
+
return { handled: true, text: await this.clearModel(message) };
|
|
99
114
|
}
|
|
100
115
|
|
|
101
116
|
if (!isValidModelRef(value)) {
|
|
102
|
-
return "Invalid model. Use provider/modelId, for example: anthropic/claude-opus-4-5";
|
|
117
|
+
return { handled: true, text: "Invalid model. Use provider/modelId, for example: anthropic/claude-opus-4-5" };
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
const session = await this.repositories.sessions.updateModel(sessionResult.session.id, value);
|
|
106
|
-
return `Session model for ${session.agentId} set to ${session.model}
|
|
121
|
+
return { handled: true, text: `Session model for ${session.agentId} set to ${session.model}.` };
|
|
107
122
|
}
|
|
108
123
|
|
|
109
|
-
private async
|
|
124
|
+
private async thinkingResult(message: InboundMessage): Promise<BotCommandResult> {
|
|
110
125
|
const sessionResult = await this.getDefaultSession(message);
|
|
111
|
-
if (!sessionResult.ok) return sessionResult.message;
|
|
126
|
+
if (!sessionResult.ok) return { handled: true, text: sessionResult.message };
|
|
112
127
|
|
|
113
128
|
const [, rawValue] = message.text.trim().split(/\s+/, 2);
|
|
114
129
|
const value = rawValue?.trim();
|
|
115
130
|
|
|
116
131
|
if (!value) {
|
|
117
|
-
return
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
return {
|
|
133
|
+
handled: true,
|
|
134
|
+
text: [
|
|
135
|
+
`Agent: ${sessionResult.agentId}`,
|
|
136
|
+
`Session thinking level: ${sessionResult.session.thinkingLevel ?? "default"}`,
|
|
137
|
+
"Choose thinking level:",
|
|
138
|
+
].join("\n"),
|
|
139
|
+
inlineKeyboard: this.thinkingKeyboard(),
|
|
140
|
+
};
|
|
123
141
|
}
|
|
124
142
|
|
|
125
143
|
if (value === "default") {
|
|
126
|
-
|
|
127
|
-
|
|
144
|
+
return { handled: true, text: await this.clearThinking(message) };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!isThinkingLevel(value)) {
|
|
148
|
+
return { handled: true, text: "Invalid thinking level. Use one of: off, low, medium, high, default" };
|
|
128
149
|
}
|
|
129
150
|
|
|
151
|
+
const session = await this.repositories.sessions.updateThinkingLevel(sessionResult.session.id, value);
|
|
152
|
+
return { handled: true, text: `Session thinking level for ${session.agentId} set to ${session.thinkingLevel}.` };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private async setModelFromChoice(message: InboundMessage): Promise<string> {
|
|
156
|
+
const sessionResult = await this.getDefaultSession(message);
|
|
157
|
+
if (!sessionResult.ok) return sessionResult.message;
|
|
158
|
+
|
|
159
|
+
const choiceIndex = Number(message.text.slice("/model:set:".length));
|
|
160
|
+
if (!Number.isSafeInteger(choiceIndex) || choiceIndex < 0 || choiceIndex >= this.modelChoices.length) {
|
|
161
|
+
return "Unknown model choice.";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const choice = this.modelChoices[choiceIndex];
|
|
165
|
+
const session = await this.repositories.sessions.updateModel(sessionResult.session.id, choice.id);
|
|
166
|
+
return `Session model for ${session.agentId} set to ${choice.label} (${session.model}).`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private async clearModel(message: InboundMessage): Promise<string> {
|
|
170
|
+
const sessionResult = await this.getDefaultSession(message);
|
|
171
|
+
if (!sessionResult.ok) return sessionResult.message;
|
|
172
|
+
|
|
173
|
+
const session = await this.repositories.sessions.updateModel(sessionResult.session.id, null);
|
|
174
|
+
return `Session model cleared for ${session.agentId}. Current: ${session.model ?? "default"}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async setThinkingFromCallback(message: InboundMessage): Promise<string> {
|
|
178
|
+
const sessionResult = await this.getDefaultSession(message);
|
|
179
|
+
if (!sessionResult.ok) return sessionResult.message;
|
|
180
|
+
|
|
181
|
+
const value = message.text.slice("/thinking:set:".length);
|
|
130
182
|
if (!isThinkingLevel(value)) {
|
|
131
183
|
return "Invalid thinking level. Use one of: off, low, medium, high, default";
|
|
132
184
|
}
|
|
@@ -135,6 +187,41 @@ export class BotCommandHandler {
|
|
|
135
187
|
return `Session thinking level for ${session.agentId} set to ${session.thinkingLevel}.`;
|
|
136
188
|
}
|
|
137
189
|
|
|
190
|
+
private async clearThinking(message: InboundMessage): Promise<string> {
|
|
191
|
+
const sessionResult = await this.getDefaultSession(message);
|
|
192
|
+
if (!sessionResult.ok) return sessionResult.message;
|
|
193
|
+
|
|
194
|
+
const session = await this.repositories.sessions.updateThinkingLevel(sessionResult.session.id, null);
|
|
195
|
+
return `Session thinking level cleared for ${session.agentId}. Current: ${session.thinkingLevel ?? "default"}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private modelKeyboard(): InlineKeyboardButton[][] {
|
|
199
|
+
const rows = chunk(
|
|
200
|
+
this.modelChoices.map((choice, index) => ({
|
|
201
|
+
text: choice.label,
|
|
202
|
+
callbackData: `model:set:${index}`,
|
|
203
|
+
})),
|
|
204
|
+
2,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
rows.push([{ text: "Use default", callbackData: "model:default" }]);
|
|
208
|
+
return rows;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private thinkingKeyboard(): InlineKeyboardButton[][] {
|
|
212
|
+
return [
|
|
213
|
+
[
|
|
214
|
+
{ text: "Off", callbackData: "thinking:set:off" },
|
|
215
|
+
{ text: "Low", callbackData: "thinking:set:low" },
|
|
216
|
+
],
|
|
217
|
+
[
|
|
218
|
+
{ text: "Medium", callbackData: "thinking:set:medium" },
|
|
219
|
+
{ text: "High", callbackData: "thinking:set:high" },
|
|
220
|
+
],
|
|
221
|
+
[{ text: "Use default", callbackData: "thinking:default" }],
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
|
|
138
225
|
private async getDefaultSession(message: InboundMessage) {
|
|
139
226
|
if (message.channel !== "telegram") {
|
|
140
227
|
return { ok: false as const, message: "Unsupported channel." };
|
|
@@ -175,3 +262,13 @@ export class BotCommandHandler {
|
|
|
175
262
|
function isThinkingLevel(value: string): value is ThinkingLevel {
|
|
176
263
|
return THINKING_LEVELS.includes(value as ThinkingLevel);
|
|
177
264
|
}
|
|
265
|
+
|
|
266
|
+
function chunk<T>(items: T[], size: number): T[][] {
|
|
267
|
+
const rows: T[][] = [];
|
|
268
|
+
|
|
269
|
+
for (let index = 0; index < items.length; index += size) {
|
|
270
|
+
rows.push(items.slice(index, index + size));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return rows;
|
|
274
|
+
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
TelegramAnswerCallbackQueryResponse,
|
|
3
|
+
TelegramGetMeResponse,
|
|
4
|
+
TelegramGetUpdatesResponse,
|
|
5
|
+
TelegramSendMessageResponse,
|
|
6
|
+
TelegramSetMyCommandsResponse,
|
|
7
|
+
TelegramUpdate,
|
|
8
|
+
TelegramUser,
|
|
9
|
+
} from "./types";
|
|
10
|
+
import type { InlineKeyboardButton } from "../types";
|
|
2
11
|
|
|
3
12
|
export type TelegramApiOptions = {
|
|
4
13
|
token: string;
|
|
@@ -13,6 +22,11 @@ export type GetUpdatesOptions = {
|
|
|
13
22
|
limit?: number;
|
|
14
23
|
};
|
|
15
24
|
|
|
25
|
+
export type TelegramBotCommand = {
|
|
26
|
+
command: string;
|
|
27
|
+
description: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
16
30
|
type TelegramApiResponse<T> = T & {
|
|
17
31
|
ok: boolean;
|
|
18
32
|
description?: string;
|
|
@@ -43,18 +57,37 @@ export class TelegramApi {
|
|
|
43
57
|
offset: options.offset,
|
|
44
58
|
timeout: options.timeoutSeconds ?? 30,
|
|
45
59
|
limit: options.limit ?? 50,
|
|
46
|
-
allowed_updates: ["message"],
|
|
60
|
+
allowed_updates: ["message", "callback_query"],
|
|
47
61
|
});
|
|
48
62
|
|
|
49
63
|
return response.result;
|
|
50
64
|
}
|
|
51
65
|
|
|
52
|
-
async sendMessage(input: {
|
|
66
|
+
async sendMessage(input: {
|
|
67
|
+
chatId: string;
|
|
68
|
+
text: string;
|
|
69
|
+
threadId?: string | null;
|
|
70
|
+
inlineKeyboard?: InlineKeyboardButton[][];
|
|
71
|
+
}): Promise<void> {
|
|
53
72
|
await this.request<TelegramSendMessageResponse>("sendMessage", {
|
|
54
73
|
chat_id: input.chatId,
|
|
55
74
|
text: input.text,
|
|
56
75
|
message_thread_id: input.threadId ? Number(input.threadId) : undefined,
|
|
57
76
|
disable_web_page_preview: true,
|
|
77
|
+
reply_markup: input.inlineKeyboard ? toTelegramInlineKeyboard(input.inlineKeyboard) : undefined,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async answerCallbackQuery(input: { callbackQueryId: string; text?: string }): Promise<void> {
|
|
82
|
+
await this.request<TelegramAnswerCallbackQueryResponse>("answerCallbackQuery", {
|
|
83
|
+
callback_query_id: input.callbackQueryId,
|
|
84
|
+
text: input.text,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async setMyCommands(commands: TelegramBotCommand[]): Promise<void> {
|
|
89
|
+
await this.request<TelegramSetMyCommandsResponse>("setMyCommands", {
|
|
90
|
+
commands,
|
|
58
91
|
});
|
|
59
92
|
}
|
|
60
93
|
|
|
@@ -150,6 +183,17 @@ class TelegramApiError extends Error {
|
|
|
150
183
|
}
|
|
151
184
|
}
|
|
152
185
|
|
|
186
|
+
function toTelegramInlineKeyboard(inlineKeyboard: InlineKeyboardButton[][]) {
|
|
187
|
+
return {
|
|
188
|
+
inline_keyboard: inlineKeyboard.map((row) =>
|
|
189
|
+
row.map((button) => ({
|
|
190
|
+
text: button.text,
|
|
191
|
+
callback_data: button.callbackData,
|
|
192
|
+
})),
|
|
193
|
+
),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
153
197
|
async function parseTelegramPayload<T extends { ok: boolean; description?: string }>(
|
|
154
198
|
response: Response,
|
|
155
199
|
): Promise<TelegramApiResponse<T> | null> {
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import type { RuntimeKvRepository } from "../../db/repositories/RuntimeKvRepository";
|
|
2
2
|
import { logger } from "../../logging/logger";
|
|
3
3
|
import type { ChannelAdapter, InboundMessage, MessageHandler, OutboundMessage } from "../types";
|
|
4
|
-
import { TelegramApi } from "./TelegramApi";
|
|
5
|
-
import type { TelegramMessage, TelegramUpdate } from "./types";
|
|
4
|
+
import { TelegramApi, type TelegramBotCommand } from "./TelegramApi";
|
|
5
|
+
import type { TelegramCallbackQuery, TelegramMessage, TelegramUpdate } from "./types";
|
|
6
6
|
|
|
7
7
|
export type TelegramPollingAdapterOptions = {
|
|
8
8
|
api: TelegramApi;
|
|
9
9
|
runtimeKv: RuntimeKvRepository;
|
|
10
10
|
pollTimeoutSeconds?: number;
|
|
11
11
|
pollIntervalMs?: number;
|
|
12
|
+
commands?: TelegramBotCommand[];
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
const offsetKey = "telegram:update_offset";
|
|
@@ -28,6 +29,10 @@ export class TelegramPollingAdapter implements ChannelAdapter {
|
|
|
28
29
|
const botUser = await this.options.api.getMe();
|
|
29
30
|
this.botUserId = String(botUser.id);
|
|
30
31
|
|
|
32
|
+
if (this.options.commands?.length) {
|
|
33
|
+
await this.options.api.setMyCommands(this.options.commands);
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
this.running = true;
|
|
32
37
|
this.loopPromise = this.poll(handler);
|
|
33
38
|
logger.info("telegram polling started", {
|
|
@@ -54,6 +59,7 @@ export class TelegramPollingAdapter implements ChannelAdapter {
|
|
|
54
59
|
chatId: message.chatId,
|
|
55
60
|
threadId: message.threadId,
|
|
56
61
|
text: message.text,
|
|
62
|
+
inlineKeyboard: message.inlineKeyboard,
|
|
57
63
|
});
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -80,6 +86,11 @@ export class TelegramPollingAdapter implements ChannelAdapter {
|
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
private async handleUpdate(update: TelegramUpdate, handler: MessageHandler): Promise<void> {
|
|
89
|
+
if (update.callback_query) {
|
|
90
|
+
await this.handleCallbackQuery(update.update_id, update.callback_query, handler);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
83
94
|
const message = update.message;
|
|
84
95
|
if (!message?.text) return;
|
|
85
96
|
if (message.from?.is_bot) return;
|
|
@@ -89,6 +100,21 @@ export class TelegramPollingAdapter implements ChannelAdapter {
|
|
|
89
100
|
await handler(normalized);
|
|
90
101
|
}
|
|
91
102
|
|
|
103
|
+
private async handleCallbackQuery(
|
|
104
|
+
updateId: number,
|
|
105
|
+
callbackQuery: TelegramCallbackQuery,
|
|
106
|
+
handler: MessageHandler,
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
await this.options.api.answerCallbackQuery({ callbackQueryId: callbackQuery.id });
|
|
109
|
+
|
|
110
|
+
if (!callbackQuery.data || !callbackQuery.message) return;
|
|
111
|
+
if (callbackQuery.from.is_bot) return;
|
|
112
|
+
if (this.botUserId && String(callbackQuery.from.id) === this.botUserId) return;
|
|
113
|
+
|
|
114
|
+
const normalized = normalizeTelegramCallbackQuery(updateId, callbackQuery);
|
|
115
|
+
await handler(normalized);
|
|
116
|
+
}
|
|
117
|
+
|
|
92
118
|
private async loadOffset(): Promise<number | undefined> {
|
|
93
119
|
const value = await this.options.runtimeKv.get(offsetKey);
|
|
94
120
|
if (!value) return undefined;
|
|
@@ -118,6 +144,23 @@ function normalizeTelegramMessage(updateId: number, message: TelegramMessage): I
|
|
|
118
144
|
};
|
|
119
145
|
}
|
|
120
146
|
|
|
147
|
+
function normalizeTelegramCallbackQuery(updateId: number, callbackQuery: TelegramCallbackQuery): InboundMessage {
|
|
148
|
+
const message = callbackQuery.message;
|
|
149
|
+
const senderName = [callbackQuery.from.first_name, callbackQuery.from.last_name].filter(Boolean).join(" ");
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
id: String(updateId),
|
|
153
|
+
channel: "telegram",
|
|
154
|
+
chatId: String(message?.chat.id),
|
|
155
|
+
threadId: message?.message_thread_id ? String(message.message_thread_id) : null,
|
|
156
|
+
senderId: String(callbackQuery.from.id),
|
|
157
|
+
senderName: senderName || callbackQuery.from.username || null,
|
|
158
|
+
text: `/${callbackQuery.data}`,
|
|
159
|
+
raw: callbackQuery,
|
|
160
|
+
receivedAt: new Date(),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
121
164
|
function sleep(ms: number): Promise<void> {
|
|
122
165
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
123
166
|
}
|
|
@@ -25,10 +25,18 @@ export type TelegramMessage = {
|
|
|
25
25
|
text?: string;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
export type TelegramCallbackQuery = {
|
|
29
|
+
id: string;
|
|
30
|
+
from: TelegramUser;
|
|
31
|
+
message?: TelegramMessage;
|
|
32
|
+
data?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
28
35
|
export type TelegramUpdate = {
|
|
29
36
|
update_id: number;
|
|
30
37
|
message?: TelegramMessage;
|
|
31
38
|
edited_message?: TelegramMessage;
|
|
39
|
+
callback_query?: TelegramCallbackQuery;
|
|
32
40
|
};
|
|
33
41
|
|
|
34
42
|
export type TelegramGetUpdatesResponse = {
|
|
@@ -48,3 +56,15 @@ export type TelegramSendMessageResponse = {
|
|
|
48
56
|
result?: TelegramMessage;
|
|
49
57
|
description?: string;
|
|
50
58
|
};
|
|
59
|
+
|
|
60
|
+
export type TelegramAnswerCallbackQueryResponse = {
|
|
61
|
+
ok: boolean;
|
|
62
|
+
result: boolean;
|
|
63
|
+
description?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type TelegramSetMyCommandsResponse = {
|
|
67
|
+
ok: boolean;
|
|
68
|
+
result: boolean;
|
|
69
|
+
description?: string;
|
|
70
|
+
};
|
package/src/channels/types.ts
CHANGED
|
@@ -12,11 +12,17 @@ export type InboundMessage = {
|
|
|
12
12
|
receivedAt: Date;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
export type InlineKeyboardButton = {
|
|
16
|
+
text: string;
|
|
17
|
+
callbackData: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
15
20
|
export type OutboundMessage = {
|
|
16
21
|
channel: ChannelId;
|
|
17
22
|
chatId: string;
|
|
18
23
|
threadId?: string | null;
|
|
19
24
|
text: string;
|
|
25
|
+
inlineKeyboard?: InlineKeyboardButton[][];
|
|
20
26
|
};
|
|
21
27
|
|
|
22
28
|
export type MessageHandler = (message: InboundMessage) => Promise<void>;
|
package/src/config/loadConfig.ts
CHANGED
package/src/config/schemas.ts
CHANGED
|
@@ -65,6 +65,11 @@ export const ProfileConfigSchema = z.object({
|
|
|
65
65
|
}),
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
export const ModelChoiceConfigSchema = z.object({
|
|
69
|
+
id: modelReferenceSchema,
|
|
70
|
+
label: z.string().min(1),
|
|
71
|
+
});
|
|
72
|
+
|
|
68
73
|
export const TelegramChatConfigSchema = z.object({
|
|
69
74
|
chatId: z.string().min(1),
|
|
70
75
|
title: z.string().optional(),
|
|
@@ -76,6 +81,7 @@ export const TelegramChatConfigSchema = z.object({
|
|
|
76
81
|
|
|
77
82
|
export const RootConfigSchema = z.object({
|
|
78
83
|
telegramChats: z.array(TelegramChatConfigSchema).default([]),
|
|
84
|
+
modelChoices: z.array(ModelChoiceConfigSchema).default([]),
|
|
79
85
|
});
|
|
80
86
|
|
|
81
87
|
export type PermissionConfig = z.infer<typeof PermissionConfigSchema>;
|
|
@@ -83,6 +89,7 @@ export type HeartbeatConfig = z.infer<typeof HeartbeatConfigSchema>;
|
|
|
83
89
|
export type AgentConfig = z.infer<typeof AgentConfigSchema>;
|
|
84
90
|
export type ProfileConfig = z.infer<typeof ProfileConfigSchema>;
|
|
85
91
|
export type TelegramChatConfig = z.infer<typeof TelegramChatConfigSchema>;
|
|
92
|
+
export type ModelChoiceConfig = z.infer<typeof ModelChoiceConfigSchema>;
|
|
86
93
|
export type RootConfig = z.infer<typeof RootConfigSchema>;
|
|
87
94
|
|
|
88
95
|
export type LoadedAgentConfig = AgentConfig & {
|
|
@@ -94,4 +101,5 @@ export type LoadedConfig = {
|
|
|
94
101
|
agents: LoadedAgentConfig[];
|
|
95
102
|
profiles: ProfileConfig[];
|
|
96
103
|
telegramChats: TelegramChatConfig[];
|
|
104
|
+
modelChoices: ModelChoiceConfig[];
|
|
97
105
|
};
|
|
@@ -24,7 +24,7 @@ export class AgentDaemon {
|
|
|
24
24
|
) {
|
|
25
25
|
this.adapters = createAdapters(repositories);
|
|
26
26
|
this.registry = new AgentRegistry(config, repositories);
|
|
27
|
-
this.commands = new BotCommandHandler(this.registry, repositories);
|
|
27
|
+
this.commands = new BotCommandHandler(this.registry, repositories, config.modelChoices);
|
|
28
28
|
this.router = new MessageRouter(repositories, this.registry);
|
|
29
29
|
this.runner = new AgentRunner(this.registry, repositories);
|
|
30
30
|
}
|
|
@@ -92,6 +92,7 @@ export class AgentDaemon {
|
|
|
92
92
|
chatId: message.chatId,
|
|
93
93
|
threadId: message.threadId,
|
|
94
94
|
text: command.text,
|
|
95
|
+
inlineKeyboard: command.inlineKeyboard,
|
|
95
96
|
});
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
@@ -158,9 +159,20 @@ function createAdapters(repositories: Repositories): ChannelAdapter[] {
|
|
|
158
159
|
runtimeKv: repositories.runtimeKv,
|
|
159
160
|
pollTimeoutSeconds: Number(process.env.TELEGRAM_POLL_TIMEOUT_SECONDS ?? 30),
|
|
160
161
|
pollIntervalMs: Number(process.env.TELEGRAM_POLL_INTERVAL_MS ?? 1000),
|
|
162
|
+
commands: telegramCommands(),
|
|
161
163
|
}),
|
|
162
164
|
);
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
return adapters;
|
|
166
168
|
}
|
|
169
|
+
|
|
170
|
+
function telegramCommands() {
|
|
171
|
+
return [
|
|
172
|
+
{ command: "help", description: "Show Pigent commands" },
|
|
173
|
+
{ command: "agents", description: "List agents available in this chat" },
|
|
174
|
+
{ command: "model", description: "Choose model for this chat session" },
|
|
175
|
+
{ command: "thinking", description: "Choose thinking level for this chat session" },
|
|
176
|
+
{ command: "agent", description: "Send message to a specific agent" },
|
|
177
|
+
];
|
|
178
|
+
}
|