|
| 1 | +# Expressive agent |
| 2 | + |
| 3 | +A free-form voice agent that demonstrates [Expressive Mode](https://docs.livekit.io/agents/build/expressive/). |
| 4 | +There is no task and no tool: you talk to it like a friend, and it matches your |
| 5 | +register. Tell it good news and it gets excited; tell it something went wrong |
| 6 | +and it drops the energy. |
| 7 | + |
| 8 | +Expressive Mode is the single `expressive=True` flag on `AgentSession`. With it |
| 9 | +enabled the framework injects the TTS provider's markup guide into the LLM |
| 10 | +prompt, so the model emits inline delivery tags (emotion, pacing, non-verbal |
| 11 | +sounds) that the TTS renders and the transcript never shows. |
| 12 | + |
| 13 | +## Architecture |
| 14 | + |
| 15 | +- `agent.py` is the composition root: session setup and the server entrypoint. |
| 16 | +- `prompt.md` holds the persona only. It steers *what* the agent says, and |
| 17 | + expressive mode owns *how* it sounds, so the two never restate each other. |
| 18 | +- `voices.py` lists the expressive-capable LiveKit Inference voices the demo can |
| 19 | + switch between. |
| 20 | + |
| 21 | +The pipeline uses LiveKit Inference with Gemma 4 31B, Deepgram Nova-3, Fish |
| 22 | +Audio S2.1 Pro, and the LiveKit turn detector. |
| 23 | + |
| 24 | +## Run locally |
| 25 | + |
| 26 | +Provide LiveKit Cloud credentials in `../.env` or the environment, then: |
| 27 | + |
| 28 | +```bash |
| 29 | +uv sync --all-extras --dev # from the repository root |
| 30 | +uv run agent.py console |
| 31 | +``` |
| 32 | + |
| 33 | +Use `uv run agent.py dev` to connect the agent to LiveKit Cloud for a frontend |
| 34 | +session. |
| 35 | + |
| 36 | +## Configuration |
| 37 | + |
| 38 | +The agent reads its dispatch metadata, so a frontend can pick the pipeline at |
| 39 | +connect time without a redeploy: |
| 40 | + |
| 41 | +```json |
| 42 | +{ "expressive": true, "tts": "fishaudio" } |
| 43 | +``` |
| 44 | + |
| 45 | +- `expressive` (default `true`) toggles Expressive Mode. |
| 46 | +- `tts` selects a voice from `voices.py`: `fishaudio`, `inworld`, or `cartesia`. |
| 47 | + |
| 48 | +Both values are echoed back as participant attributes (`expressive`, |
| 49 | +`tts_provider`, `tts_label`) so the frontend can display the active pipeline. |
| 50 | + |
| 51 | +Every voice in `voices.py` publishes the `lk.expression` attribute, so switching |
| 52 | +provider never leaves a frontend mood indicator dark. See that file for what a |
| 53 | +provider needs to qualify. |
| 54 | + |
| 55 | +## Trying it with and without expressive |
| 56 | + |
| 57 | +The comparison is the point of the demo. Run it once with `expressive=True` and |
| 58 | +once with `expressive=False`, and say the same thing to each. The words come out |
| 59 | +much the same; the delivery does not. |
| 60 | + |
| 61 | +Expressive Mode requires a `livekit.agents.inference.TTS` model that declares a |
| 62 | +markup dialect. Fish Audio, Inworld TTS 2, and Cartesia Sonic 3 qualify; |
| 63 | +providers without a dialect synthesize normally and the flag stays inert. |
0 commit comments