Skip to content
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

Zg/evi python chat history example #121

Merged
merged 7 commits into from
Dec 6, 2024
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
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@ This repository contains examples of how to use the [Hume API](https://docs.hume
| [`evi-typescript-chat-history-example`](/evi-typescript-chat-history-example/README.md) | Typescript | |
| [`evi-typescript-function-calling`](/evi-typescript-function-calling/README.md) | Typescript | |
| [`evi-embed-vue`](/evi-embed-vue/README.md) | Typescript | Vue |
| [`evi-python-example`](/evi-python-example/README.md) | Python | Hume Python SDK |
| [`evi-python-function-calling`](/evi-python-function-calling/README.md) | Python | Hume Python SDK |
| [`evi-python-example`](/evi-python-example/README.md) | Python | |
| [`evi-python-chat-history-example`](/evi-python-chat-history-example/README.md) | Python | |
| [`evi-python-function-calling`](/evi-python-function-calling/README.md) | Python | |
| [`evi-python-api-example`](/evi-python-api-example/README.md) | Python | |
| [`evi-touchdesigner-example`](/evi-touchdesigner-example/README.md) | Python | TouchDesigner |
| [`evi-custom-language-model`](/evi-custom-language-model/README.md) | Python | |
| [`evi-modal-clm`](/evi-modal-clm/README.md) | Python | Modal |

## [Expression Measurement API](https://dev.hume.ai/docs/expression-measurement-api/overview)

| Name | Models | Language | Framework |
| ---------------------------------------------------------------------------- | ------------------------------------- | ---------- | --------------- |
| [`python-top-emotions`](/python-top-emotions/top_emotions.py) | `face` | Python | Hume Python SDK |
| [`visualization-example`](./visualization-example/example-notebook.ipynb) | `face` | Python | |
| [`typescript-next-api-language`](./typescript-next-api-language/README.md) | `language` | Typescript | Next.js |
| [`typescript-streaming-sandbox`](./typescript-streaming-sandbox/README.md) | `language`, `face`, `burst`, `speech` | Typescript | Next.js |
| [`typescript-raw-text-processor`](./typescript-raw-text-processor/README.md) | `language` | Typescript | |
| Name | Models | Language | Framework |
| ---------------------------------------------------------------------------- | ------------------------------------- | ---------- | ----------- |
| [`python-top-emotions`](/python-top-emotions/top_emotions.py) | `face` | Python | |
| [`visualization-example`](./visualization-example/example-notebook.ipynb) | `face` | Python | |
| [`typescript-next-api-language`](./typescript-next-api-language/README.md) | `language` | Typescript | Next.js |
| [`typescript-streaming-sandbox`](./typescript-streaming-sandbox/README.md) | `language`, `face`, `burst`, `speech` | Typescript | Next.js |
| [`typescript-raw-text-processor`](./typescript-raw-text-processor/README.md) | `language` | Typescript | |
1 change: 1 addition & 0 deletions evi-python-chat-history-example/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
HUME_API_KEY="PASTE_API_KEY_HERE"
2 changes: 2 additions & 0 deletions evi-python-chat-history-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env*.local
.env
80 changes: 80 additions & 0 deletions evi-python-chat-history-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<div align="center">
<img src="https://storage.googleapis.com/hume-public-logos/hume/hume-banner.png">
<h1>Empathic Voice Interface | Chat History</h1>
<p>
<strong>Fetch Chat Events, Generate a Transcript, and Identify Top Emotions</strong>
</p>
</div>

## Overview

**This project demonstrates how to:**

- Retrieve all chat events for a specified Chat ID from Hume's Empathic Voice Interface (EVI) using the TypeScript SDK.
- Parse user and assistant messages to produce a formatted chat transcript.
- Compute the top three average emotion scores from user messages.

**Key Features:**

- **Transcript generation:** Outputs a human-readable `.txt` file capturing the conversation between user and assistant.
- **Top 3 emotions:** Identifies the three emotions with the highest average scores across all user messages.

## Prerequisites

Ensure your environment meets the following requirements:

- **Poetry**: Version `1.7.1` or higher

Check versions on macOS:
```sh
poetry --version
```

If you need to update or install Poetry, visit the [official Poetry website](https://python-poetry.org/).

### Setting up credentials

- **Obtain Your API Key**: Follow the instructions in the Hume documentation to acquire your API key.
- **Create a `.env` File**: In the project's root directory, create a `.env` file if it doesn't exist. Add your API key:

```sh
HUME_API_KEY="<YOUR_API_KEY>"
```

Refer to `.env.example` as a template.

### Specifying the Chat ID

In the main function within `main.oy`, set the `CHAT_ID` variable to the target conversation ID:

```python
async def main():
# Replace with your actual Chat ID
CHAT_ID = "<YOUR_CHAT_ID>"
# ...
```

This determines which Chat's events to fetch and process.

### Installation and usage

1. **Install dependencies**:
```sh
poetry install
```
2. **Run the project**:
```sh
poetry run python main.py
```

#### What happens when run:

- The script fetches all events for the specified `CHAT_ID`.
- It generates a `transcript_<CHAT_ID>.txt` file containing the user and assistant messages with timestamps.
- It logs the top 3 average emotions to the console:

```sh
Top 3 Emotions: {'Joy': 0.7419108072916666, 'Interest': 0.63111979166666666, 'Amusement': 0.63061116536458334}
```

(These keys and scores are just examples; the actual output depends on the Chat's content.)
93 changes: 93 additions & 0 deletions evi-python-chat-history-example/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import asyncio
import os
import json
from typing import cast, TypedDict
from dotenv import load_dotenv
from hume.client import AsyncHumeClient
from hume.empathic_voice.types import ReturnChatEvent

load_dotenv()

class EmotionScore(TypedDict):
emotion: str
score: float

async def main():
# Replace with your actual Chat ID
CHAT_ID = "<YOUR_CHAT_ID>"

chat_events = await fetch_all_chat_events(CHAT_ID)
transcript = generate_transcript(chat_events)

# Write the transcript to a text file
transcript_file_name = f"transcript_{CHAT_ID}.txt"

with open(transcript_file_name, "w", encoding="utf-8") as f:
f.write(transcript)
print(f"Transcript saved to {transcript_file_name}")

# Calculate and print the top 3 emotions (on average)
top_emotions = get_top_emotions(chat_events)
print("Top 3 Emotions:", top_emotions)


async def fetch_all_chat_events(chat_id: str) -> list[ReturnChatEvent]:
api_key = os.environ.get("HUME_API_KEY")
if not api_key:
raise ValueError("HUME_API_KEY is not set in the environment variables.")

client = AsyncHumeClient(api_key=api_key)

all_chat_events: list[ReturnChatEvent] = []
# The response is an iterator over chat events
response = await client.empathic_voice.chats.list_chat_events(id=chat_id, page_number=0, ascending_order=True)
async for event in response:
all_chat_events.append(event)
return all_chat_events

def generate_transcript(chat_events: list[ReturnChatEvent]) -> str:
# Filter for user and assistant messages
relevant_events = [e for e in chat_events if e.type in ("USER_MESSAGE", "AGENT_MESSAGE")]

lines: list[str] = []
for event in relevant_events:
role = "User" if event.role == "USER" else "Assistant"
timestamp = event.timestamp
from datetime import datetime
dt = datetime.fromtimestamp(timestamp / 1000.0)
readable_time = dt.strftime("%Y-%m-%d %H:%M:%S")
lines.append(f"[{readable_time}] {role}: {event.message_text}")

return "\n".join(lines)

def get_top_emotions(chat_events: list[ReturnChatEvent]) -> dict[str, float]:
# Filter user messages that have emotion features
user_messages = [e for e in chat_events if e.type == "USER_MESSAGE" and e.emotion_features]

total_messages = len(user_messages)

# Parse the emotion features of the first user message to determine emotion keys
first_message_emotions = cast(dict[str, float], json.loads(cast(str, user_messages[0].emotion_features)))
emotion_keys: list[str] = list(first_message_emotions.keys())

# Initialize sums for all emotions to 0
emotion_sums = {key: 0.0 for key in emotion_keys}

# Accumulate emotion scores from each user message
for event in user_messages:
emotions = json.loads(cast(str, event.emotion_features))
for key in emotion_keys:
emotion_sums[key] += emotions[key]

# Compute average scores for each emotion
average_emotions: list[EmotionScore] = [{"emotion": key, "score": emotion_sums[key] / total_messages} for key in emotion_keys]

# Sort by average score (descending) and return top 3
average_emotions.sort(key=lambda x: x["score"], reverse=True)
top_3 = average_emotions[:3]

# Convert top 3 into a dictionary of { emotion: score }
return {item["emotion"]: item["score"] for item in top_3}

if __name__ == "__main__":
asyncio.run(main())
Loading