gateway

package
v1.161.0 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Overview

* ChatCLI - Command Line Interface for LLM interaction * Copyright (c) 2024 Edilson Freitas * License: Apache-2.0

* ChatCLI - Command Line Interface for LLM interaction * Copyright (c) 2024 Edilson Freitas * License: Apache-2.0

* Package gateway lets ChatCLI run as a long-lived daemon that talks to the * user from messaging platforms (Telegram today; Discord/Slack/etc. plug in * through the same Adapter interface). It is intentionally dependency-free: * adapters use the platforms' plain HTTP APIs, so no third-party SDKs enter * go.mod. * * Architecture: * * Adapter (per platform) --inbound chan--> Runner --AgentFunc--> reply * <-------- Send(reply) --------/ * * The Runner owns session routing (one conversation per platform:chat), * bounded concurrency, and graceful shutdown. Adapters only know how to * receive and send on their platform.

* ChatCLI - Command Line Interface for LLM interaction * Copyright (c) 2024 Edilson Freitas * License: Apache-2.0 * * Shared media download for the channel adapters. Each platform resolves a * voice/audio message to a URL (directly or after an API lookup) and then * fetches the bytes through fetchAudioBytes, which enforces a single, * configurable size cap so a hostile or accidental large upload can't exhaust * memory.

* ChatCLI - Command Line Interface for LLM interaction * Copyright (c) 2024 Edilson Freitas * License: Apache-2.0

* ChatCLI - Command Line Interface for LLM interaction * Copyright (c) 2024 Edilson Freitas * License: Apache-2.0

* ChatCLI - Command Line Interface for LLM interaction * Copyright (c) 2024 Edilson Freitas * License: Apache-2.0

* ChatCLI - Command Line Interface for LLM interaction * Copyright (c) 2024 Edilson Freitas * License: Apache-2.0

* telemetry.go — observability for the messaging daemon's external comms. * * The daemon talks to third-party services (Telegram/Slack/Discord/WhatsApp * APIs) over plain HTTP. loggingTransport wraps each adapter's http.Client so * every outbound request to those services is recorded — method, host, * secret-stripped path, status and latency — giving the operator a full audit * trail of the external communication. Idle long-polls (Telegram getUpdates) * log at Debug so the steady-state log stays readable; sends and errors log at * Info/Warn so they're always visible.

* ChatCLI - Per-conversation voice reply preferences. * Copyright (c) 2024 Edilson Freitas * License: Apache-2.0 * * Users control voice replies in the conversation itself ("answer me in * audio" / "stop sending audio"): the model calls the @voice tool, which * stores a per-session preference here. The preference outranks the global * CHATCLI_GATEWAY_VOICE_REPLY mode and persists as JSON under ~/.chatcli, so * a daemon restart keeps every conversation's choice. * * Concurrency model: the gateway serializes agent runs under a mutex, so the * "active session" stamped around each run cannot interleave; the map itself * is still lock-protected because the Runner reads preferences from delivery * goroutines.

* ChatCLI - Command Line Interface for LLM interaction * Copyright (c) 2024 Edilson Freitas * License: Apache-2.0

* ChatCLI - Command Line Interface for LLM interaction * Copyright (c) 2024 Edilson Freitas * License: Apache-2.0

Index

Constants

View Source
const (
	VoiceModeInKind = "in-kind"
	VoiceModeAlways = "always"
)

Voice reply modes. VoiceModeInKind (the default) speaks only when spoken to: the final answer carries audio when the inbound message itself was a voice message. VoiceModeAlways attaches audio to every final answer.

View Source
const (
	VoicePrefAlways = "always" // user asked for audio replies in this chat
	VoicePrefNever  = "never"  // user asked to stop audio replies
)

Per-session voice preference values. Empty (unset) defers to the global mode.

View Source
const DefaultMaxConcurrent = 4

DefaultMaxConcurrent bounds how many messages are processed in parallel.

Variables

This section is empty.

Functions

func Progress

func Progress(ctx context.Context) func(string)

Progress returns the progress emitter on ctx, or a no-op when none is set, so callers can always emit unconditionally.

func RegisterBuilder

func RegisterBuilder(name string, build func() (Adapter, error))

RegisterBuilder registers a named adapter builder. A builder returns (nil, nil) when the platform is not configured (e.g. missing token), so the runner can skip it without treating it as an error.

func RegisteredNames

func RegisteredNames() []string

RegisteredNames returns the names of all registered builders.

func VoiceDecision added in v1.132.0

func VoiceDecision(prefs *VoicePrefs, session, globalMode string, inboundAudio bool) bool

VoiceDecision is the single rule for "should this reply be spoken": the session's own preference (set via the @voice tool) first, then the global mode — always, or the default in-kind where voice answers voice. Shared by the Runner and by the gateway agent loop, which uses it to tell the model up front that its reply will be heard, not read.

func WithInbound

func WithInbound(ctx context.Context, msg InboundMessage) context.Context

WithInbound returns a context carrying the originating message. The Runner installs it per inbound message before invoking the AgentFunc.

func WithProgress

func WithProgress(ctx context.Context, emit func(string)) context.Context

WithProgress returns a context carrying a progress emitter that an AgentFunc may call to stream intermediate updates back to the user. The Runner installs one per inbound message and throttles delivery; a nil emit leaves ctx as-is.

Types

type Adapter

type Adapter interface {
	Name() string
	Start(ctx context.Context, inbound chan<- InboundMessage) error
	Send(ctx context.Context, msg OutboundMessage) error
}

Adapter is a platform integration. Implementations must be safe to Start once; Start blocks until ctx is canceled, pushing received messages to inbound. Send delivers a reply. Name identifies the platform.

func BuildConfigured

func BuildConfigured() ([]Adapter, error)

BuildConfigured instantiates every registered adapter that is configured. Builders returning (nil, nil) are skipped. The first hard error aborts.

type AgentFunc

type AgentFunc func(ctx context.Context, session string, text string) (string, error)

AgentFunc turns an inbound user message into a final reply. It receives the session key so the implementation can keep per-conversation context. To stream progress while it works, it can pull a throttled emitter from ctx via Progress(ctx) and call it zero or more times; the returned string is the final reply delivered after the work finishes. Implementations must be safe for concurrent calls across sessions.

The full InboundMessage (Platform/UserID, for cross-channel identity) is carried on ctx — use InboundFromContext(ctx) to recover it.

type DiscordAdapter

type DiscordAdapter struct {
	// contains filtered or unexported fields
}

DiscordAdapter implements a real Discord Gateway (v10) client over WebSocket for receiving messages, plus the REST API for sending. It handles the HELLO/heartbeat(+ACK)/IDENTIFY lifecycle and reconnects on failure.

func NewDiscordAdapter

func NewDiscordAdapter(token string, logger *zap.Logger) *DiscordAdapter

NewDiscordAdapter builds a Discord adapter from a bot token.

func (*DiscordAdapter) Name

func (d *DiscordAdapter) Name() string

Name implements Adapter.

func (*DiscordAdapter) Send

Send delivers a reply via the REST API.

func (*DiscordAdapter) SetLogger

func (d *DiscordAdapter) SetLogger(l *zap.Logger)

SetLogger implements LoggerAware: inject the daemon logger and trace the HTTP client's calls to the Discord API.

func (*DiscordAdapter) Start

func (d *DiscordAdapter) Start(ctx context.Context, inbound chan<- InboundMessage) error

Start connects to the gateway and streams messages until ctx is canceled, reconnecting with backoff on transient failures.

type InboundAudio added in v1.129.0

type InboundAudio struct {
	Data     []byte // the raw audio, populated by the adapter after download
	MimeType string // e.g. "audio/ogg" — best-effort, may be empty
	FileName string // best-effort original name, may be empty
	// contains filtered or unexported fields
}

InboundAudio is a voice/audio attachment on an inbound message.

type InboundImage added in v1.143.0

type InboundImage struct {
	Data     []byte // the raw image, populated by the adapter after download
	MimeType string // e.g. "image/jpeg" — best-effort, may be empty
	FileName string // best-effort original name, may be empty
	// contains filtered or unexported fields
}

InboundImage is a photo/image attachment on an inbound message. It mirrors InboundAudio's two-phase (parse metadata → hydrate bytes) lifecycle.

type InboundMessage

type InboundMessage struct {
	Platform string // "telegram", "discord", ...
	ChatID   string // platform-specific conversation id
	UserID   string // platform-specific sender id
	UserName string // display name, best-effort
	Text     string // message body (may be empty for a voice-only message)

	// Audio carries a voice/audio attachment when the message has one. The
	// pure parser fills its metadata (and the unexported download handle); the
	// owning adapter then downloads the bytes into Data before dispatch.
	// Consumers read Data/MimeType/FileName only.
	Audio *InboundAudio

	// Image carries a photo/image attachment when the message has one. Same
	// two-phase lifecycle as Audio: the parser fills metadata + download
	// handle, the adapter hydrates Data, consumers read Data/MimeType/FileName.
	// Fed to vision-capable models (or the describe-fallback) by the agent func.
	// A single pointer (not a slice) keeps InboundMessage comparable; adapters
	// carry the primary attachment.
	Image *InboundImage
}

InboundMessage is a normalized message received from any platform.

func InboundFromContext

func InboundFromContext(ctx context.Context) (InboundMessage, bool)

InboundFromContext returns the originating message on ctx, if any.

func (InboundMessage) SessionKey

func (m InboundMessage) SessionKey() string

SessionKey is the stable conversation identity used to scope history.

type LoggerAware

type LoggerAware interface {
	SetLogger(*zap.Logger)
}

LoggerAware lets the daemon inject its real logger into an adapter that a registry builder created with a no-op logger (builders run at import time, before the daemon's logger exists). Implementations should also route their HTTP client through newLoggingClient so external requests are traced.

type OutboundAudio added in v1.130.0

type OutboundAudio struct {
	Data     []byte
	Mime     string // e.g. "audio/ogg", "audio/mpeg"
	FileName string // e.g. "reply.ogg"
}

OutboundAudio is a synthesized voice reply to deliver alongside/instead of text on adapters that support it.

type OutboundImage added in v1.143.0

type OutboundImage struct {
	Data     []byte
	Mime     string // e.g. "image/png"
	FileName string // e.g. "reply.png"
}

OutboundImage is an image reply (generated/edited) to deliver alongside or instead of text on adapters that support sending photos.

type OutboundMessage

type OutboundMessage struct {
	ChatID string
	Text   string
	Audio  *OutboundAudio
	Image  *OutboundImage
}

OutboundMessage is a reply to deliver back to a conversation. When Audio is set, adapters that support voice replies send the clip (using Text as the caption); adapters that don't simply send Text, so a reply is never lost.

type Runner

type Runner struct {
	// contains filtered or unexported fields
}

Runner wires adapters to the agent: it fans inbound messages out to the AgentFunc (bounded concurrency) and delivers replies through the adapter that received them.

func NewRunner

func NewRunner(adapters []Adapter, agent AgentFunc, logger *zap.Logger, maxConcurrent int) *Runner

NewRunner builds a runner. maxConcurrent <= 0 uses DefaultMaxConcurrent.

func (*Runner) Run

func (r *Runner) Run(ctx context.Context) error

Run starts every adapter and processes inbound messages until ctx is canceled. It blocks until shutdown. Returns the first fatal adapter error, or nil on clean ctx cancellation.

func (*Runner) SetImageProvider added in v1.143.0

func (r *Runner) SetImageProvider(fn func(ctx context.Context, session string) *OutboundImage)

SetImageProvider enables image replies: the runner calls fn with the session key when building the final reply, and attaches any returned image so photo-capable adapters send it. Used to deliver a picture the agent generated/edited during the run. A nil fn (default) keeps replies image-free.

func (*Runner) SetThinkingNotice added in v1.129.0

func (r *Runner) SetThinkingNotice(s string)

SetThinkingNotice sets the localized "the assistant is working" message used on channels without a native typing indicator. Empty keeps a built-in default.

func (*Runner) SetVoiceMode added in v1.132.0

func (r *Runner) SetVoiceMode(mode string)

SetVoiceMode selects when the synthesizer runs: VoiceModeAlways speaks every final reply; any other value (including empty) means VoiceModeInKind — the reply carries audio only when the inbound message itself was voice.

func (*Runner) SetVoicePrefs added in v1.132.0

func (r *Runner) SetVoicePrefs(p *VoicePrefs)

SetVoicePrefs attaches the per-session preference store. A session's stored choice (set by the user asking in the conversation) outranks the global mode.

func (*Runner) SetVoiceSynthesizer added in v1.130.0

func (r *Runner) SetVoiceSynthesizer(fn func(ctx context.Context, text string) *OutboundAudio)

SetVoiceSynthesizer enables voice replies: the runner calls fn with the final reply text and, when fn returns non-nil audio, attaches it to the outbound message so audio-capable adapters speak it. A nil fn (the default) keeps replies text-only.

type SlackAdapter

type SlackAdapter struct {
	// contains filtered or unexported fields
}

SlackAdapter integrates with Slack via the Events API (inbound HTTP) and chat.postMessage (outbound), over plain HTTP — no SDK. Inbound requests are verified with the Slack signing secret (HMAC-SHA256) and a timestamp freshness check, per Slack's security guidance.

func NewSlackAdapter

func NewSlackAdapter(botToken, signingSecret, addr, path string, logger *zap.Logger) *SlackAdapter

NewSlackAdapter builds a Slack adapter.

func (*SlackAdapter) Name

func (s *SlackAdapter) Name() string

Name implements Adapter.

func (*SlackAdapter) Send

func (s *SlackAdapter) Send(ctx context.Context, msg OutboundMessage) error

Send posts a reply via chat.postMessage.

func (*SlackAdapter) SetLogger

func (s *SlackAdapter) SetLogger(l *zap.Logger)

SetLogger implements LoggerAware: inject the daemon logger and trace the HTTP client's calls to the Slack API.

func (*SlackAdapter) Start

func (s *SlackAdapter) Start(ctx context.Context, inbound chan<- InboundMessage) error

Start runs the Events API HTTP server until ctx is canceled.

type TelegramAdapter

type TelegramAdapter struct {
	// contains filtered or unexported fields
}

TelegramAdapter integrates with Telegram via the plain HTTP Bot API (getUpdates long-polling + sendMessage). No third-party SDK.

func NewTelegramAdapter

func NewTelegramAdapter(token string, allowedUserIDs []string, logger *zap.Logger) *TelegramAdapter

NewTelegramAdapter builds an adapter from explicit config.

func (*TelegramAdapter) Name

func (t *TelegramAdapter) Name() string

Name implements Adapter.

func (*TelegramAdapter) Send

Send delivers a reply via sendMessage.

func (*TelegramAdapter) SendTyping added in v1.129.0

func (t *TelegramAdapter) SendTyping(ctx context.Context, chatID string) error

SendTyping shows the native "typing…" indicator in the chat. It expires after ~5s, so the Runner refreshes it while the agent works. Implements TypingAware.

func (*TelegramAdapter) SetLogger

func (t *TelegramAdapter) SetLogger(l *zap.Logger)

SetLogger implements LoggerAware: the daemon injects its real logger and we route the HTTP client through it so every Bot API call is traced.

func (*TelegramAdapter) Start

func (t *TelegramAdapter) Start(ctx context.Context, inbound chan<- InboundMessage) error

Start long-polls getUpdates until ctx is canceled, pushing messages to inbound. Transient errors are logged and retried with a short backoff.

type TypingAware added in v1.129.0

type TypingAware interface {
	SendTyping(ctx context.Context, chatID string) error
}

TypingAware is an optional adapter capability: a platform that can show a native "typing…" indicator implements it. The Runner refreshes it while the agent works, so the user sees activity without message clutter.

type VoicePrefs added in v1.132.0

type VoicePrefs struct {
	// contains filtered or unexported fields
}

VoicePrefs stores per-session voice reply preferences with JSON persistence.

func NewVoicePrefs added in v1.132.0

func NewVoicePrefs(path string) *VoicePrefs

NewVoicePrefs builds a store persisted at path. An empty path keeps the store memory-only (used by tests); a missing or corrupt file starts empty — losing a preference degrades to the global default, never to a crash.

func SharedVoicePrefs added in v1.132.0

func SharedVoicePrefs() *VoicePrefs

SharedVoicePrefs returns the process-wide store backed by ~/.chatcli/gateway_voice_prefs.json. The Runner, the gateway agent loop and the @voice tool all share it.

func (*VoicePrefs) ActiveSession added in v1.132.0

func (v *VoicePrefs) ActiveSession() string

ActiveSession returns the session currently being served, or "" outside a gateway run (the @voice tool uses this to refuse REPL invocations).

func (*VoicePrefs) Get added in v1.132.0

func (v *VoicePrefs) Get(session string) string

Get returns the stored preference for session, or "" when unset.

func (*VoicePrefs) Set added in v1.132.0

func (v *VoicePrefs) Set(session, mode string) error

Set stores mode for session ("" deletes, returning the session to the global default) and persists the file atomically.

func (*VoicePrefs) SetActiveSession added in v1.132.0

func (v *VoicePrefs) SetActiveSession(session string)

SetActiveSession stamps the session the agent loop is serving right now. Gateway runs are serialized, so there is exactly one at a time.

type WebhookAdapter

type WebhookAdapter struct {
	// contains filtered or unexported fields
}

WebhookAdapter is a generic, platform-agnostic adapter: it runs an HTTP server that accepts inbound messages as JSON and delivers replies by POSTing to a configured callback URL. Any platform or custom integration that can send/receive an HTTP POST can use it — no per-platform code.

Inbound (POST <path>): {"chat_id":"...", "user_id":"...", "text":"..."} Outbound (POST callbackURL): {"chat_id":"...", "text":"..."}

SecOps: an optional shared secret is required in the X-ChatCLI-Secret header and compared in constant time.

func NewWebhookAdapter

func NewWebhookAdapter(addr, path, secret, callbackURL string, logger *zap.Logger) *WebhookAdapter

NewWebhookAdapter builds a generic webhook adapter.

func (*WebhookAdapter) Name

func (w *WebhookAdapter) Name() string

Name implements Adapter.

func (*WebhookAdapter) Send

Send POSTs the reply to the configured callback URL.

func (*WebhookAdapter) SetLogger

func (w *WebhookAdapter) SetLogger(l *zap.Logger)

SetLogger implements LoggerAware: inject the daemon logger and trace the HTTP client's calls to the configured callback URL.

func (*WebhookAdapter) Start

func (w *WebhookAdapter) Start(ctx context.Context, inbound chan<- InboundMessage) error

Start runs the HTTP server until ctx is canceled.

type WhatsAppAdapter

type WhatsAppAdapter struct {
	// contains filtered or unexported fields
}

WhatsAppAdapter integrates with the WhatsApp Cloud API: inbound via the Meta webhook (GET verification handshake + POST message events) and outbound via the Graph API messages endpoint. Plain HTTP, no SDK.

func NewWhatsAppAdapter

func NewWhatsAppAdapter(accessToken, phoneID, verifyToken, addr, path string, logger *zap.Logger) *WhatsAppAdapter

NewWhatsAppAdapter builds a WhatsApp Cloud API adapter.

func (*WhatsAppAdapter) Name

func (a *WhatsAppAdapter) Name() string

Name implements Adapter.

func (*WhatsAppAdapter) Send

Send delivers a reply via the Graph API.

func (*WhatsAppAdapter) SetLogger

func (a *WhatsAppAdapter) SetLogger(l *zap.Logger)

SetLogger implements LoggerAware: inject the daemon logger and trace the HTTP client's calls to the WhatsApp Cloud API.

func (*WhatsAppAdapter) Start

func (a *WhatsAppAdapter) Start(ctx context.Context, inbound chan<- InboundMessage) error

Start runs the webhook HTTP server until ctx is canceled.