live-agent-go

module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 30, 2026 License: MIT

README

Live-Agent-Go

Live-Agent-Go is a Go toolkit that adds a voice layer to your text-based agents, transforming them into voice agents out of the box.

Features

  • Plug in your existing agent: you implement the Brain interface; Live-Agent-Go handles the voice layer around it
  • Composable pipeline: swappable speech-to-text, brain, and text-to-speech components
  • WebRTC transport: built-in session management with Opus audio in and out
  • Live transcripts: stream user and agent text to the client over a WebRTC data channel
  • End-to-end streaming: token streaming from brain through TTS, with parallel audio and text output
  • Pluggable speech-to-text: built-in integrations for Google Cloud Speech, Deepgram, and OpenAI Realtime transcription, or implement your own transcriber
  • Pluggable text-to-speech: built-in integrations for Google Cloud TTS and Deepgram, or implement your own synthesizer
  • Voice activity detection: optional Silero VAD for server-side speech detection, or rely on VAD from your speech-to-text provider
  • Barge-in / interruption: users can cut off the agent mid-response
  • Controls during response generation: adding filler speech, playing background audio, marking output as non-interruptible, etc.
  • Ice-breaking greetings: agent can speak first when a session connects
  • Background audio mixing: mix synthesized speech with audio tracks

Build & Installation

Requirements
  • Go 1.25+
  • libopus and libopusfile development packages (required by gopkg.in/hraban/opus.v2, used for WebRTC audio encoding)
Install native dependencies

macOS:

brew install pkg-config opus opusfile

Ubuntu:

sudo apt-get install pkg-config libopus-dev libopusfile-dev
Docker

If you build in Docker, install the native libraries before compiling (see Using in Docker (hraban/opus)).

Example multi-stage Dockerfile:

FROM golang:1.25-bookworm AS build

RUN apt-get update && apt-get install -y pkg-config libopus-dev libopusfile-dev

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o example ./cmd/example

FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y libopus0 libopusfile0 ca-certificates

WORKDIR /app
COPY --from=build /app/example .

EXPOSE 8080
CMD ["./example"]

Quick Start

This example has three parts:

  • HTTP server and WebRTC signaling (main.go): hosts WebRTC voice sessions and exposes POST /offer to exchange SDP offer/answer.
  • Voice agent pipeline (main.go): a SessionAgent wired with Deepgram speech-to-text, a stub Brain, and Deepgram text-to-speech.
  • Browser client (web/index.html): captures microphone audio, sends it over WebRTC, plays the agent's audio response, and displays live transcripts over a data channel.
main.go
package main

import (
	"context"
	"embed"
	"io"
	"io/fs"
	"log"
	"log/slog"
	"net/http"
	"os"

	"github.com/google/uuid"
	"github.com/josephnhtam/live-agent-go/voice"
	"github.com/josephnhtam/live-agent-go/voice/synthesizer"
	"github.com/josephnhtam/live-agent-go/voice/transcriber"
	"github.com/josephnhtam/live-agent-go/voice/transport/webrtc"
)

//go:embed web
var webFS embed.FS

type MyBrain struct{}

var _ voice.Brain = (*MyBrain)(nil)

func (b *MyBrain) Generate(ctx context.Context, prompt string, tools voice.DialogTools, tokens chan<- voice.Token) error {
	if prompt == "" {
		// Ice-breaking: speak first when the session connects.
		tokens <- voice.Token{Text: "Hi there! How can I help you today?", MessageID: uuid.NewString()}
		return nil
	}
	tokens <- voice.Token{Text: "I am a mock agent echoing your speech: " + prompt, MessageID: uuid.NewString()}
	return nil
}

func main() {
	logger := slog.Default()

	manager, err := webrtc.NewManager(
		webrtc.NewDefaultAPIFactory(nil),
		webrtc.NewManagerOptions().WithMessageChannel("transcript-channel"), // must match browser createDataChannel('transcript-channel')
	)
	if err != nil {
		log.Fatal(err)
	}

	mux := http.NewServeMux()
	mux.HandleFunc("POST /offer", func(w http.ResponseWriter, r *http.Request) {
		handleOffer(w, r, manager, logger)
	})

	webContent, _ := fs.Sub(webFS, "web")
	mux.Handle("GET /", http.FileServer(http.FS(webContent)))

	log.Fatal(http.ListenAndServe(":8080", mux))
}

func handleOffer(w http.ResponseWriter, r *http.Request, manager *webrtc.Manager, logger *slog.Logger) {
	offerSDP, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "failed to read SDP offer", http.StatusBadRequest)
		return
	}

	answerSDP, session, err := manager.AcceptOffer(r.Context(), string(offerSDP))
	if err != nil {
		http.Error(w, "failed to establish connection", http.StatusInternalServerError)
		return
	}

	if err := runSessionAgent(r.Context(), session, logger); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/sdp")
	w.WriteHeader(http.StatusCreated)
	_, _ = w.Write([]byte(answerSDP))
}

func runSessionAgent(ctx context.Context, session voice.Session, logger *slog.Logger) error {
	transcriber := transcriber.NewDeepgramTranscriber(
		transcriber.DeepgramTranscriberConfig{APIKey: os.Getenv("DEEPGRAM_API_KEY")},
		transcriber.NewDeepgramOptions(),
	)
	synthesizer := synthesizer.NewDeepgramSynthesizer(
		synthesizer.DeepgramSynthesizerConfig{
			APIKey: os.Getenv("DEEPGRAM_API_KEY"),
			Model:  "aura-2-thalia-en",
		},
		synthesizer.NewDeepgramSynthesizerOptions(),
	)

	sa, err := voice.NewSessionAgent(session, voice.SessionAgentConfig{
		AgentConfig: voice.AgentConfig{
			Transcriber: transcriber,
			Synthesizer: synthesizer,
			Brain:       &MyBrain{},
		},
		Logger: logger,
	}, voice.NewSessionAgentOptions().WithIceBreaking())
	if err != nil {
		_ = session.Close(context.Background())
		return err
	}

	go func() {
		defer sa.Close(context.Background())
		_ = sa.Run(context.Background())
	}()

	return nil
}
web/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>live-agent-go-quick-start</title>
  </head>
  <body>
    <p id="status">Disconnected</p>
    <button id="connect">Connect</button>
    <pre id="transcript"></pre>

    <script>
      const status = document.getElementById("status");
      const transcript = document.getElementById("transcript");
      const btnConnect = document.getElementById("connect");

      let pc = null;
      let localStream = null;
      const messageOrder = [];
      const messages = {};

      function renderTranscript() {
        transcript.textContent = messageOrder
          .map((id) => {
            const m = messages[id];
            return (m.role === "user" ? "You: " : "Agent: ") + m.text;
          })
          .join("\n");
      }

      function handleDataMessage(raw) {
        try {
          const msg = JSON.parse(raw);
          if (!messages[msg.message_id]) {
            messages[msg.message_id] = { role: msg.role, text: "" };
            messageOrder.push(msg.message_id);
          }
          const entry = messages[msg.message_id];
          entry.text = msg.role === "user" ? msg.text : entry.text + msg.text;
          renderTranscript();
        } catch (e) {
          console.warn("failed to parse data channel message", e);
        }
      }

      function cleanup() {
        pc?.close();
        pc = null;
        localStream?.getTracks().forEach((t) => t.stop());
        localStream = null;
        btnConnect.disabled = false;
      }

      async function connect() {
        btnConnect.disabled = true;
        status.textContent = "Connecting...";

        try {
          localStream = await navigator.mediaDevices.getUserMedia({
            audio: true,
          });
          pc = new RTCPeerConnection();
          localStream
            .getTracks()
            .forEach((track) => pc.addTrack(track, localStream));

          const audio = new Audio();
          audio.autoplay = true;
          pc.ontrack = (e) => {
            audio.srcObject = e.streams[0] || new MediaStream([e.track]);
          };

          const dc = pc.createDataChannel("transcript-channel"); // must match manager WithMessageChannel('transcript-channel')
          dc.onmessage = (e) => handleDataMessage(e.data);

          pc.oniceconnectionstatechange = () => {
            const s = pc.iceConnectionState;
            if (s === "connected" || s === "completed") {
              status.textContent = "Connected";
            } else if (
              s === "disconnected" ||
              s === "failed" ||
              s === "closed"
            ) {
              status.textContent = "Disconnected";
              cleanup();
            }
          };

          const offer = await pc.createOffer();
          await pc.setLocalDescription(offer);

          // Send the SDP offer to the server and receive the SDP answer to complete WebRTC signaling.
          const resp = await fetch("/offer", {
            method: "POST",
            headers: { "Content-Type": "application/sdp" },
            body: offer.sdp,
          });
          if (!resp.ok) throw new Error("signaling failed: " + resp.status);

          await pc.setRemoteDescription({
            type: "answer",
            sdp: await resp.text(),
          });
        } catch (err) {
          console.error(err);
          status.textContent = "Failed to connect";
          cleanup();
        }
      }

      btnConnect.addEventListener("click", connect);
    </script>
  </body>
</html>

Requires Go 1.25+ and native Opus libraries (Install native dependencies). Get a Deepgram API key (used for both STT and TTS).

go get github.com/josephnhtam/live-agent-go
export DEEPGRAM_API_KEY=your-deepgram-api-key
go run .

Open http://localhost:8080 and click Connect.

License

This project is licensed under the terms of the MIT license.

Directories

Path Synopsis
cmd
example command
internal/dialog/mock_dialog
Package mock_dialog is a generated GoMock package.
Package mock_dialog is a generated GoMock package.
internal/speech/mock_speech
Package mock_speech is a generated GoMock package.
Package mock_speech is a generated GoMock package.
mock_voice
Package mock_voice is a generated GoMock package.
Package mock_voice is a generated GoMock package.
vad

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL