@microsoft/m365agentsplayground-cli 0.2.26 → 0.2.27-alpha.20260518-462fbee.0

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
@@ -1,147 +0,0 @@
1
- /**
2
- * HTTP server for multi-turn conversation execution.
3
- *
4
- * Provides a factory function that creates an HTTP server with:
5
- * POST /run-conversation — execute a multi-turn conversation
6
- * GET /health — health check
7
- *
8
- * All diagnostic logging goes to stderr. The returned port can be
9
- * written to stdout by the caller (CLI wrapper).
10
- */
11
-
12
- import * as http from "http";
13
- import { runConversation, log, logError } from "./runConversation";
14
- import type { E2EConfig, ConversationInput } from "./conversationTypes";
15
-
16
- // ─────────────────────────────────────────────────────────────────────────────
17
- // Public types
18
- // ─────────────────────────────────────────────────────────────────────────────
19
-
20
- export interface ConversationServerOptions {
21
- /** Port to listen on. Default: 0 (OS-assigned). */
22
- port?: number;
23
- /** Host to bind to. Default: "127.0.0.1". */
24
- host?: string;
25
- }
26
-
27
- export interface ConversationServer {
28
- /** The port the server is listening on. */
29
- port: number;
30
- /** Gracefully shut down the server. */
31
- close: () => Promise<void>;
32
- }
33
-
34
- // ─────────────────────────────────────────────────────────────────────────────
35
- // Request body parsing
36
- // ─────────────────────────────────────────────────────────────────────────────
37
-
38
- function readBody(req: http.IncomingMessage): Promise<string> {
39
- return new Promise((resolve, reject) => {
40
- const chunks: Buffer[] = [];
41
- req.on("data", (chunk: Buffer) => chunks.push(chunk));
42
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
43
- req.on("error", reject);
44
- });
45
- }
46
-
47
- // ─────────────────────────────────────────────────────────────────────────────
48
- // Request handler
49
- // ─────────────────────────────────────────────────────────────────────────────
50
-
51
- interface RunConversationRequest {
52
- config: E2EConfig;
53
- scenario: string;
54
- input: ConversationInput;
55
- }
56
-
57
- async function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
58
- const url = req.url ?? "";
59
- const method = req.method ?? "";
60
-
61
- if (method === "GET" && url === "/health") {
62
- res.writeHead(200, { "Content-Type": "application/json" });
63
- res.end(JSON.stringify({ status: "ok" }));
64
- return;
65
- }
66
-
67
- if (method === "POST" && url === "/run-conversation") {
68
- let body: RunConversationRequest;
69
- try {
70
- const raw = await readBody(req);
71
- body = JSON.parse(raw) as RunConversationRequest;
72
- } catch (err) {
73
- const message = err instanceof Error ? err.message : String(err);
74
- res.writeHead(400, { "Content-Type": "application/json" });
75
- res.end(JSON.stringify({ error: `Invalid JSON: ${message}` }));
76
- return;
77
- }
78
-
79
- if (!body.config || !body.scenario || !body.input) {
80
- res.writeHead(400, { "Content-Type": "application/json" });
81
- res.end(JSON.stringify({ error: "Missing required fields: config, scenario, input" }));
82
- return;
83
- }
84
-
85
- try {
86
- log(`HTTP request: ${body.scenario}`);
87
- const result = await runConversation(body.config, body.scenario, body.input);
88
-
89
- res.writeHead(200, { "Content-Type": "application/json" });
90
- res.end(JSON.stringify(result));
91
- } catch (err) {
92
- const message = err instanceof Error ? err.message : String(err);
93
- logError(`Unhandled error in runConversation: ${message}`);
94
- res.writeHead(500, { "Content-Type": "application/json" });
95
- res.end(JSON.stringify({ error: message }));
96
- }
97
- return;
98
- }
99
-
100
- res.writeHead(404, { "Content-Type": "application/json" });
101
- res.end(JSON.stringify({ error: "Not found" }));
102
- }
103
-
104
- // ─────────────────────────────────────────────────────────────────────────────
105
- // Factory
106
- // ─────────────────────────────────────────────────────────────────────────────
107
-
108
- /**
109
- * Create and start a conversation server.
110
- *
111
- * Returns a handle with the assigned port and a close() method.
112
- */
113
- export async function createConversationServer(
114
- options?: ConversationServerOptions
115
- ): Promise<ConversationServer> {
116
- const port = options?.port ?? 0;
117
- const host = options?.host ?? "127.0.0.1";
118
-
119
- const server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
120
- handleRequest(req, res).catch((err) => {
121
- logError(`Request handler crashed: ${String(err)}`);
122
- if (!res.headersSent) {
123
- res.writeHead(500, { "Content-Type": "application/json" });
124
- }
125
- res.end(JSON.stringify({ error: "Internal server error" }));
126
- });
127
- });
128
-
129
- return new Promise<ConversationServer>((resolve, reject) => {
130
- server.once("error", reject);
131
-
132
- server.listen(port, host, () => {
133
- const addr = server.address();
134
- const assignedPort = typeof addr === "object" && addr ? addr.port : port;
135
-
136
- log(`Server listening on http://${host}:${assignedPort}`);
137
-
138
- resolve({
139
- port: assignedPort,
140
- close: () =>
141
- new Promise<void>((res, rej) => {
142
- server.close((err: Error | undefined) => (err ? rej(err) : res()));
143
- }),
144
- });
145
- });
146
- });
147
- }
@@ -1,157 +0,0 @@
1
- /**
2
- * Type definitions for multi-turn conversation execution.
3
- */
4
-
5
- import type { BotConfig } from "./types";
6
- import type { CustomActivityTemplateType } from "schema";
7
- import type { CardValidationError } from "./cardValidator";
8
-
9
- /**
10
- * Identity for a user persona in notification activities.
11
- */
12
- export interface PersonaConfig {
13
- id: string;
14
- name: string;
15
- email?: string;
16
- }
17
-
18
- /**
19
- * Server configuration passed per /run-conversation request.
20
- */
21
- export interface E2EConfig {
22
- /** Bot endpoint URL (e.g., "http://localhost:3978/api/messages") */
23
- botEndpoint: string;
24
-
25
- /** Response timeout in milliseconds (default: 120000) */
26
- timeout?: number;
27
-
28
- /** Activity delivery mode (default: "expectReplies") */
29
- deliveryMode?: "expectReplies" | "default";
30
-
31
- /** Bot identity configuration */
32
- bot?: BotConfig;
33
-
34
- /** Named personas for notification activities */
35
- personas?: Record<string, PersonaConfig>;
36
-
37
- /**
38
- * Default chat context for all "chat" turns in this conversation.
39
- * Can be overridden per-turn via turn.chat_type.
40
- * Default: "personal"
41
- */
42
- chatType?: "personal" | "group" | "channel";
43
-
44
- /**
45
- * Quiet-period fallback (ms) for streaming responses.
46
- * Primary: resolves on streamType:"final" event (teams-ai / teams.ts SDK).
47
- * Fallback: resolves after this many ms with no new updateActivity calls.
48
- * Default: 800. Has no effect for bots with stream: false.
49
- */
50
- streamingSettleDelayMs?: number;
51
- }
52
-
53
- /**
54
- * A single turn to execute in a conversation.
55
- */
56
- export interface Turn {
57
- /** Unique identifier for this turn */
58
- test_id: string;
59
-
60
- /** Message text or notification body to send */
61
- prompt: string;
62
-
63
- /**
64
- * Activity type. Default: "chat" (standard message).
65
- * Any CustomActivityTemplateType value (e.g., "sendEmail", "mentionInWord")
66
- * sends the corresponding notification activity.
67
- */
68
- turn_type?: "chat" | "card_action" | CustomActivityTemplateType;
69
-
70
- /**
71
- * Chat context for "chat" turn_type. Default: "personal".
72
- * - "personal": 1:1 personal chat (default)
73
- * - "group": group chat
74
- * - "channel": team channel
75
- */
76
- chat_type?: "personal" | "group" | "channel";
77
-
78
- /** Persona name override for this turn (looks up in config.personas) */
79
- persona?: string;
80
-
81
- /** Extra metadata for the turn */
82
- prompt_metadata?: Record<string, string>;
83
-
84
- /**
85
- * For "card_action" turn_type: simulate clicking an Adaptive Card button.
86
- * The server must already have a card message in this conversation to target.
87
- */
88
- card_action?: {
89
- /**
90
- * The verb of the Action.Execute button to click (required for Action.Execute).
91
- */
92
- verb?: string;
93
- /**
94
- * Data payload to send with the action (merged with button's own data).
95
- */
96
- data?: Record<string, unknown>;
97
- /**
98
- * test_id of the turn whose first bot response card to target.
99
- * If omitted, uses the last bot message that has an attachment.
100
- */
101
- reply_to_turn?: string;
102
- /**
103
- * Action type. Default: "Action.Execute".
104
- * Use "Action.Submit" for legacy bots.
105
- */
106
- action_type?: "Action.Execute" | "Action.Submit";
107
- };
108
- }
109
-
110
- /**
111
- * Input for a single conversation: a list of turns to execute sequentially.
112
- */
113
- export interface ConversationInput {
114
- /** Default persona for all turns */
115
- persona?: string;
116
-
117
- /** Ordered turns to execute */
118
- turns: Turn[];
119
- }
120
-
121
- /**
122
- * A single validated Adaptive Card attachment returned with a turn result.
123
- */
124
- export interface TurnAttachment {
125
- /** Attachment content type (e.g. "application/vnd.microsoft.card.adaptive") */
126
- contentType: string;
127
- /** The raw card JSON content */
128
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
- content: Record<string, unknown>;
130
- /** Schema validation errors for this card. Empty array means valid. */
131
- card_errors: CardValidationError[];
132
- }
133
-
134
- /**
135
- * Result of a single turn execution.
136
- */
137
- export interface TurnResult {
138
- test_id: string;
139
- prompt: string;
140
- actual_response: string | null;
141
- /** Adaptive Card attachments returned by the bot this turn, with validation results */
142
- attachments: TurnAttachment[];
143
- status: "Completed" | "TimedOut" | "Errored" | "Skipped";
144
- error_message?: string;
145
- duration_seconds: number;
146
- }
147
-
148
- /**
149
- * Result of a full conversation execution.
150
- */
151
- export interface ConversationResult {
152
- type: "conversation_result";
153
- scenario: string;
154
- status: "Completed" | "TimedOut" | "Errored";
155
- duration_seconds: number;
156
- turns: TurnResult[];
157
- }
package/src/index.ts DELETED
@@ -1,37 +0,0 @@
1
- export { TestClient } from "./testClient";
2
- export { ServerManager } from "./serverManager";
3
- export { ResponseCapture } from "./responseCapture";
4
- export type { TestClientConfig, BotConfig, BotResponse, PendingResponse } from "./types";
5
-
6
- // Re-export WebSocket event types for consumers
7
- export { ActionType, LogActionType } from "./types";
8
- export type {
9
- IAction,
10
- ICreateMessageAction,
11
- IUpdateMessageAction,
12
- ITypingAction,
13
- IActionMessage,
14
- // Log action types
15
- ILogAction,
16
- IAppendLogAction,
17
- LogItem,
18
- } from "./types";
19
-
20
- // Conversation runner
21
- export { runConversation, log, logError } from "./runConversation";
22
- export { sendNotificationAndWait } from "./notificationSender";
23
- export { createConversationServer } from "./conversationServer";
24
- export type { ConversationServer, ConversationServerOptions } from "./conversationServer";
25
- export type {
26
- E2EConfig,
27
- PersonaConfig,
28
- Turn,
29
- TurnAttachment,
30
- ConversationInput,
31
- TurnResult,
32
- ConversationResult,
33
- } from "./conversationTypes";
34
-
35
- // Adaptive Card validation
36
- export { validateAdaptiveCard } from "./cardValidator";
37
- export type { CardValidationError } from "./cardValidator";
@@ -1,103 +0,0 @@
1
- /**
2
- * Notification sender for multi-turn conversation execution.
3
- *
4
- * Sends custom notification activities through the playground's Factory
5
- * infrastructure and polls ConversationManager for bot responses.
6
- */
7
-
8
- import { Factory } from "server";
9
- import type { CustomActivityTemplateType } from "schema";
10
- import type { PersonaConfig, Turn } from "./conversationTypes";
11
-
12
- const POLL_INTERVAL_MS = 200;
13
-
14
- function log(msg: string): void {
15
- process.stderr.write(`[notification] ${msg}\n`);
16
- }
17
-
18
- /**
19
- * Send a notification activity and wait for the bot's response.
20
- *
21
- * The turn's turn_type must be a valid CustomActivityTemplateType
22
- * (e.g., "sendEmail", "mentionInWord"). It is passed directly to
23
- * the playground's CustomActivityService.
24
- */
25
- export async function sendNotificationAndWait(
26
- conversationId: string,
27
- turn: Turn,
28
- persona: PersonaConfig | undefined,
29
- timeout: number
30
- ): Promise<string> {
31
- const templateType = turn.turn_type as CustomActivityTemplateType;
32
- const customActivityService = Factory.getCustomActivityService();
33
- const accessor = Factory.getBaseUserBotAccessor();
34
- const conversationManager = Factory.getConversationManager();
35
-
36
- // Count existing messages before sending.
37
- const messagesBefore = conversationManager.listMessages(conversationId);
38
- const countBefore = messagesBefore.length;
39
-
40
- // Generate the activity template as a mutable plain object.
41
- const template = customActivityService.generateTemplate(conversationId, templateType) as Record<
42
- string,
43
- unknown
44
- >;
45
-
46
- // Override the from field if a persona is provided.
47
- if (persona) {
48
- const from = (template.from ?? {}) as Record<string, unknown>;
49
- template.from = {
50
- ...from,
51
- id: persona.email ?? persona.id,
52
- name: persona.name,
53
- };
54
- }
55
-
56
- // Customize the activity body based on turn type.
57
- if (templateType === "sendEmail") {
58
- const entities = template.entities as Array<Record<string, unknown>> | undefined;
59
- const emailEntity = entities?.find((e) => e.type === "emailNotification");
60
- if (emailEntity) {
61
- emailEntity.htmlBody = turn.prompt;
62
- }
63
- } else if (templateType === "mentionInWord") {
64
- template.text = turn.prompt;
65
- if (turn.prompt_metadata?.documentUrl) {
66
- const attachments = template.attachments as Array<Record<string, unknown>> | undefined;
67
- if (attachments?.[0]) {
68
- attachments[0].contentUrl = turn.prompt_metadata.documentUrl;
69
- }
70
- }
71
- if (turn.prompt_metadata?.documentName) {
72
- const attachments = template.attachments as Array<Record<string, unknown>> | undefined;
73
- if (attachments?.[0]) {
74
- attachments[0].name = turn.prompt_metadata.documentName;
75
- }
76
- }
77
- }
78
-
79
- log(`Sending ${templateType} activity to conversation ${conversationId}`);
80
-
81
- // sendActivity expects the Activity class from the server's dependency tree.
82
- // We cast through unknown to bridge the inferred template type.
83
- await accessor.sendActivity(template as Parameters<typeof accessor.sendActivity>[0]);
84
-
85
- // Poll for new bot messages.
86
- const deadline = Date.now() + timeout;
87
- while (Date.now() < deadline) {
88
- const messages = conversationManager.listMessages(conversationId);
89
- const newBotMessages = messages.slice(countBefore).filter((m) => m.createdBy === "bot");
90
-
91
- if (newBotMessages.length > 0) {
92
- const responseText = newBotMessages
93
- .map((m) => m.content.text ?? "")
94
- .filter((t) => t.length > 0)
95
- .join("\n\n");
96
- return responseText;
97
- }
98
-
99
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
100
- }
101
-
102
- throw new Error(`Timeout waiting for bot response to ${templateType} after ${timeout}ms`);
103
- }
@@ -1,145 +0,0 @@
1
- import { BotConnectorService, ConversationManager, Message } from "server";
2
- import type { CreateActivity } from "server";
3
- import { PendingResponse } from "./types";
4
-
5
- /**
6
- * Captures bot responses by hooking into BotConnectorService.processActivity
7
- */
8
- export class ResponseCapture {
9
- private pendingResponses: Map<string, PendingResponse> = new Map();
10
- private originalProcessActivity?: typeof BotConnectorService.prototype.processActivity;
11
-
12
- /**
13
- * Hook into BotConnectorService to intercept responses
14
- */
15
- hookIntoBotConnectorService(service: BotConnectorService): void {
16
- if (this.originalProcessActivity) {
17
- // Already hooked
18
- return;
19
- }
20
-
21
- this.originalProcessActivity = service.processActivity.bind(service);
22
-
23
- service.processActivity = async (
24
- conversationId: string,
25
- activity: CreateActivity
26
- ): Promise<{ resIds: string[]; shouldRespond: boolean; statusCode: number }> => {
27
- if (this.originalProcessActivity === undefined) {
28
- throw new Error("BotConnectorService.processActivity is undefined");
29
- }
30
-
31
- try {
32
- const result = await this.originalProcessActivity(conversationId, activity);
33
-
34
- // Only resolve waiting when an actual message was created (resIds non-empty).
35
- // Typing indicators and trace activities return resIds:[] and must NOT
36
- // resolve the waiting promise prematurely.
37
- if (result.resIds.length > 0) {
38
- this.resolveWaiting(conversationId);
39
- }
40
-
41
- return result;
42
- } catch (error) {
43
- // Log the error for debugging — this is the actual error that causes 500s
44
- // when the bot POSTs back to the connector.
45
- console.error(
46
- `[ResponseCapture] processActivity error for conversation ${conversationId}:`,
47
- error
48
- );
49
- const activityAny = activity as Record<string, unknown>;
50
- const entities = Array.isArray(activityAny.entities) ? activityAny.entities : [];
51
- console.error(
52
- `[ResponseCapture] Activity type: ${String(activityAny.type)}, ` +
53
- `entities: ${JSON.stringify(entities.map((e: Record<string, unknown>) => e.type))}`
54
- );
55
-
56
- // Still resolve waiting on error — the test should see whatever messages
57
- // were created before the error, rather than timing out silently.
58
- this.resolveWaiting(conversationId);
59
-
60
- throw error;
61
- }
62
- };
63
- }
64
-
65
- /**
66
- * Wait for a response in the given conversation
67
- */
68
- async waitForResponse(
69
- conversationId: string,
70
- conversationManager: ConversationManager,
71
- timeout: number
72
- ): Promise<Message[]> {
73
- const messageCountBefore = conversationManager.listMessages(conversationId).length;
74
-
75
- return new Promise<Message[]>((resolve, reject) => {
76
- const timer = setTimeout(() => {
77
- this.pendingResponses.delete(conversationId);
78
- // Check one more time before rejecting
79
- const messages = conversationManager.listMessages(conversationId);
80
- const newMessages = messages.slice(messageCountBefore);
81
- const botMessages = newMessages.filter((m) => m.createdBy === "bot");
82
- if (botMessages.length > 0) {
83
- resolve(botMessages);
84
- } else {
85
- reject(new Error(`Timeout waiting for bot response after ${timeout}ms`));
86
- }
87
- }, timeout);
88
-
89
- this.pendingResponses.set(conversationId, {
90
- resolve: (messages: Message[]) => {
91
- clearTimeout(timer);
92
- resolve(messages);
93
- },
94
- reject,
95
- timer,
96
- messageCountBefore,
97
- });
98
- });
99
- }
100
-
101
- /**
102
- * Resolve waiting promises when a response arrives
103
- */
104
- private resolveWaiting(conversationId: string): void {
105
- const pending = this.pendingResponses.get(conversationId);
106
- if (!pending) {
107
- return;
108
- }
109
-
110
- // Use setImmediate to allow the message to be stored first
111
- setImmediate(() => {
112
- // The pending might have been removed by timeout
113
- if (!this.pendingResponses.has(conversationId)) {
114
- return;
115
- }
116
-
117
- // We don't have direct access to conversationManager here,
118
- // so we just signal completion. The TestClient will check for new messages.
119
- this.pendingResponses.delete(conversationId);
120
- clearTimeout(pending.timer);
121
- pending.resolve([]);
122
- });
123
- }
124
-
125
- /**
126
- * Clear all pending responses
127
- */
128
- clear(): void {
129
- this.pendingResponses.forEach((pending) => {
130
- clearTimeout(pending.timer);
131
- });
132
- this.pendingResponses.clear();
133
- }
134
-
135
- /**
136
- * Clear pending responses for a specific conversation only
137
- */
138
- clearConversation(conversationId: string): void {
139
- const pending = this.pendingResponses.get(conversationId);
140
- if (pending) {
141
- clearTimeout(pending.timer);
142
- this.pendingResponses.delete(conversationId);
143
- }
144
- }
145
- }