Rip out Ollama: direct_route only, no LLM layer
Pete moves to a remote host without Ollama access. Every source must declare a direct_route channel; the classifier, explainer, semantic dedup, !explain summaries, feed_hint, and the recent_headlines / classification_log tables are gone. Deterministic dedup (canonical URL, headline_norm, per-channel cooldown) remains.
This commit is contained in:
@@ -14,7 +14,6 @@ var envBracketRe = regexp.MustCompile(`\$\{([^}]+)\}`)
|
||||
|
||||
type Config struct {
|
||||
Matrix MatrixConfig `yaml:"matrix"`
|
||||
Ollama OllamaConfig `yaml:"ollama"`
|
||||
Posting PostingConfig `yaml:"posting"`
|
||||
Storage StorageConfig `yaml:"storage"`
|
||||
Web WebConfig `yaml:"web"`
|
||||
@@ -40,12 +39,6 @@ type MatrixConfig struct {
|
||||
Channels map[string]string `yaml:"channels"`
|
||||
}
|
||||
|
||||
type OllamaConfig struct {
|
||||
BaseURL string `yaml:"base_url"`
|
||||
Model string `yaml:"model"`
|
||||
TimeoutSeconds int `yaml:"timeout_seconds"`
|
||||
}
|
||||
|
||||
type PostingConfig struct {
|
||||
MinIntervalSeconds int `yaml:"min_interval_seconds"`
|
||||
BurstCapCount int `yaml:"burst_cap_count"`
|
||||
@@ -58,27 +51,25 @@ type PostingConfig struct {
|
||||
}
|
||||
|
||||
// RoundRobinConfig switches Pete from immediate-on-classify posting to a
|
||||
// paced rotation: one story per IntervalHours, picking the next source in
|
||||
// config order that has a postable story (skip-and-advance, newest-first).
|
||||
// paced rotation: one story per IntervalHours, picking the next channel in
|
||||
// rotation order that has a postable story (skip-and-advance, newest-first).
|
||||
type RoundRobinConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
IntervalHours int `yaml:"interval_hours"`
|
||||
}
|
||||
|
||||
type StorageConfig struct {
|
||||
DBPath string `yaml:"db_path"`
|
||||
RecentWindowHours int `yaml:"recent_window_hours"`
|
||||
ClassificationLogDays int `yaml:"classification_log_days"`
|
||||
DBPath string `yaml:"db_path"`
|
||||
RecentWindowHours int `yaml:"recent_window_hours"`
|
||||
}
|
||||
|
||||
type SourceConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
FeedURL string `yaml:"feed_url"`
|
||||
Tier int `yaml:"tier"`
|
||||
PollIntervalMinutes int `yaml:"poll_interval_minutes"`
|
||||
FeedHint string `yaml:"feed_hint"`
|
||||
DirectRoute *string `yaml:"direct_route"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Name string `yaml:"name"`
|
||||
FeedURL string `yaml:"feed_url"`
|
||||
Tier int `yaml:"tier"`
|
||||
PollIntervalMinutes int `yaml:"poll_interval_minutes"`
|
||||
DirectRoute string `yaml:"direct_route"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
@@ -125,12 +116,6 @@ func (c *Config) validate() error {
|
||||
if len(c.Matrix.Channels) == 0 {
|
||||
return fmt.Errorf("matrix.channels must have at least one entry")
|
||||
}
|
||||
if c.Ollama.BaseURL == "" {
|
||||
return fmt.Errorf("ollama.base_url is required")
|
||||
}
|
||||
if c.Ollama.Model == "" {
|
||||
return fmt.Errorf("ollama.model is required")
|
||||
}
|
||||
if c.Storage.DBPath == "" {
|
||||
return fmt.Errorf("storage.db_path is required")
|
||||
}
|
||||
@@ -148,6 +133,15 @@ func (c *Config) validate() error {
|
||||
if s.PollIntervalMinutes <= 0 {
|
||||
return fmt.Errorf("sources[%d].poll_interval_minutes must be > 0", i)
|
||||
}
|
||||
if !s.Enabled {
|
||||
continue
|
||||
}
|
||||
if s.DirectRoute == "" {
|
||||
return fmt.Errorf("sources[%d] (%s): direct_route is required for enabled sources", i, s.Name)
|
||||
}
|
||||
if _, ok := c.Matrix.Channels[s.DirectRoute]; !ok {
|
||||
return fmt.Errorf("sources[%d] (%s): direct_route %q is not a configured matrix.channels key", i, s.Name, s.DirectRoute)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -163,9 +157,6 @@ func (c *Config) applyDefaults() {
|
||||
if c.Matrix.PickleKey == "" {
|
||||
c.Matrix.PickleKey = "pete_pickle_key"
|
||||
}
|
||||
if c.Ollama.TimeoutSeconds == 0 {
|
||||
c.Ollama.TimeoutSeconds = 30
|
||||
}
|
||||
if c.Posting.MinIntervalSeconds == 0 {
|
||||
c.Posting.MinIntervalSeconds = 300
|
||||
}
|
||||
@@ -184,9 +175,6 @@ func (c *Config) applyDefaults() {
|
||||
if c.Storage.RecentWindowHours == 0 {
|
||||
c.Storage.RecentWindowHours = 24
|
||||
}
|
||||
if c.Storage.ClassificationLogDays == 0 {
|
||||
c.Storage.ClassificationLogDays = 7
|
||||
}
|
||||
if c.Web.ListenAddr == "" {
|
||||
c.Web.ListenAddr = ":8080"
|
||||
}
|
||||
|
||||
@@ -19,16 +19,14 @@ matrix:
|
||||
|
||||
func TestLoadValid(t *testing.T) {
|
||||
yaml := validMatrix + `
|
||||
ollama:
|
||||
base_url: "http://localhost:11434"
|
||||
model: "gemma4:27b"
|
||||
timeout_seconds: 60
|
||||
storage:
|
||||
db_path: "/tmp/test.db"
|
||||
sources:
|
||||
- name: "Test Source"
|
||||
feed_url: "https://example.com/rss"
|
||||
tier: 1
|
||||
poll_interval_minutes: 20
|
||||
direct_route: "tech"
|
||||
enabled: true
|
||||
`
|
||||
cfg := loadFromString(t, yaml)
|
||||
@@ -39,15 +37,15 @@ sources:
|
||||
if cfg.Matrix.UserID != "@pete:example.org" {
|
||||
t.Errorf("user_id = %q", cfg.Matrix.UserID)
|
||||
}
|
||||
if cfg.Ollama.TimeoutSeconds != 60 {
|
||||
t.Errorf("timeout = %d, want 60", cfg.Ollama.TimeoutSeconds)
|
||||
}
|
||||
if len(cfg.Sources) != 1 {
|
||||
t.Fatalf("sources = %d, want 1", len(cfg.Sources))
|
||||
}
|
||||
if cfg.Sources[0].Name != "Test Source" {
|
||||
t.Errorf("source name = %q", cfg.Sources[0].Name)
|
||||
}
|
||||
if cfg.Sources[0].DirectRoute != "tech" {
|
||||
t.Errorf("direct_route = %q, want tech", cfg.Sources[0].DirectRoute)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarExpansion(t *testing.T) {
|
||||
@@ -60,9 +58,6 @@ matrix:
|
||||
password: "${TEST_PETE_PASS}"
|
||||
channels:
|
||||
tech: "!tech:example.org"
|
||||
ollama:
|
||||
base_url: "http://localhost:11434"
|
||||
model: "gemma4:27b"
|
||||
storage:
|
||||
db_path: "/tmp/test.db"
|
||||
sources: []
|
||||
@@ -76,15 +71,14 @@ sources: []
|
||||
|
||||
func TestDefaults(t *testing.T) {
|
||||
yaml := validMatrix + `
|
||||
ollama:
|
||||
base_url: "http://localhost:11434"
|
||||
model: "gemma4:27b"
|
||||
storage:
|
||||
db_path: "/tmp/test.db"
|
||||
sources:
|
||||
- name: "S"
|
||||
feed_url: "https://example.com/rss"
|
||||
tier: 1
|
||||
poll_interval_minutes: 20
|
||||
direct_route: "tech"
|
||||
enabled: true
|
||||
`
|
||||
cfg := loadFromString(t, yaml)
|
||||
@@ -95,9 +89,6 @@ sources:
|
||||
if cfg.Matrix.DisplayName != "Pete" {
|
||||
t.Errorf("display_name default = %q, want Pete", cfg.Matrix.DisplayName)
|
||||
}
|
||||
if cfg.Ollama.TimeoutSeconds != 30 {
|
||||
t.Errorf("timeout default = %d, want 30", cfg.Ollama.TimeoutSeconds)
|
||||
}
|
||||
if cfg.Posting.MinIntervalSeconds != 300 {
|
||||
t.Errorf("min_interval default = %d, want 300", cfg.Posting.MinIntervalSeconds)
|
||||
}
|
||||
@@ -110,27 +101,6 @@ sources:
|
||||
if cfg.Storage.RecentWindowHours != 24 {
|
||||
t.Errorf("recent_window default = %d, want 24", cfg.Storage.RecentWindowHours)
|
||||
}
|
||||
if cfg.Storage.ClassificationLogDays != 7 {
|
||||
t.Errorf("classification_log default = %d, want 7", cfg.Storage.ClassificationLogDays)
|
||||
}
|
||||
if cfg.Sources[0].PollIntervalMinutes != 20 {
|
||||
t.Errorf("poll_interval default = %d, want 20", cfg.Sources[0].PollIntervalMinutes)
|
||||
}
|
||||
}
|
||||
|
||||
// shortMatrix returns a minimal valid matrix block for validation error tests.
|
||||
func shortMatrix(overrides string) string {
|
||||
base := `
|
||||
matrix:
|
||||
homeserver: "https://h"
|
||||
user_id: "@p:h"
|
||||
password: "pw"
|
||||
channels: {tech: "!t:e"}
|
||||
`
|
||||
if overrides != "" {
|
||||
return overrides
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func TestValidationErrors(t *testing.T) {
|
||||
@@ -143,7 +113,6 @@ matrix:
|
||||
user_id: "@p:h"
|
||||
password: "pw"
|
||||
channels: {tech: "!t:e"}
|
||||
ollama: {base_url: "http://l", model: "m"}
|
||||
storage: {db_path: "/tmp/t.db"}
|
||||
`},
|
||||
{"missing user_id", `
|
||||
@@ -151,7 +120,6 @@ matrix:
|
||||
homeserver: "https://h"
|
||||
password: "pw"
|
||||
channels: {tech: "!t:e"}
|
||||
ollama: {base_url: "http://l", model: "m"}
|
||||
storage: {db_path: "/tmp/t.db"}
|
||||
`},
|
||||
{"missing password", `
|
||||
@@ -159,7 +127,6 @@ matrix:
|
||||
homeserver: "https://h"
|
||||
user_id: "@p:h"
|
||||
channels: {tech: "!t:e"}
|
||||
ollama: {base_url: "http://l", model: "m"}
|
||||
storage: {db_path: "/tmp/t.db"}
|
||||
`},
|
||||
{"no channels", `
|
||||
@@ -167,25 +134,6 @@ matrix:
|
||||
homeserver: "https://h"
|
||||
user_id: "@p:h"
|
||||
password: "pw"
|
||||
ollama: {base_url: "http://l", model: "m"}
|
||||
storage: {db_path: "/tmp/t.db"}
|
||||
`},
|
||||
{"missing ollama base_url", `
|
||||
matrix:
|
||||
homeserver: "https://h"
|
||||
user_id: "@p:h"
|
||||
password: "pw"
|
||||
channels: {tech: "!t:e"}
|
||||
ollama: {model: "m"}
|
||||
storage: {db_path: "/tmp/t.db"}
|
||||
`},
|
||||
{"missing ollama model", `
|
||||
matrix:
|
||||
homeserver: "https://h"
|
||||
user_id: "@p:h"
|
||||
password: "pw"
|
||||
channels: {tech: "!t:e"}
|
||||
ollama: {base_url: "http://l"}
|
||||
storage: {db_path: "/tmp/t.db"}
|
||||
`},
|
||||
{"missing db_path", `
|
||||
@@ -194,7 +142,6 @@ matrix:
|
||||
user_id: "@p:h"
|
||||
password: "pw"
|
||||
channels: {tech: "!t:e"}
|
||||
ollama: {base_url: "http://l", model: "m"}
|
||||
storage: {}
|
||||
`},
|
||||
{"invalid tier", `
|
||||
@@ -203,12 +150,14 @@ matrix:
|
||||
user_id: "@p:h"
|
||||
password: "pw"
|
||||
channels: {tech: "!t:e"}
|
||||
ollama: {base_url: "http://l", model: "m"}
|
||||
storage: {db_path: "/tmp/t.db"}
|
||||
sources:
|
||||
- name: "S"
|
||||
feed_url: "https://e.com/rss"
|
||||
tier: 5
|
||||
poll_interval_minutes: 20
|
||||
direct_route: "tech"
|
||||
enabled: true
|
||||
`},
|
||||
{"missing source name", `
|
||||
matrix:
|
||||
@@ -216,11 +165,42 @@ matrix:
|
||||
user_id: "@p:h"
|
||||
password: "pw"
|
||||
channels: {tech: "!t:e"}
|
||||
ollama: {base_url: "http://l", model: "m"}
|
||||
storage: {db_path: "/tmp/t.db"}
|
||||
sources:
|
||||
- feed_url: "https://e.com/rss"
|
||||
tier: 1
|
||||
poll_interval_minutes: 20
|
||||
direct_route: "tech"
|
||||
enabled: true
|
||||
`},
|
||||
{"enabled source missing direct_route", `
|
||||
matrix:
|
||||
homeserver: "https://h"
|
||||
user_id: "@p:h"
|
||||
password: "pw"
|
||||
channels: {tech: "!t:e"}
|
||||
storage: {db_path: "/tmp/t.db"}
|
||||
sources:
|
||||
- name: "S"
|
||||
feed_url: "https://e.com/rss"
|
||||
tier: 1
|
||||
poll_interval_minutes: 20
|
||||
enabled: true
|
||||
`},
|
||||
{"direct_route not in channels", `
|
||||
matrix:
|
||||
homeserver: "https://h"
|
||||
user_id: "@p:h"
|
||||
password: "pw"
|
||||
channels: {tech: "!t:e"}
|
||||
storage: {db_path: "/tmp/t.db"}
|
||||
sources:
|
||||
- name: "S"
|
||||
feed_url: "https://e.com/rss"
|
||||
tier: 1
|
||||
poll_interval_minutes: 20
|
||||
direct_route: "gaming"
|
||||
enabled: true
|
||||
`},
|
||||
}
|
||||
|
||||
@@ -237,37 +217,25 @@ sources:
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectRouteNullable(t *testing.T) {
|
||||
func TestDisabledSourceSkipsDirectRouteCheck(t *testing.T) {
|
||||
// A disabled source without direct_route must not fail validation.
|
||||
yaml := `
|
||||
matrix:
|
||||
homeserver: "https://h"
|
||||
user_id: "@p:h"
|
||||
password: "pw"
|
||||
channels: {tech: "!t:e"}
|
||||
ollama: {base_url: "http://l", model: "m"}
|
||||
storage: {db_path: "/tmp/t.db"}
|
||||
sources:
|
||||
- name: "Direct"
|
||||
- name: "Off"
|
||||
feed_url: "https://e.com/rss"
|
||||
tier: 1
|
||||
direct_route: "politics"
|
||||
enabled: true
|
||||
- name: "LLM"
|
||||
feed_url: "https://e.com/rss2"
|
||||
tier: 1
|
||||
direct_route: null
|
||||
enabled: true
|
||||
poll_interval_minutes: 20
|
||||
enabled: false
|
||||
`
|
||||
cfg := loadFromString(t, yaml)
|
||||
|
||||
if cfg.Sources[0].DirectRoute == nil {
|
||||
t.Fatal("direct source should have non-nil DirectRoute")
|
||||
}
|
||||
if *cfg.Sources[0].DirectRoute != "politics" {
|
||||
t.Errorf("direct_route = %q, want politics", *cfg.Sources[0].DirectRoute)
|
||||
}
|
||||
if cfg.Sources[1].DirectRoute != nil {
|
||||
t.Errorf("null source should have nil DirectRoute, got %v", cfg.Sources[1].DirectRoute)
|
||||
if cfg.Sources[0].DirectRoute != "" {
|
||||
t.Errorf("expected empty direct_route, got %q", cfg.Sources[0].DirectRoute)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user