Tributary 將 Google Reader 與 Yahoo Pipes 概念整合為自託管 AI RSS 閲讀器
I Brought Google Reader and Yahoo Pipes Back From the Dead with AI at it's core!
Ekomjah Denis 開發 Tributary,將 Google Reader 式訂閲閲讀與 Yahoo Pipes 式視覺化管線整合成自託管 RSS 閲讀器。它透過 Ollama 在本機執行 gemma3:1b,提供摘要、語意篩選及文章重要性説明;搜尋功能可選用 SerpApi。作者表示尚未微調模型,也未量度 AI 功能品質。
This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine
Google Reader closed in 2013. Yahoo Pipes closed in 2015. Nothing has replaced either of them, and I have been quietly annoyed about it ever since.
Reader was the calm inbox: unread counts, stars, j and k, no algorithm deciding what I saw. Pipes was the clever one: drag a few boxes onto a canvas, wire them together, and get a brand-new feed out the other end. Both were killed because someone else owned the server.
So for "Ghost in the Machine" I put the two ghosts in one body. Tributary is a self-hosted feed reader whose feeds you shape with a visual pipe builder, and a small local Gemma sits inside as one of the boxes. It runs on my 2013 laptop: 8 GB of RAM, no GPU, Ubuntu.
The short version
- A reader (Google Reader's quiet inbox) plus a canvas (Yahoo Pipes' wiring) in one app. The pipe is the main object. The reader is where its output lands.
- 11 boxes. Three of them call a local
gemma3:1bthrough Ollama: Summarize, Filter by meaning, Why you'd care. - One SQLite file, no accounts. Pipes are plain JSON you own.
- Self-hosted only today. The cloud tier is coming soon.
- I have not fine-tuned anything and I have not measured how good the AI boxes are. I say so again below, with the reasons.
What I Built
Two dead products, one app. Here is what I kept from each:
| Ghost | What it was | What Tributary keeps |
|---|---|---|
| Google Reader (gone 2013) | A quiet inbox for the sites you chose | Views for All, Unread and Starred, labels, per-feed unread counts, and keyboard shortcuts (j/k, o, s, m, r, /). No ranking, ever. |
| Yahoo Pipes (gone 2015) | Wire boxes together to reshape a stream | A React Flow canvas at /pipes. Drag boxes, connect them, run, inspect every step, and publish the result back into the reader as a feed. |
The first ghost is the lost product: the thing you loved that vanished. The second is the one in the title, the model inside the machine, doing a small job quietly on hardware that should have been retired years ago.
A pipe is an ordered list of boxes, and every box takes a list of items and returns a list of items. That one rule is the whole engine.
| Group | Boxes |
|---|---|
| Sources |
fetch (your stored feeds), search (SerpApi, for the web beyond your subscriptions) |
| Combine |
merge, dedupe
|
| Transform |
filter, sort, limit
|
| Enrich |
fulltext (fetch and extract the article body) |
| Local AI |
summarize, meaning, whycare
|
A pipe I actually use looks like this on the canvas:
fetch (Hacker News + 3 blogs) -> dedupe -> fulltext -> meaning ("practical engineering, no hype") -> summarize -> sort -> limit
Run it and it comes back as a feed in the reader, in order, with one-sentence summaries. That is the Pipes idea finally sitting inside the Reader idea.
Demo
VIDEO DEMO COMING SOON
There is no public demo link, and that is deliberate. The self-hosted build has no login, so putting it on the open web would hand everyone a free reader and a free model on my bill. The hosted version needs accounts and per-user limits first. That is the cloud tier, and it is coming soon.
Run it yourself in about ten minutes. You need Python 3.11+, uv, Node with pnpm, and Ollama.
git clone https://github.com/Ekomjah/tributary
cd tributary
ollama pull gemma3:1b
cp .env.example .env
cd backend
uv sync
uv run tributary init-db
uv run tributary serve
In a second terminal:
cd frontend
pnpm install
pnpm dev
Or you can simply run make setup , then make dev to spin it all up quickly
Open http://localhost:3000, click Add feed, and paste https://news.ycombinator.com/rss. Paste a blog's homepage instead and Tributary will look for its feed. Then run uv run tributary doctor to see what is wired up.
Two things are optional, and both are off until you set a key in .env:
-
SERPAPI_API_KEYturns on thesearchbox. -
SENTRY_DSNturns on tracing, so you can watch each pipe run as a trace.
A free-tier key for either is enough to try the features
How I Built It
| Layer | Choice | Job |
|---|---|---|
| Reader and editor | Next.js 16, React 19, TanStack Query, Tailwind 4, React Flow | Feed list, article view, drag-and-wire canvas |
| API and engine | Python, FastAPI, Pydantic, SQLAlchemy | Validate and run pipes, serve the reader |
| Ingestion | feedparser, httpx, trafilatura | Read feeds, extract readable article text |
| Storage | SQLite | Feeds, items, labels, pipes, runs, caches |
| Local model | Ollama running gemma3:1b
|
Summaries, meaning filter, relevance notes |
| Web search | SerpApi, opt-in | Results beyond your subscribed feeds |
| Monitoring | Sentry, opt-in | Traces per run, metrics, GenAI spans |
A box is a small list transform. This is the shape of every one of them:
class WhyCareBox:
box = "whycare"
def run(self, items: list[ItemOut], context: RunContext) -> list[ItemOut]:
...
Because every box has the same signature, boxes compose in any order, and each one is easy to test. The backend validates a pipe's JSON with strict Pydantic models before a run starts, so unknown keys and bad values are rejected up front.
The model's job is small and the failure mode is boring. I did not want a 1B model making decisions it cannot be trusted with, so the boxes lean toward keeping things:
- Summarize reads the extracted full text and stores one sentence. An item with no full text is skipped without a model call, so it never summarizes a stub.
- Filter by meaning judges the best available text against a criterion you write in plain English. An item with no text is kept, not silently dropped.
- Why you'd care is handed your latest starred titles as context and writes one sentence on why the story matters to you.
Answers are cached by (item, box, params hash, model). The hash includes a prompt version and a SHA-256 of the text, so an edited article or a changed criterion misses the cache, and repeating a run is free. That matters on a CPU-only laptop. Model errors report the host and failure type, never the prompt or the article.
Search starts with a local model. You write "AI regulation this week" and Gemma rewrites it into a structured query before SerpApi sees anything:
"AI regulation this week"
|
v
Ollama refinement (local) -> q, engine, gl, hl, freshness, max_results
|
v
SerpApi (cloud, opt-in)
|
v
normalize -> items -> six-hour cache -> pipe / reader
The refinement uses a Pydantic schema at temperature 0, an allowlisted set of engines, clamped result counts, and a fallback to your raw query if Ollama is down or returns bad JSON. The CLI prints the query it sent, so you can see what the model did to your words.
Privacy is spelled out, not implied.
| What | Where it goes |
|---|---|
| Feeds, items, stars, labels, extracted text | A local SQLite file |
| Article text for AI boxes | Your local Ollama only (cloud model is a separate opt-in) |
| SerpApi | The refined query and options. Never article text or reading history |
| Sentry | Nothing, unless you set a DSN. API keys and URL query strings are redacted |
What Broke
RSS feeds don't deliver data in the same structure There is no way to guess the structure in which an RSS feed gives us it's data. While one may simply dump the article's text in the XML, others may just include a link to it with a brief summary of the article. This was a challenge since m RSS file had to properly display it's reader data without making it's users go somewhere else to read more on it. I had to use BeautifulSoup and trafilatura python packages to fetch and transform inconsistent RSS feeds into well formatted HTML to be parsed by the reader
My first summaries summarized nothing. The Hacker News feed does not contain the articles. Each entry is a title and a line that says Article URL: https://.... So my first Summarize box was faithfully summarizing a link. The fix was the fulltext box, which fetches the page and extracts the body with trafilatura, and a rule that Summarize only reads real text.
A blog URL "worked" and gave me nothing. I pasted a blog's homepage, got a 200 response, zero items, and a healthy-looking feed. I asked a coding agent to audit my fetcher and it returned 34 findings. The worst one was exactly this: an HTML page was being counted as a successful fetch. The fetcher now checks that the response really is a feed, rejects empty bodies and HTML pages, follows redirects one hop at a time and re-checks every hop so a public feed cannot bounce into a private address, caps responses at 2 MB, and has a discovery step that finds a feed from a blog URL.
Apostrophes arrived as ’. Feed text carries HTML entities, and my cleanup skipped short snippets. Titles and summaries are now normalized and feed HTML is sanitized before it is stored.
Where It Is Still Rough
-
I have no quality numbers for the AI boxes. There is a Keep/Drop button in the reader and an export that writes your decisions as JSONL, which is the data a fine-tune would need. I have not trained anything, so I am not claiming a tuned model or a measured accuracy. The model in this post is the base
gemma3:1b. -
There is no scheduler. Feeds refresh when you click Refresh or when cron calls
tributary refresh. - Pipe runs are not durable. They run on an in-process thread. A restart marks interrupted runs as failed, and a run cannot be cancelled yet. I plan on integrating temporal for durable runs even after a restart
- No OPML import, no nested labels, no read-aloud, single user only.
Why Does Open Innovation Matter?
Google Reader and Yahoo Pipes are not gone because they were bad. They are gone because they lived on someone else's server and that someone changed their mind. That is the whole argument for open, and it shaped Tributary:
- A pipe is a JSON file. You can save it, diff it, share it, and run it from the CLI with no browser. If Tributary vanished tomorrow, your pipes and your data would not.
- Your library is one SQLite file. It is not locked in a service, and the Keep/Drop export is already there.
- It works with the lights off. With Ollama local, the summaries, the meaning filter, and the query rewriting need no internet. Only fetching the feeds themselves does.
- Your reading stays yours. Article text goes to a model on your own machine. Nothing reconstructs your reading history on someone else's disk.
-
The model is a setting.
OLLAMA_MODELswaps it. A bigger model on a better machine is one environment variable away. - It costs nothing per summary. The only meter running is my laptop fan.
Where a closed model would be better. A frontier model would almost certainly judge "is this practical engineering without hype?" better than a 1B model, and would write sharper summaries. I have not measured the gap, so I will not put a number on it. The honest trade is quality against ownership, and for a tool I want to still have in ten years I chose ownership. There is also an opt-in cloud model (gemma4:3b through Ollama Cloud) for people who want a stronger model. When it is on, article text and prompts do leave your machine, and the docs say so.
The Spirit of It
The tools that quietly let you read what you wanted, in the order you wanted, until someone shut the server off.
Prize Categories
-
Best Use of Gemma. Every AI box runs on
gemma3:1blocally through Ollama: Summarize, Filter by meaning, Why you'd care, and the search query refinement. It is the default and it works fully offline. An opt-ingemma4:3bcloud mode is available for larger models. -
Best Use of Sentry Agent Tracing. One trace per pipe run with a span per box (item counts, duration), plus
gen_ai.chatspans with model and token counts for every AI and refinement call, and feed-refresh and search metrics. Keys and URL query strings are redacted, and prompt capture is a separate switch that is off by default. -
Best Use of SerpApi.
searchis a real source box, not a bolt-on. A local model refines the query, SerpApi fetches results, they are cached for six hours, and a fallback keeps it working if the refinement fails. Search is off unless a key is set. - Best Use of Entire. The agent session behind the project is linked above, including the fetcher audit.
- Best Use of .tech domain. The app is live but cloud features are yet to be integrated
What Is Next
The cloud tier. The code is built around the same service layer, with PostgreSQL and Auth0 accounts, kept as an adapter rather than a fork so the self-hosted build never grows a login screen. I am not opening signups until per-user limits and abuse protections are in. After that: a built-in scheduler, durable pipe runs, OPML, and an evaluation set from my Keep/Drop labels so I can finally try a fine-tune and report numbers, whether or not they flatter me.
來源:DEV Community · dev.to