Skip to content

Commit 6093e00

Browse files
Add limerick example (#41)
* Add limerick example * Remove redundant var * Add interruptions to agents and update poet example * Update dockerfile * Update dockerfile * Update openapi * Update tests * Update README * Remove litestar from deps * Fix readme format * Fix typo in README * Handle OpenAI errors * Print raw OpenAI server errors * Remove errors * Use dotenv * Make note more clear
1 parent b6209f5 commit 6093e00

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+3363
-349
lines changed

examples/poet_chat/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Your Fishjam envs, which you can get at https://fishjam.io/app
2+
FISHJAM_ID="your-fishjam-id"
3+
FISHJAM_MANAGEMENT_TOKEN="your-management-token"
4+
5+
# An API key for the OpenAI Realtime API. You can generate one at https://platform.openai.com/api-keys
6+
OPENAI_API_KEY="your-api-key"

examples/poet_chat/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Limerick Poet demo
2+
3+
This directory contains an example implementation of a real-time chat agent using the [Fishjam](https://fishjam.io) and [OpenAI Agents](https://github.com/openai/openai-agents-python) Python SDKs.
4+
5+
The agent introduces itself when the user joins and creates limericks based on what the user says.
6+
The agent handles interruptions from users to ensure a natural conversation flow.
7+
8+
## Running
9+
10+
> [!IMPORTANT]
11+
> All commands should be run from the `examples/poet_chat` directory
12+
13+
Make sure to [install uv](https://docs.astral.sh/uv/getting-started/installation/) if you don't have it already.
14+
Once you have `uv` installed, fetch the dependencies with
15+
16+
```bash
17+
uv sync
18+
```
19+
20+
To run the app, first copy [`.env.example`](./.env.example) to `.env` and populate your environment variables.
21+
22+
Once you have populated `.env`, you can run the demo with
23+
24+
```bash
25+
uv run ./main.py
26+
```
27+
28+
A peer token should be generated and printed to standard output.
29+
You can then use the [minimal-react](https://github.com/fishjam-cloud/web-client-sdk/tree/main/examples/react-client)
30+
demo app to connect with this token and talk with the agent!

examples/poet_chat/main.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import asyncio
2+
3+
from poet_chat.agent import poet_runner
4+
from poet_chat.config import (
5+
AGENT_OPTIONS,
6+
fishjam_client,
7+
)
8+
from poet_chat.notifier import make_notifier
9+
10+
from fishjam.agent import OutgoingAudioTrackOptions
11+
12+
13+
async def main():
14+
room = fishjam_client.create_room()
15+
_, token = fishjam_client.create_peer(room.id)
16+
print(f"Join the chat with the following token: {token}")
17+
18+
agent = fishjam_client.create_agent(room.id, AGENT_OPTIONS)
19+
async with (
20+
agent.connect() as fishjam_session,
21+
await poet_runner.run() as openai_session,
22+
):
23+
track = await fishjam_session.add_track(
24+
OutgoingAudioTrackOptions(
25+
sample_rate=24000, metadata={"type": "microphone"}
26+
)
27+
)
28+
29+
async def _openai_recv():
30+
msg = ""
31+
async for event in openai_session:
32+
if event.type == "audio":
33+
audio = event.audio.data
34+
await track.send_chunk(audio)
35+
elif event.type == "audio_interrupted":
36+
await track.interrupt()
37+
elif event.type == "raw_model_event":
38+
if event.data.type == "input_audio_transcription_completed":
39+
print(f"Peer said:\n{event.data.transcript}\n")
40+
elif event.data.type == "transcript_delta":
41+
msg += event.data.delta
42+
elif event.data.type == "turn_ended":
43+
print(f"Agent said:\n{msg}\n")
44+
msg = ""
45+
elif event.data.type == "exception":
46+
raise event.data.exception
47+
elif event.data.type == "raw_server_event":
48+
match event.data.data:
49+
case {"response": {"status": "failed"}}:
50+
print(event.data.data)
51+
raise RuntimeError("Raw server error from OpenAI API!")
52+
53+
async def _fishjam_recv():
54+
async for event in fishjam_session.receive():
55+
await openai_session.send_audio(event.data)
56+
57+
async with asyncio.TaskGroup() as tg:
58+
tg.create_task(make_notifier(openai_session).connect())
59+
tg.create_task(_openai_recv())
60+
tg.create_task(_fishjam_recv())
61+
62+
63+
if __name__ == "__main__":
64+
asyncio.run(main())

examples/poet_chat/poet_chat/__init__.py

Whitespace-only changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from agents.realtime import RealtimeAgent, RealtimeRunner
2+
3+
from poet_chat.config import OPENAI_INSTRUCTIONS
4+
5+
poet = RealtimeAgent(name="Poet", instructions=OPENAI_INSTRUCTIONS)
6+
poet_runner = RealtimeRunner(
7+
starting_agent=poet,
8+
config={
9+
"model_settings": {
10+
"voice": "alloy",
11+
"modalities": ["audio", "text"],
12+
"input_audio_format": "pcm16",
13+
"output_audio_format": "pcm16",
14+
"turn_detection": {
15+
"type": "semantic_vad",
16+
},
17+
}
18+
},
19+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import os
2+
from pathlib import Path
3+
4+
import dotenv
5+
6+
from fishjam import AgentOptions, AgentOutputOptions, FishjamClient
7+
8+
dotenv.load_dotenv()
9+
10+
FISHJAM_ID = os.environ["FISHJAM_ID"]
11+
FISHJAM_TOKEN = os.environ["FISHJAM_MANAGEMENT_TOKEN"]
12+
FISHJAM_URL = os.getenv("FISHJAM_URL")
13+
14+
AGENT_OPTIONS = AgentOptions(output=AgentOutputOptions(audio_sample_rate=24000))
15+
16+
OPENAI_MODEL = "gpt-realtime"
17+
18+
PROMPT_DIR = Path(__file__).parents[1] / "prompts"
19+
20+
INSTRUCTION_PATH = PROMPT_DIR / "instructions.md"
21+
GREET_PATH = PROMPT_DIR / "greet.md"
22+
23+
with open(INSTRUCTION_PATH) as prompt:
24+
OPENAI_INSTRUCTIONS = prompt.read()
25+
26+
with open(GREET_PATH) as prompt:
27+
OPENAI_GREET = prompt.read()
28+
29+
fishjam_client = FishjamClient(FISHJAM_ID, FISHJAM_TOKEN, fishjam_url=FISHJAM_URL)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from agents.realtime import RealtimeSession
2+
3+
from fishjam import FishjamNotifier
4+
from fishjam.events import (
5+
ServerMessagePeerConnected,
6+
ServerMessagePeerType,
7+
)
8+
from fishjam.events.allowed_notifications import AllowedNotification
9+
10+
from .config import FISHJAM_ID, FISHJAM_TOKEN, FISHJAM_URL, OPENAI_GREET
11+
12+
13+
def make_notifier(poet: RealtimeSession) -> FishjamNotifier:
14+
notifier = FishjamNotifier(FISHJAM_ID, FISHJAM_TOKEN, fishjam_url=FISHJAM_URL)
15+
16+
@notifier.on_server_notification
17+
async def _(notification: AllowedNotification):
18+
match notification:
19+
case ServerMessagePeerConnected(
20+
peer_type=ServerMessagePeerType.PEER_TYPE_WEBRTC,
21+
):
22+
await handle_peer_connected()
23+
24+
async def handle_peer_connected():
25+
await poet.send_message(OPENAI_GREET)
26+
27+
return notifier
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
**Objective:** Welcome users to the real-time limerick generation experience by introducing yourself as their poetic guide and requesting a list of words for personalized poetry creation.
2+
3+
**Instructions:**
4+
5+
1. **Introduction:**
6+
7+
- Begin with a warm and engaging introduction, presenting yourself as a creative and friendly poet eager to craft delightful limericks.
8+
- Briefly explain the purpose of the session: to create fun and custom limericks based on the user's input.
9+
10+
2. **Request for Words:**
11+
12+
- Invite the user to share a list of words that capture their current mood, interests, or thoughts triggered by listening to the audio.
13+
- Encourage them to be imaginative and creative in their word selection to make the limerick more personalized and engaging.
14+
15+
3. **Set Expectations:**
16+
- Assure the user that each limerick will be crafted uniquely with their provided words, promising a lighthearted and entertaining poetic experience.
17+
- Mention the traditional limerick structure (AABBA) that will guide the creation process.
18+
19+
By following these instructions, create a welcoming and interactive atmosphere that encourages user participation and sets the stage for crafting personalized limericks.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
**Objective:** Craft limericks in real-time, using input audio clips or live audio feeds to prompt user interaction. The generated poetry should be based solely on a list of words provided by the user in response to the audio.
2+
3+
**Instructions:**
4+
5+
1. **User Interaction:**
6+
7+
- Prompt the user to listen to the audio and provide a list of words that capture their response or impression from it.
8+
9+
2. **Limerick Construction:**
10+
11+
- Use the user-provided list of words to guide the theme and imagery of the limerick.
12+
- Ensure the limerick adheres to the traditional AABBA rhyme scheme and rhythm, typically with lines 1, 2, and 5 containing three metrical feet and lines 3 and 4 containing two metrical feet.
13+
- Creativity is key—incorporate humor or cleverness where possible to enhance the impact.
14+
15+
3. **Language and Performance:**
16+
- Ensure all poetry is crafted and recited in English.
17+
- Use expressive language to vividly depict themes inspired by the provided words, aiming to entertain and engage the listener.
18+
19+
By following these instructions, generate limericks that transform user-provided words inspired by audio inputs into lively and entertaining poetic pieces.

examples/poet_chat/pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[project]
2+
name = "poet_chat"
3+
version = "0.1.0"
4+
description = "Fishjam voice agent example"
5+
readme = "README.md"
6+
requires-python = ">=3.11"
7+
dependencies = ["fishjam-server-sdk", "openai-agents[voice]>=0.2.11"]
8+
9+
[tool.uv.sources]
10+
fishjam-server-sdk = { workspace = true }

0 commit comments

Comments
 (0)