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..3d6f0442 --- /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.2" + }, + "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.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", + "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..81f7873d --- /dev/null +++ b/evi/evi-typescript-control-plane/package.json @@ -0,0 +1,24 @@ +{ + "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", + "test": "tsx src/test.ts" + }, + "dependencies": { + "hume": "0.15.3-beta.2", + "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..352e7e95 --- /dev/null +++ b/evi/evi-typescript-control-plane/src/index.ts @@ -0,0 +1,216 @@ +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"); +} + +/** + * Send a control message to an active chat using the SDK's control plane API + */ +async function sendControlMessage(chatId: string, message: any): Promise { + const client = new HumeClient({ apiKey: API_KEY }); + await client.empathicVoice.controlPlane.send(chatId, message); +} + +/** + * 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. + */ +export 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_input", + text: "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. + */ +export 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) => { + // Use the SDK's control plane connect method + // Note: connect() returns a Promise that resolves to a ControlPlaneSocket + 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)); + } + }); + + 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); + }); + }); +} + +/** + * 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 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); 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"] +}