opc-agent 4.0.7 → 4.0.9

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.
@@ -1,3 +1,4 @@
1
+ import type { Message } from '../core/types';
1
2
  import { BaseChannel } from './index';
2
3
  /**
3
4
  * Telegram channel — supports both long-polling and webhook modes.
@@ -35,6 +36,10 @@ export declare class TelegramChannel extends BaseChannel {
35
36
  private startWebhook;
36
37
  private stopWebhook;
37
38
  private processUpdate;
39
+ private streamResponse;
40
+ private streamHandler?;
41
+ setStreamHandler(handler: (msg: Message) => AsyncIterable<string>): void;
42
+ sendMarkdown(chatId: number | string, text: string, replyTo?: number): Promise<any>;
38
43
  sendMessage(chatId: number | string, text: string): Promise<void>;
39
44
  private apiCall;
40
45
  private splitText;
@@ -152,12 +152,21 @@ class TelegramChannel extends index_1.BaseChannel {
152
152
  // ─── Shared ──────────────────────────────────────────────
153
153
  async processUpdate(update) {
154
154
  const message = update.message || update.edited_message;
155
- if (!message?.text || !this.handler)
155
+ if (!message || !this.handler)
156
+ return;
157
+ // Handle /start command
158
+ if (message.text === '/start') {
159
+ await this.sendMarkdown(message.chat.id, '👋 Hello! I\'m ready to help. Send me a message to get started.');
160
+ return;
161
+ }
162
+ // Handle text, photo captions, document captions
163
+ const text = message.text || message.caption;
164
+ if (!text)
156
165
  return;
157
166
  const msg = {
158
167
  id: `tg_${message.message_id}`,
159
168
  role: 'user',
160
- content: message.text,
169
+ content: text,
161
170
  timestamp: message.date * 1000,
162
171
  metadata: {
163
172
  sessionId: `tg_${message.chat.id}`,
@@ -167,21 +176,132 @@ class TelegramChannel extends index_1.BaseChannel {
167
176
  firstName: message.from?.first_name,
168
177
  platform: 'telegram',
169
178
  chatType: message.chat.type,
179
+ replyToMessageId: message.message_id,
170
180
  },
171
181
  };
172
- const response = await this.handler(msg);
173
- await this.sendMessage(message.chat.id, response.content);
182
+ // Show typing indicator
183
+ await this.apiCall('sendChatAction', { chat_id: message.chat.id, action: 'typing' });
184
+ const typingInterval = setInterval(() => {
185
+ this.apiCall('sendChatAction', { chat_id: message.chat.id, action: 'typing' }).catch(() => { });
186
+ }, 4000);
187
+ try {
188
+ // Try streaming if provider supports it
189
+ if (this.streamHandler) {
190
+ await this.streamResponse(message.chat.id, msg, message.message_id);
191
+ }
192
+ else {
193
+ const response = await this.handler(msg);
194
+ await this.sendMarkdown(message.chat.id, response.content, message.message_id);
195
+ }
196
+ }
197
+ catch (err) {
198
+ console.error('[TelegramChannel] Error processing message:', err);
199
+ await this.sendMarkdown(message.chat.id, '⚠️ Sorry, something went wrong. Please try again.');
200
+ }
201
+ finally {
202
+ clearInterval(typingInterval);
203
+ }
174
204
  }
175
- async sendMessage(chatId, text) {
176
- // Telegram max message length is 4096
205
+ async streamResponse(chatId, msg, replyTo) {
206
+ if (!this.streamHandler)
207
+ return;
208
+ let sentMessageId = null;
209
+ let fullText = '';
210
+ let lastEditTime = 0;
211
+ const EDIT_INTERVAL = 1000; // Edit at most once per second
212
+ let pendingEdit = false;
213
+ const doEdit = async () => {
214
+ if (!sentMessageId || !fullText)
215
+ return;
216
+ pendingEdit = false;
217
+ try {
218
+ await this.apiCall('editMessageText', {
219
+ chat_id: chatId,
220
+ message_id: sentMessageId,
221
+ text: fullText,
222
+ parse_mode: 'Markdown',
223
+ });
224
+ lastEditTime = Date.now();
225
+ }
226
+ catch (err) {
227
+ // If Markdown fails, try plain text
228
+ if (err?.message?.includes('parse') || err?.message?.includes('entities')) {
229
+ try {
230
+ await this.apiCall('editMessageText', {
231
+ chat_id: chatId,
232
+ message_id: sentMessageId,
233
+ text: fullText,
234
+ });
235
+ }
236
+ catch { }
237
+ }
238
+ }
239
+ };
240
+ try {
241
+ for await (const chunk of this.streamHandler(msg)) {
242
+ fullText += chunk;
243
+ if (!sentMessageId) {
244
+ // Send first message
245
+ const result = await this.sendMarkdown(chatId, fullText, replyTo);
246
+ sentMessageId = result?.message_id;
247
+ lastEditTime = Date.now();
248
+ }
249
+ else {
250
+ const now = Date.now();
251
+ if (now - lastEditTime >= EDIT_INTERVAL) {
252
+ await doEdit();
253
+ }
254
+ else if (!pendingEdit) {
255
+ pendingEdit = true;
256
+ }
257
+ }
258
+ }
259
+ // Final edit with complete text
260
+ if (sentMessageId && fullText) {
261
+ await doEdit();
262
+ }
263
+ }
264
+ catch (err) {
265
+ // If streaming fails, send what we have
266
+ if (!sentMessageId && fullText) {
267
+ await this.sendMarkdown(chatId, fullText, replyTo);
268
+ }
269
+ throw err;
270
+ }
271
+ }
272
+ // Stream handler — set by runtime when provider supports streaming
273
+ streamHandler;
274
+ setStreamHandler(handler) {
275
+ this.streamHandler = handler;
276
+ }
277
+ async sendMarkdown(chatId, text, replyTo) {
177
278
  const chunks = this.splitText(text, 4096);
279
+ let lastResult = null;
178
280
  for (const chunk of chunks) {
179
- await this.apiCall('sendMessage', {
180
- chat_id: chatId,
181
- text: chunk,
182
- parse_mode: 'Markdown',
183
- });
281
+ try {
282
+ lastResult = await this.apiCall('sendMessage', {
283
+ chat_id: chatId,
284
+ text: chunk,
285
+ parse_mode: 'Markdown',
286
+ ...(replyTo ? { reply_to_message_id: replyTo } : {}),
287
+ });
288
+ // Only reply to first chunk
289
+ replyTo = undefined;
290
+ }
291
+ catch (err) {
292
+ // Markdown parse failed — send as plain text
293
+ lastResult = await this.apiCall('sendMessage', {
294
+ chat_id: chatId,
295
+ text: chunk,
296
+ ...(replyTo ? { reply_to_message_id: replyTo } : {}),
297
+ });
298
+ replyTo = undefined;
299
+ }
184
300
  }
301
+ return lastResult?.result;
302
+ }
303
+ async sendMessage(chatId, text) {
304
+ await this.sendMarkdown(chatId, text);
185
305
  }
186
306
  async apiCall(method, body) {
187
307
  const url = `https://api.telegram.org/bot${this.token}/${method}`;
@@ -165,10 +165,21 @@ class AgentRuntime {
165
165
  this.logger.info('Bound web channel', { port });
166
166
  }
167
167
  else if (ch.type === 'telegram') {
168
- this.agent.bindChannel(new telegram_1.TelegramChannel({
168
+ const tgChannel = new telegram_1.TelegramChannel({
169
169
  token: ch.config?.token,
170
170
  port: ch.port,
171
- }));
171
+ });
172
+ // Wire up streaming for real-time message editing
173
+ const agentRef = this.agent;
174
+ tgChannel.setStreamHandler(async function* (msg) {
175
+ const history = [msg];
176
+ const provider = agentRef.provider;
177
+ const systemPrompt = agentRef.getSystemPrompt();
178
+ for await (const chunk of provider.chatStream(history, systemPrompt)) {
179
+ yield chunk;
180
+ }
181
+ });
182
+ this.agent.bindChannel(tgChannel);
172
183
  this.logger.info('Bound telegram channel');
173
184
  }
174
185
  else if (ch.type === 'websocket') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opc-agent",
3
- "version": "4.0.7",
3
+ "version": "4.0.9",
4
4
  "description": "Open Agent Framework — Build, test, and run AI Agents for business workstations",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,4 +1,5 @@
1
1
  import type { Message } from '../core/types';
2
+ import type { LLMProvider } from '../providers/index';
2
3
  import { BaseChannel } from './index';
3
4
 
4
5
  /**
@@ -152,12 +153,22 @@ export class TelegramChannel extends BaseChannel {
152
153
 
153
154
  private async processUpdate(update: any): Promise<void> {
154
155
  const message = update.message || update.edited_message;
155
- if (!message?.text || !this.handler) return;
156
+ if (!message || !this.handler) return;
157
+
158
+ // Handle /start command
159
+ if (message.text === '/start') {
160
+ await this.sendMarkdown(message.chat.id, '👋 Hello! I\'m ready to help. Send me a message to get started.');
161
+ return;
162
+ }
163
+
164
+ // Handle text, photo captions, document captions
165
+ const text = message.text || message.caption;
166
+ if (!text) return;
156
167
 
157
168
  const msg: Message = {
158
169
  id: `tg_${message.message_id}`,
159
170
  role: 'user',
160
- content: message.text,
171
+ content: text,
161
172
  timestamp: message.date * 1000,
162
173
  metadata: {
163
174
  sessionId: `tg_${message.chat.id}`,
@@ -167,23 +178,133 @@ export class TelegramChannel extends BaseChannel {
167
178
  firstName: message.from?.first_name,
168
179
  platform: 'telegram',
169
180
  chatType: message.chat.type,
181
+ replyToMessageId: message.message_id,
170
182
  },
171
183
  };
172
184
 
173
- const response = await this.handler(msg);
174
- await this.sendMessage(message.chat.id, response.content);
185
+ // Show typing indicator
186
+ await this.apiCall('sendChatAction', { chat_id: message.chat.id, action: 'typing' });
187
+ const typingInterval = setInterval(() => {
188
+ this.apiCall('sendChatAction', { chat_id: message.chat.id, action: 'typing' }).catch(() => {});
189
+ }, 4000);
190
+
191
+ try {
192
+ // Try streaming if provider supports it
193
+ if (this.streamHandler) {
194
+ await this.streamResponse(message.chat.id, msg, message.message_id);
195
+ } else {
196
+ const response = await this.handler(msg);
197
+ await this.sendMarkdown(message.chat.id, response.content, message.message_id);
198
+ }
199
+ } catch (err) {
200
+ console.error('[TelegramChannel] Error processing message:', err);
201
+ await this.sendMarkdown(message.chat.id, '⚠️ Sorry, something went wrong. Please try again.');
202
+ } finally {
203
+ clearInterval(typingInterval);
204
+ }
175
205
  }
176
206
 
177
- async sendMessage(chatId: number | string, text: string): Promise<void> {
178
- // Telegram max message length is 4096
207
+ private async streamResponse(chatId: number | string, msg: Message, replyTo?: number): Promise<void> {
208
+ if (!this.streamHandler) return;
209
+
210
+ let sentMessageId: number | null = null;
211
+ let fullText = '';
212
+ let lastEditTime = 0;
213
+ const EDIT_INTERVAL = 1000; // Edit at most once per second
214
+ let pendingEdit = false;
215
+
216
+ const doEdit = async () => {
217
+ if (!sentMessageId || !fullText) return;
218
+ pendingEdit = false;
219
+ try {
220
+ await this.apiCall('editMessageText', {
221
+ chat_id: chatId,
222
+ message_id: sentMessageId,
223
+ text: fullText,
224
+ parse_mode: 'Markdown',
225
+ });
226
+ lastEditTime = Date.now();
227
+ } catch (err: any) {
228
+ // If Markdown fails, try plain text
229
+ if (err?.message?.includes('parse') || err?.message?.includes('entities')) {
230
+ try {
231
+ await this.apiCall('editMessageText', {
232
+ chat_id: chatId,
233
+ message_id: sentMessageId,
234
+ text: fullText,
235
+ });
236
+ } catch {}
237
+ }
238
+ }
239
+ };
240
+
241
+ try {
242
+ for await (const chunk of this.streamHandler(msg)) {
243
+ fullText += chunk;
244
+
245
+ if (!sentMessageId) {
246
+ // Send first message
247
+ const result = await this.sendMarkdown(chatId, fullText, replyTo);
248
+ sentMessageId = result?.message_id;
249
+ lastEditTime = Date.now();
250
+ } else {
251
+ const now = Date.now();
252
+ if (now - lastEditTime >= EDIT_INTERVAL) {
253
+ await doEdit();
254
+ } else if (!pendingEdit) {
255
+ pendingEdit = true;
256
+ }
257
+ }
258
+ }
259
+
260
+ // Final edit with complete text
261
+ if (sentMessageId && fullText) {
262
+ await doEdit();
263
+ }
264
+ } catch (err) {
265
+ // If streaming fails, send what we have
266
+ if (!sentMessageId && fullText) {
267
+ await this.sendMarkdown(chatId, fullText, replyTo);
268
+ }
269
+ throw err;
270
+ }
271
+ }
272
+
273
+ // Stream handler — set by runtime when provider supports streaming
274
+ private streamHandler?: (msg: Message) => AsyncIterable<string>;
275
+
276
+ setStreamHandler(handler: (msg: Message) => AsyncIterable<string>): void {
277
+ this.streamHandler = handler;
278
+ }
279
+
280
+ async sendMarkdown(chatId: number | string, text: string, replyTo?: number): Promise<any> {
179
281
  const chunks = this.splitText(text, 4096);
282
+ let lastResult: any = null;
180
283
  for (const chunk of chunks) {
181
- await this.apiCall('sendMessage', {
182
- chat_id: chatId,
183
- text: chunk,
184
- parse_mode: 'Markdown',
185
- });
284
+ try {
285
+ lastResult = await this.apiCall('sendMessage', {
286
+ chat_id: chatId,
287
+ text: chunk,
288
+ parse_mode: 'Markdown',
289
+ ...(replyTo ? { reply_to_message_id: replyTo } : {}),
290
+ });
291
+ // Only reply to first chunk
292
+ replyTo = undefined;
293
+ } catch (err: any) {
294
+ // Markdown parse failed — send as plain text
295
+ lastResult = await this.apiCall('sendMessage', {
296
+ chat_id: chatId,
297
+ text: chunk,
298
+ ...(replyTo ? { reply_to_message_id: replyTo } : {}),
299
+ });
300
+ replyTo = undefined;
301
+ }
186
302
  }
303
+ return lastResult?.result;
304
+ }
305
+
306
+ async sendMessage(chatId: number | string, text: string): Promise<void> {
307
+ await this.sendMarkdown(chatId, text);
187
308
  }
188
309
 
189
310
  private async apiCall(method: string, body?: Record<string, unknown>): Promise<any> {
@@ -136,10 +136,21 @@ export class AgentRuntime {
136
136
  this.agent.bindChannel(webChannel);
137
137
  this.logger.info('Bound web channel', { port });
138
138
  } else if (ch.type === 'telegram') {
139
- this.agent.bindChannel(new TelegramChannel({
139
+ const tgChannel = new TelegramChannel({
140
140
  token: ch.config?.token as string,
141
141
  port: ch.port,
142
- }));
142
+ });
143
+ // Wire up streaming for real-time message editing
144
+ const agentRef = this.agent;
145
+ tgChannel.setStreamHandler(async function* (msg) {
146
+ const history = [msg];
147
+ const provider = agentRef.provider;
148
+ const systemPrompt = agentRef.getSystemPrompt();
149
+ for await (const chunk of provider.chatStream(history, systemPrompt)) {
150
+ yield chunk;
151
+ }
152
+ });
153
+ this.agent.bindChannel(tgChannel);
143
154
  this.logger.info('Bound telegram channel');
144
155
  } else if (ch.type === 'websocket') {
145
156
  this.agent.bindChannel(new WebSocketChannel(ch.port ?? 3002));