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:
prosolis
2026-05-24 22:07:13 -07:00
parent fbd4b84eaf
commit e88483526d
23 changed files with 146 additions and 1675 deletions

84
main.go
View File

@@ -10,9 +10,7 @@ import (
"syscall"
"time"
"pete/internal/classifier"
"pete/internal/config"
"pete/internal/explainer"
"pete/internal/ingestion"
"pete/internal/matrix"
"pete/internal/poster"
@@ -43,7 +41,6 @@ func main() {
slog.Info("config loaded",
"sources", len(cfg.Sources),
"channels", len(cfg.Matrix.Channels),
"ollama_model", cfg.Ollama.Model,
)
// Initialize database
@@ -72,17 +69,11 @@ func main() {
os.Exit(1)
}
// Create Ollama client and classifier
ollamaClient := classifier.NewOllamaClient(cfg.Ollama)
cls := classifier.NewClassifier(ollamaClient, mx)
// Create post queue
queue := poster.NewQueue(cfg.Posting, mx)
// Wire reaction handler + ❓ → summary hook
// Wire reaction handler
mx.SetReactionHandler(poster.HandleReaction)
exp := explainer.New(mx, ollamaClient)
poster.SetReactionCallback(exp.Handle)
// Wire !post command: force-publish next queued story for the room's channel.
mx.SetMessageHandler(func(channel string, roomID id.RoomID, eventID id.EventID, sender id.UserID, body string) {
@@ -142,70 +133,37 @@ func main() {
roundRobinMode := cfg.Posting.RoundRobin.Enabled
// Build the pipeline callback: classify → enqueue (or just classify+store
// when round-robin mode is enabled; the scheduler does the enqueueing).
processItem := func(ctx context.Context, item *ingestion.FeedItem) {
result, err := cls.Classify(ctx, item)
if err != nil {
slog.Error("classification failed, will retry next cycle",
"guid", item.GUID,
"err", err,
)
return // story stays unclassified for retry
}
// Add to recent headlines for future dedup
storage.InsertRecentHeadline(item.GUID, item.Headline, item.Source)
if !result.ShouldPost {
// Mark classified with sentinel channel so it's not retried
// Pipeline callback: route the story to its direct_route channel, then
// either enqueue immediately or leave it for the round-robin scheduler.
processItem := func(_ context.Context, item *ingestion.FeedItem) {
if item.DirectRoute == "" {
slog.Error("item has no direct_route, skipping", "guid", item.GUID, "source", item.Source)
storage.MarkClassified(item.GUID, "_discarded", "[]")
slog.Info("story gated out",
"guid", item.GUID,
"rationale", result.Rationale,
)
return
}
// Mark classified with actual channel
platforms := storage.MarshalPlatforms(result.Platforms)
storage.MarkClassified(item.GUID, result.Channel, platforms)
if result.DuplicateOf != nil {
// Overwrite with sentinel so the round-robin scheduler doesn't pick
// this up later. Immediate-mode posting already returned above.
storage.MarkClassified(item.GUID, "_duplicate", "[]")
slog.Info("story deduplicated",
"guid", item.GUID,
"duplicate_of", *result.DuplicateOf,
)
return
}
channel := item.DirectRoute
storage.MarkClassified(item.GUID, channel, "[]")
if roundRobinMode {
// Story stays in DB classified+postable; the scheduler will pick
// it up on the next rotation tick.
slog.Info("story classified, awaiting round-robin tick",
"guid", item.GUID, "source", item.Source, "channel", result.Channel)
slog.Info("story routed, awaiting round-robin tick",
"guid", item.GUID, "source", item.Source, "channel", channel)
return
}
// Validate image
imageURL := ""
if item.ImageURL != "" && ingestion.ValidateImageURL(item.ImageURL) {
imageURL = item.ImageURL
}
// Enqueue for posting
queue.Enqueue(poster.QueueItem{
GUID: item.GUID,
Headline: item.Headline,
Lede: item.Lede,
ImageURL: imageURL,
ArticleURL: item.ArticleURL,
Source: item.Source,
Channel: result.Channel,
Platforms: result.Platforms,
Source: item.Source,
Channel: channel,
})
}
@@ -238,7 +196,7 @@ func main() {
}
// Run maintenance on startup
storage.RunMaintenance(cfg.Storage.RecentWindowHours, cfg.Storage.ClassificationLogDays)
storage.RunMaintenance()
// Wait for shutdown signal
sig := <-sigCh
@@ -272,15 +230,13 @@ func runSeed(cfg *config.Config) {
Lede: item.Lede,
ImageURL: item.ImageURL,
ArticleURL: item.ArticleURL,
Source: src.Name,
FeedHint: src.FeedHint,
Source: src.Name,
Classified: true,
SeenAt: timeNow(),
}); err != nil {
slog.Error("seed: insert failed", "guid", item.GUID, "err", err)
continue
}
storage.InsertRecentHeadline(item.GUID, item.Headline, src.Name)
total++
}
slog.Info("seed: source done", "source", src.Name, "items", len(items))
@@ -307,8 +263,6 @@ func runTest(cfg *config.Config, sourceName string) {
if len(items) > 0 {
item := items[0]
item.Source = s.Name
item.FeedHint = s.FeedHint
item.Tier = s.Tier
item.DirectRoute = s.DirectRoute
testItem = &item
src = s
@@ -326,10 +280,10 @@ func runTest(cfg *config.Config, sourceName string) {
"guid", testItem.GUID,
)
// Determine channel
channel := "politics" // default
if testItem.DirectRoute != nil {
channel = *testItem.DirectRoute
channel := testItem.DirectRoute
if channel == "" {
slog.Error("test: source has no direct_route", "source", src.Name)
os.Exit(1)
}
// Validate image
@@ -351,7 +305,7 @@ func runTest(cfg *config.Config, sourceName string) {
Headline: testItem.Headline,
ArticleURL: testItem.ArticleURL,
Lede: testItem.Lede,
Source: testItem.Source,
Source: testItem.Source,
Channel: channel,
}