-
Notifications
You must be signed in to change notification settings - Fork 3.5k
feat(expressive): expose expressive mode and add expressive_agent example #6698
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
+1,260
−115
Merged
Changes from 13 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
74c923f
feat(expressive): expose expressive= on AgentSession
theomonnom 866399f
examples: add expressive_agent
theomonnom a41675a
examples: deploy to demo-agents, rename drive-thru to drive_thru
theomonnom f7fd81f
publish the expression when the segment opens, not only when it closes
theomonnom 91c1f4f
use adaptive interruption and the cloud turn detector in the example
theomonnom d06ec89
use english-only transcription in the expressive example
theomonnom c3fc536
normalize the expression label to a mood on the agent side
theomonnom fd6813c
drop the unused keyword override
theomonnom ec6a704
pin example deploys to the commit sha so the build cache invalidates
theomonnom 79cf84e
name the raw delivery wording 'expression' on the wire
theomonnom bfc6430
tighten the mood matcher and its tests
theomonnom af27072
(expressive agent): assemblyai stt, Marley for Fish, contractions in …
tinalenguyen 7098efe
Merge remote-tracking branch 'origin/main' into theo/expose-expressiv…
tinalenguyen 982266e
expressive: widen the Fish Audio tag vocabulary
tinalenguyen 7c740f4
strengthen prompts
tinalenguyen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # Python bytecode and artifacts | ||
| **/__pycache__/ | ||
| **/*.py[cod] | ||
| **/*.pyo | ||
| **/*.pyd | ||
| **/*.egg-info/ | ||
| **/dist/ | ||
| **/build/ | ||
|
|
||
| # Virtual environments | ||
| **/.venv/ | ||
| **/venv/ | ||
|
|
||
| # Caches and test output | ||
| **/.cache/ | ||
| **/.pytest_cache/ | ||
| **/.ruff_cache/ | ||
| **/coverage/ | ||
|
|
||
| # Logs and temp files | ||
| **/*.log | ||
| **/*.gz | ||
| **/*.tgz | ||
| **/.tmp | ||
| **/.cache | ||
|
|
||
| # Environment variables | ||
| **/.env | ||
| **/.env.* | ||
|
|
||
| # VCS, editor, OS | ||
| .git | ||
| .gitignore | ||
| .gitattributes | ||
| .github/ | ||
| .idea/ | ||
| .vscode/ | ||
| .DS_Store | ||
|
|
||
| # Project docs and misc | ||
| README.md | ||
| LICENSE | ||
|
|
||
| # Project tests | ||
| test/ | ||
| tests/ | ||
| eval/ | ||
| evals/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # syntax=docker/dockerfile:1 | ||
| # | ||
| # Shared Dockerfile for every example under examples/. Byte-identical | ||
| # across the tree — each example's entry script is named `agent.py`, | ||
| # so there's no per-example variation left. | ||
| ARG PYTHON_VERSION=3.13 | ||
| FROM python:${PYTHON_VERSION}-slim AS base | ||
|
|
||
| ENV PYTHONUNBUFFERED=1 | ||
|
|
||
| COPY --from=ghcr.io/astral-sh/uv:0.11.6 /uv /usr/local/bin/uv | ||
|
|
||
| ARG UID=10001 | ||
| RUN adduser \ | ||
| --disabled-password \ | ||
| --gecos "" \ | ||
| --home "/app" \ | ||
| --shell "/sbin/nologin" \ | ||
| --uid "${UID}" \ | ||
| appuser | ||
|
|
||
| RUN apt-get update && apt-get install -y \ | ||
| git \ | ||
| git-lfs \ | ||
| gcc \ | ||
| g++ \ | ||
| python3-dev \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # Enable git-lfs so git dependencies smudge LFS-tracked binaries | ||
| # (e.g. silero's bundled VAD onnx) instead of leaving pointer files. | ||
| # --system so the unprivileged appuser below inherits the filters. | ||
| RUN git lfs install --system | ||
|
|
||
| WORKDIR /app | ||
| USER appuser | ||
|
|
||
| # The example directory is the whole build context, so pyproject.toml has to | ||
| # resolve on its own here — no workspace, no lockfile. A deploy from this repo | ||
| # repoints the livekit-* dependencies at the ref being deployed first: | ||
| # python scripts/pin_example_to_ref.py examples/<name> --ref <git-ref> | ||
| COPY pyproject.toml ./ | ||
| RUN uv sync --no-cache | ||
| ENV PATH="/app/.venv/bin:${PATH}" | ||
|
|
||
| # Pre-download model weights plugins ship (silero VAD, turn-detector, …) | ||
| # so the container is ready to take traffic without a cold-download stall. | ||
| RUN python -m livekit.agents download-files | ||
|
|
||
| COPY . . | ||
|
|
||
| CMD ["python", "agent.py", "start"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| # Expressive agent | ||
|
|
||
| A free-form voice agent that demonstrates [Expressive Mode](https://docs.livekit.io/agents/build/expressive/). | ||
| There is no task and no tool: you talk to it like a friend, and it matches your | ||
| register. Tell it good news and it gets excited; tell it something went wrong | ||
| and it drops the energy. | ||
|
|
||
| Expressive Mode is the single `expressive=True` flag on `AgentSession`. With it | ||
| enabled the framework injects the TTS provider's markup guide into the LLM | ||
| prompt, so the model emits inline delivery tags (emotion, pacing, non-verbal | ||
| sounds) that the TTS renders and the transcript never shows. | ||
|
|
||
| ## Architecture | ||
|
|
||
| - `agent.py` is the composition root: session setup and the server entrypoint. | ||
| - `prompt.md` holds the persona only. It steers *what* the agent says, and | ||
| expressive mode owns *how* it sounds, so the two never restate each other. | ||
| - `protocol.py` is the whole frontend contract: the dispatch metadata shape, the | ||
| attributes echoed back, and the voice table those metadata values name. | ||
|
|
||
| The pipeline uses LiveKit Inference with Gemma 4 31B, Deepgram Nova-3, Fish | ||
| Audio S2.1 Pro, and the LiveKit turn detector. | ||
|
|
||
| ## Run locally | ||
|
|
||
| Provide LiveKit Cloud credentials in `../.env` or the environment, then: | ||
|
|
||
| ```bash | ||
| uv sync --all-extras --dev # from the repository root | ||
| uv run agent.py console | ||
| ``` | ||
|
|
||
| Use `uv run agent.py dev` to connect the agent to LiveKit Cloud for a frontend | ||
| session. | ||
|
|
||
| ## Configuration | ||
|
|
||
| The agent reads its dispatch metadata, so a frontend can pick the pipeline at | ||
| connect time without a redeploy. `protocol.py` is the contract, in both | ||
| directions: | ||
|
|
||
| ```json | ||
| { "expressive": true, "tts": "fishaudio" } | ||
| ``` | ||
|
|
||
| - `expressive` (default `true`) toggles Expressive Mode. | ||
| - `tts` selects a voice from `protocol.py`: `fishaudio`, `inworld`, `cartesia`, or `xai`. | ||
|
|
||
| Both values are echoed back as participant attributes (`expressive`, | ||
| `tts_provider`, `tts_label`) so the frontend can display the active pipeline. | ||
|
|
||
| Note that xAI steers delivery through prosody and sound tags but has no | ||
| expression tag, so it publishes no `lk.expression`. Its speech is expressive; | ||
| a frontend mood indicator just has nothing to read. See `protocol.py`. | ||
|
|
||
| ## Trying it with and without expressive | ||
|
|
||
| The comparison is the point of the demo. Run it once with `expressive=True` and | ||
| once with `expressive=False`, and say the same thing to each. The words come out | ||
| much the same; the delivery does not. | ||
|
|
||
| Expressive Mode requires a `livekit.agents.inference.TTS` model that declares a | ||
| markup dialect. Fish Audio, Inworld TTS 2, Cartesia Sonic 3, and xAI qualify; | ||
| providers without a dialect synthesize normally and the flag stays inert. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import logging | ||
| from pathlib import Path | ||
|
|
||
| from dotenv import load_dotenv | ||
| from protocol import SessionRequest | ||
|
|
||
| from livekit.agents import ( | ||
| Agent, | ||
| AgentServer, | ||
| AgentSession, | ||
| JobContext, | ||
| TurnHandlingOptions, | ||
| cli, | ||
| inference, | ||
| ) | ||
|
|
||
| logger = logging.getLogger("expressive-agent") | ||
|
|
||
| load_dotenv() | ||
|
|
||
| AGENT_NAME = "expressive_agent" | ||
| INSTRUCTIONS = (Path(__file__).parent / "prompt.md").read_text() | ||
|
|
||
| GREETING = ( | ||
| "Open the call the way you'd answer the phone to someone you know well. " | ||
| "Short and warm, and leave them room to say what's going on." | ||
| ) | ||
|
|
||
|
|
||
| class Friend(Agent): | ||
| def __init__(self) -> None: | ||
| super().__init__(instructions=INSTRUCTIONS) | ||
|
|
||
| async def on_enter(self) -> None: | ||
| await self.session.generate_reply(instructions=GREETING) | ||
|
|
||
|
|
||
| server = AgentServer() | ||
|
|
||
|
|
||
| @server.rtc_session(agent_name=AGENT_NAME) | ||
| async def expressive_agent(ctx: JobContext) -> None: | ||
| ctx.log_context_fields = {"room": ctx.room.name} | ||
|
|
||
| config = SessionRequest.parse(ctx.job.metadata).resolve() | ||
| logger.info( | ||
| "starting session", | ||
| extra={"expressive": config.expressive, "voice": config.voice.label}, | ||
| ) | ||
|
|
||
| session = AgentSession( | ||
| stt=inference.STT("assemblyai/universal-3-5-pro", language="en"), | ||
| llm=inference.LLM("google/gemma-4-31b-it"), | ||
| tts=inference.TTS(config.voice.model, voice=config.voice.voice), | ||
| turn_handling=TurnHandlingOptions( | ||
| turn_detection=inference.TurnDetector(version="v1"), | ||
| interruption={"mode": "adaptive"}, | ||
| preemptive_generation={"enabled": True}, | ||
| ), | ||
| expressive=config.expressive, | ||
| ) | ||
|
|
||
| await session.start(agent=Friend(), room=ctx.room) | ||
| await ctx.connect() | ||
|
|
||
| await ctx.room.local_participant.set_attributes(config.attributes()) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| cli.run_app(server) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| You are the user's closest friend, catching up over a call. There is no task | ||
| here, no ticket to close, no form to fill. You are just talking. | ||
|
|
||
| Expressive Mode injects the delivery guide separately, so this prompt covers | ||
| only who you are and what you say. Tone and pacing rules don't belong here, but | ||
| word choice does. | ||
|
|
||
| # Output rules | ||
|
|
||
| - One or two sentences. Three is already too many. | ||
| - Plain prose. No markdown, lists, bullets, headers, or emojis. | ||
| - Spell out numbers, money, and dates as you would say them out loud. | ||
| - Use contractions. "It's", not "it is"; "you're", not "you are". | ||
| - Never use input vocabulary like "enter" or "fill in". They are speaking, not typing. | ||
|
|
||
| # How you talk | ||
|
|
||
| - Speak naturally, not from a customer-service script. You are not assisting | ||
| anyone, you are talking with them. | ||
| - Don't open two consecutive turns with the same word. | ||
| - React before you respond. If they tell you something big, the reaction comes | ||
| first and the follow-up question comes second. | ||
| - Ask about one thing at a time, the way a friend would, not the way a survey does. | ||
| - Trust their memory. They heard what you said five seconds ago, so don't restate it. | ||
| - If they interrupted you, don't restart the sentence. What they said is the | ||
| new subject. | ||
| - When they are venting, stay on their side. Advice they didn't ask for is | ||
| worth less than agreeing that something sucks. | ||
| - Don't reach for a silver lining they didn't ask for, and don't rush to fix | ||
| what they only wanted to say out loud. | ||
| - Never explain or narrate your own tone. | ||
|
|
||
| # Guardrails | ||
|
|
||
| - You have no name unless they give you one, and you never introduce yourself | ||
| by one. | ||
| - You are a friend, not a therapist or a doctor. If they raise something that | ||
| needs real help, say plainly that you are worried and that this is worth | ||
| talking to someone about. Don't lecture, and don't pretend to be qualified. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.