Skip to content

About

Telnyx provider implementation for OmniVoice - the voice abstraction layer for PlexusOne.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

122 Commits

Folders and files

Repository files navigation

Omni-Telnyx

Go CI Go Lint Go SAST Docs Visualization License

Telnyx provider implementation for OmniVoice - the voice abstraction layer for PlexusOne.

Features

  • 📞 CallSystem: PSTN call handling via Telnyx Call Control API
  • 📡 Transport: Telnyx Media Streaming for real-time audio
  • 🎙️ Voice Gateway: Full STT→LLM→TTS pipeline with registry integration
  • 🔌 Provider Injection: Inject your own LLM providers for full SDK access
  • 💬 SMS/MMS/RCS: Messaging via SMSProvider and OmniChat interfaces

Installation

go get github.com/plexusone/omni-telnyx

Quick Start

Making Outbound Calls

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/plexusone/omni-telnyx/omnivoice/callsystem"
)

func main() {
    // Create Telnyx provider
    provider, err := callsystem.New(
        callsystem.WithAPIKey("YOUR_TELNYX_API_KEY"),
        callsystem.WithPhoneNumber("+15551234567"),
        callsystem.WithConnectionID("your-connection-id"),
        callsystem.WithWebhookURL("https://your-server.com/webhooks"),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer provider.Close()

    // Make outbound call
    call, err := provider.MakeCall(context.Background(), "+15559876543")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Call initiated: %s\n", call.ID())
}

Sending Keypad Digits

To send keypad tones (DTMF) once an outbound call is answered, for example to join a meeting's phone dial-in line, pass WithSendDigits from omnivoice-core's callsystem package:

import corecallsystem "github.com/plexusone/omnivoice-core/callsystem"

// Wait 4 seconds, enter the meeting ID and #, wait 8 seconds, then enter
// * and the passcode and #.
call, err := provider.MakeCall(ctx, "+16695550100",
    corecallsystem.WithSendDigits("WWWW85012345678#WWWWWWWW*482913#"),
)

Digits use dial-string notation: 0-9, *, #, and A-D are tones, w waits half a second, and W waits one second. Invalid digits return an error before the call is placed.

The digits are sent with Telnyx's send_dtmf command when the call is answered. The voice gateway does this on the call.answered webhook; when you use the call system without the gateway, call SendPendingDigits on the *callsystem.Call when you receive call.answered. Digits are sent once per call. Through the gateway, use gw.MakeCallWithOptions(ctx, to, opts...).

Handling Incoming Calls

import (
    "net/http"

    "github.com/plexusone/omnivoice-core/callsystem"
    telnyxcs "github.com/plexusone/omni-telnyx/omnivoice/callsystem"
)

func main() {
    provider, _ := telnyxcs.New(
        telnyxcs.WithAPIKey("YOUR_TELNYX_API_KEY"),
        telnyxcs.WithPhoneNumber("+15551234567"),
    )

    // Set incoming call handler
    provider.OnIncomingCall(func(call callsystem.Call) error {
        fmt.Printf("Incoming call from %s\n", call.From())
        return call.Answer(context.Background())
    })

    // Handle Telnyx webhooks
    http.HandleFunc("/webhooks", func(w http.ResponseWriter, r *http.Request) {
        // Parse Telnyx webhook payload
        // callControlID, from, to := parseWebhook(r)
        // provider.HandleIncomingWebhook(callControlID, from, to)
    })

    http.ListenAndServe(":8080", nil)
}

Media Streaming

import "github.com/plexusone/omni-telnyx/omnivoice/transport"

// Create transport for Media Streaming
tr, _ := transport.New()

// Listen for Media Streaming connections
connCh, _ := tr.Listen(ctx, "/media-stream")

// Handle WebSocket upgrades in your HTTP handler
http.HandleFunc("/media-stream", func(w http.ResponseWriter, r *http.Request) {
    tr.HandleWebSocket(w, r, "/media-stream")
})

// Process connections
for conn := range connCh {
    go func(c transport.Connection) {
        // Read audio from caller
        audio := make([]byte, 1024)
        for {
            n, err := c.AudioOut().Read(audio)
            if err != nil {
                break
            }
            // Process audio...

            // Send audio back
            c.AudioIn().Write(responseAudio)
        }
    }(conn)
}

Audio Mode (Raw Call Audio)

By default the gateway runs a voice pipeline (STT, LLM, TTS) on every call. Set Mode: coregateway.PipelineModeAudio (from github.com/plexusone/omnivoice-core/gateway) to run no pipeline and handle the call's audio yourself, for example to record a call or to build an agent that attends a meeting by phone and speaks only when addressed:

sess, err := gw.MakeCallWithOptions(ctx, "+16695550100")
if err != nil {
    return err
}
audio := sess.(coregateway.AudioSession)

go func() {
    for chunk := range audio.ReceiveAudio() { // mu-law (PCMU), 8 kHz, mono
        recorder.Write(chunk)
    }
}()

if err := audio.SendAudio(mulawSpeech); err != nil {
    return err
}

In audio mode the gateway starts Media Streaming with bidirectional PCMU RTP, so audio sent with SendAudio plays on the call. ReceiveAudio carries the remote party's audio and is closed when the media stream stops. To end the call, call Hangup(ctx) on the session: Close stops only the media stream and leaves a Telnyx call connected. SendAudio returns ErrAudioNotConnected before the stream connects and ErrSessionClosed after the call ends; ClearAudio stops audio that has not played yet.

When you use the call system without the gateway, pass callsystem.WithBidirectionalPCMU() to StartMediaStreaming to send mu-law audio back over the stream.

Call Control Features

// After call is answered...
call, _ := provider.MakeCall(ctx, "+15559876543")

// Start media streaming
telnyxCall := call.(*telnyxcs.Call)
telnyxCall.StartMediaStreaming(ctx, "wss://your-server.com/media-stream")

// Use built-in TTS
telnyxCall.Speak(ctx, "Hello, how can I help you?", "", "")

// Play audio file
telnyxCall.PlayAudio(ctx, "https://example.com/audio.mp3")

// Start real-time transcription
telnyxCall.StartTranscription(ctx, "en")

// Stop transcription
telnyxCall.StopTranscription(ctx)

// Hang up
call.Hangup(ctx)

SMS Messaging

// Send SMS using default number
msg, err := provider.SendSMS(ctx, "+15559876543", "Hello from OmniVoice!")

// Send SMS from specific number
msg, err = provider.SendSMSFrom(ctx, "+15559876543", "+15551234567", "Hello!")

fmt.Printf("Message sent: %s\n", msg.ID)

Registry Integration

Use omnivoice-core's provider registry for automatic discovery:

import (
    omnivoice "github.com/plexusone/omnivoice-core"
    "github.com/plexusone/omnivoice-core/registry"
    _ "github.com/plexusone/omni-telnyx/omnivoice/gateway" // Auto-register
)

// Get gateway via registry
gw, err := omnivoice.GetGatewayProvider("telnyx",
    registry.WithAPIKey("TELNYX_API_KEY"),
    registry.WithExtension("phoneNumber", "+15551234567"),
)

LLM Client Injection

Inject your own thick LLM provider instead of using the default:

import (
    "github.com/plexusone/omni-telnyx/omnivoice/gateway"
    "github.com/plexusone/omnillm" // Your thick provider
)

// Create LLM client with full SDK access
llmClient, _ := omnillm.NewClient(omnillm.ClientConfig{
    Providers: []omnillm.ProviderConfig{{
        Provider: "anthropic",
        APIKey:   os.Getenv("ANTHROPIC_API_KEY"),
    }},
})

// Inject into gateway config
gw, err := gateway.New(gateway.Config{
    APIKey:    "TELNYX_API_KEY",
    LLMClient: llmClient,
})

Configuration

Environment Variables

export TELNYX_API_KEY="your-api-key"

Options

provider, _ := callsystem.New(
    callsystem.WithAPIKey("your-api-key"),
    callsystem.WithPhoneNumber("+15551234567"),      // Default outbound number
    callsystem.WithConnectionID("connection-id"),    // Telnyx Connection ID
    callsystem.WithWebhookURL("https://..."),        // Webhook URL for events
)

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Phone Call Flow                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Caller ←→ Telnyx PSTN ←→ Media Streaming ←→ Your Server    │
│                                                             │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────┐  │
│  │ CallSystem  │    │  Transport  │    │     Agent       │  │
│  │  (calls)    │←──→│  (audio)    │←──→│   (TTS/STT)     │  │
│  └─────────────┘    └─────────────┘    └─────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Telnyx Concepts

Call Control ID

Every call has a unique call_control_id used to send commands via the Call Control API.

Connection ID

A Telnyx Connection represents a configuration for handling calls (SIP, PSTN, etc.). Required for outbound calls.

Media Streaming

Real-time bidirectional audio via WebSocket. Audio is base64-encoded mu-law or a-law at 8kHz.

Requirements

  • Go 1.21+
  • Telnyx Account with API Key
  • Telnyx Connection ID (for outbound calls)
  • Public webhook URL for call events
  • WebSocket endpoint for Media Streaming

Testing

Run conformance tests:

# Unit tests (no API key required)
go test ./...

# Integration tests (requires API key)
export TELNYX_API_KEY="your-api-key"
export TELNYX_PHONE_NUMBER="+15551234567"
export TELNYX_TO_NUMBER="+15559876543"
export TELNYX_CONNECTION_ID="your-connection-id"
go test -tags integration ./...

Related Packages

License

MIT

About

Telnyx provider implementation for OmniVoice - the voice abstraction layer for PlexusOne.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages