Production projectlive · adios.hr

AdiosAnalysis — news aggregation that understands the story.

A production AI-powered news platform. Ingests RSS feeds from major publishers, runs every article through an LLM analysis and vector-embedding pipeline, and clusters cross-source coverage of the same event. Co-built with a colleague; live at adios.hr, with a multilingual, multi-tenant architecture designed to spin up additional countries by changing a single config flag.

OpenAI Responses API text-embedding-3-small pgvector .NET 10 ASP.NET Core MVC EF Core Razor PostgreSQL Serilog → Seq JWT + Google OAuth ASP.NET Identity .resx localisation

01What it does

The product is the kind of news experience you'd want for yourself: open one site, see the day's stories grouped by event rather than by publisher, with a short editorial summary written in your language, signals for whether the headline is clickbait or vague, and a "background" panel for anything missing context. Behind the scenes, every article from every source goes through the same pipeline before it ever reaches a reader.

RSS feeds from major publishers come in continuously. Each new article is sent to an LLM for a structured editorial pass, embedded into a vector representation, compared against recent stories from other outlets to figure out which ones are covering the same event, and then surfaced through the reader app via theme-based search. Five separate hosted services run the stages in sequence with bounded channels and per-row locks, so we never double-process an article across replicas.

02The AI pipeline

Structured editorial analysis from the LLM

Each article is sent to OpenAI with a custom prompt that returns strict JSON: an editorial summary, a normalised title (cleaning up the publisher's clickbait headline), an additional-context paragraph for stories that need background, an SEO title and description, a topic set, a tag set, a named-entity set, an affected-country set, and four perceptual scores — clickbait, vagueness, typo, and emotional-language. The prompt is engineered for strict schema compliance and tuned for editorial voice: lightly witty on soft news, sober on hard news, no "according to the source" boilerplate.

Config-driven multilingual prompting

The prompt template is language-agnostic. Per-country config injects placeholders for the active language and the local country, so the model never sees the "wrong" language for the deployment. Adding a new country's prompt is one config entry plus a translation pass on the editorial-voice phrasing — the analysis logic itself doesn't change.

Hybrid vector + text clustering

Each article is embedded into four separate 1,536-dimension vectors: one for the normalised title, one for a packed semantic-metadata blob, one for the raw publisher title, and one for a sorted-words signature that catches near-duplicates with reordered phrasing. A weighted blend — 0.45 / 0.25 / 0.15 / 0.10 — is combined with text-similarity, shared-tag, and theme-overlap signals. Anything above an 80% combined score is clustered as the same event across sources.

"Embed it four ways" beats "embed it once well" — different vectors catch different kinds of similarity, and the blend filters out the false positives any single one would produce.

Vector-driven theme search

Reader queries are embedded and matched against article themes via cosine similarity. The system surfaces the top 40 themes per query out of a 500-article candidate pool — fast enough to feel live, narrow enough to stay relevant.

Background services, retry semantics

The five hosted services — RSS crawl, AI analysis, vectorisation, similarity clustering, theme/trending refresh — communicate via bounded channels. Per-row processing locks prevent double-work across replicas; transient failures retry with exponential backoff; clustering requeues an article gracefully when its vectors aren't ready yet. The pipeline keeps moving even when one stage stalls.

Language-specific country classifier

A per-country signal dictionary — place names in the active language plus a glossary of foreign-country names also in the active language — decides whether a story is local or international before it reaches the reader. "Italian" looks different in different languages, so the classifier respects whichever language the deployment is running in.

03Multi-tenant by deployment

One country equals one deployment equals one database, gated by a single config switch. Adding a new country is roughly a 60-line config file plus a translation resource file plus two dictionary entries — zero code changes to the analysis pipeline. Around 1,500 resource keys are translated per language, covering identity, email, SEO, and AI output. The same model and embedding stack runs everywhere; only the prompt placeholders, the country signal dictionaries, and the translation bundle differ.

Vectors / article

4× 1,536-dim

Resource keys

~1,500per language

Add a country

~60config lines

04Status

Live in production at adios.hr. Processing real RSS feeds from major Croatian publishers — 24sata, Index, Slobodna Dalmacija — with continuous LLM annotation, vectorisation, and clustering running 24/7. Architected so the next country is a config change, not an engineering project.

On the roadmap: more publishers per country, an eval harness for the editorial-voice prompts, and a tighter feedback loop from reader engagement back into the clickbait/vagueness scoring.

Resources

Repoprivate — happy to walk through it

Want a code walkthrough?

Happy to share screen on the AI pipeline, multi-tenant design, or the clustering math.

[email protected]