|
1 | 1 | # Voiceflow CLI API Server |
2 | 2 |
|
3 | | -The Voiceflow CLI now includes an HTTP API server that exposes test execution functionality as REST endpoints with auto-generated OpenAPI/Swagger documentation. |
| 3 | +The Voiceflow CLI now includes an HTTP API server that exposes test execution functionality as REST endpoints with auto-generated OpenAPI/Swagger documentation and **WebSocket support for real-time log streaming**. |
4 | 4 |
|
5 | 5 | ## Features |
6 | 6 |
|
7 | 7 | - **HTTP API**: Execute test suites via REST endpoints |
| 8 | +- **WebSocket API**: Execute, cancel, and monitor test suites over a persistent WebSocket connection with real-time log streaming |
8 | 9 | - **Real-time Logging**: Capture and return test execution logs in API responses |
9 | 10 | - **OpenAPI/Swagger**: Auto-generated API documentation at `/swagger/index.html` |
10 | 11 | - **Asynchronous Execution**: Non-blocking test execution with status tracking |
@@ -140,6 +141,157 @@ Once the server is running, you can access the interactive API documentation at: |
140 | 141 | http://localhost:8080/swagger/index.html |
141 | 142 | ``` |
142 | 143 |
|
| 144 | +## WebSocket API |
| 145 | + |
| 146 | +Connect to `ws://localhost:8080/ws` for a persistent connection with real-time log streaming. The WebSocket endpoint exposes the same functionality as the REST API but pushes log lines as they happen instead of requiring polling. |
| 147 | + |
| 148 | +### Protocol |
| 149 | + |
| 150 | +All messages are JSON objects. |
| 151 | + |
| 152 | +**Client → Server:** |
| 153 | + |
| 154 | +| Action | Description | Fields | |
| 155 | +|--------|-------------|--------| |
| 156 | +| `execute` | Start a test suite | `{ "action": "execute", "data": <TestExecutionRequest> }` | |
| 157 | +| `cancel` | Cancel a running execution | `{ "action": "cancel", "id": "<execution-id>" }` | |
| 158 | +| `status` | Get execution status | `{ "action": "status", "id": "<execution-id>" }` | |
| 159 | + |
| 160 | +**Server → Client:** |
| 161 | + |
| 162 | +| Type | Description | |
| 163 | +|------|-------------| |
| 164 | +| `log` | Real-time log line: `{ "type": "log", "id": "...", "message": "..." }` | |
| 165 | +| `status` | Status update: `{ "type": "status", "id": "...", "data": <TestStatusResponse> }` | |
| 166 | +| `result` | Final result when execution finishes: `{ "type": "result", "id": "...", "data": <TestStatusResponse> }` | |
| 167 | +| `error` | Error message: `{ "type": "error", "message": "..." }` | |
| 168 | + |
| 169 | +### WebSocket Examples |
| 170 | + |
| 171 | +#### Using websocat (CLI) |
| 172 | + |
| 173 | +```bash |
| 174 | +# Install: brew install websocat |
| 175 | +websocat ws://localhost:8080/ws |
| 176 | +``` |
| 177 | + |
| 178 | +Then paste JSON messages: |
| 179 | + |
| 180 | +```json |
| 181 | +{"action":"execute","data":{"suite":{"name":"Example Suite","description":"Test","environment_name":"production","tests":[{"id":"test_1","test":{"name":"Example test","description":"Test","interactions":[{"id":"t1","user":{"type":"text","text":"hi"},"agent":{"validate":[{"type":"contains","value":"hello"}]}}]}}]}}} |
| 182 | +``` |
| 183 | + |
| 184 | +Cancel a running execution: |
| 185 | + |
| 186 | +```json |
| 187 | +{"action":"cancel","id":"<execution-id-from-status-message>"} |
| 188 | +``` |
| 189 | + |
| 190 | +#### Using JavaScript |
| 191 | + |
| 192 | +```javascript |
| 193 | +const ws = new WebSocket('ws://localhost:8080/ws'); |
| 194 | + |
| 195 | +ws.onopen = () => { |
| 196 | + // Execute a test suite |
| 197 | + ws.send(JSON.stringify({ |
| 198 | + action: 'execute', |
| 199 | + data: { |
| 200 | + api_key: 'your_api_key', |
| 201 | + suite: { |
| 202 | + name: 'Example Suite', |
| 203 | + description: 'Suite used as an example', |
| 204 | + environment_name: 'production', |
| 205 | + tests: [ |
| 206 | + { |
| 207 | + id: 'test_1', |
| 208 | + test: { |
| 209 | + name: 'Example test', |
| 210 | + description: 'These are some tests', |
| 211 | + interactions: [ |
| 212 | + { |
| 213 | + id: 'test_1_1', |
| 214 | + user: { type: 'text', text: 'hi' }, |
| 215 | + agent: { validate: [{ type: 'contains', value: 'hello' }] } |
| 216 | + } |
| 217 | + ] |
| 218 | + } |
| 219 | + } |
| 220 | + ] |
| 221 | + } |
| 222 | + } |
| 223 | + })); |
| 224 | +}; |
| 225 | + |
| 226 | +ws.onmessage = (event) => { |
| 227 | + const msg = JSON.parse(event.data); |
| 228 | + switch (msg.type) { |
| 229 | + case 'log': |
| 230 | + console.log(`[LOG] ${msg.message}`); |
| 231 | + break; |
| 232 | + case 'status': |
| 233 | + console.log(`[STATUS] ${msg.id}: ${msg.message || msg.data.status}`); |
| 234 | + // Cancel example: ws.send(JSON.stringify({ action: 'cancel', id: msg.id })); |
| 235 | + break; |
| 236 | + case 'result': |
| 237 | + console.log('[RESULT]', msg.data); |
| 238 | + break; |
| 239 | + case 'error': |
| 240 | + console.error('[ERROR]', msg.message); |
| 241 | + break; |
| 242 | + } |
| 243 | +}; |
| 244 | +``` |
| 245 | + |
| 246 | +#### Using Python |
| 247 | + |
| 248 | +```python |
| 249 | +import asyncio |
| 250 | +import json |
| 251 | +import websockets |
| 252 | + |
| 253 | +async def run_tests(): |
| 254 | + async with websockets.connect('ws://localhost:8080/ws') as ws: |
| 255 | + # Execute a test suite |
| 256 | + await ws.send(json.dumps({ |
| 257 | + 'action': 'execute', |
| 258 | + 'data': { |
| 259 | + 'api_key': 'your_api_key', |
| 260 | + 'suite': { |
| 261 | + 'name': 'Example Suite', |
| 262 | + 'description': 'Test', |
| 263 | + 'environment_name': 'production', |
| 264 | + 'tests': [{ |
| 265 | + 'id': 'test_1', |
| 266 | + 'test': { |
| 267 | + 'name': 'Example test', |
| 268 | + 'description': 'Test', |
| 269 | + 'interactions': [{ |
| 270 | + 'id': 't1', |
| 271 | + 'user': {'type': 'text', 'text': 'hi'}, |
| 272 | + 'agent': {'validate': [{'type': 'contains', 'value': 'hello'}]} |
| 273 | + }] |
| 274 | + } |
| 275 | + }] |
| 276 | + } |
| 277 | + } |
| 278 | + })) |
| 279 | + |
| 280 | + # Read messages until execution completes |
| 281 | + async for message in ws: |
| 282 | + msg = json.loads(message) |
| 283 | + if msg['type'] == 'log': |
| 284 | + print(f"[LOG] {msg['message']}") |
| 285 | + elif msg['type'] == 'result': |
| 286 | + print(f"[RESULT] {msg['data']}") |
| 287 | + break |
| 288 | + elif msg['type'] == 'error': |
| 289 | + print(f"[ERROR] {msg['message']}") |
| 290 | + break |
| 291 | + |
| 292 | +asyncio.run(run_tests()) |
| 293 | +``` |
| 294 | + |
143 | 295 | ## Usage Examples |
144 | 296 |
|
145 | 297 | ### Using curl |
|
0 commit comments