@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.
Files changed (51) hide show
  1. package/README.md +341 -0
  2. package/build/cardValidator.d.ts +18 -0
  3. package/build/cardValidator.d.ts.map +1 -0
  4. package/build/cardValidator.js +47 -0
  5. package/build/conversationServer.d.ts +29 -0
  6. package/build/conversationServer.d.ts.map +1 -0
  7. package/build/conversationServer.js +127 -0
  8. package/build/conversationTypes.d.ts +146 -0
  9. package/build/conversationTypes.d.ts.map +1 -0
  10. package/build/conversationTypes.js +5 -0
  11. package/build/index.d.ts +14 -0
  12. package/build/index.d.ts.map +1 -0
  13. package/build/index.js +25 -0
  14. package/build/notificationSender.d.ts +16 -0
  15. package/build/notificationSender.d.ts.map +1 -0
  16. package/build/notificationSender.js +120 -0
  17. package/build/responseCapture.d.ts +29 -0
  18. package/build/responseCapture.d.ts.map +1 -0
  19. package/build/responseCapture.js +119 -0
  20. package/build/runConversation.d.ts +17 -0
  21. package/build/runConversation.d.ts.map +1 -0
  22. package/build/runConversation.js +338 -0
  23. package/build/serverManager.d.ts +46 -0
  24. package/build/serverManager.d.ts.map +1 -0
  25. package/build/serverManager.js +149 -0
  26. package/build/start-server.d.ts +9 -0
  27. package/build/start-server.d.ts.map +1 -0
  28. package/build/start-server.js +23 -0
  29. package/build/testClient.d.ts +146 -0
  30. package/build/testClient.d.ts.map +1 -0
  31. package/build/testClient.js +434 -0
  32. package/build/types.d.ts +125 -0
  33. package/build/types.d.ts.map +1 -0
  34. package/build/types.js +7 -0
  35. package/build/websocketClient.d.ts +56 -0
  36. package/build/websocketClient.d.ts.map +1 -0
  37. package/build/websocketClient.js +129 -0
  38. package/package.json +36 -0
  39. package/src/cardValidator.ts +56 -0
  40. package/src/conversationServer.ts +147 -0
  41. package/src/conversationTypes.ts +169 -0
  42. package/src/index.ts +37 -0
  43. package/src/notificationSender.ts +135 -0
  44. package/src/responseCapture.ts +145 -0
  45. package/src/runConversation.ts +379 -0
  46. package/src/serverManager.ts +172 -0
  47. package/src/start-server.ts +26 -0
  48. package/src/testClient.ts +515 -0
  49. package/src/types.ts +155 -0
  50. package/src/websocketClient.ts +153 -0
  51. package/tsconfig.json +16 -0
package/README.md ADDED
@@ -0,0 +1,341 @@
1
+ # playground-cli
2
+
3
+ A programmatic simulation library for Microsoft 365 bots/agents. Drive your bot's responses without a browser or manual interaction — ideal for automated testing, CI pipelines, and LLM-based evaluation.
4
+
5
+ ## Installation
6
+
7
+ The package is part of the monorepo. Link it in your project:
8
+
9
+ ```json
10
+ {
11
+ "dependencies": {
12
+ "@microsoft/m365agentsplayground-cli": "file:../../packages/playground-cli"
13
+ }
14
+ }
15
+ ```
16
+
17
+ Build the package before use:
18
+
19
+ ```bash
20
+ npm run build --workspace=packages/playground-cli
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```typescript
26
+ import { TestClient } from "@microsoft/m365agentsplayground-cli";
27
+
28
+ const client = new TestClient({
29
+ botEndpoint: "http://localhost:3978/api/messages",
30
+ });
31
+
32
+ await client.start();
33
+ const [response] = await client.sendMessage("Hello!");
34
+ console.log(response.text);
35
+ await client.stop();
36
+ ```
37
+
38
+ ## API
39
+
40
+ ### TestClient
41
+
42
+ The main class for interacting with your bot.
43
+
44
+ ```typescript
45
+ new TestClient(config: TestClientConfig)
46
+ ```
47
+
48
+ #### TestClientConfig
49
+
50
+ | Property | Type | Default | Description |
51
+ | -------------- | ------------------------------ | ---------- | -------------------------------- |
52
+ | `botEndpoint` | `string` | _required_ | Your bot's endpoint URL |
53
+ | `timeout` | `number` | `5000` | Response timeout in milliseconds |
54
+ | `bot` | `BotConfig` | - | Bot identity configuration |
55
+ | `deliveryMode` | `"expectReplies" \| "default"` | - | How the bot delivers responses |
56
+
57
+ #### BotConfig
58
+
59
+ | Property | Type | Description |
60
+ | --------------- | ---------------------------------- | ------------------------------------------------------ |
61
+ | `id` | `string` | Bot's unique identifier |
62
+ | `name` | `string` | Display name (max 42 chars) |
63
+ | `role` | `"user" \| "bot" \| "agenticUser"` | Bot role |
64
+ | `tenantId` | `string` | Tenant ID (also sets `activity.conversation.tenantId`) |
65
+ | `agenticUserId` | `string` | User ID for agentic bots |
66
+ | `agenticAppId` | `string` | App ID for agentic bots |
67
+
68
+ #### Methods
69
+
70
+ | Method | Returns | Description |
71
+ | --------------------- | ------------------------ | ------------------------------------ |
72
+ | `start()` | `Promise<void>` | Initialize the client |
73
+ | `stop()` | `Promise<void>` | Shut down and clean up |
74
+ | `sendMessage(text)` | `Promise<BotResponse[]>` | Send a message, wait for response(s) |
75
+ | `getMessages()` | `Message[]` | All messages in the conversation |
76
+ | `getLastBotMessage()` | `Message \| undefined` | Most recent bot message |
77
+ | `getConversationId()` | `string` | Current conversation ID |
78
+ | `newConversation()` | `void` | Reset conversation state |
79
+
80
+ #### WebSocket Events
81
+
82
+ TestClient extends EventEmitter and emits real-time events:
83
+
84
+ | Event | Payload | Description |
85
+ | ----------------- | ---------------------- | ------------------------------------- |
86
+ | `message:created` | `ICreateMessageAction` | New message from bot or user |
87
+ | `message:updated` | `IUpdateMessageAction` | Message was updated (streaming, edit) |
88
+ | `typing` | `ITypingAction` | Bot typing indicator |
89
+ | `websocket:error` | `Error` | WebSocket connection error |
90
+ | `websocket:close` | - | WebSocket connection closed |
91
+
92
+ ### BotResponse
93
+
94
+ Returned by `sendMessage()`.
95
+
96
+ | Property | Type | Description |
97
+ | ------------- | --------------- | ------------------------------------------ |
98
+ | `messageId` | `string` | Unique message ID |
99
+ | `text` | `string?` | Text content |
100
+ | `attachments` | `Attachment[]?` | Attachments (Adaptive Cards, etc.) |
101
+ | `timestamp` | `number` | Unix timestamp |
102
+ | `raw` | `Message` | Raw Message object for detailed inspection |
103
+
104
+ ## Conversation Server
105
+
106
+ An HTTP wrapper around TestClient so non-Node languages (Python, curl, etc.) can drive bot conversations by POSTing JSON.
107
+
108
+ ```bash
109
+ # From the repo root (after install + build):
110
+ npm run server --workspace=packages/agents-simulator -- --port 9000
111
+ ```
112
+
113
+ Or programmatically:
114
+
115
+ ```typescript
116
+ import { createConversationServer } from "agents-simulator";
117
+ const server = await createConversationServer({ port: 9000 });
118
+ ```
119
+
120
+ ### `POST /run-conversation`
121
+
122
+ Run a multi-turn conversation:
123
+
124
+ ```json
125
+ {
126
+ "config": {
127
+ "botEndpoint": "http://localhost:3978/api/messages",
128
+ "timeout": 120000,
129
+ "deliveryMode": "expectReplies",
130
+ "bot": { "id": "bot@tenant.onmicrosoft.com", "name": "My Bot" },
131
+ "personas": {
132
+ "alice": { "id": "alice-id", "name": "Alice", "email": "alice@example.com" }
133
+ }
134
+ },
135
+ "scenario": "greeting-test",
136
+ "input": {
137
+ "turns": [
138
+ { "test_id": "t1", "prompt": "Hello!" },
139
+ { "test_id": "t2", "prompt": "What can you do?" }
140
+ ]
141
+ }
142
+ }
143
+ ```
144
+
145
+ **Response:**
146
+
147
+ ```json
148
+ {
149
+ "type": "conversation_result",
150
+ "scenario": "greeting-test",
151
+ "status": "Completed",
152
+ "duration_seconds": 4.2,
153
+ "turns": [
154
+ {
155
+ "test_id": "t1",
156
+ "prompt": "Hello!",
157
+ "actual_response": "Hi! I'd be happy to help. What do you need?",
158
+ "status": "Completed",
159
+ "duration_seconds": 1.8
160
+ },
161
+ {
162
+ "test_id": "t2",
163
+ "prompt": "What can you do?",
164
+ "actual_response": "I can help you with...",
165
+ "status": "Completed",
166
+ "duration_seconds": 2.4
167
+ }
168
+ ]
169
+ }
170
+ ```
171
+
172
+ ### Configuration
173
+
174
+ The `config` object in the request body:
175
+
176
+ | Field | Type | Default | Description |
177
+ | -------------- | ------------------------------- | ----------------- | ------------------------------------- |
178
+ | `botEndpoint` | `string` | _required_ | Bot's messaging endpoint URL |
179
+ | `timeout` | `number` | `120000` | Per-turn response timeout (ms) |
180
+ | `deliveryMode` | `"expectReplies" \| "default"` | `"expectReplies"` | How the bot delivers responses |
181
+ | `chatType` | `"personal" \| "group" \| "channel"` | `"personal"` | Default chat context for all turns |
182
+ | `streamingSettleDelayMs` | `number` | `800` | Quiet-period (ms) for streaming bots — see below |
183
+ | `bot` | `BotConfig` | - | Bot identity (see above) |
184
+ | `personas` | `Record<string, PersonaConfig>` | - | Named personas for notification turns |
185
+
186
+ > **Streaming bots (teams-ai / teams.ts `stream: true`):** When the simulator receives the placeholder `"Loading stream results..."`, it automatically switches into streaming mode. It first waits for a `streamType:"final"` WebSocket event (the precise end-of-stream signal from teams-ai and teams.ts SDK) — this is instant and accurate. If the bot doesn't send `streamType`, it falls back to a quiet-period: waits until no new `updateActivity` events arrive for `streamingSettleDelayMs` ms (default: 800 ms). Increase `streamingSettleDelayMs` only if you have a slow LLM that pauses mid-stream for over 800 ms. For bots with `stream: false` this option has no effect.
187
+
188
+ ### Turn Types
189
+
190
+ Each turn has an optional `turn_type` that controls what kind of Bot Framework activity is sent:
191
+
192
+ | Turn type | Description |
193
+ | ------------------- | ------------------------------------------------ |
194
+ | `"chat"` | Standard chat message (default) |
195
+ | `"sendEmail"` | Email notification activity |
196
+ | `"mentionInWord"` | Word document mention notification activity |
197
+ | `"meetingStart"` | Meeting started event |
198
+ | `"meetingEnd"` | Meeting ended event |
199
+ | `"participantJoin"` | Meeting participant joined event |
200
+ | `"participantLeave"`| Meeting participant left event |
201
+ | `"messageReaction"` | Reaction added to a message (like, heart, etc.) |
202
+ | `"install"` | Bot installation update |
203
+ | `"userAdded"` | User added to conversation |
204
+ | `"botAdded"` | Bot added to conversation |
205
+ | `"channelCreated"` | Channel created (team context required) |
206
+ | `"teamRenamed"` | Team renamed (team context required) |
207
+
208
+ All values from `CustomActivityTemplateType` are supported. You can mix turn types within a single conversation:
209
+
210
+ ```json
211
+ {
212
+ "input": {
213
+ "turns": [
214
+ { "test_id": "t1", "prompt": "Hello", "turn_type": "chat" },
215
+ {
216
+ "test_id": "t2",
217
+ "prompt": "<html><body>Customer complaint about order #789...</body></html>",
218
+ "turn_type": "sendEmail",
219
+ "persona": "customer1"
220
+ },
221
+ { "test_id": "t3", "prompt": "Summarize the email I just received", "turn_type": "chat" },
222
+ { "test_id": "t4", "prompt": "", "turn_type": "meetingStart" },
223
+ { "test_id": "t5", "prompt": "", "turn_type": "messageReaction", "reaction_type": "like" }
224
+ ]
225
+ }
226
+ }
227
+ ```
228
+
229
+ #### Turn-specific fields
230
+
231
+ | Field | Applies to | Description |
232
+ | ----------------- | ------------------------------- | -------------------------------------------------------- |
233
+ | `reaction_type` | `messageReaction` | Reaction emoji: `like` (default), `heart`, `laugh`, `surprised`, `sad`, `angry` |
234
+ | `reply_to_id` | `messageReaction` | Message ID to react to. Defaults to last bot message. |
235
+ | `prompt_metadata.meetingTitle` | `meetingStart`, `meetingEnd` | Override the meeting title in the event. |
236
+ | `prompt_metadata.documentUrl` | `mentionInWord` | Override the document URL. |
237
+ | `prompt_metadata.documentName`| `mentionInWord` | Override the document name. |
238
+
239
+ ### Chat Types
240
+
241
+ The `chat_type` field on a turn (or `chatType` in config) controls the conversation context:
242
+
243
+ | Chat type | Description |
244
+ | ------------ | ---------------------------------------------- |
245
+ | `"personal"` | 1:1 personal chat with the bot (default) |
246
+ | `"group"` | Group chat context |
247
+ | `"channel"` | Team channel context (uses general channel) |
248
+
249
+ Set the default for the whole conversation in `config.chatType`, or override per turn with `turn.chat_type`:
250
+
251
+ ```json
252
+ {
253
+ "config": {
254
+ "botEndpoint": "http://localhost:3978/api/messages",
255
+ "chatType": "group"
256
+ },
257
+ "scenario": "group-chat-test",
258
+ "input": {
259
+ "turns": [
260
+ { "test_id": "t1", "prompt": "@bot Hello everyone!", "turn_type": "chat" },
261
+ { "test_id": "t2", "prompt": "Personal follow-up", "turn_type": "chat", "chat_type": "personal" }
262
+ ]
263
+ }
264
+ }
265
+ ```
266
+
267
+ ### Parallel Testing
268
+
269
+ Multiple `TestClient` instances in the same process each get a unique `conversationId`, so concurrent tests don't interfere with each other's messages.
270
+
271
+ To test multiple bot endpoints in parallel, run separate `conversationServer` processes on different ports:
272
+
273
+ ```bash
274
+ # Terminal 1 — bot-a on port 9001
275
+ npm run server --workspace=packages/agents-simulator -- --port 9001
276
+
277
+ # Terminal 2 — bot-b on port 9002
278
+ npm run server --workspace=packages/agents-simulator -- --port 9002
279
+ ```
280
+
281
+ Then point each test suite at its own server URL.
282
+
283
+ ### Personas
284
+
285
+ Personas set the `from` identity on notification activities. Define them in `config.personas` and reference by name:
286
+
287
+ - **Conversation-level default:** Set `input.persona` to apply to all turns
288
+ - **Per-turn override:** Set `turn.persona` to override for a specific turn
289
+ - **Persona fields:** `id` (required), `name` (required), `email` (optional — used as `from.id` when present)
290
+
291
+ ### Turn Result Statuses
292
+
293
+ | Status | Description |
294
+ | ----------- | ----------------------------------------------- |
295
+ | `Completed` | Bot responded successfully |
296
+ | `TimedOut` | No response within the configured timeout |
297
+ | `Errored` | An error occurred during execution |
298
+ | `Skipped` | Turn was skipped because a previous turn failed |
299
+
300
+ ### Example: curl
301
+
302
+ ```bash
303
+ curl http://localhost:9000/health
304
+
305
+ curl -X POST http://localhost:9000/run-conversation \
306
+ -H "Content-Type: application/json" \
307
+ -d '{
308
+ "config": { "botEndpoint": "http://localhost:3978/api/messages" },
309
+ "scenario": "smoke-test",
310
+ "input": { "turns": [{ "test_id": "t1", "prompt": "Hello" }] }
311
+ }'
312
+ ```
313
+
314
+ ### Example: Python
315
+
316
+ ```python
317
+ import requests
318
+
319
+ resp = requests.post("http://localhost:9000/run-conversation", json={
320
+ "config": {"botEndpoint": "http://localhost:3978/api/messages"},
321
+ "scenario": "my-test",
322
+ "input": {
323
+ "turns": [
324
+ {"test_id": "t1", "prompt": "Hello"},
325
+ {"test_id": "t2", "prompt": "What can you do?"},
326
+ ]
327
+ },
328
+ })
329
+
330
+ for turn in resp.json()["turns"]:
331
+ print(f"[{turn['status']}] {turn['actual_response'][:80]}")
332
+ ```
333
+
334
+ ### `GET /health`
335
+
336
+ Returns `{ "status": "ok" }`.
337
+
338
+ ## Example Project
339
+
340
+ See `samples/agents-simulator-example` for a complete example with Mocha tests and a sanity-test script.
341
+
@@ -0,0 +1,18 @@
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
+ export interface CardValidationError {
8
+ /** Human-readable description of the issue */
9
+ message: string;
10
+ /** "parsing" | "schema" */
11
+ phase: string;
12
+ }
13
+ /**
14
+ * Validate a raw Adaptive Card JSON payload.
15
+ * Returns an empty array if the card is valid.
16
+ */
17
+ export declare function validateAdaptiveCard(payload: Record<string, unknown>): CardValidationError[];
18
+ //# sourceMappingURL=cardValidator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cardValidator.d.ts","sourceRoot":"","sources":["../src/cardValidator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,mBAAmB;IAClC,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAElC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,mBAAmB,EAAE,CAgCvB"}
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ /**
3
+ * Adaptive Card schema validation using the official `adaptivecards` package.
4
+ *
5
+ * Validates that a card payload is a well-formed Adaptive Card that Teams can
6
+ * parse. Does NOT check visual rendering — only structural/schema correctness.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.validateAdaptiveCard = void 0;
10
+ const adaptivecards_1 = require("adaptivecards");
11
+ /**
12
+ * Validate a raw Adaptive Card JSON payload.
13
+ * Returns an empty array if the card is valid.
14
+ */
15
+ function validateAdaptiveCard(
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ payload) {
18
+ const errors = [];
19
+ // Basic type guard: must have type === "AdaptiveCard"
20
+ if (payload.type !== "AdaptiveCard") {
21
+ errors.push({
22
+ message: `Expected type "AdaptiveCard", got "${payload.type ?? "(missing)"}"`,
23
+ phase: "schema",
24
+ });
25
+ return errors; // No point parsing further
26
+ }
27
+ try {
28
+ const card = new adaptivecards_1.AdaptiveCard();
29
+ const context = new adaptivecards_1.SerializationContext();
30
+ card.parse(payload, context);
31
+ for (let i = 0; i < context.eventCount; i++) {
32
+ const event = context.getEventAt(i);
33
+ errors.push({
34
+ message: event.message ?? String(event),
35
+ phase: event.phase !== undefined ? String(event.phase) : "parsing",
36
+ });
37
+ }
38
+ }
39
+ catch (err) {
40
+ errors.push({
41
+ message: err instanceof Error ? err.message : String(err),
42
+ phase: "parsing",
43
+ });
44
+ }
45
+ return errors;
46
+ }
47
+ exports.validateAdaptiveCard = validateAdaptiveCard;
@@ -0,0 +1,29 @@
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
+ export interface ConversationServerOptions {
12
+ /** Port to listen on. Default: 0 (OS-assigned). */
13
+ port?: number;
14
+ /** Host to bind to. Default: "127.0.0.1". */
15
+ host?: string;
16
+ }
17
+ export interface ConversationServer {
18
+ /** The port the server is listening on. */
19
+ port: number;
20
+ /** Gracefully shut down the server. */
21
+ close: () => Promise<void>;
22
+ }
23
+ /**
24
+ * Create and start a conversation server.
25
+ *
26
+ * Returns a handle with the assigned port and a close() method.
27
+ */
28
+ export declare function createConversationServer(options?: ConversationServerOptions): Promise<ConversationServer>;
29
+ //# sourceMappingURL=conversationServer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversationServer.d.ts","sourceRoot":"","sources":["../src/conversationServer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAUH,MAAM,WAAW,yBAAyB;IACxC,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AA4ED;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,CAAC,EAAE,yBAAyB,GAClC,OAAO,CAAC,kBAAkB,CAAC,CAgC7B"}
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ /**
3
+ * HTTP server for multi-turn conversation execution.
4
+ *
5
+ * Provides a factory function that creates an HTTP server with:
6
+ * POST /run-conversation — execute a multi-turn conversation
7
+ * GET /health — health check
8
+ *
9
+ * All diagnostic logging goes to stderr. The returned port can be
10
+ * written to stdout by the caller (CLI wrapper).
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createConversationServer = void 0;
37
+ const http = __importStar(require("http"));
38
+ const runConversation_1 = require("./runConversation");
39
+ // ─────────────────────────────────────────────────────────────────────────────
40
+ // Request body parsing
41
+ // ─────────────────────────────────────────────────────────────────────────────
42
+ function readBody(req) {
43
+ return new Promise((resolve, reject) => {
44
+ const chunks = [];
45
+ req.on("data", (chunk) => chunks.push(chunk));
46
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
47
+ req.on("error", reject);
48
+ });
49
+ }
50
+ async function handleRequest(req, res) {
51
+ const url = req.url ?? "";
52
+ const method = req.method ?? "";
53
+ if (method === "GET" && url === "/health") {
54
+ res.writeHead(200, { "Content-Type": "application/json" });
55
+ res.end(JSON.stringify({ status: "ok" }));
56
+ return;
57
+ }
58
+ if (method === "POST" && url === "/run-conversation") {
59
+ let body;
60
+ try {
61
+ const raw = await readBody(req);
62
+ body = JSON.parse(raw);
63
+ }
64
+ catch (err) {
65
+ const message = err instanceof Error ? err.message : String(err);
66
+ res.writeHead(400, { "Content-Type": "application/json" });
67
+ res.end(JSON.stringify({ error: `Invalid JSON: ${message}` }));
68
+ return;
69
+ }
70
+ if (!body.config || !body.scenario || !body.input) {
71
+ res.writeHead(400, { "Content-Type": "application/json" });
72
+ res.end(JSON.stringify({ error: "Missing required fields: config, scenario, input" }));
73
+ return;
74
+ }
75
+ try {
76
+ (0, runConversation_1.log)(`HTTP request: ${body.scenario}`);
77
+ const result = await (0, runConversation_1.runConversation)(body.config, body.scenario, body.input);
78
+ res.writeHead(200, { "Content-Type": "application/json" });
79
+ res.end(JSON.stringify(result));
80
+ }
81
+ catch (err) {
82
+ const message = err instanceof Error ? err.message : String(err);
83
+ (0, runConversation_1.logError)(`Unhandled error in runConversation: ${message}`);
84
+ res.writeHead(500, { "Content-Type": "application/json" });
85
+ res.end(JSON.stringify({ error: message }));
86
+ }
87
+ return;
88
+ }
89
+ res.writeHead(404, { "Content-Type": "application/json" });
90
+ res.end(JSON.stringify({ error: "Not found" }));
91
+ }
92
+ // ─────────────────────────────────────────────────────────────────────────────
93
+ // Factory
94
+ // ─────────────────────────────────────────────────────────────────────────────
95
+ /**
96
+ * Create and start a conversation server.
97
+ *
98
+ * Returns a handle with the assigned port and a close() method.
99
+ */
100
+ async function createConversationServer(options) {
101
+ const port = options?.port ?? 0;
102
+ const host = options?.host ?? "127.0.0.1";
103
+ const server = http.createServer((req, res) => {
104
+ handleRequest(req, res).catch((err) => {
105
+ (0, runConversation_1.logError)(`Request handler crashed: ${String(err)}`);
106
+ if (!res.headersSent) {
107
+ res.writeHead(500, { "Content-Type": "application/json" });
108
+ }
109
+ res.end(JSON.stringify({ error: "Internal server error" }));
110
+ });
111
+ });
112
+ return new Promise((resolve, reject) => {
113
+ server.once("error", reject);
114
+ server.listen(port, host, () => {
115
+ const addr = server.address();
116
+ const assignedPort = typeof addr === "object" && addr ? addr.port : port;
117
+ (0, runConversation_1.log)(`Server listening on http://${host}:${assignedPort}`);
118
+ resolve({
119
+ port: assignedPort,
120
+ close: () => new Promise((res, rej) => {
121
+ server.close((err) => (err ? rej(err) : res()));
122
+ }),
123
+ });
124
+ });
125
+ });
126
+ }
127
+ exports.createConversationServer = createConversationServer;