Skip to content

Introduce EVI proxy example #178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Whether you're using Python, TypeScript, Flutter, or Next.js, there's something
| [`evi-next-js-pages-router-quickstart`](/evi/evi-next-js-pages-router-quickstart/README.md)| TypeScript | Next.js |
| [`evi-next-js-function-calling`](/evi/evi-next-js-function-calling/README.md) | TypeScript | Next.js |
| [`evi-prompting-examples`](/evi/evi-prompting-examples/README.md) | | |
| [`evi-proxy`](/evi/evi-proxy/README.md) | | |
| [`evi-python-chat-history`](/evi/evi-python-chat-history/README.md) | Python | |
| [`evi-python-clm-sse`](/evi/evi-python-clm-sse/README.md) | Python | |
| [`evi-python-clm-wss`](/evi/evi-python-clm-wss/README.md) | Python | |
Expand Down Expand Up @@ -76,4 +77,4 @@ Whether you're using Python, TypeScript, Flutter, or Next.js, there's something

## License

All projects are licensed under the MIT License - see the [LICENSE.txt](/LICENSE) file for details.
All projects are licensed under the MIT License - see the [LICENSE.txt](/LICENSE) file for details.
6 changes: 6 additions & 0 deletions evi/evi-proxy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules

CLAUDE.md
**/.claude/settings.local.json
out/
app/recording.jsonl
118 changes: 118 additions & 0 deletions evi/evi-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# EVI Proxy

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.

This app is useful as an example in its own right: it demonstrates
* how to connect to EVI from a Typescript backend,
* how to accept websocket connections, process messages, and send them upstream to EVI

See [upstream.ts](app/upstream.ts) and [downstream.ts](app/downstream.ts) for more details.

It is also useful as a debugging tool: it supports
* recording and replaying EVI conversations,
* simulating error conditions that you might want to handle to make your EVI application more robust.

## Prerequisites

- Node.js (for running the proxy and building the web frontend)
- Hume AI API credentials

## Installation

1. Clone this repository:
```bash
git clone <repository-url>
cd eviproxy
```

2. Install dependencies for both app and web components:
```bash
cd app && npm install
cd ../web && npm install && npm run build
cd ..
```

## Environment Variables

Create a `.env` file in the `app/` directory with the following variables:

```bash
HUME_API_KEY=your_hume_api_key_here
HUME_CONFIG_ID=your_config_id_here # Optional
```

To get your API key:
1. Log into the [Hume AI Platform](https://platform.hume.ai/)
2. Visit the [API keys page](https://platform.hume.ai/settings/keys)
3. See the [documentation](https://dev.hume.ai/docs/introduction/api-key) for detailed instructions

## Usage

### Start the Proxy Server

```bash
cd app && npm start
```

This starts the WebSocket proxy server on port 3000 with an interactive CLI interface. The CLI allows you to:
- Switch between record and playback modes
- Control recording sessions
- Manage saved conversation scripts

### Connect Your Own Applications

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:

**TypeScript/JavaScript:**
```typescript
const hume = new HumeClient({
environment: "http://localhost:3000"
});
```

**Python:**
```python
client = AsyncHumeClient(
environment="http://localhost:3000",
)
```

### Access the Web Interface

The proxy also includes a built-in web interface available at:
```
http://localhost:3000
```
The interface is built using [Vite](https://vitejs.dev). If you modify any
frontend code, run `npm run build` in the `web/` directory again to rebuild the
static assets.

### Recording and Playback

1. **Record Mode**: Captures real conversations with Hume EVI and saves them to JSONL files
2. **Playback Mode**: Replays saved conversations for testing and debugging
3. **Script Files**: Conversations are saved in JSONL format (default: `recording.jsonl`)

## Project Structure

```
eviproxy/
├── app/ # Main proxy server (Node.js)
│ ├── main.ts # Entry point and state machine
│ ├── cli.ts # Interactive CLI interface
│ ├── upstream.ts # Hume API connections
│ ├── downstream.ts # Client WebSocket server
│ ├── api.ts # HTTP API endpoints for web-based control
│ └── util.ts # Helpers
├── web/ # React frontend
│ ├── app.tsx # Main React application entry point
│ ├── EVIChat.tsx # Main chat interface using @humeai/voice-react
│ ├── ChatControls.tsx # Voice controls (mute, stop, etc.)
│ ├── ChatMessages.tsx # Message display component
│ ├── StartCall.tsx # Call initiation component
│ ├── WebSocketControls.tsx # WebSocket connection controls
│ ├── index.html # HTML entry point
│ └── package.json # Frontend dependencies
└── shared/ # Shared TypeScript types
└── types.ts # Common interfaces and types
```
92 changes: 92 additions & 0 deletions evi/evi-proxy/app/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as http from "http";
import type { State, AppEvent } from "../shared/types.ts";

export class Api {
private apiEventQueue: AppEvent[] = [];
private sseClients = new Set<http.ServerResponse>();

// State broadcasting for SSE
broadcastState(state: State): void {
const stateData = `data: ${JSON.stringify(state)}\n\n`;
this.sseClients.forEach((client) => {
client.write(stateData);
});
}

// Get next event from API queue
getNextAPIEvent(): AppEvent | undefined {
return this.apiEventQueue.shift();
}

// Check if API queue has events
hasAPIEvents(): boolean {
return this.apiEventQueue.length > 0;
}

// Handle complete API request flow
handleRequest(
req: http.IncomingMessage,
res: http.ServerResponse,
currentState: State,
): boolean {
if (req.method === "POST") {
this.handlePostAppEvent(req, res);
return true;
}

if (req.method === "GET") {
this.handleSubscribeAppEvent(req, res, currentState);
return true;
}

return false;
}

// Handle POST /api requests (event submission)
private handlePostAppEvent(
req: http.IncomingMessage,
res: http.ServerResponse,
): void {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
try {
const event: AppEvent = JSON.parse(body);
this.apiEventQueue.push(event);
res.writeHead(200, { "Content-Type": "application/json" });
res.write(JSON.stringify({ success: true }));
res.end();
} catch (error) {
res.writeHead(400, { "Content-Type": "application/json" });
res.write(JSON.stringify({ error: "Invalid JSON" }));
res.end();
}
});
}

// Handle GET /api requests (SSE connections)
private handleSubscribeAppEvent(
req: http.IncomingMessage,
res: http.ServerResponse,
currentState: State,
): void {
// Server-Sent Events for state snapshots
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
});

this.sseClients.add(res);

// Send initial state
res.write(`data: ${JSON.stringify(currentState)}\n\n`);

req.on("close", () => {
this.sseClients.delete(res);
});
}
}
Loading