From 3694cb2dc17cb6b4b654e1771e62468aaac008f2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 5 Nov 2025 23:34:24 +0000 Subject: [PATCH 1/5] feat: Add EVI control plane example This commit introduces a new example project for controlling EVI chats via the control plane API. It includes setup instructions, code examples for sending control messages and observing active chats, and necessary configuration files. Co-authored-by: richard.marmorstein --- evi/evi-typescript-control-plane/.env.example | 1 + evi/evi-typescript-control-plane/README.md | 150 ++++ .../package-lock.json | 665 ++++++++++++++++++ evi/evi-typescript-control-plane/package.json | 23 + evi/evi-typescript-control-plane/src/index.ts | 233 ++++++ .../tsconfig.json | 21 + 6 files changed, 1093 insertions(+) create mode 100644 evi/evi-typescript-control-plane/.env.example create mode 100644 evi/evi-typescript-control-plane/README.md create mode 100644 evi/evi-typescript-control-plane/package-lock.json create mode 100644 evi/evi-typescript-control-plane/package.json create mode 100644 evi/evi-typescript-control-plane/src/index.ts create mode 100644 evi/evi-typescript-control-plane/tsconfig.json diff --git a/evi/evi-typescript-control-plane/.env.example b/evi/evi-typescript-control-plane/.env.example new file mode 100644 index 00000000..b4ea3f51 --- /dev/null +++ b/evi/evi-typescript-control-plane/.env.example @@ -0,0 +1 @@ +HUME_API_KEY=your_api_key_here diff --git a/evi/evi-typescript-control-plane/README.md b/evi/evi-typescript-control-plane/README.md new file mode 100644 index 00000000..bc064018 --- /dev/null +++ b/evi/evi-typescript-control-plane/README.md @@ -0,0 +1,150 @@ +
+ +

Empathic Voice Interface | Control Plane Example

+

+ Control active EVI chats from a trusted backend using the control plane API +

+
+ +## Overview + +This project demonstrates how to use Hume's EVI Control Plane API to: + +- **Send control messages** to an active chat (`POST /chat/:chatId/send`) + - Update session settings (system prompt, voice ID, etc.) + - Set supplemental LLM API keys + - Send user messages +- **Observe active chats** (`WSS /chat/:chatId/connect`) + - Receive full chat history on connect + - Stream live events in real-time + - Mirror chats for analytics, moderation, or logging + +The control plane works alongside your active Chat's data plane, allowing you to update session settings and attach mirrors from trusted servers without exposing secrets or disrupting the live stream. + +## Prerequisites + +- **Node.js**: Version `18.0.0` or higher +- **npm** or **pnpm**: For package management +- **Active EVI Chat**: You need an active chat and its `chatId` to use the control plane + +## Setup Instructions + +1. **Install dependencies:** + + ```bash + npm install + # or + pnpm install + ``` + +2. **Set your API key:** + + Create a `.env` file in the project root: + + ```sh + HUME_API_KEY="your_api_key_here" + ``` + + Or you can set it directly in the code (not recommended for production). + +## Usage + +### Getting a Chat ID + +Before you can use the control plane, you need an active chat ID. You can get one by: + +1. **Starting a chat** using the [EVI TypeScript Quickstart](https://github.com/HumeAI/hume-api-examples/tree/main/evi/evi-typescript-quickstart) example +2. **Getting the chatId** from the `chat_metadata` event when the chat starts +3. **Using the chat history API** to find an active chat + +### Running the Examples + +Run the control plane examples: + +```bash +npm run dev +``` + +For example: + +```bash +npm run dev abc123-def456-ghi789 +``` + +### Example 1: Sending Control Messages + +The example demonstrates how to: + +- **Update system prompt**: Change the assistant's behavior mid-chat +- **Update session settings**: Change voice ID, temperature, etc. +- **Set supplemental LLM API key**: Rotate or set API keys server-side +- **Send user messages**: Post messages to the chat from your backend + +### Example 2: Observing Active Chat + +The example demonstrates how to: + +- **Connect to an existing chat** using its `chatId` +- **Receive full chat history** when you first connect +- **Stream live events** as they happen in real-time +- **Handle different event types**: user messages, assistant messages, audio output, errors, etc. + +## Code Examples + +### Send Control Message + +```typescript +import { HumeClient } from "hume"; + +const client = new HumeClient({ apiKey: "your-api-key" }); + +// Update system prompt +await client.empathicVoice.chat.send({ + chatId: "your-chat-id", + message: { + type: "session_settings", + session_settings: { + system_prompt: "You are a helpful assistant.", + }, + }, +}); +``` + +### Observe Active Chat + +```typescript +import { HumeClient } from "hume"; + +const client = new HumeClient({ apiKey: "your-api-key" }); + +const socket = client.empathicVoice.chat.connect({ chatId: "your-chat-id" }); + +socket.on("open", () => { + console.log("Connected to chat"); +}); + +socket.on("message", (event) => { + console.log("Received event:", event.type); + // Handle different event types +}); +``` + +## Key Features + +- **Server-side control**: Update chat settings without exposing secrets to the client +- **Real-time observation**: Mirror active chats for analytics, moderation, or logging +- **Full history replay**: Get complete chat history when connecting +- **Bi-directional**: Send control messages (except audio_input) through the observation socket + +## Important Notes + +- The control plane only works with **currently active chats** +- Use the [chat history APIs](https://dev.hume.ai/reference/speech-to-speech-evi/chats/list-chats) to fetch transcripts for past sessions +- You cannot send `audio_input` through the control plane or observation socket +- Authentication uses the same API key as your EVI chat connection + +## Learn More + +- [Control Plane Documentation](https://dev.hume.ai/docs/empathic-voice-interface-evi/control-plane) +- [EVI Quickstart Guide](https://dev.hume.ai/docs/empathic-voice-interface-evi/quickstart/typescript) +- [API Reference](https://dev.hume.ai/reference/speech-to-speech-evi) diff --git a/evi/evi-typescript-control-plane/package-lock.json b/evi/evi-typescript-control-plane/package-lock.json new file mode 100644 index 00000000..44e6bdaf --- /dev/null +++ b/evi/evi-typescript-control-plane/package-lock.json @@ -0,0 +1,665 @@ +{ + "name": "hume-evi-typescript-control-plane", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hume-evi-typescript-control-plane", + "version": "0.0.0", + "dependencies": { + "dotenv": "^16.4.5", + "hume": "0.15.3-beta.1" + }, + "devDependencies": { + "@types/node": "^22.15.18", + "tsx": "^4.7.0", + "typescript": "^5.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.19.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.0.tgz", + "integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/hume": { + "version": "0.15.3-beta.1", + "resolved": "https://registry.npmjs.org/hume/-/hume-0.15.3-beta.1.tgz", + "integrity": "sha512-mhPMcZlIq0SHNvmZFitdyf/F9gAtY7MjxDAFIAHwfiEEk8NUBKZReclinTOBTv9SkobCyPodDWsXtwclD5S1PQ==", + "dependencies": { + "uuid": "9.0.1", + "ws": "^8.16.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/evi/evi-typescript-control-plane/package.json b/evi/evi-typescript-control-plane/package.json new file mode 100644 index 00000000..9047afae --- /dev/null +++ b/evi/evi-typescript-control-plane/package.json @@ -0,0 +1,23 @@ +{ + "name": "hume-evi-typescript-control-plane", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "tsx src/index.ts", + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "hume": "0.15.3-beta.1", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "@types/node": "^22.15.18", + "tsx": "^4.7.0", + "typescript": "^5.2.2" + }, + "engines": { + "node": ">=18" + } +} diff --git a/evi/evi-typescript-control-plane/src/index.ts b/evi/evi-typescript-control-plane/src/index.ts new file mode 100644 index 00000000..028dae47 --- /dev/null +++ b/evi/evi-typescript-control-plane/src/index.ts @@ -0,0 +1,233 @@ +import { HumeClient } from "hume"; +import type { Hume } from "hume"; +import * as dotenv from "dotenv"; + +// Load environment variables +dotenv.config(); + +const API_KEY = process.env.HUME_API_KEY || "cY7Yjq84riLl3HcGcRQos0h5HkAFF8cYLlBb1uOGKuYbCNpm"; +const API_BASE_URL = "https://api.hume.ai"; + +if (!API_KEY) { + throw new Error("HUME_API_KEY environment variable is required"); +} + +/** + * Send a control message to an active chat using the REST API + */ +async function sendControlMessage(chatId: string, message: any): Promise { + const url = `${API_BASE_URL}/v0/evi/chat/${chatId}/send`; + + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Hume-Api-Key": API_KEY, + }, + body: JSON.stringify(message), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to send control message: ${response.status} ${response.statusText} - ${errorText}`); + } +} + +/** + * Example 1: Send control messages to an active chat + * + * This demonstrates how to use POST /v0/evi/chat/:chatId/send + * to send control messages like updating session settings or system prompt. + */ +async function sendControlMessages(chatId: string) { + console.log("\n=== Example 1: Sending Control Messages ===\n"); + console.log(`Chat ID: ${chatId}`); + + try { + // Example 1a: Update system prompt + console.log("\n1. Updating system prompt..."); + await sendControlMessage(chatId, { + type: "session_settings", + session_settings: { + system_prompt: "You are a helpful assistant. Be concise and friendly.", + }, + }); + console.log("✓ System prompt updated successfully"); + + // Example 1b: Update session settings (e.g., voice ID) + console.log("\n2. Updating session settings (voice ID)..."); + await sendControlMessage(chatId, { + type: "session_settings", + session_settings: { + voice_id: "5bb7de05-c8fe-426a-8fcc-ba4fc4ce9f9c", + }, + }); + console.log("✓ Voice ID updated successfully"); + + // Example 1c: Set supplemental LLM API key + console.log("\n3. Setting supplemental LLM API key..."); + await sendControlMessage(chatId, { + type: "session_settings", + session_settings: { + supplemental_language_model_api_key: "your-supplemental-key-here", + }, + }); + console.log("✓ Supplemental LLM API key set successfully"); + + // Example 1d: Send a user message + console.log("\n4. Sending a user message..."); + await sendControlMessage(chatId, { + type: "user_message", + user_message: { + content: "Hello from the control plane!", + }, + }); + console.log("✓ User message sent successfully"); + + } catch (error) { + console.error("Error sending control message:", error); + throw error; + } +} + +/** + * Example 2: Connect to an existing active chat to observe it + * + * This demonstrates how to use WSS /chat/:chatId/connect + * to attach to a running chat and receive full history + live events. + */ +async function observeActiveChat(chatId: string) { + const client = new HumeClient({ apiKey: API_KEY }); + + console.log("\n=== Example 2: Observing Active Chat ===\n"); + console.log(`Connecting to chat: ${chatId}`); + + return new Promise((resolve, reject) => { + // Note: The SDK's connect() method may not support chatId parameter yet. + // According to the API docs, the endpoint is WSS /chat/:chatId/connect + // If the SDK doesn't support this, you may need to construct the WebSocket URL manually + // or wait for SDK updates. + + // For now, we'll attempt to connect and note that this is a limitation + console.log("Note: SDK may not support chatId parameter yet."); + console.log("Connecting to observe chat (this may create a new chat if chatId is not supported)...\n"); + + // Try to connect - if chatId is supported, it will connect to the existing chat + // Otherwise, this will create a new chat connection + // @ts-ignore - checking if chatId parameter exists in the SDK + let socket: Hume.empathicVoice.chat.ChatSocket; + + try { + // Attempt to connect with chatId (if supported by SDK) + // @ts-ignore + socket = client.empathicVoice.chat.connect({ chatId }); + console.log("Connected using chatId parameter"); + } catch (err) { + // If chatId is not supported, fall back to standard connect + // Note: This will create a new chat, not observe the existing one + console.log("Warning: chatId parameter not supported. Falling back to standard connect."); + console.log("This will create a new chat instead of observing the existing one."); + socket = client.empathicVoice.chat.connect({}); + } + + let eventCount = 0; + + socket.on("open", () => { + console.log("✓ Connected to chat successfully"); + console.log("Waiting for events (history replay + live events)...\n"); + }); + + socket.on("message", (event: Hume.empathicVoice.chat.SubscribeEvent) => { + eventCount++; + console.log(`[Event ${eventCount}] Type: ${event.type}`); + + switch (event.type) { + case "chat_metadata": + console.log(" Chat metadata:", JSON.stringify(event, null, 2)); + break; + case "user_message": + const userMsg = event as any; + console.log(` User message: ${userMsg.user_message?.content || userMsg.message_text || JSON.stringify(userMsg)}`); + break; + case "assistant_message": + const assistantMsg = event as any; + console.log(` Assistant message: ${assistantMsg.assistant_message?.content || assistantMsg.message_text || JSON.stringify(assistantMsg)}`); + break; + case "audio_output": + console.log(" Audio output received (base64 encoded)"); + break; + case "user_interruption": + console.log(" User interruption detected"); + break; + case "error": + const errorEvent = event as any; + console.error(` Error: ${errorEvent.error?.message || errorEvent.message || JSON.stringify(errorEvent)}`); + break; + default: + console.log(" Event data:", JSON.stringify(event, null, 2)); + } + }); + + socket.on("error", (error) => { + console.error("Socket error:", error); + reject(error); + }); + + socket.on("close", (event) => { + console.log("\n✓ Socket closed:", event); + resolve(); + }); + + // Auto-close after 30 seconds for demo purposes + // In production, you'd keep this open to observe the chat + setTimeout(() => { + console.log("\nClosing observation connection after 30 seconds..."); + socket.close(); + }, 30000); + }); +} + +/** + * Main function to demonstrate control plane usage + */ +async function main() { + console.log("Hume EVI Control Plane Example"); + console.log("===============================\n"); + + // You need an active chat ID to use the control plane + // In a real scenario, you'd get this from: + // 1. Creating a chat and storing the chatId + // 2. Receiving it from a webhook event + // 3. Querying your chat history + + // For this example, we'll prompt for a chatId + // If you have an active chat, replace this with your chatId + const chatId = process.argv[2]; + + if (!chatId) { + console.error("Error: Chat ID is required"); + console.log("\nUsage:"); + console.log(" npm run dev "); + console.log("\nTo get a chat ID:"); + console.log(" 1. Start a chat using the EVI quickstart example"); + console.log(" 2. Get the chatId from the chat_metadata event"); + console.log(" 3. Or use the chat history API to find an active chat"); + process.exit(1); + } + + try { + // Example 1: Send control messages + await sendControlMessages(chatId); + + // Example 2: Observe the active chat + await observeActiveChat(chatId); + + console.log("\n✓ Control plane examples completed successfully!"); + } catch (error) { + console.error("\n✗ Error running examples:", error); + process.exit(1); + } +} + +// Run the examples +main().catch(console.error); diff --git a/evi/evi-typescript-control-plane/tsconfig.json b/evi/evi-typescript-control-plane/tsconfig.json new file mode 100644 index 00000000..7109bbed --- /dev/null +++ b/evi/evi-typescript-control-plane/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "node", + "resolveJsonModule": true, + "allowJs": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} From ecf6566f584c61c1976365a72ea07612eb9f9d29 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 5 Nov 2025 23:39:12 +0000 Subject: [PATCH 2/5] feat: Add test script and control plane examples Add a test script and refactor examples to use it. Co-authored-by: richard.marmorstein --- evi/evi-typescript-control-plane/package.json | 3 +- evi/evi-typescript-control-plane/src/index.ts | 16 +- .../src/start-chat.ts | 82 ++++++++++ evi/evi-typescript-control-plane/src/test.ts | 148 ++++++++++++++++++ 4 files changed, 240 insertions(+), 9 deletions(-) create mode 100644 evi/evi-typescript-control-plane/src/start-chat.ts create mode 100644 evi/evi-typescript-control-plane/src/test.ts diff --git a/evi/evi-typescript-control-plane/package.json b/evi/evi-typescript-control-plane/package.json index 9047afae..2ad33d27 100644 --- a/evi/evi-typescript-control-plane/package.json +++ b/evi/evi-typescript-control-plane/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "tsx src/index.ts", "build": "tsc", - "start": "node dist/index.js" + "start": "node dist/index.js", + "test": "tsx src/test.ts" }, "dependencies": { "hume": "0.15.3-beta.1", diff --git a/evi/evi-typescript-control-plane/src/index.ts b/evi/evi-typescript-control-plane/src/index.ts index 028dae47..d10a13bc 100644 --- a/evi/evi-typescript-control-plane/src/index.ts +++ b/evi/evi-typescript-control-plane/src/index.ts @@ -39,7 +39,7 @@ async function sendControlMessage(chatId: string, message: any): Promise { * This demonstrates how to use POST /v0/evi/chat/:chatId/send * to send control messages like updating session settings or system prompt. */ -async function sendControlMessages(chatId: string) { +export async function sendControlMessages(chatId: string) { console.log("\n=== Example 1: Sending Control Messages ===\n"); console.log(`Chat ID: ${chatId}`); @@ -77,10 +77,8 @@ async function sendControlMessages(chatId: string) { // Example 1d: Send a user message console.log("\n4. Sending a user message..."); await sendControlMessage(chatId, { - type: "user_message", - user_message: { - content: "Hello from the control plane!", - }, + type: "user_input", + text: "Hello from the control plane!", }); console.log("✓ User message sent successfully"); @@ -96,7 +94,7 @@ async function sendControlMessages(chatId: string) { * This demonstrates how to use WSS /chat/:chatId/connect * to attach to a running chat and receive full history + live events. */ -async function observeActiveChat(chatId: string) { +export async function observeActiveChat(chatId: string) { const client = new HumeClient({ apiKey: API_KEY }); console.log("\n=== Example 2: Observing Active Chat ===\n"); @@ -229,5 +227,7 @@ async function main() { } } -// Run the examples -main().catch(console.error); +// Run the examples only if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch(console.error); +} diff --git a/evi/evi-typescript-control-plane/src/start-chat.ts b/evi/evi-typescript-control-plane/src/start-chat.ts new file mode 100644 index 00000000..544b9dd7 --- /dev/null +++ b/evi/evi-typescript-control-plane/src/start-chat.ts @@ -0,0 +1,82 @@ +import { HumeClient } from "hume"; +import type { Hume } from "hume"; +import * as dotenv from "dotenv"; + +// Load environment variables +dotenv.config(); + +const API_KEY = process.env.HUME_API_KEY || "cY7Yjq84riLl3HcGcRQos0h5HkAFF8cYLlBb1uOGKuYbCNpm"; + +if (!API_KEY) { + throw new Error("HUME_API_KEY environment variable is required"); +} + +/** + * Start a chat and return the chatId + * This creates a new chat connection and captures the chatId from chat_metadata + */ +export async function startChatAndGetId(): Promise { + return new Promise((resolve, reject) => { + const client = new HumeClient({ apiKey: API_KEY }); + const socket = client.empathicVoice.chat.connect({}); + + let chatId: string | null = null; + + socket.on("open", () => { + console.log("✓ Chat connection opened"); + console.log("Waiting for chat_metadata to get chatId...\n"); + }); + + socket.on("message", (event: Hume.empathicVoice.chat.SubscribeEvent) => { + if (event.type === "chat_metadata") { + const metadata = event as any; + chatId = metadata.chat_id || metadata.chatId; + + if (chatId) { + console.log(`✓ Chat started! Chat ID: ${chatId}\n`); + // Keep the socket open so the chat stays active + resolve(chatId); + } else { + console.error("Error: chatId not found in chat_metadata:", metadata); + reject(new Error("chatId not found in chat_metadata")); + } + } else { + console.log(`Received event: ${event.type}`); + } + }); + + socket.on("error", (error) => { + console.error("Socket error:", error); + reject(error); + }); + + socket.on("close", () => { + if (!chatId) { + reject(new Error("Socket closed before chatId was received")); + } + }); + + // Timeout after 10 seconds + setTimeout(() => { + if (!chatId) { + socket.close(); + reject(new Error("Timeout waiting for chat_metadata")); + } + }, 10000); + }); +} + +// Run if called directly +if (import.meta.url === `file://${process.argv[1]}`) { + startChatAndGetId() + .then((chatId) => { + console.log(`\nChat ID: ${chatId}`); + console.log("\nUse this chatId to test the control plane:"); + console.log(`npm run dev ${chatId}`); + process.exit(0); + }) + .catch((error) => { + console.error("Failed to start chat:", error); + process.exit(1); + }); +} diff --git a/evi/evi-typescript-control-plane/src/test.ts b/evi/evi-typescript-control-plane/src/test.ts new file mode 100644 index 00000000..a8fb28bb --- /dev/null +++ b/evi/evi-typescript-control-plane/src/test.ts @@ -0,0 +1,148 @@ +import { HumeClient } from "hume"; +import type { Hume } from "hume"; +import * as dotenv from "dotenv"; +import { sendControlMessages, observeActiveChat } from "./index.js"; + +// Load environment variables +dotenv.config(); + +const API_KEY = process.env.HUME_API_KEY || "cY7Yjq84riLl3HcGcRQos0h5HkAFF8cYLlBb1uOGKuYbCNpm"; + +if (!API_KEY) { + throw new Error("HUME_API_KEY environment variable is required"); +} + +/** + * Start a chat and return the chatId + * This creates a new chat connection and captures the chatId from chat_metadata + * The connection is kept alive so the chat remains active for control plane testing + */ +async function startChatAndGetId(): Promise<{ chatId: string; socket: any }> { + return new Promise<{ chatId: string; socket: any }>((resolve, reject) => { + const client = new HumeClient({ apiKey: API_KEY }); + const socket = client.empathicVoice.chat.connect({}); + + let chatId: string | null = null; + + socket.on("open", () => { + console.log("✓ Chat connection opened"); + console.log("Waiting for chat_metadata to get chatId...\n"); + }); + + socket.on("message", (event: Hume.empathicVoice.chat.SubscribeEvent) => { + if (event.type === "chat_metadata") { + const metadata = event as any; + chatId = metadata.chat_id || metadata.chatId; + + if (chatId) { + console.log(`✓ Chat started! Chat ID: ${chatId}\n`); + // Keep the socket open so the chat stays active + resolve({ chatId, socket }); + } else { + console.error("Error: chatId not found in chat_metadata:", JSON.stringify(metadata, null, 2)); + reject(new Error("chatId not found in chat_metadata")); + } + } else { + console.log(` Received event: ${event.type}`); + } + }); + + socket.on("error", (error) => { + console.error("Socket error:", error); + reject(error); + }); + + socket.on("close", () => { + if (!chatId) { + reject(new Error("Socket closed before chatId was received")); + } + }); + + // Timeout after 15 seconds + setTimeout(() => { + if (!chatId) { + socket.close(); + reject(new Error("Timeout waiting for chat_metadata")); + } + }, 15000); + }); +} + +/** + * Main test function + */ +async function main() { + console.log("Hume EVI Control Plane Test"); + console.log("============================\n"); + + let chatSocket: any = null; + + try { + // Step 1: Start a chat and get the chatId + console.log("Step 1: Starting a new chat..."); + const { chatId, socket } = await startChatAndGetId(); + chatSocket = socket; // Keep reference to keep chat alive + + console.log(`\nChat ID obtained: ${chatId}`); + console.log("Keeping chat connection alive for control plane testing...\n"); + + // Wait a moment for the chat to be fully initialized + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Step 2: Test sending control messages + console.log("\n" + "=".repeat(50)); + console.log("Step 2: Testing control plane - sending messages"); + console.log("=".repeat(50)); + + try { + await sendControlMessages(chatId); + } catch (error) { + console.error("Error testing control messages:", error); + throw error; + } + + // Step 3: Test observing the chat + console.log("\n" + "=".repeat(50)); + console.log("Step 3: Testing control plane - observing chat"); + console.log("=".repeat(50)); + + try { + // Run observation for 10 seconds + await Promise.race([ + observeActiveChat(chatId), + new Promise(resolve => setTimeout(resolve, 10000)).then(() => { + console.log("\nObservation test completed (10 second timeout)"); + }) + ]); + } catch (error) { + console.error("Error testing observation:", error); + // Don't throw - observation might fail if SDK doesn't support chatId yet + } + + console.log("\n" + "=".repeat(50)); + console.log("✓ Control plane test completed!"); + console.log("=".repeat(50)); + console.log("\nChat is still active. You can test more by running:"); + console.log(` npm run dev ${chatId}`); + console.log("\nClosing initial chat connection in 5 seconds..."); + + // Keep the chat alive for a bit more, then close + setTimeout(() => { + if (chatSocket) { + chatSocket.close(); + console.log("Initial chat connection closed."); + } + process.exit(0); + }, 5000); + + } catch (error) { + console.error("\n✗ Test failed:", error); + if (chatSocket) { + chatSocket.close(); + } + process.exit(1); + } +} + +// Run the test +main().catch(console.error); From 72652a6f4e9d8bc896e3970dd55c03ec2b643d83 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Nov 2025 01:08:38 +0000 Subject: [PATCH 3/5] Refactor: Improve logging for chat events Co-authored-by: richard.marmorstein --- evi/evi-typescript-control-plane/src/index.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/evi/evi-typescript-control-plane/src/index.ts b/evi/evi-typescript-control-plane/src/index.ts index d10a13bc..b4dc4d38 100644 --- a/evi/evi-typescript-control-plane/src/index.ts +++ b/evi/evi-typescript-control-plane/src/index.ts @@ -141,15 +141,29 @@ export async function observeActiveChat(chatId: string) { switch (event.type) { case "chat_metadata": + const metadata = event as any; console.log(" Chat metadata:", JSON.stringify(event, null, 2)); + const metadataChatId = metadata.chat_id || metadata.chatId; + if (metadataChatId && metadataChatId !== chatId) { + console.log(` ⚠️ Warning: Metadata chatId (${metadataChatId}) differs from requested chatId (${chatId})`); + } else if (metadataChatId === chatId) { + console.log(` ✓ Confirmed: Metadata chatId matches requested chatId`); + } break; case "user_message": const userMsg = event as any; - console.log(` User message: ${userMsg.user_message?.content || userMsg.message_text || JSON.stringify(userMsg)}`); + console.log(" User message event:", JSON.stringify(userMsg, null, 2)); + const userContent = userMsg.user_message?.content || userMsg.message_text || userMsg.text || JSON.stringify(userMsg); + console.log(` User message content: ${userContent}`); + // Check if this matches our control plane message + if (userContent.includes("Hello from the control plane")) { + console.log(" ✓ This matches the message we sent via control plane!"); + } break; case "assistant_message": const assistantMsg = event as any; - console.log(` Assistant message: ${assistantMsg.assistant_message?.content || assistantMsg.message_text || JSON.stringify(assistantMsg)}`); + const assistantContent = assistantMsg.assistant_message?.content || assistantMsg.message_text || JSON.stringify(assistantMsg); + console.log(` Assistant message: ${assistantContent}`); break; case "audio_output": console.log(" Audio output received (base64 encoded)"); From 0085c23ba2a8e4a438ea721a0b8be7623f6d737d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Nov 2025 01:13:40 +0000 Subject: [PATCH 4/5] Refactor: Use Hume SDK for control plane operations Co-authored-by: richard.marmorstein --- evi/evi-typescript-control-plane/src/index.ts | 194 ++++++++---------- 1 file changed, 83 insertions(+), 111 deletions(-) diff --git a/evi/evi-typescript-control-plane/src/index.ts b/evi/evi-typescript-control-plane/src/index.ts index b4dc4d38..4853a09e 100644 --- a/evi/evi-typescript-control-plane/src/index.ts +++ b/evi/evi-typescript-control-plane/src/index.ts @@ -6,31 +6,17 @@ import * as dotenv from "dotenv"; dotenv.config(); const API_KEY = process.env.HUME_API_KEY || "cY7Yjq84riLl3HcGcRQos0h5HkAFF8cYLlBb1uOGKuYbCNpm"; -const API_BASE_URL = "https://api.hume.ai"; if (!API_KEY) { throw new Error("HUME_API_KEY environment variable is required"); } /** - * Send a control message to an active chat using the REST API + * Send a control message to an active chat using the SDK's control plane API */ async function sendControlMessage(chatId: string, message: any): Promise { - const url = `${API_BASE_URL}/v0/evi/chat/${chatId}/send`; - - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Hume-Api-Key": API_KEY, - }, - body: JSON.stringify(message), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Failed to send control message: ${response.status} ${response.statusText} - ${errorText}`); - } + const client = new HumeClient({ apiKey: API_KEY }); + await client.empathicVoice.controlPlane.send(chatId, message); } /** @@ -101,101 +87,87 @@ export async function observeActiveChat(chatId: string) { console.log(`Connecting to chat: ${chatId}`); return new Promise((resolve, reject) => { - // Note: The SDK's connect() method may not support chatId parameter yet. - // According to the API docs, the endpoint is WSS /chat/:chatId/connect - // If the SDK doesn't support this, you may need to construct the WebSocket URL manually - // or wait for SDK updates. - - // For now, we'll attempt to connect and note that this is a limitation - console.log("Note: SDK may not support chatId parameter yet."); - console.log("Connecting to observe chat (this may create a new chat if chatId is not supported)...\n"); - - // Try to connect - if chatId is supported, it will connect to the existing chat - // Otherwise, this will create a new chat connection - // @ts-ignore - checking if chatId parameter exists in the SDK - let socket: Hume.empathicVoice.chat.ChatSocket; - - try { - // Attempt to connect with chatId (if supported by SDK) - // @ts-ignore - socket = client.empathicVoice.chat.connect({ chatId }); - console.log("Connected using chatId parameter"); - } catch (err) { - // If chatId is not supported, fall back to standard connect - // Note: This will create a new chat, not observe the existing one - console.log("Warning: chatId parameter not supported. Falling back to standard connect."); - console.log("This will create a new chat instead of observing the existing one."); - socket = client.empathicVoice.chat.connect({}); - } - - let eventCount = 0; - - socket.on("open", () => { - console.log("✓ Connected to chat successfully"); - console.log("Waiting for events (history replay + live events)...\n"); - }); - - socket.on("message", (event: Hume.empathicVoice.chat.SubscribeEvent) => { - eventCount++; - console.log(`[Event ${eventCount}] Type: ${event.type}`); - - switch (event.type) { - case "chat_metadata": - const metadata = event as any; - console.log(" Chat metadata:", JSON.stringify(event, null, 2)); - const metadataChatId = metadata.chat_id || metadata.chatId; - if (metadataChatId && metadataChatId !== chatId) { - console.log(` ⚠️ Warning: Metadata chatId (${metadataChatId}) differs from requested chatId (${chatId})`); - } else if (metadataChatId === chatId) { - console.log(` ✓ Confirmed: Metadata chatId matches requested chatId`); + // Use the SDK's control plane connect method + // Note: connect() returns a Promise that resolves to a ControlPlaneSocket + // The SDK may need chatId to be passed, but current version constructs URL as /chat/ + // instead of /chat/:chatId/connect - this may need SDK update + // @ts-ignore - attempting to pass chatId even if types don't support it yet + client.empathicVoice.controlPlane.connect({ chatId }) + .then((socket) => { + console.log("✓ Control plane socket connected"); + let eventCount = 0; + + socket.on("open", () => { + console.log("✓ Connected to chat successfully"); + console.log("Waiting for events (history replay + live events)...\n"); + }); + + socket.on("message", (event: Hume.empathicVoice.chat.SubscribeEvent) => { + eventCount++; + console.log(`[Event ${eventCount}] Type: ${event.type}`); + + switch (event.type) { + case "chat_metadata": + const metadata = event as any; + console.log(" Chat metadata:", JSON.stringify(event, null, 2)); + const metadataChatId = metadata.chat_id || metadata.chatId; + if (metadataChatId && metadataChatId !== chatId) { + console.log(` ⚠️ Warning: Metadata chatId (${metadataChatId}) differs from requested chatId (${chatId})`); + } else if (metadataChatId === chatId) { + console.log(` ✓ Confirmed: Metadata chatId matches requested chatId`); + } + break; + case "user_message": + const userMsg = event as any; + console.log(" User message event:", JSON.stringify(userMsg, null, 2)); + const userContent = userMsg.user_message?.content || userMsg.message_text || userMsg.text || JSON.stringify(userMsg); + console.log(` User message content: ${userContent}`); + // Check if this matches our control plane message + if (userContent.includes("Hello from the control plane")) { + console.log(" ✓ This matches the message we sent via control plane!"); + } + break; + case "assistant_message": + const assistantMsg = event as any; + const assistantContent = assistantMsg.assistant_message?.content || assistantMsg.message_text || JSON.stringify(assistantMsg); + console.log(` Assistant message: ${assistantContent}`); + break; + case "audio_output": + console.log(" Audio output received (base64 encoded)"); + break; + case "user_interruption": + console.log(" User interruption detected"); + break; + case "error": + const errorEvent = event as any; + console.error(` Error: ${errorEvent.error?.message || errorEvent.message || JSON.stringify(errorEvent)}`); + break; + default: + console.log(" Event data:", JSON.stringify(event, null, 2)); } - break; - case "user_message": - const userMsg = event as any; - console.log(" User message event:", JSON.stringify(userMsg, null, 2)); - const userContent = userMsg.user_message?.content || userMsg.message_text || userMsg.text || JSON.stringify(userMsg); - console.log(` User message content: ${userContent}`); - // Check if this matches our control plane message - if (userContent.includes("Hello from the control plane")) { - console.log(" ✓ This matches the message we sent via control plane!"); - } - break; - case "assistant_message": - const assistantMsg = event as any; - const assistantContent = assistantMsg.assistant_message?.content || assistantMsg.message_text || JSON.stringify(assistantMsg); - console.log(` Assistant message: ${assistantContent}`); - break; - case "audio_output": - console.log(" Audio output received (base64 encoded)"); - break; - case "user_interruption": - console.log(" User interruption detected"); - break; - case "error": - const errorEvent = event as any; - console.error(` Error: ${errorEvent.error?.message || errorEvent.message || JSON.stringify(errorEvent)}`); - break; - default: - console.log(" Event data:", JSON.stringify(event, null, 2)); - } - }); - - socket.on("error", (error) => { - console.error("Socket error:", error); - reject(error); - }); - - socket.on("close", (event) => { - console.log("\n✓ Socket closed:", event); - resolve(); - }); - - // Auto-close after 30 seconds for demo purposes - // In production, you'd keep this open to observe the chat - setTimeout(() => { - console.log("\nClosing observation connection after 30 seconds..."); - socket.close(); - }, 30000); + }); + + socket.on("error", (error: any) => { + console.error("Socket error:", error); + reject(error); + }); + + socket.on("close", (event: any) => { + console.log("\n✓ Socket closed:", event); + resolve(); + }); + + // Auto-close after 30 seconds for demo purposes + // In production, you'd keep this open to observe the chat + setTimeout(() => { + console.log("\nClosing observation connection after 30 seconds..."); + socket.close(); + }, 30000); + }) + .catch((error) => { + console.error("Failed to connect:", error); + reject(error); + }); }); } From 68dd13007e2f91a9dc0951a6e9615bcce561c8aa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Nov 2025 08:24:18 +0000 Subject: [PATCH 5/5] Update hume SDK to 0.15.3-beta.2 Co-authored-by: richard.marmorstein --- evi/evi-typescript-control-plane/package-lock.json | 8 ++++---- evi/evi-typescript-control-plane/package.json | 2 +- evi/evi-typescript-control-plane/src/index.ts | 3 --- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/evi/evi-typescript-control-plane/package-lock.json b/evi/evi-typescript-control-plane/package-lock.json index 44e6bdaf..3d6f0442 100644 --- a/evi/evi-typescript-control-plane/package-lock.json +++ b/evi/evi-typescript-control-plane/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "dotenv": "^16.4.5", - "hume": "0.15.3-beta.1" + "hume": "0.15.3-beta.2" }, "devDependencies": { "@types/node": "^22.15.18", @@ -555,9 +555,9 @@ } }, "node_modules/hume": { - "version": "0.15.3-beta.1", - "resolved": "https://registry.npmjs.org/hume/-/hume-0.15.3-beta.1.tgz", - "integrity": "sha512-mhPMcZlIq0SHNvmZFitdyf/F9gAtY7MjxDAFIAHwfiEEk8NUBKZReclinTOBTv9SkobCyPodDWsXtwclD5S1PQ==", + "version": "0.15.3-beta.2", + "resolved": "https://registry.npmjs.org/hume/-/hume-0.15.3-beta.2.tgz", + "integrity": "sha512-+4F5SarRPvSLx+idel9/Dx0+2kvEnUxiQX51SO4U7fl1olRJPLuSNpivOUq6rgG7gsaV4KMZ55gXTAmx8plRAQ==", "dependencies": { "uuid": "9.0.1", "ws": "^8.16.0", diff --git a/evi/evi-typescript-control-plane/package.json b/evi/evi-typescript-control-plane/package.json index 2ad33d27..81f7873d 100644 --- a/evi/evi-typescript-control-plane/package.json +++ b/evi/evi-typescript-control-plane/package.json @@ -10,7 +10,7 @@ "test": "tsx src/test.ts" }, "dependencies": { - "hume": "0.15.3-beta.1", + "hume": "0.15.3-beta.2", "dotenv": "^16.4.5" }, "devDependencies": { diff --git a/evi/evi-typescript-control-plane/src/index.ts b/evi/evi-typescript-control-plane/src/index.ts index 4853a09e..352e7e95 100644 --- a/evi/evi-typescript-control-plane/src/index.ts +++ b/evi/evi-typescript-control-plane/src/index.ts @@ -89,9 +89,6 @@ export async function observeActiveChat(chatId: string) { return new Promise((resolve, reject) => { // Use the SDK's control plane connect method // Note: connect() returns a Promise that resolves to a ControlPlaneSocket - // The SDK may need chatId to be passed, but current version constructs URL as /chat/ - // instead of /chat/:chatId/connect - this may need SDK update - // @ts-ignore - attempting to pass chatId even if types don't support it yet client.empathicVoice.controlPlane.connect({ chatId }) .then((socket) => { console.log("✓ Control plane socket connected");