From 55fda5a1e336756fc826d27802373e501e79f7a5 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Fri, 3 Apr 2026 13:00:13 +0200 Subject: [PATCH] docs: add README, gitignore, and Ollama integration tests --- .gitignore | 2 + README.md | 132 +++++++++++++++++++++++++++++++ internal/llm/integration_test.go | 75 ++++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 internal/llm/integration_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc56163 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +reddit-reader +*.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6cffeb --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# reddit-reader + +A Go TUI that monitors subreddits for interesting posts, filters them by keyword and LLM relevance scoring, generates bullet-point summaries, and presents everything in an interactive reading list. + +Runs as a systemd user service for continuous monitoring. The TUI connects on launch via gRPC. + +## Architecture + +Single binary, three subcommands: + +- `reddit-reader serve` — monitor daemon + gRPC server +- `reddit-reader tui` — interactive reading list client +- `reddit-reader setup` — first-run configuration wizard + +``` +Reddit API ──► Monitor ──► Keyword Filter ──► LLM Scorer ──► SQLite + │ + TUI ◄──── gRPC ◄──── Server ◄────┘ +``` + +## Features + +- **Keyword + LLM filtering** — cheap regex/keyword pre-filter, then LLM relevance scoring against your interests +- **Local-first LLM** — Ollama/llama.cpp by default, Mistral API as fallback +- **5-bullet summaries** — generated by the LLM for posts that pass the relevance threshold +- **Feedback loop** — thumbs up/down on posts feeds back into relevance scoring as few-shot examples +- **Live streaming** — new posts push to the TUI in real-time via gRPC server-side streaming +- **Systemd integration** — user service for the daemon, socket activation for on-demand startup + +## Requirements + +- Go 1.26+ +- A local LLM via [Ollama](https://ollama.com) (recommended) or a [Mistral API](https://mistral.ai) key +- Reddit API credentials ([script app](https://www.reddit.com/prefs/apps)) + +## Install + +```bash +go install somegit.dev/vikingowl/reddit-reader@latest +``` + +Or build from source: + +```bash +git clone https://somegit.dev/vikingowl/reddit-reader.git +cd reddit-reader +go build -o reddit-reader . +``` + +## Setup + +```bash +reddit-reader setup +``` + +Walks you through: +1. Reddit OAuth credentials +2. LLM backend selection (auto-detects Ollama) +3. Subreddits to monitor +4. Your interests (used for relevance scoring) +5. Optional systemd unit installation + +Config is stored at `~/.config/reddit-reader/config.toml`. + +## Usage + +Start the monitor daemon: + +```bash +reddit-reader serve +``` + +Or enable it as a systemd user service: + +```bash +systemctl --user enable --now reddit-reader.socket +systemctl --user start reddit-reader.service +``` + +Launch the TUI: + +```bash +reddit-reader tui +``` + +### TUI Keybindings + +| Key | Action | +|-----|--------| +| `j/k` | Navigate up/down | +| `enter` | Expand/collapse summary | +| `s` | Star post | +| `d` | Dismiss post | +| `o` | Open in browser | +| `+/-` | Vote on relevance (feeds back into scoring) | +| `tab` | Switch view (Reading List / Starred / Archive / Settings) | +| `g/G` | Jump to top/bottom | +| `q` | Quit | + +## Configuration + +`~/.config/reddit-reader/config.toml`: + +```toml +[reddit] +client_id = "..." +client_secret = "..." +username = "..." +password = "..." + +[llm] +backend = "ollama" # ollama | llamacpp | mistral +endpoint = "http://localhost:11434" +model = "ministral-3:8b" +relevance_threshold = 0.6 + +[interests] +description = "Go programming, Linux, NixOS, systems programming" + +[monitor] +poll_interval = "2m" +max_posts_per_poll = 25 + +[grpc] +socket = "/run/user/1000/reddit-reader.sock" +``` + +All config values can be overridden with environment variables: `REDDIT_READER_REDDIT_CLIENT_ID`, `REDDIT_READER_LLM_API_KEY`, etc. + +## License + +MIT diff --git a/internal/llm/integration_test.go b/internal/llm/integration_test.go new file mode 100644 index 0000000..f6b0743 --- /dev/null +++ b/internal/llm/integration_test.go @@ -0,0 +1,75 @@ +package llm_test + +import ( + "context" + "net/http" + "os" + "testing" + "time" + + "somegit.dev/vikingowl/reddit-reader/internal/domain" + "somegit.dev/vikingowl/reddit-reader/internal/llm" +) + +func ollamaAvailable() bool { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + req, _ := http.NewRequestWithContext(ctx, "GET", "http://localhost:11434/api/tags", nil) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false + } + resp.Body.Close() + return resp.StatusCode == http.StatusOK +} + +func TestIntegrationOllamaScore(t *testing.T) { + if os.Getenv("INTEGRATION") == "" { + t.Skip("set INTEGRATION=1 to run Ollama integration tests") + } + if !ollamaAvailable() { + t.Skip("Ollama not running at localhost:11434") + } + + client := llm.NewOpenAIClient("http://localhost:11434", "ministral-3:8b") + post := domain.Post{ + Title: "Go 1.26 introduces new iterator patterns for range-over-func", + SelfText: "The Go team has released version 1.26 with significant improvements to the iterator protocol. Range-over-func now supports push and pull iterators natively, eliminating the need for channel-based iteration patterns that were common before.", + } + interests := domain.Interests{ + Description: "Go programming, systems programming, Linux, NixOS", + } + + score, err := client.Score(context.Background(), post, interests) + if err != nil { + t.Fatalf("Score: %v", err) + } + t.Logf("Score: %.2f", score) + if score < 0.0 || score > 1.0 { + t.Errorf("score %.2f out of range [0, 1]", score) + } +} + +func TestIntegrationOllamaSummarize(t *testing.T) { + if os.Getenv("INTEGRATION") == "" { + t.Skip("set INTEGRATION=1 to run Ollama integration tests") + } + if !ollamaAvailable() { + t.Skip("Ollama not running at localhost:11434") + } + + client := llm.NewOpenAIClient("http://localhost:11434", "ministral-3:8b") + post := domain.Post{ + Title: "Systemd 256 brings major changes to socket activation", + SelfText: "The latest systemd release includes reworked socket activation logic, new unit file directives for resource management, improved journal performance, and better container integration. The socket activation changes affect how services handle inherited file descriptors, with a new API for querying activation state. Container support now includes native OCI image pulling and integrated rootless operation. The journal subsystem saw a 40% improvement in write throughput through batched fsync operations.", + } + + summary, err := client.Summarize(context.Background(), post) + if err != nil { + t.Fatalf("Summarize: %v", err) + } + t.Logf("Summary:\n%s", summary) + if len(summary) < 20 { + t.Error("summary too short") + } +}