@microsoft/m365agentsplayground-cli 0.2.25-alpha.20260507-efe1416.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 +341 -0
- package/build/cardValidator.d.ts +18 -0
- package/build/cardValidator.d.ts.map +1 -0
- package/build/cardValidator.js +47 -0
- package/build/conversationServer.d.ts +29 -0
- package/build/conversationServer.d.ts.map +1 -0
- package/build/conversationServer.js +127 -0
- package/build/conversationTypes.d.ts +146 -0
- package/build/conversationTypes.d.ts.map +1 -0
- package/build/conversationTypes.js +5 -0
- package/build/index.d.ts +14 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +25 -0
- package/build/notificationSender.d.ts +16 -0
- package/build/notificationSender.d.ts.map +1 -0
- package/build/notificationSender.js +120 -0
- package/build/responseCapture.d.ts +29 -0
- package/build/responseCapture.d.ts.map +1 -0
- package/build/responseCapture.js +119 -0
- package/build/runConversation.d.ts +17 -0
- package/build/runConversation.d.ts.map +1 -0
- package/build/runConversation.js +338 -0
- package/build/serverManager.d.ts +46 -0
- package/build/serverManager.d.ts.map +1 -0
- package/build/serverManager.js +149 -0
- package/build/start-server.d.ts +9 -0
- package/build/start-server.d.ts.map +1 -0
- package/build/start-server.js +23 -0
- package/build/testClient.d.ts +146 -0
- package/build/testClient.d.ts.map +1 -0
- package/build/testClient.js +434 -0
- package/build/types.d.ts +125 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +7 -0
- package/build/websocketClient.d.ts +56 -0
- package/build/websocketClient.d.ts.map +1 -0
- package/build/websocketClient.js +129 -0
- package/package.json +36 -0
- package/src/cardValidator.ts +56 -0
- package/src/conversationServer.ts +147 -0
- package/src/conversationTypes.ts +169 -0
- package/src/index.ts +37 -0
- package/src/notificationSender.ts +135 -0
- package/src/responseCapture.ts +145 -0
- package/src/runConversation.ts +379 -0
- package/src/serverManager.ts +172 -0
- package/src/start-server.ts +26 -0
- package/src/testClient.ts +515 -0
- package/src/types.ts +155 -0
- package/src/websocketClient.ts +153 -0
- package/tsconfig.json +16 -0
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { AccountRole } from "schema";
|
|
2
|
+
import { Message } from "server";
|
|
3
|
+
import type { Attachment } from "server";
|
|
4
|
+
export { ActionType, LogActionType } from "schema";
|
|
5
|
+
export type { IAction, ICreateMessageAction, IUpdateMessageAction, ITypingAction, IActionMessage, ILogAction, IAppendLogAction, LogItem, } from "schema";
|
|
6
|
+
/**
|
|
7
|
+
* Bot configuration matching the .m365agentsplayground.yml format
|
|
8
|
+
*/
|
|
9
|
+
export interface BotConfig {
|
|
10
|
+
/**
|
|
11
|
+
* Bot's unique identifier
|
|
12
|
+
*/
|
|
13
|
+
id: string;
|
|
14
|
+
/**
|
|
15
|
+
* Bot display name (max 42 characters)
|
|
16
|
+
*/
|
|
17
|
+
name: string;
|
|
18
|
+
/**
|
|
19
|
+
* User ID for agentic-role bots
|
|
20
|
+
*/
|
|
21
|
+
agenticUserId?: string;
|
|
22
|
+
/**
|
|
23
|
+
* App ID for agentic-role bots
|
|
24
|
+
*/
|
|
25
|
+
agenticAppId?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Bot's tenant ID (defaults to root tenant)
|
|
28
|
+
*/
|
|
29
|
+
tenantId?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Bot role: "user" | "bot" | "agenticUser"
|
|
32
|
+
*/
|
|
33
|
+
role?: AccountRole;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Configuration for the TestClient
|
|
37
|
+
*/
|
|
38
|
+
export interface TestClientConfig {
|
|
39
|
+
/**
|
|
40
|
+
* The bot endpoint URL (e.g., "http://localhost:3978/api/messages")
|
|
41
|
+
*/
|
|
42
|
+
botEndpoint: string;
|
|
43
|
+
/**
|
|
44
|
+
* Timeout in milliseconds for waiting for bot responses.
|
|
45
|
+
* Default: 5000
|
|
46
|
+
*/
|
|
47
|
+
timeout?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Port to run the test server on.
|
|
50
|
+
* Default: auto-assigned
|
|
51
|
+
*/
|
|
52
|
+
port?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Bot configuration (id, name, role, etc.)
|
|
55
|
+
* If not provided, uses default bot config.
|
|
56
|
+
*/
|
|
57
|
+
bot?: BotConfig;
|
|
58
|
+
/**
|
|
59
|
+
* Chat type to use when sending messages.
|
|
60
|
+
* - "personal": 1:1 personal chat with the bot (default)
|
|
61
|
+
* - "group": group chat context
|
|
62
|
+
* - "channel": team channel context
|
|
63
|
+
*/
|
|
64
|
+
chatType?: "personal" | "group" | "channel";
|
|
65
|
+
/**
|
|
66
|
+
* Delivery mode for activities sent to the bot.
|
|
67
|
+
* - "expectReplies": Bot responses come inline in the HTTP response.
|
|
68
|
+
* - "default": Bot posts responses back to the connector URL.
|
|
69
|
+
*/
|
|
70
|
+
deliveryMode?: "expectReplies" | "default";
|
|
71
|
+
/**
|
|
72
|
+
* Quiet-period fallback (ms) for streaming bot responses.
|
|
73
|
+
*
|
|
74
|
+
* When the simulator detects a streaming placeholder (`"Loading stream results..."`),
|
|
75
|
+
* it first waits for a `streamType:"final"` WebSocket event (the precise end-of-stream
|
|
76
|
+
* signal sent by teams-ai / teams.ts SDK). If no `streamType:"final"` arrives, it falls
|
|
77
|
+
* back to a quiet-period: resolves after this many milliseconds with no new
|
|
78
|
+
* `updateActivity` events.
|
|
79
|
+
*
|
|
80
|
+
* Default: 800 ms. Has no effect for bots with `stream: false`.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* // Increase for slow LLMs
|
|
84
|
+
* const client = new TestClient({
|
|
85
|
+
* botEndpoint: "http://localhost:3978/api/messages",
|
|
86
|
+
* streamingSettleDelayMs: 2000,
|
|
87
|
+
* });
|
|
88
|
+
*/
|
|
89
|
+
streamingSettleDelayMs?: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A response from the bot
|
|
93
|
+
*/
|
|
94
|
+
export interface BotResponse {
|
|
95
|
+
/**
|
|
96
|
+
* The unique message ID
|
|
97
|
+
*/
|
|
98
|
+
messageId: string;
|
|
99
|
+
/**
|
|
100
|
+
* The text content of the response
|
|
101
|
+
*/
|
|
102
|
+
text?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Attachments in the response (e.g., Adaptive Cards)
|
|
105
|
+
*/
|
|
106
|
+
attachments?: Attachment[];
|
|
107
|
+
/**
|
|
108
|
+
* Timestamp when the message was created
|
|
109
|
+
*/
|
|
110
|
+
timestamp: number;
|
|
111
|
+
/**
|
|
112
|
+
* The raw Message object from ConversationManager
|
|
113
|
+
*/
|
|
114
|
+
raw: Message;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Promise handlers for async response waiting
|
|
118
|
+
*/
|
|
119
|
+
export interface PendingResponse {
|
|
120
|
+
resolve: (messages: Message[]) => void;
|
|
121
|
+
reject: (error: Error) => void;
|
|
122
|
+
timer: NodeJS.Timeout;
|
|
123
|
+
messageCountBefore: number;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAGzC,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACnD,YAAY,EACV,OAAO,EACP,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACb,cAAc,EAEd,UAAU,EACV,gBAAgB,EAChB,OAAO,GACR,MAAM,QAAQ,CAAC;AAEhB;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,GAAG,CAAC,EAAE,SAAS,CAAC;IAEhB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;IAE5C;;;;OAIG;IACH,YAAY,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;IAE3C;;;;;;;;;;;;;;;;;OAiBG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAE3B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,GAAG,EAAE,OAAO,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IACvC,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;CAC5B"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LogActionType = exports.ActionType = void 0;
|
|
4
|
+
// Re-export action types for consumers
|
|
5
|
+
var schema_1 = require("schema");
|
|
6
|
+
Object.defineProperty(exports, "ActionType", { enumerable: true, get: function () { return schema_1.ActionType; } });
|
|
7
|
+
Object.defineProperty(exports, "LogActionType", { enumerable: true, get: function () { return schema_1.LogActionType; } });
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { IAction, ILogAction } from "schema";
|
|
2
|
+
export interface WebSocketClientOptions {
|
|
3
|
+
port: number;
|
|
4
|
+
onMessage: (action: IAction) => void;
|
|
5
|
+
onError?: (error: Error) => void;
|
|
6
|
+
onClose?: () => void;
|
|
7
|
+
}
|
|
8
|
+
export interface LogWebSocketClientOptions {
|
|
9
|
+
port: number;
|
|
10
|
+
onLogMessage: (action: ILogAction) => void;
|
|
11
|
+
onError?: (error: Error) => void;
|
|
12
|
+
onClose?: () => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* WebSocket client for connecting to the conversation WebSocket endpoint.
|
|
16
|
+
* Receives real-time message and typing events from the server.
|
|
17
|
+
*/
|
|
18
|
+
export declare class WebSocketClient {
|
|
19
|
+
private ws;
|
|
20
|
+
private options;
|
|
21
|
+
constructor(options: WebSocketClientOptions);
|
|
22
|
+
/**
|
|
23
|
+
* Connect to the WebSocket server
|
|
24
|
+
*/
|
|
25
|
+
connect(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Close the WebSocket connection
|
|
28
|
+
*/
|
|
29
|
+
close(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Check if the WebSocket is connected
|
|
32
|
+
*/
|
|
33
|
+
isConnected(): boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* WebSocket client for connecting to the log WebSocket endpoint.
|
|
37
|
+
* Receives real-time log events from the server.
|
|
38
|
+
*/
|
|
39
|
+
export declare class LogWebSocketClient {
|
|
40
|
+
private ws;
|
|
41
|
+
private options;
|
|
42
|
+
constructor(options: LogWebSocketClientOptions);
|
|
43
|
+
/**
|
|
44
|
+
* Connect to the log WebSocket server
|
|
45
|
+
*/
|
|
46
|
+
connect(): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Close the WebSocket connection
|
|
49
|
+
*/
|
|
50
|
+
close(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Check if the WebSocket is connected
|
|
53
|
+
*/
|
|
54
|
+
isConnected(): boolean;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=websocketClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocketClient.d.ts","sourceRoot":"","sources":["../src/websocketClient.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAK7C,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAC3C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;;GAGG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,OAAO,CAAyB;gBAE5B,OAAO,EAAE,sBAAsB;IAI3C;;OAEG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCxB;;OAEG;IACH,KAAK,IAAI,IAAI;IAOb;;OAEG;IACH,WAAW,IAAI,OAAO;CAGvB;AAED;;;GAGG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,OAAO,CAA4B;gBAE/B,OAAO,EAAE,yBAAyB;IAI9C;;OAEG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCxB;;OAEG;IACH,KAAK,IAAI,IAAI;IAOb;;OAEG;IACH,WAAW,IAAI,OAAO;CAGvB"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LogWebSocketClient = exports.WebSocketClient = void 0;
|
|
7
|
+
const ws_1 = __importDefault(require("ws"));
|
|
8
|
+
const CONVERSATION_WS_PATH = "/_ws_conversation/v1/conversations/stream";
|
|
9
|
+
const LOG_WS_PATH = "/_ws/log/stream";
|
|
10
|
+
/**
|
|
11
|
+
* WebSocket client for connecting to the conversation WebSocket endpoint.
|
|
12
|
+
* Receives real-time message and typing events from the server.
|
|
13
|
+
*/
|
|
14
|
+
class WebSocketClient {
|
|
15
|
+
ws = null;
|
|
16
|
+
options;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Connect to the WebSocket server
|
|
22
|
+
*/
|
|
23
|
+
connect() {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const url = `ws://localhost:${this.options.port}${CONVERSATION_WS_PATH}`;
|
|
26
|
+
this.ws = new ws_1.default(url);
|
|
27
|
+
this.ws.on("open", () => {
|
|
28
|
+
// Small delay to ensure server has processed the connection
|
|
29
|
+
setTimeout(resolve, 50);
|
|
30
|
+
});
|
|
31
|
+
this.ws.on("message", (data) => {
|
|
32
|
+
try {
|
|
33
|
+
const message = JSON.parse(data.toString());
|
|
34
|
+
this.options.onMessage(message);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
console.error("Failed to parse WebSocket message:", err);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
this.ws.on("error", (err) => {
|
|
41
|
+
if (this.options.onError) {
|
|
42
|
+
this.options.onError(err);
|
|
43
|
+
}
|
|
44
|
+
reject(err);
|
|
45
|
+
});
|
|
46
|
+
this.ws.on("close", () => {
|
|
47
|
+
if (this.options.onClose) {
|
|
48
|
+
this.options.onClose();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Close the WebSocket connection
|
|
55
|
+
*/
|
|
56
|
+
close() {
|
|
57
|
+
if (this.ws) {
|
|
58
|
+
this.ws.close();
|
|
59
|
+
this.ws = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if the WebSocket is connected
|
|
64
|
+
*/
|
|
65
|
+
isConnected() {
|
|
66
|
+
return this.ws !== null && this.ws.readyState === ws_1.default.OPEN;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.WebSocketClient = WebSocketClient;
|
|
70
|
+
/**
|
|
71
|
+
* WebSocket client for connecting to the log WebSocket endpoint.
|
|
72
|
+
* Receives real-time log events from the server.
|
|
73
|
+
*/
|
|
74
|
+
class LogWebSocketClient {
|
|
75
|
+
ws = null;
|
|
76
|
+
options;
|
|
77
|
+
constructor(options) {
|
|
78
|
+
this.options = options;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Connect to the log WebSocket server
|
|
82
|
+
*/
|
|
83
|
+
connect() {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const url = `ws://localhost:${this.options.port}${LOG_WS_PATH}`;
|
|
86
|
+
this.ws = new ws_1.default(url);
|
|
87
|
+
this.ws.on("open", () => {
|
|
88
|
+
// Small delay to ensure server has processed the connection
|
|
89
|
+
setTimeout(resolve, 50);
|
|
90
|
+
});
|
|
91
|
+
this.ws.on("message", (data) => {
|
|
92
|
+
try {
|
|
93
|
+
const message = JSON.parse(data.toString());
|
|
94
|
+
this.options.onLogMessage(message);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.error("Failed to parse log WebSocket message:", err);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
this.ws.on("error", (err) => {
|
|
101
|
+
if (this.options.onError) {
|
|
102
|
+
this.options.onError(err);
|
|
103
|
+
}
|
|
104
|
+
reject(err);
|
|
105
|
+
});
|
|
106
|
+
this.ws.on("close", () => {
|
|
107
|
+
if (this.options.onClose) {
|
|
108
|
+
this.options.onClose();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Close the WebSocket connection
|
|
115
|
+
*/
|
|
116
|
+
close() {
|
|
117
|
+
if (this.ws) {
|
|
118
|
+
this.ws.close();
|
|
119
|
+
this.ws = null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if the WebSocket is connected
|
|
124
|
+
*/
|
|
125
|
+
isConnected() {
|
|
126
|
+
return this.ws !== null && this.ws.readyState === ws_1.default.OPEN;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
exports.LogWebSocketClient = LogWebSocketClient;
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@microsoft/m365agentsplayground-cli",
|
|
3
|
+
"version": "0.2.25-alpha.20260507-efe1416.0",
|
|
4
|
+
"main": "build/index.js",
|
|
5
|
+
"types": "build/index.d.ts",
|
|
6
|
+
"bin": {
|
|
7
|
+
"playground-cli-server": "build/start-server.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc --build",
|
|
11
|
+
"server": "node build/start-server.js"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/chai": "^4.3.5",
|
|
15
|
+
"@types/express": "5.0.1",
|
|
16
|
+
"@types/express-serve-static-core": "5.0.6",
|
|
17
|
+
"@types/mocha": "^10.0.1",
|
|
18
|
+
"@types/node": "^20.3.1",
|
|
19
|
+
"@types/ws": "^8.5.10",
|
|
20
|
+
"chai": "^4.3.7",
|
|
21
|
+
"mocha": "^10.2.0",
|
|
22
|
+
"ts-node": "^10.9.1",
|
|
23
|
+
"typescript": "^4.9.5"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"adaptivecards": "^3.0.4",
|
|
27
|
+
"express": "^5.2.0",
|
|
28
|
+
"schema": "^0.2.25-alpha.20260507-efe1416.0",
|
|
29
|
+
"server": "^0.2.25-alpha.20260507-efe1416.0",
|
|
30
|
+
"ws": "^8.18.0"
|
|
31
|
+
},
|
|
32
|
+
"bundledDependencies": [
|
|
33
|
+
"server",
|
|
34
|
+
"schema"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive Card schema validation using the official `adaptivecards` package.
|
|
3
|
+
*
|
|
4
|
+
* Validates that a card payload is a well-formed Adaptive Card that Teams can
|
|
5
|
+
* parse. Does NOT check visual rendering — only structural/schema correctness.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AdaptiveCard, SerializationContext } from "adaptivecards";
|
|
9
|
+
|
|
10
|
+
export interface CardValidationError {
|
|
11
|
+
/** Human-readable description of the issue */
|
|
12
|
+
message: string;
|
|
13
|
+
/** "parsing" | "schema" */
|
|
14
|
+
phase: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate a raw Adaptive Card JSON payload.
|
|
19
|
+
* Returns an empty array if the card is valid.
|
|
20
|
+
*/
|
|
21
|
+
export function validateAdaptiveCard(
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
payload: Record<string, unknown>
|
|
24
|
+
): CardValidationError[] {
|
|
25
|
+
const errors: CardValidationError[] = [];
|
|
26
|
+
|
|
27
|
+
// Basic type guard: must have type === "AdaptiveCard"
|
|
28
|
+
if (payload.type !== "AdaptiveCard") {
|
|
29
|
+
errors.push({
|
|
30
|
+
message: `Expected type "AdaptiveCard", got "${payload.type ?? "(missing)"}"`,
|
|
31
|
+
phase: "schema",
|
|
32
|
+
});
|
|
33
|
+
return errors; // No point parsing further
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const card = new AdaptiveCard();
|
|
38
|
+
const context = new SerializationContext();
|
|
39
|
+
card.parse(payload, context);
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < context.eventCount; i++) {
|
|
42
|
+
const event = context.getEventAt(i);
|
|
43
|
+
errors.push({
|
|
44
|
+
message: event.message ?? String(event),
|
|
45
|
+
phase: event.phase !== undefined ? String(event.phase) : "parsing",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
errors.push({
|
|
50
|
+
message: err instanceof Error ? err.message : String(err),
|
|
51
|
+
phase: "parsing",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return errors;
|
|
56
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
}
|