Skip to content

Commit 0e5d452

Browse files
committed
Add evi proxy example
1 parent f076b50 commit 0e5d452

26 files changed

+5342
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Whether you're using Python, TypeScript, Flutter, or Next.js, there's something
4040
| [`evi-next-js-pages-router-quickstart`](/evi/evi-next-js-pages-router-quickstart/README.md)| TypeScript | Next.js |
4141
| [`evi-next-js-function-calling`](/evi/evi-next-js-function-calling/README.md) | TypeScript | Next.js |
4242
| [`evi-prompting-examples`](/evi/evi-prompting-examples/README.md) | | |
43+
| [`evi-proxy`](/evi/evi-proxy/README.md) | | |
4344
| [`evi-python-chat-history`](/evi/evi-python-chat-history/README.md) | Python | |
4445
| [`evi-python-clm-sse`](/evi/evi-python-clm-sse/README.md) | Python | |
4546
| [`evi-python-clm-wss`](/evi/evi-python-clm-wss/README.md) | Python | |
@@ -76,4 +77,4 @@ Whether you're using Python, TypeScript, Flutter, or Next.js, there's something
7677

7778
## License
7879

79-
All projects are licensed under the MIT License - see the [LICENSE.txt](/LICENSE) file for details.
80+
All projects are licensed under the MIT License - see the [LICENSE.txt](/LICENSE) file for details.

evi/evi-proxy/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
3+
CLAUDE.md
4+
**/.claude/settings.local.json
5+
out/
6+
script.jsonl

evi/evi-proxy/README.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# EVI Proxy
2+
3+
This example contains an EVI "proxy" that accepts a websocket connection from a client, connects to EVI, and forwards messages back and forth between the client and EVI.
4+
5+
This app is useful as an example in its own right: it demonstrates
6+
* how to connect to EVI from a Typescript backend,
7+
* how to accept websocket connections, process messages, and send them upstream to EVI
8+
9+
See [upstream.ts](app/upstream.ts) and [downstream.ts](app/downstream.ts) for more details.
10+
11+
It is also useful as a debugging tool: it supports
12+
* recording and replaying EVI conversations,
13+
* simulating error conditions that you might want to handle to make your EVI application more robust.
14+
15+
## Prerequisites
16+
17+
- Node.js (for running the proxy and building the web frontend)
18+
- Hume AI API credentials
19+
20+
## Installation
21+
22+
1. Clone this repository:
23+
```bash
24+
git clone <repository-url>
25+
cd eviproxy
26+
```
27+
28+
2. Install dependencies for both app and web components:
29+
```bash
30+
cd app && npm install
31+
cd ../web && npm install && npm run build
32+
cd ..
33+
```
34+
35+
## Environment Variables
36+
37+
Create a `.env` file in the `app/` directory with the following variables:
38+
39+
```bash
40+
HUME_API_KEY=your_hume_api_key_here
41+
HUME_CONFIG_ID=your_config_id_here # Optional
42+
```
43+
44+
To get your API key:
45+
1. Log into the [Hume AI Platform](https://platform.hume.ai/)
46+
2. Visit the [API keys page](https://platform.hume.ai/settings/keys)
47+
3. See the [documentation](https://dev.hume.ai/docs/introduction/api-key) for detailed instructions
48+
49+
## Usage
50+
51+
### Start the Proxy Server
52+
53+
```bash
54+
cd app && npm start
55+
```
56+
57+
This starts the WebSocket proxy server on port 3000 with an interactive CLI interface. The CLI allows you to:
58+
- Switch between record and playback modes
59+
- Control recording sessions
60+
- Manage saved conversation scripts
61+
62+
### Connect Your Own Applications
63+
64+
To connect your own Hume EVI applications to this proxy instead of directly to Hume's servers, configure them to use `http://localhost:3000` as the environment:
65+
66+
**TypeScript/JavaScript:**
67+
```typescript
68+
const hume = new HumeClient({
69+
environment: "http://localhost:3000"
70+
});
71+
```
72+
73+
**Python:**
74+
```python
75+
client = AsyncHumeClient(
76+
environment="http://localhost:3000",
77+
)
78+
```
79+
80+
### Access the Web Interface
81+
82+
The proxy also includes a built-in web interface available at:
83+
```
84+
http://localhost:3000
85+
```
86+
The interface is built using [Vite](https://vitejs.dev). If you modify any
87+
frontend code, run `npm run build` in the `web/` directory again to rebuild the
88+
static assets.
89+
90+
### Recording and Playback
91+
92+
1. **Record Mode**: Captures real conversations with Hume EVI and saves them to JSONL files
93+
2. **Playback Mode**: Replays saved conversations for testing and debugging
94+
3. **Script Files**: Conversations are saved in JSONL format (default: `script.jsonl`)
95+
96+
## Project Structure
97+
98+
```
99+
eviproxy/
100+
├── app/ # Main proxy server (Node.js)
101+
│ ├── main.ts # Entry point and state machine
102+
│ ├── cli.ts # Interactive CLI interface
103+
│ ├── upstream.ts # Hume API connections
104+
│ ├── downstream.ts # Client WebSocket server
105+
│ ├── api.ts # HTTP API endpoints for web-based control
106+
│ └── util.ts # Helpers
107+
├── web/ # React frontend
108+
│ ├── app.tsx # Main React application entry point
109+
│ ├── EVIChat.tsx # Main chat interface using @humeai/voice-react
110+
│ ├── ChatControls.tsx # Voice controls (mute, stop, etc.)
111+
│ ├── ChatMessages.tsx # Message display component
112+
│ ├── StartCall.tsx # Call initiation component
113+
│ ├── WebSocketControls.tsx # WebSocket connection controls
114+
│ ├── index.html # HTML entry point
115+
│ └── package.json # Frontend dependencies
116+
└── shared/ # Shared TypeScript types
117+
└── types.ts # Common interfaces and types
118+
```

evi/evi-proxy/app/api.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import * as http from "http";
2+
import type { State, AppEvent } from '../shared/types.ts';
3+
4+
export class Api {
5+
private apiEventQueue: AppEvent[] = [];
6+
private sseClients = new Set<http.ServerResponse>();
7+
8+
// State broadcasting for SSE
9+
broadcastState(state: State): void {
10+
const stateData = `data: ${JSON.stringify(state)}\n\n`;
11+
this.sseClients.forEach((client) => {
12+
client.write(stateData);
13+
});
14+
}
15+
16+
// Get next event from API queue
17+
getNextAPIEvent(): AppEvent | undefined {
18+
return this.apiEventQueue.shift();
19+
}
20+
21+
// Check if API queue has events
22+
hasAPIEvents(): boolean {
23+
return this.apiEventQueue.length > 0;
24+
}
25+
26+
// Handle complete API request flow
27+
handleRequest(req: http.IncomingMessage, res: http.ServerResponse, currentState: State): boolean {
28+
if (req.method === "POST") {
29+
this.handlePostAppEvent(req, res);
30+
return true;
31+
}
32+
33+
if (req.method === "GET") {
34+
this.handleSubscribeAppEvent(req, res, currentState);
35+
return true;
36+
}
37+
38+
return false;
39+
}
40+
41+
// Handle POST /api requests (event submission)
42+
private handlePostAppEvent(req: http.IncomingMessage, res: http.ServerResponse): void {
43+
let body = "";
44+
req.on("data", (chunk) => {
45+
body += chunk.toString();
46+
});
47+
req.on("end", () => {
48+
try {
49+
const event: AppEvent = JSON.parse(body);
50+
this.apiEventQueue.push(event);
51+
res.writeHead(200, { "Content-Type": "application/json" });
52+
res.write(JSON.stringify({ success: true }));
53+
res.end();
54+
} catch (error) {
55+
res.writeHead(400, { "Content-Type": "application/json" });
56+
res.write(JSON.stringify({ error: "Invalid JSON" }));
57+
res.end();
58+
}
59+
});
60+
}
61+
62+
// Handle GET /api requests (SSE connections)
63+
private handleSubscribeAppEvent(req: http.IncomingMessage, res: http.ServerResponse, currentState: State): void {
64+
// Server-Sent Events for state snapshots
65+
res.writeHead(200, {
66+
"Content-Type": "text/event-stream",
67+
"Cache-Control": "no-cache",
68+
"Connection": "keep-alive",
69+
"Access-Control-Allow-Origin": "*",
70+
});
71+
72+
this.sseClients.add(res);
73+
74+
// Send initial state
75+
res.write(`data: ${JSON.stringify(currentState)}\n\n`);
76+
77+
req.on("close", () => {
78+
this.sseClients.delete(res);
79+
});
80+
}
81+
}

0 commit comments

Comments
 (0)