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
- func Progress(ctx context.Context) func(string)
- func RegisterBuilder(name string, build func() (Adapter, error))
- func RegisteredNames() []string
- func VoiceDecision(prefs *VoicePrefs, session, globalMode string, inboundAudio bool) bool
- func WithInbound(ctx context.Context, msg InboundMessage) context.Context
- func WithProgress(ctx context.Context, emit func(string)) context.Context
- type Adapter
- type AgentFunc
- type DiscordAdapter
- type InboundAudio
- type InboundImage
- type InboundMessage
- type LoggerAware
- type OutboundAudio
- type OutboundImage
- type OutboundMessage
- type Runner
- func (r *Runner) Run(ctx context.Context) error
- func (r *Runner) SetImageProvider(fn func(ctx context.Context, session string) *OutboundImage)
- func (r *Runner) SetThinkingNotice(s string)
- func (r *Runner) SetVoiceMode(mode string)
- func (r *Runner) SetVoicePrefs(p *VoicePrefs)
- func (r *Runner) SetVoiceSynthesizer(fn func(ctx context.Context, text string) *OutboundAudio)
- type SlackAdapter
- type TelegramAdapter
- func (t *TelegramAdapter) Name() string
- func (t *TelegramAdapter) Send(ctx context.Context, msg OutboundMessage) error
- func (t *TelegramAdapter) SendTyping(ctx context.Context, chatID string) error
- func (t *TelegramAdapter) SetLogger(l *zap.Logger)
- func (t *TelegramAdapter) Start(ctx context.Context, inbound chan<- InboundMessage) error
- type TypingAware
- type VoicePrefs
- type WebhookAdapter
- type WhatsAppAdapter
Constants ¶
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.
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.
const DefaultMaxConcurrent = 4
DefaultMaxConcurrent bounds how many messages are processed in parallel.
Variables ¶
This section is empty.
Functions ¶
func Progress ¶
Progress returns the progress emitter on ctx, or a no-op when none is set, so callers can always emit unconditionally.
func RegisterBuilder ¶
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 ¶
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 ¶
BuildConfigured instantiates every registered adapter that is configured. Builders returning (nil, nil) are skipped. The first hard error aborts.
type AgentFunc ¶
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) Send ¶
func (d *DiscordAdapter) Send(ctx context.Context, msg OutboundMessage) error
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 ¶
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 (*Runner) Run ¶
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
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
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) 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) Send ¶
func (t *TelegramAdapter) Send(ctx context.Context, msg OutboundMessage) error
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
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) Send ¶
func (w *WebhookAdapter) Send(ctx context.Context, msg OutboundMessage) error
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) Send ¶
func (a *WhatsAppAdapter) Send(ctx context.Context, msg OutboundMessage) error
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.