neuron-explorer
Qwen3.5‑4B 전용 조향형 뉴런 탐색기, 모든 MLP 뉴런을 밀어 결과를 보관·탐색
Voice agents in YAML, compiled to Pipecat or LiveKit. Production code you own, deployed anywhere.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
One voice agent spec. Compiled to the runtime you pick.
Unmute is a command line compiler for voice agents. You write a small package of
YAML and Markdown that says who the agent is, which models it uses, which tools
it can call, what it saves as the call goes on, and how the work is broken into
steps. Unmute turns that into a real Python project for the orchestrator you
picked, and refuses to compile the mistakes you would otherwise find on a live
call.README 전문 보기 ↗Unmute is a command line compiler for voice agents. You write a small package of YAML and Markdown that says who the agent is, which models it uses, which tools it can call, what it saves as the call goes on, and how the work is broken into steps. Unmute turns that into a real Python project for the orchestrator you picked, and refuses to compile the mistakes you would otherwise find on a live call.
The project it writes is yours. Pinned dependencies, a Dockerfile, a runbook, and no dependency on Unmute at runtime. Unmute compiles ahead of time and gets out of the way. It is never in the call path.
[!TIP] The Quickstart goes from nothing to a voice you can talk to in your browser. The full guide lives at unmute.ai.
brew install slng-ai/tap/unmute # macOS
go install github.com/slng-ai/unmute@latest # anywhere with Go 1.26+
unmute init my-agent # agent.yaml, instructions.md, targets.yaml, tools/, .env.example
cd my-agent
cp .env.example .env # then fill in OPENAI_API_KEY and SLNG_API_KEY
unmute validate
unmute dev # compile, run the container, open the browser
Allow the microphone and say hello. The agent speaks first, because agent.yaml
says so.
Windows uses Scoop, Linux takes the archive from the
releases page, and every release
also carries a signed checksums.txt and one SBOM per archive.
Installation covers all four
ways, plus the one thing running an agent needs: Docker with Compose for the
LiveKit browser loop, or uv for Pipecat.
This is a whole agent. It has agent.yaml, instructions.md, a tools/end_call.yaml
that names the built-in hang-up, and a state.py. unmute init writes all of them.
# agent.yaml
name: my-agent
entry_agent: assistant
agents:
- name: assistant
prompt: instructions.md
think: assistant_model
speak: assistant_voice
tools:
- end_call
secrets:
- OPENAI_API_KEY
- SLNG_API_KEY
models:
think:
assistant_model:
provider: openai
model: gpt-5.6-terra
params:
# A reasoning model answering with function tools on chat completions
# needs this exact value, or OpenAI returns 400 on every turn.
reasoning_effort: "none"
speak:
assistant_voice:
provider: slng
model: "deepgram/aura:2"
voice: "aura-2-thalia-en"
params:
# Speech goes through the SLNG gateway nearest your callers.
world_part: eu-north
listen:
transcriber:
provider: slng
model: "deepgram/nova:3"
params:
world_part: eu-north
turn:
detector:
provider: local
model: silero
conversation:
greeting:
speaks_first: agent
say: "Hi, how can I help?"
An agent.yaml written for unmute 0.8 or older has a version: line, and unmute
refuses it. See Migrate an older package.
Three things to notice:
assistant_model
at a different model and every agent using it follows.# state.py
from pydantic import BaseModel
class State(BaseModel):
"""What one call knows, shared by every agent and task in it."""
state.py is where the call's saved values get their types. This one holds none
yet. See customer-intake for one that does.
# targets.yaml
channels:
- name: web
kind: realtime_audio
capacity:
peak_sessions: 10
max_sessions: 20
avg_session_duration: 5m
targets:
pipecat:
provider: pipecat
version: "1.12.0"
livekit:
provider: livekit
version: "1.8.8"
sdk_language: python
models:
detector:
provider: livekit
model: turn-detector-mini
One agent, two targets, and unmute compile writes both projects. Where a
runtime cannot run a model as defined, it overrides that single entry by name,
the way LiveKit does with the turn detector above. The agent itself does not
change.
| Target | Kind | What you get |
|---|---|---|
| LiveKit Agents | code | build/livekit/agent.py, a Python project you host and run |
| Pipecat | code | build/pipecat/bot.py, a Python project you host and run |
| SLNG | hosted | unmute deploy pushes a deployment body and SLNG runs the agent, with each local tool as an SLNG code tool |
| Twilio | phone app | build/<target>/app.py, a small app Twilio ConversationRelay calls; you host it |
pipecat, livekit, slng and twilio are the only values provider
accepts. A code target gives you something to host. The hosted target has
nothing to host, and no unmute dev. The Twilio target answers inbound phone
calls only, and has no unmute dev either. SLNG and Twilio run the package with
their own runtimes, without unmute-core, so they refuse the keys that need the
library, such as a tool entry's save and outcomes, a tool's precheck,
entry_by, and a step's confirm. The error names each one. mcp: and slng: tools and every
built-in but end_call run on SLNG only for now.
One directory per target, under build/.
build/livekit/
├── agent.json # the agent, as data
├── spec.py # the spec the library reads
├── speech.py # speech providers
├── agent.py # the entry point
├── state.py # your state.py, copied
├── tools/ # your local Python handlers, copied
├── knowledge/ # your documents, when the package has any
├── pyproject.toml # pinned dependencies, with unmute-core
├── .python-version
├── Dockerfile
├── .dockerignore
├── compose.dev.yaml
├── .env.example # exactly the variables you supply
└── compile-report.json
Pipecat gets the same shape with bot.py as its entry point. Both projects
import the unmute-core library from
PyPI, which holds the call logic, state and tools. Neither imports Unmute.
You deploy a code target with its platform's own tool, such as lk agent create
or pcc deploy, using the Dockerfile. Unmute does not write a deploy file for
them yet.
Treat build/ as output. Edit the package and compile again. Anything you change
inside build/ is overwritten on the next compile.
The agent at the top of this page is the floor. Everything below is written in
the same package, next to what you have already read, and every piece of it
is checked before a call happens rather than during one. The snippets are from
customer-intake and
salon-concierge.
You write what a call can hold as a Pydantic class in state.py, next to
agent.yaml. The model is told the format, and the value is checked where it
enters, not where it is used.
# state.py
from datetime import time
from typing import Literal
from pydantic import BaseModel, EmailStr, Field
class Contact(BaseModel):
name: str
email: EmailStr
class CustomerRecord(BaseModel):
record_id: str
enquiry: Literal["new_customer", "existing_customer", "complaint", "other"]
class State(BaseModel):
contact: Contact | None = Field(
None,
description="Who the caller is and where their confirmation goes, the name and the email address held as two separate parts.",
)
callback_time: time | None = Field(
None,
description="A good time of day to ring the caller back, on the 24 hour clock. Leave it out when the caller has not named one.",
)
notes: list[str] = []
record: CustomerRecord | None = None
agent.yaml lists a value only when it needs a source: or a confirm:. A
prompt reads a value by name, as {{state.contact.name}}.
Core Pydantic types are open to you, with date, time, EmailStr, Literal,
lists, your own models and | None. So are phone numbers, currencies and
languages from pydantic-extra-types. A two-field model such as Contact holds
a name and an address as separate parts, so a prompt can use the name without
reading the address out loud. unmute validate reads state.py through uv, so uv has to be on your
PATH for a package that has one.
An address the model misheard is refused with the format, inside the same turn, so the model fixes it rather than saving something wrong. The email check never asks DNS: this runs while the caller is on the line, and a slow resolver would hold them in silence.
A step takes one part of a result with a dotted path. + appends instead of
replacing, and ? lets the step end without that field.
save:
- contact: result.contact
- caller_email: result.contact.email
- callback_time: result.callback_time?
One task out of an agent's tasks: list.
- name: manage_booking
prompt: tasks/booking.md
start:
say:
- Let me have a look.
call: find_slots
with:
- service: state.requested_service?
- date: state.requested_day?
tools:
- name: find_slots
outcomes:
- if:
- offered:
- true
say:
- "{{result.offer}}"
- name: book_appointment
confirm:
- Shall I book your {{input.service}} for {{input.date}} at {{input.time}}?
outcomes:
- if:
- status:
- booked
save:
- appointment: result.appointment
say:
- Lovely, you're booked for {{result.appointment.date}} at {{result.appointment.time}}.
then: done
start: runs as the step opens. Code says one line, calls find_slots with
saved values, and says the offer. No model request is needed for any of it.
confirm: is read back by code, and the tool runs only on the caller's yes.
Each outcomes: entry matches the tool's typed result. then: done ends the
step, and its say: is the receipt. The booking goes through and the step is
over. None of those tools runs again in that step, because a second booking
that ran and was then refused is still a second booking.
An if: value has to be one the tool's result can hold. A field that cannot
hold it is refused when you compile, with the file and the line.
Inside the agent that runs it:
flows:
- name: book
when: >-
The caller wants to create, move or cancel a booking. Includes a change to
an appointment just made.
fills:
- requested_service
- requested_day
steps:
- task: verify_customer
skip_when_confirmed: customer_phone
- manage_booking
Verification runs the first time and is skipped once the number is agreed.
skip_when_confirmed: has to name a value that this same step confirms, so the
two cannot drift apart. Entering the step again withdraws what it confirmed,
which is what a caller correcting their number needs. fills: are the State
values the agent passes as it starts the flow.
prefetch:
- name: today
clock: now
timezone: Europe/Madrid
save:
- booking_date: result.date
- name: caller
source: from_number
save:
- customer_phone: result.value
Each entry runs before the greeting, on its own budget, and cannot raise. When one skips, the variable keeps its default and the step that asks runs instead. The zone sits on the entry, because a container's clock is UTC and would name the wrong day for everybody who is not on it.
# tools/create_customer_record.yaml
description: Open the customer record once the caller's details are taken.
with:
- phone: "state.caller_phone"
- email: "state.caller_email"
- name: "state.contact.name"
local:
handler: tools/intake.py:create_customer_record
The handler's Python signature is the tool's input, and its return type is the
output. The values under with: come straight out of saved state and are
hidden from the model, so a digit cannot change between the turn it was agreed
on and the turn it was written down.
A value can also carry confirm: verify_contact in variables:. Until that step hears a
yes, the value renders in no prompt but that step's own, and any tool reading
it with with: refuses itself and names the step to run first.
models:
speak:
voice:
provider: slng
model: "deepgram/aura:2"
voice: "aura-2-thalia-en"
params:
world_part: eu-north
listen:
transcriber:
provider: slng
model: "deepgram/nova:3"
params:
world_part: eu-north
world_part picks one of 13 SLNG speech gateways, on each listen and speak
model separately. On Pipecat it is passed as world_part="eu-north". On
LiveKit it becomes the host slng_base_url="eu-north.api.slng.ai".
The 13 are us-east, us-west, br, eu-west, eu-north, gb, za, il,
jp, sg, id, in and au. Pipecat requires world_part, because its SLNG
plugin has no default gateway. On LiveKit, leave it out and the existing default
URL stands. Reasoning through the SLNG Context Router reads the same key and the
same 13 values, so one word covers listening, thinking and speaking. Where the
worker itself runs is a separate choice, deployment_region in targets.yaml,
and it does not have to match: salon-concierge deploys to eu-central and
speaks through eu-north.
SLNG deployment takes the agents API's region names, a separate list: SLNG's own
are spelled like the world parts, such as us-east, and LiveKit Cloud's end in
-livekit, such as us-east-livekit. The location of each service is still
chosen separately.
models:
turn:
detector:
provider: local
model: silero
pace: patient
pace: is snappy, balanced or patient. It sets how long the agent waits
after the caller stops talking. A desk that asks people to spell an email
address out loud wants patient, because they pause between the characters.
unmute validate reads the package the way the compiler does, and names the
file, the line and often the column when something is wrong. It is offline and
needs no account. A few of the things it refuses:
if: naming a value the tool's own result cannot holdskip_when_confirmed: naming a value that step does not confirmtype: outside the grammar, with the column inside the expressionworld_part that is not a gateway, listing the ones that areWarnings are the other half. A credential a package uses but never declares in
secrets:, an agent holding every tool of its own step so the step's save:
may never run, a turn field that reaches nothing: each one names the thing to
change, goes to standard error, and still exits 0.
Each of these has a page in the guide.
| Where it is taught | |
|---|---|
| Tools that call a webhook, run local Python, reach an MCP server, use one the runtime already has, or search your own documents | Tools |
| Tasks, task groups and handoffs, for when one prompt stops being enough, and the steps that end on their own tool | Orchestration |
| Typed values, declared once and checked where they enter: phone numbers, dates, email addresses, a closed set of words, lists, and models of your own | Variables |
| Escalation to a person, cold or warm depending on the phone route | Transfers |
| Phone calls, inbound and outbound, through Twilio, SIP trunks or a carrier stream | Phone calls |
| Pre-fetch, so a known fact is in the prompt before the caller finishes the first sentence | Pre-fetch |
| Tracing to Langfuse or Coval, with per-turn latency and tool calls | Tracing |
| Turn taking, the context router, and regional infrastructure | Optimization |
| Secrets, so a credential never sits in a package file | Secrets |
| What to declare, and what to leave out, once a package has more than a handful of values |
Every key of every package file is listed under configuration.
| Command | What it does |
|---|---|
init | scaffold a new package, or open an interactive console with no name |
validate | check a package against its targets, naming the file and the line when a field is wrong |
compile | write the generated project for every target |
dev | compile, run locally, and talk to the agent in your browser |
deploy | validate, compile and push a package to SLNG |
pull | fetch each SLNG-hosted tool's definition into the package |
resources | list the tools, MCP servers and phone numbers your SLNG organisation offers |
skill | install the Unmute skill so a coding assistant can build packages |
validate, compile, dev, deploy and pull take the package directory as
an optional argument. From inside the package you run them bare; from anywhere
else you pass the path, as in unmute dev my-agent.
validate and compile work offline. SLNG deployment resolves hosted tools
by name with unmute deploy: no hash, mirror or pull step is needed.
unmute pull fetches a hosted tool's definition into the package.
deploy, pull and resources use your SLNG account.
Warnings go to standard error and still exit 0. Errors exit 1.
unmute skill install writes the Unmute skill into your repository, so Claude
Code, Cursor, Codex and the rest know how to author a package. The skill ships
inside the binary, so nothing is downloaded, and the files travel with the repo
so a team shares one skill. The docs are also readable as
llms.txt and
llms-full.txt, or one page at a
time by adding .md to its URL. See
Coding agents.
Ready-made agents live in their own repository: slng-ai/unmute-templates.
Each folder there is one finished agent you can run, copy and change. The repository README lists every template, what it shows, and which API keys it needs.
git clone https://github.com/slng-ai/unmute-templates
cd unmute-templates
unmute validate single-prompt # check the files
unmute compile single-prompt # write the projects
unmute dev single-prompt --target pipecat # talk to it in your browser
Copy the folder you like, change the name: line in its agent.yaml, and it
is yours.
To add a template, open a pull request on slng-ai/unmute-templates, not here. A good one is one folder showing one idea, it validates and compiles, it carries a README saying what to listen for and which keys it needs, it adds a row to that repository's table, and it holds no secrets. CONTRIBUTING.md is the long version.
Templates are agents to start from. Examples below are packages to read: one per feature of the compiler, held to every gate in this repository.
examples/ holds one package per feature. These five are the
places to start.
customer-intake is the small one: one agent,
three tasks, one local tool. It takes a caller's number, name, email address,
enquiry and callback time, saves each under its own type, and hands them to a
tool the model cannot type over. Every type in the grammar appears once. Start
here if the question is about types, saved values or injection.salon-concierge is the big one: two agents, four
tasks, a flow that skips verification once the number is agreed, three
steps that end on their own tool with then: done, handoffs in both directions, a
cold manager transfer, tracing, and inbound phone on both code targets. Every
tool is local Python, so nothing remote has to be up before the greeting.salon-concierge-single-prompt is
the same salon in one prompt, with no tasks and no handoffs, so the one above
can be read against something. Tools, pre-fetch, model, transport and turn
taking are held identical, so a difference you hear is a difference the
structure made.salon-concierge-unoptimized is the
same one prompt with the pre-fetch taken out as well, so it has none of the
optimizations: the starting point to measure them against.hotel-concierge is the hosted target's
showcase: a concierge line whose tools are all references SLNG already holds,
with template variables, a pinned argument, a line said as a tool starts, named MCP
tools and a fallback. It produces no runnable project:
unmute deploy compiles a deployment body and pushes it.If you want a package to start from rather than one to read, run
unmute init my-agent.
make install # builds and puts unmute on your PATH
make test # go test -race ./... , pure Go, no Python needed
make lint
make fmt
make smoke proves the emitted Python is valid and needs Python installed, so it
is opt-in and never the pull request gate. make contracts re-fetches the
published SLNG conformance fixtures and needs network.
Contributions are welcome, from anyone. Unmute is MIT licensed and every part of it is open: the compiler, the four targets, the examples, the skill and the docs site.
A pull request needs five things:
examples/, with your new key in its authored files and the
code path running on a real call, so we can compile it, dial it and hear it.docs-site/ that explain how the
feature works, not only what the key is called.CONTRIBUTING.md is the whole thing: what the example has to exercise, where the package goes, what a public example has to pass, and every check CI runs with its local command.
Say hello on Discord before you start something large. It is the fastest way to find out whether somebody is already on it.
docs/ARCHITECTURE.md explains the design and points
at the load-bearing code.Unmute is MIT licensed. See LICENSE.
| State design |