@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.
- package/README.md +8 -21
- package/{build → dist}/conversationTypes.d.ts +5 -0
- package/{build → dist}/index.d.ts +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.LICENSE.txt +157 -0
- package/dist/responseCapture.d.ts +42 -0
- package/{build → dist}/serverManager.d.ts +1 -2
- package/dist/start-server.js +9 -0
- package/dist/start-server.js.LICENSE.txt +157 -0
- package/{build → dist}/testClient.d.ts +1 -1
- package/{build → dist}/types.d.ts +9 -0
- package/package.json +22 -12
- package/build/cardValidator.d.ts.map +0 -1
- package/build/cardValidator.js +0 -46
- package/build/conversationServer.d.ts.map +0 -1
- package/build/conversationServer.js +0 -136
- package/build/conversationTypes.d.ts.map +0 -1
- package/build/conversationTypes.js +0 -5
- package/build/index.d.ts.map +0 -1
- package/build/index.js +0 -25
- package/build/notificationSender.d.ts.map +0 -1
- package/build/notificationSender.js +0 -83
- package/build/responseCapture.d.ts +0 -29
- package/build/responseCapture.d.ts.map +0 -1
- package/build/responseCapture.js +0 -119
- package/build/runConversation.d.ts.map +0 -1
- package/build/runConversation.js +0 -351
- package/build/serverManager.d.ts.map +0 -1
- package/build/serverManager.js +0 -149
- package/build/start-server.d.ts.map +0 -1
- package/build/start-server.js +0 -23
- package/build/testClient.d.ts.map +0 -1
- package/build/testClient.js +0 -434
- package/build/types.d.ts.map +0 -1
- package/build/types.js +0 -7
- package/build/websocketClient.d.ts.map +0 -1
- package/build/websocketClient.js +0 -129
- package/src/cardValidator.ts +0 -56
- package/src/conversationServer.ts +0 -147
- package/src/conversationTypes.ts +0 -157
- package/src/index.ts +0 -37
- package/src/notificationSender.ts +0 -103
- package/src/responseCapture.ts +0 -145
- package/src/runConversation.ts +0 -382
- package/src/serverManager.ts +0 -172
- package/src/start-server.ts +0 -26
- package/src/testClient.ts +0 -512
- package/src/types.ts +0 -155
- package/src/websocketClient.ts +0 -153
- package/tsconfig.json +0 -14
- /package/{build → dist}/cardValidator.d.ts +0 -0
- /package/{build → dist}/conversationServer.d.ts +0 -0
- /package/{build → dist}/notificationSender.d.ts +0 -0
- /package/{build → dist}/runConversation.d.ts +0 -0
- /package/{build → dist}/start-server.d.ts +0 -0
- /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
|
-
}
|
package/src/conversationTypes.ts
DELETED
|
@@ -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
|
-
}
|
package/src/responseCapture.ts
DELETED
|
@@ -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
|
-
}
|