@microsoft/m365agentsplayground-cli 0.2.26 → 0.2.27

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.
Files changed (56) hide show
  1. package/README.md +8 -21
  2. package/{build → dist}/conversationTypes.d.ts +5 -0
  3. package/{build → dist}/index.d.ts +1 -1
  4. package/dist/index.js +8 -0
  5. package/dist/index.js.LICENSE.txt +157 -0
  6. package/dist/responseCapture.d.ts +42 -0
  7. package/{build → dist}/serverManager.d.ts +1 -2
  8. package/dist/start-server.js +9 -0
  9. package/dist/start-server.js.LICENSE.txt +157 -0
  10. package/{build → dist}/testClient.d.ts +1 -1
  11. package/{build → dist}/types.d.ts +9 -0
  12. package/package.json +22 -12
  13. package/build/cardValidator.d.ts.map +0 -1
  14. package/build/cardValidator.js +0 -46
  15. package/build/conversationServer.d.ts.map +0 -1
  16. package/build/conversationServer.js +0 -136
  17. package/build/conversationTypes.d.ts.map +0 -1
  18. package/build/conversationTypes.js +0 -5
  19. package/build/index.d.ts.map +0 -1
  20. package/build/index.js +0 -25
  21. package/build/notificationSender.d.ts.map +0 -1
  22. package/build/notificationSender.js +0 -83
  23. package/build/responseCapture.d.ts +0 -29
  24. package/build/responseCapture.d.ts.map +0 -1
  25. package/build/responseCapture.js +0 -119
  26. package/build/runConversation.d.ts.map +0 -1
  27. package/build/runConversation.js +0 -351
  28. package/build/serverManager.d.ts.map +0 -1
  29. package/build/serverManager.js +0 -149
  30. package/build/start-server.d.ts.map +0 -1
  31. package/build/start-server.js +0 -23
  32. package/build/testClient.d.ts.map +0 -1
  33. package/build/testClient.js +0 -434
  34. package/build/types.d.ts.map +0 -1
  35. package/build/types.js +0 -7
  36. package/build/websocketClient.d.ts.map +0 -1
  37. package/build/websocketClient.js +0 -129
  38. package/src/cardValidator.ts +0 -56
  39. package/src/conversationServer.ts +0 -147
  40. package/src/conversationTypes.ts +0 -157
  41. package/src/index.ts +0 -37
  42. package/src/notificationSender.ts +0 -103
  43. package/src/responseCapture.ts +0 -145
  44. package/src/runConversation.ts +0 -382
  45. package/src/serverManager.ts +0 -172
  46. package/src/start-server.ts +0 -26
  47. package/src/testClient.ts +0 -512
  48. package/src/types.ts +0 -155
  49. package/src/websocketClient.ts +0 -153
  50. package/tsconfig.json +0 -14
  51. /package/{build → dist}/cardValidator.d.ts +0 -0
  52. /package/{build → dist}/conversationServer.d.ts +0 -0
  53. /package/{build → dist}/notificationSender.d.ts +0 -0
  54. /package/{build → dist}/runConversation.d.ts +0 -0
  55. /package/{build → dist}/start-server.d.ts +0 -0
  56. /package/{build → dist}/websocketClient.d.ts +0 -0
package/src/testClient.ts DELETED
@@ -1,512 +0,0 @@
1
- import { EventEmitter } from "events";
2
- import { ActionType, ICreateMessageRequest, LogActionType, IUpdateMessageAction } from "schema";
3
- import { Factory, Message } from "server";
4
- import { ResponseCapture } from "./responseCapture";
5
- import { ServerManager } from "./serverManager";
6
- import { BotResponse, TestClientConfig } from "./types";
7
- import { LogWebSocketClient, WebSocketClient } from "./websocketClient";
8
-
9
- /**
10
- * Test client for sending messages to a bot and receiving responses.
11
- * Designed for testing multi-turn conversations.
12
- *
13
- * @example
14
- * ```typescript
15
- * const client = new TestClient({
16
- * botEndpoint: 'http://localhost:3978/api/messages',
17
- * });
18
- *
19
- * await client.start();
20
- *
21
- * // Multi-turn conversation
22
- * const [r1] = await client.sendMessage('Hello');
23
- * expect(r1.text).to.include('Hi');
24
- *
25
- * const [r2] = await client.sendMessage('Book a flight');
26
- * expect(r2.text).to.include('Where');
27
- *
28
- * await client.stop();
29
- * ```
30
- *
31
- * @example
32
- * ```typescript
33
- * // With WebSocket events
34
- * const client = new TestClient({
35
- * botEndpoint: 'http://localhost:3978/api/messages',
36
- * });
37
- *
38
- * client.on('message:created', (event) => {
39
- * console.log('New message:', event.message.text);
40
- * });
41
- *
42
- * client.on('typing', (event) => {
43
- * console.log('Bot is typing...');
44
- * });
45
- *
46
- * await client.start();
47
- * await client.sendMessage('Hello');
48
- * await client.stop();
49
- * ```
50
- *
51
- * @example
52
- * ```typescript
53
- * // With Log WebSocket events
54
- * const client = new TestClient({
55
- * botEndpoint: 'http://localhost:3978/api/messages',
56
- * });
57
- *
58
- * client.on('log:append', (event) => {
59
- * console.log('Log entry:', event.logItem);
60
- * });
61
- *
62
- * await client.start();
63
- * await client.sendMessage('Hello');
64
- * await client.stop();
65
- * ```
66
- */
67
- export class TestClient extends EventEmitter {
68
- /** The placeholder text teams-ai sends as the first streaming activity */
69
- static readonly STREAMING_PLACEHOLDER = "Loading stream results...";
70
-
71
- private serverManager: ServerManager;
72
- private responseCapture: ResponseCapture;
73
- private wsClient: WebSocketClient | null = null;
74
- private logWsClient: LogWebSocketClient | null = null;
75
- private config: TestClientConfig;
76
- private conversationId = "";
77
- private userId = "";
78
- private started = false;
79
-
80
- constructor(config: TestClientConfig) {
81
- super();
82
- this.config = {
83
- timeout: 5000,
84
- ...config,
85
- };
86
- this.serverManager = new ServerManager();
87
- this.responseCapture = ServerManager.getResponseCapture();
88
- }
89
-
90
- /**
91
- * Start the test client and server
92
- */
93
- async start(): Promise<void> {
94
- if (this.started) {
95
- throw new Error("TestClient is already started");
96
- }
97
-
98
- await this.serverManager.start(this.config);
99
-
100
- // Create a unique conversation for this client
101
- this.registerUniqueConversation();
102
-
103
- // Connect to WebSocket for real-time events
104
- this.wsClient = new WebSocketClient({
105
- port: this.serverManager.port,
106
- onMessage: (action) => {
107
- switch (action.action) {
108
- case ActionType.CreateMessage:
109
- this.emit("message:created", action);
110
- break;
111
- case ActionType.UpdateMessage:
112
- this.emit("message:updated", action);
113
- break;
114
- case ActionType.Typing:
115
- this.emit("typing", action);
116
- break;
117
- }
118
- },
119
- onError: (err) => {
120
- this.emit("websocket:error", err);
121
- },
122
- onClose: () => {
123
- this.emit("websocket:close");
124
- },
125
- });
126
-
127
- await this.wsClient.connect();
128
-
129
- // Connect to Log WebSocket for log events
130
- this.logWsClient = new LogWebSocketClient({
131
- port: this.serverManager.port,
132
- onLogMessage: (action) => {
133
- if (action.logAction === LogActionType.AppendLog) {
134
- this.emit("log:append", action);
135
- }
136
- },
137
- onError: (err) => {
138
- this.emit("log:websocket:error", err);
139
- },
140
- onClose: () => {
141
- this.emit("log:websocket:close");
142
- },
143
- });
144
-
145
- await this.logWsClient.connect();
146
-
147
- this.started = true;
148
- }
149
-
150
- /**
151
- * Stop the test client and server
152
- */
153
- async stop(): Promise<void> {
154
- if (!this.started) {
155
- return;
156
- }
157
-
158
- // Close WebSocket connections
159
- if (this.wsClient) {
160
- this.wsClient.close();
161
- this.wsClient = null;
162
- }
163
-
164
- if (this.logWsClient) {
165
- this.logWsClient.close();
166
- this.logWsClient = null;
167
- }
168
-
169
- this.responseCapture.clearConversation(this.conversationId);
170
- await this.serverManager.stop();
171
- this.started = false;
172
- }
173
-
174
- /**
175
- * Send a message to the bot and wait for response(s)
176
- *
177
- * @param text The message text to send
178
- * @returns Array of bot responses
179
- */
180
- async sendMessage(text: string): Promise<BotResponse[]> {
181
- if (!this.started) {
182
- throw new Error("TestClient is not started. Call start() first.");
183
- }
184
-
185
- const conversationManager = Factory.getConversationManager();
186
- const messageService = Factory.getMessageService();
187
-
188
- // Get the conversation
189
- const conversation = conversationManager.getConversation(this.conversationId);
190
-
191
- // Track message updates received via WebSocket (for streaming bots).
192
- const messageUpdates = new Map<string, string>(); // messageId → latest text
193
- let streamFinalReceived = false; // set when streamType:"final" arrives
194
- let lastUpdateTime = Date.now(); // epoch ms of last update (or conversation start)
195
- const updateListener = (action: IUpdateMessageAction) => {
196
- const id = action.message?.messageId;
197
- const msg = action.message as { text?: string; streamEntity?: { streamType?: string } };
198
- if (id && msg.text !== undefined) {
199
- messageUpdates.set(id, msg.text);
200
- lastUpdateTime = Date.now();
201
- if (msg.streamEntity?.streamType === "final") {
202
- streamFinalReceived = true;
203
- }
204
- }
205
- };
206
- this.on("message:updated", updateListener);
207
-
208
- // Create the message request
209
- const messagePayload: ICreateMessageRequest = {
210
- text,
211
- textFormat: "plain",
212
- from: { id: this.userId },
213
- };
214
-
215
- // Start waiting for response before sending (to avoid race conditions)
216
- const responsePromise = this.responseCapture.waitForResponse(
217
- this.conversationId,
218
- conversationManager,
219
- this.config.timeout ?? 5000
220
- );
221
-
222
- // Send the message
223
- const { afterAll } = messageService.createMessage(conversation, messagePayload);
224
-
225
- // Execute the async send
226
- if (afterAll) {
227
- afterAll();
228
- }
229
-
230
- // Wait for the first bot response (may be a streaming placeholder)
231
- await responsePromise;
232
-
233
- // Get the initial bot messages to check for streaming placeholder
234
- const allMessages = conversationManager.listMessages(this.conversationId);
235
- const lastUserIdx = allMessages.reduce(
236
- (lastIdx, msg, idx) => (msg.createdBy === "client" ? idx : lastIdx),
237
- -1
238
- );
239
- const initialBotMessages = allMessages
240
- .slice(lastUserIdx + 1)
241
- .filter((m) => m.createdBy === "bot");
242
-
243
- const isStreamingPlaceholder = initialBotMessages.some(
244
- (m) => m.content.text === TestClient.STREAMING_PLACEHOLDER
245
- );
246
-
247
- if (isStreamingPlaceholder) {
248
- // Streaming bot detected.
249
- // Primary signal: wait for streamType:"final" WS event (teams-ai / teams.ts SDK).
250
- // Fallback: quiet-period in case the bot doesn't send streamType at all.
251
- const quietPeriodMs = this.config.streamingSettleDelayMs ?? 800;
252
- const maxWaitMs = this.config.timeout ?? 30000;
253
- await this.waitForStreamFinal(
254
- () => streamFinalReceived,
255
- () => lastUpdateTime,
256
- quietPeriodMs,
257
- maxWaitMs
258
- );
259
- }
260
-
261
- this.off("message:updated", updateListener);
262
-
263
- // Re-read messages after any streaming updates have landed
264
- const messages = conversationManager.listMessages(this.conversationId);
265
- const lastUserMessageIndex = messages.reduce((lastIndex, msg, index) => {
266
- if (msg.createdBy === "client") {
267
- return index;
268
- }
269
- return lastIndex;
270
- }, -1);
271
-
272
- const recentBotMessages = messages
273
- .slice(lastUserMessageIndex + 1)
274
- .filter((m) => m.createdBy === "bot");
275
-
276
- return recentBotMessages.map((msg) => {
277
- const response = this.toBotResponse(msg);
278
- // If this message was updated via streaming, use the final streamed text
279
- const streamedText = messageUpdates.get(response.messageId);
280
- if (streamedText !== undefined) {
281
- response.text = streamedText;
282
- }
283
- return response;
284
- });
285
- }
286
-
287
- /**
288
- * Wait for streaming to complete.
289
- *
290
- * Primary: resolves immediately when `streamType:"final"` WS event arrives.
291
- * Fallback: resolves after `quietPeriodMs` of no new updateActivity events,
292
- * for bots that don't send streamType (e.g. non-teams-ai streaming bots).
293
- * Hard cap: always resolves after `maxWaitMs`.
294
- */
295
- private async waitForStreamFinal(
296
- isFinalReceived: () => boolean,
297
- getLastUpdateTime: () => number,
298
- quietPeriodMs: number,
299
- maxWaitMs: number
300
- ): Promise<void> {
301
- return new Promise<void>((resolve) => {
302
- const deadline = Date.now() + maxWaitMs;
303
-
304
- const poll = () => {
305
- // Primary signal: streamType:"final" received
306
- if (isFinalReceived()) {
307
- resolve();
308
- return;
309
- }
310
-
311
- const now = Date.now();
312
- if (now >= deadline) {
313
- resolve();
314
- return;
315
- }
316
-
317
- const lastUpdate = getLastUpdateTime();
318
- // Fallback: quiet period after last update
319
- if (lastUpdate > 0 && now - lastUpdate >= quietPeriodMs) {
320
- resolve();
321
- return;
322
- }
323
-
324
- setTimeout(poll, 50);
325
- };
326
-
327
- setTimeout(poll, 50);
328
- });
329
- }
330
-
331
- /**
332
- * Start a new conversation (resets conversation state)
333
- */
334
- newConversation(): void {
335
- if (!this.started) {
336
- throw new Error("TestClient is not started. Call start() first.");
337
- }
338
-
339
- // Clear pending responses for the old conversation
340
- this.responseCapture.clearConversation(this.conversationId);
341
-
342
- // Create a fresh unique conversation
343
- this.registerUniqueConversation();
344
- }
345
-
346
- /**
347
- * Get all messages in the current conversation
348
- */
349
- getMessages(): Message[] {
350
- if (!this.started) {
351
- throw new Error("TestClient is not started. Call start() first.");
352
- }
353
-
354
- const conversationManager = Factory.getConversationManager();
355
- return conversationManager.listMessages(this.conversationId);
356
- }
357
-
358
- /**
359
- * Get the last bot message in the current conversation
360
- */
361
- getLastBotMessage(): Message | undefined {
362
- const messages = this.getMessages();
363
- const botMessages = messages.filter((m) => m.createdBy === "bot");
364
- return botMessages[botMessages.length - 1];
365
- }
366
-
367
- /**
368
- * Get the current conversation ID
369
- */
370
- getConversationId(): string {
371
- return this.conversationId;
372
- }
373
-
374
- /**
375
- * Get the port the test server is running on
376
- */
377
- getPort(): number {
378
- return this.serverManager.port;
379
- }
380
-
381
- /**
382
- * Simulate clicking an Adaptive Card button (Action.Execute).
383
- *
384
- * @param messageId The message ID of the card message (from BotResponse.messageId)
385
- * @param verb The Action.Execute verb defined on the button
386
- * @param data Optional extra data to merge with the button's own data
387
- * @returns The invoke response body from the bot
388
- */
389
- async clickCardButton(
390
- messageId: string,
391
- verb: string,
392
- data: Record<string, unknown> = {}
393
- ): Promise<unknown> {
394
- if (!this.started) {
395
- throw new Error("TestClient is not started. Call start() first.");
396
- }
397
-
398
- const port = this.serverManager.port;
399
- const meId = Factory.getTenantManager().getMe().id;
400
- const body = {
401
- name: "adaptiveCard/action",
402
- conversation: { id: this.conversationId },
403
- from: { id: meId },
404
- replyToId: messageId,
405
- value: {
406
- action: {
407
- type: "Action.Execute",
408
- verb,
409
- data,
410
- },
411
- },
412
- };
413
-
414
- const res = await fetch(`http://localhost:${port}/_conversation/v1/invoke`, {
415
- method: "POST",
416
- headers: { "Content-Type": "application/json" },
417
- body: JSON.stringify(body),
418
- });
419
-
420
- if (!res.ok) {
421
- throw new Error(`Card action invoke failed: HTTP ${res.status}`);
422
- }
423
-
424
- return res.json();
425
- }
426
-
427
- /**
428
- * Simulate submitting an Adaptive Card form (Action.Submit / legacy bots).
429
- *
430
- * @param messageId The message ID of the card message
431
- * @param data The form data to submit
432
- */
433
- async submitCardForm(messageId: string, data: Record<string, unknown>): Promise<unknown> {
434
- if (!this.started) {
435
- throw new Error("TestClient is not started. Call start() first.");
436
- }
437
-
438
- const port = this.serverManager.port;
439
- const meId = Factory.getTenantManager().getMe().id;
440
- const body = {
441
- name: "message/submitAction",
442
- conversation: { id: this.conversationId },
443
- from: { id: meId },
444
- replyToId: messageId,
445
- value: data,
446
- };
447
-
448
- const res = await fetch(`http://localhost:${port}/_conversation/v1/invoke`, {
449
- method: "POST",
450
- headers: { "Content-Type": "application/json" },
451
- body: JSON.stringify(body),
452
- });
453
-
454
- if (!res.ok) {
455
- throw new Error(`Card form submit failed: HTTP ${res.status}`);
456
- }
457
-
458
- return res.json();
459
- }
460
-
461
- /**
462
- * Create a unique conversation for this client based on chatType config.
463
- * - personal: creates a new personal chat with a unique userId
464
- * - group: creates a new group chat
465
- * - channel: uses the team's general channel
466
- */
467
- private registerUniqueConversation(): void {
468
- const chatType = this.config.chatType ?? "personal";
469
- const chatManager = Factory.getChatManager();
470
- const conversationManager = Factory.getConversationManager();
471
-
472
- if (chatType === "group") {
473
- const chat = chatManager.createGroupChat(`test-group-${Date.now()}`);
474
- const conversation = conversationManager.createConversation(chat);
475
- this.conversationId = conversation.id;
476
- this.userId = Factory.getTenantManager().getMe().id;
477
- } else if (chatType === "channel") {
478
- const teamManager = Factory.getTeamManager();
479
- const channel = teamManager.getGeneralChannel();
480
- const conversation = conversationManager.createConversation(channel);
481
- this.conversationId = conversation.id;
482
- this.userId = Factory.getTenantManager().getMe().id;
483
- } else {
484
- this.userId = `test-user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
485
- const chat = chatManager.createPersonalChat(this.userId, "Test Chat");
486
- const conversation = conversationManager.createConversation(chat);
487
- this.conversationId = conversation.id;
488
-
489
- // Replace the fake userId in the chat's user set with the real "me" user so that:
490
- // 1. isOtherUserChat() returns false → WebSocket events are broadcast to this client
491
- // 2. getChatUsers() doesn't throw when looking up the fake userId in the tenant
492
- // Using 'as any' is intentional — ChatManager has no public addUserToChat API and
493
- // this is simulator-only setup code, not production logic.
494
- const meId = Factory.getTenantManager().getMe().id;
495
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
496
- (chatManager as any)._chatUsers.set(chat.id, new Set([meId]));
497
- }
498
- }
499
-
500
- /**
501
- * Convert a Message to a BotResponse
502
- */
503
- private toBotResponse(message: Message): BotResponse {
504
- return {
505
- messageId: message.content.id,
506
- text: message.content.text,
507
- attachments: message.content.attachments,
508
- timestamp: message.content.timestamp.getTime(),
509
- raw: message,
510
- };
511
- }
512
- }
package/src/types.ts DELETED
@@ -1,155 +0,0 @@
1
- import { AccountRole } from "schema";
2
- import { Message } from "server";
3
- import type { Attachment } from "server";
4
-
5
- // Re-export action types for consumers
6
- export { ActionType, LogActionType } from "schema";
7
- export type {
8
- IAction,
9
- ICreateMessageAction,
10
- IUpdateMessageAction,
11
- ITypingAction,
12
- IActionMessage,
13
- // Log action types
14
- ILogAction,
15
- IAppendLogAction,
16
- LogItem,
17
- } from "schema";
18
-
19
- /**
20
- * Bot configuration matching the .m365agentsplayground.yml format
21
- */
22
- export interface BotConfig {
23
- /**
24
- * Bot's unique identifier
25
- */
26
- id: string;
27
-
28
- /**
29
- * Bot display name (max 42 characters)
30
- */
31
- name: string;
32
-
33
- /**
34
- * User ID for agentic-role bots
35
- */
36
- agenticUserId?: string;
37
-
38
- /**
39
- * App ID for agentic-role bots
40
- */
41
- agenticAppId?: string;
42
-
43
- /**
44
- * Bot's tenant ID (defaults to root tenant)
45
- */
46
- tenantId?: string;
47
-
48
- /**
49
- * Bot role: "user" | "bot" | "agenticUser"
50
- */
51
- role?: AccountRole;
52
- }
53
-
54
- /**
55
- * Configuration for the TestClient
56
- */
57
- export interface TestClientConfig {
58
- /**
59
- * The bot endpoint URL (e.g., "http://localhost:3978/api/messages")
60
- */
61
- botEndpoint: string;
62
-
63
- /**
64
- * Timeout in milliseconds for waiting for bot responses.
65
- * Default: 5000
66
- */
67
- timeout?: number;
68
-
69
- /**
70
- * Port to run the test server on.
71
- * Default: auto-assigned
72
- */
73
- port?: number;
74
-
75
- /**
76
- * Bot configuration (id, name, role, etc.)
77
- * If not provided, uses default bot config.
78
- */
79
- bot?: BotConfig;
80
-
81
- /**
82
- * Chat type to use when sending messages.
83
- * - "personal": 1:1 personal chat with the bot (default)
84
- * - "group": group chat context
85
- * - "channel": team channel context
86
- */
87
- chatType?: "personal" | "group" | "channel";
88
-
89
- /**
90
- * Delivery mode for activities sent to the bot.
91
- * - "expectReplies": Bot responses come inline in the HTTP response.
92
- * - "default": Bot posts responses back to the connector URL.
93
- */
94
- deliveryMode?: "expectReplies" | "default";
95
-
96
- /**
97
- * Quiet-period fallback (ms) for streaming bot responses.
98
- *
99
- * When the simulator detects a streaming placeholder (`"Loading stream results..."`),
100
- * it first waits for a `streamType:"final"` WebSocket event (the precise end-of-stream
101
- * signal sent by teams-ai / teams.ts SDK). If no `streamType:"final"` arrives, it falls
102
- * back to a quiet-period: resolves after this many milliseconds with no new
103
- * `updateActivity` events.
104
- *
105
- * Default: 800 ms. Has no effect for bots with `stream: false`.
106
- *
107
- * @example
108
- * // Increase for slow LLMs
109
- * const client = new TestClient({
110
- * botEndpoint: "http://localhost:3978/api/messages",
111
- * streamingSettleDelayMs: 2000,
112
- * });
113
- */
114
- streamingSettleDelayMs?: number;
115
- }
116
-
117
- /**
118
- * A response from the bot
119
- */
120
- export interface BotResponse {
121
- /**
122
- * The unique message ID
123
- */
124
- messageId: string;
125
-
126
- /**
127
- * The text content of the response
128
- */
129
- text?: string;
130
-
131
- /**
132
- * Attachments in the response (e.g., Adaptive Cards)
133
- */
134
- attachments?: Attachment[];
135
-
136
- /**
137
- * Timestamp when the message was created
138
- */
139
- timestamp: number;
140
-
141
- /**
142
- * The raw Message object from ConversationManager
143
- */
144
- raw: Message;
145
- }
146
-
147
- /**
148
- * Promise handlers for async response waiting
149
- */
150
- export interface PendingResponse {
151
- resolve: (messages: Message[]) => void;
152
- reject: (error: Error) => void;
153
- timer: NodeJS.Timeout;
154
- messageCountBefore: number;
155
- }