chore: docker, config, delivery sinks, dialogue, and agent docs
- Dockerfile: multi-stage build with CGO_ENABLED=0, embedder model copy, non-root user, healthcheck, and /data volume. - docker-compose.yml: mavend + mavweb services with shared volume, health checks, and restart policy. - .gitignore: ignore models/llm/*.gguf, deploy/telegram.env, tmp artifacts. - deploy/mavend.json: add LLM, phraser, voice sections (embedder, model paths, wake sensitivity). Add telegram token env-var expansion. - deploy/telegram.env.example: template for telegram bot token. - internal/config/config.go: add LLM config struct, voice config struct (embedder, llama, wake sensitivity), telegram token loading. - telegramsink: add chat intent delivery support alongside existing types. - voicesink: skip empty payloads in delivery. - dialogue/session: add chat intent to anaphora resolution, test coverage. - AGENTS.md: update with LLM embedder, LFM model download/configure steps, new UI conventions. - REARCH.md: architecture research document. - cmd/mavend/main.go: wire LLM config, phraser, embedder, telegram config, WebAuthn, IPC event/routine handlers, and reactive notes.
This commit is contained in:
@@ -335,8 +335,12 @@ func Load(path string) (*Config, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("config: read %s: %w", path, err)
|
||||
}
|
||||
// Expand ${VAR} or $VAR patterns from environment variables. This lets
|
||||
// secrets live in env (docker-compose env_file) rather than the config
|
||||
// file committed to git.
|
||||
expanded := os.ExpandEnv(string(b))
|
||||
var c Config
|
||||
if err := json.Unmarshal(b, &c); err != nil {
|
||||
if err := json.Unmarshal([]byte(expanded), &c); err != nil {
|
||||
return nil, fmt.Errorf("config: parse %s: %w", path, err)
|
||||
}
|
||||
c.applyDefaults()
|
||||
|
||||
@@ -51,21 +51,21 @@ const DefaultTimeout = 10 * time.Second
|
||||
type Config struct {
|
||||
// BotToken — the telegram bot token from BotFather. required. sent in the
|
||||
// URL path (the only place telegram accepts it), not in the body.
|
||||
BotToken string
|
||||
BotToken string `json:"bot_token"`
|
||||
|
||||
// ChatID — destination chat. may be a numeric user/group id (sent as a
|
||||
// JSON number) or @channelusername (sent as a JSON string). required.
|
||||
ChatID string
|
||||
ChatID string `json:"chat_id"`
|
||||
|
||||
// BaseURL — telegram API base. empty = DefaultBaseURL. override to point
|
||||
// at a self-hosted API bridge if the proxy path isn't used.
|
||||
BaseURL string
|
||||
BaseURL string `json:"base_url,omitempty"`
|
||||
|
||||
// Proxy — URL of an HTTP/HTTPS/SOCKS5 relay used to reach the telegram
|
||||
// API (region-restricted direct egress). empty = direct (won't work from
|
||||
// the homesrv without a relay; kept configurable for tests + future
|
||||
// topology change).
|
||||
Proxy string
|
||||
Proxy string `json:"proxy,omitempty"`
|
||||
|
||||
// Timeout — per-request; 0 = DefaultTimeout. a dead relay can't hang the
|
||||
// tick loop.
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/kami/maven/internal/audio"
|
||||
"github.com/kami/maven/internal/delivery"
|
||||
"github.com/kami/maven/internal/tts"
|
||||
"github.com/kami/maven/internal/ttsnorm"
|
||||
"github.com/kami/maven/internal/voice"
|
||||
)
|
||||
|
||||
@@ -75,7 +76,7 @@ func (s *Sink) Send(ctx context.Context, send delivery.Sendable) error {
|
||||
// in the phraser behind routing).
|
||||
text = send.Summary
|
||||
}
|
||||
out, err := s.tts.Synthesize(ctx, text)
|
||||
out, err := s.tts.Synthesize(ctx, ttsnorm.Speakable(text))
|
||||
if err != nil {
|
||||
return fmt.Errorf("voicesink: synthesize: %w", err)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ const (
|
||||
IntentFact Intent = "fact"
|
||||
IntentNote Intent = "note"
|
||||
IntentQuery Intent = "query"
|
||||
IntentChat Intent = "chat"
|
||||
IntentSystem Intent = "system"
|
||||
)
|
||||
|
||||
|
||||
@@ -54,6 +54,24 @@ func TestSessionStoreDefaultTTL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionStoreCustomTTL(t *testing.T) {
|
||||
// A session with an explicit TTL should use that instead of the default.
|
||||
now := time.Date(2026, 7, 6, 12, 0, 0, 0, time.UTC)
|
||||
store := NewSessionStore(2 * time.Minute)
|
||||
|
||||
sess := &Session{Intent: IntentQuery, Timestamp: now, TTL: 15 * time.Minute}
|
||||
store.Put("chatty", sess)
|
||||
|
||||
// Should still be alive at 10 minutes (past the 2m default).
|
||||
if s := store.Get("chatty", now.Add(10*time.Minute)); s == nil {
|
||||
t.Error("chat session with 15m TTL expired at 10m — custom TTL not applied")
|
||||
}
|
||||
// Should be expired after 20 minutes.
|
||||
if s := store.Get("chatty", now.Add(20*time.Minute)); s != nil {
|
||||
t.Error("chat session with 15m TTL should be expired at 20m")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInheritSlots(t *testing.T) {
|
||||
now := time.Date(2026, 7, 6, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user