WorkU1ADIOS

AdiosAnalysis

News aggregation that understands the story.

Status: Live in production

Role: Co-built: back end and front end

A story’s chronology on adios.hr: water safety in Lika and on Pag, followed from 30 September to 4 November in 12 steps built from 28 articles across 6 portals, with a timeline of the steps and the outlets behind the first oneA story’s analysis on adios.hr: the summary, extra context, scores for sensationalism, exaggeration and emotional language, the story’s themes, and the three outlets that covered itThe adios.hr news map: stories pinned to where they happened across Croatia and its neighbours, with the latest listed beside itadios.hr in numbers: 1,476 articles from 10 portals in the last 24 hours, with an hour-by-hour chart
The same story’s chronology on a phone: the headline, 12 steps from 28 articles across 6 portals, and the start of the timelineA story page on a phone: the illustration, the source, the headline and the start of the analysisThe news map on a phone, with numbered clusters of stories over Croatia and its neighbours

Key specifications

U1 · ADIOS · Rev 2026.11

Role
Co-built: back end and front end
Status
Live in production
Repo
Private; happy to walk through it
Capabilities
  • Back-end & data
  • Front-end
  • AI and LLM integration
  • Infra and delivery
Stack
  • 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

Signal path · 5 stages

How it works

Adios, from input to output

  1. RSS feeds: 24sata · Index · Slobodna Dalmacija
  2. LLM analysis: summary · tags · 4 perceptual scores
  3. Embeddings: 4 per article · 1,536-⁠d
  4. Story clusters: weighted blend · 80% threshold
  5. adios.hr: one deploy per country

What it does

Adios is the kind of news experience you’d want for yourself. You open one site and see the day’s stories grouped by event rather than by publisher. Each story comes with a short editorial summary in your language, signals for whether the headline is clickbait or vague, and a “background” panel for anything that lacks context.

Behind the scenes, every article from every source goes through the same pipeline before it reaches a reader. RSS feeds from major publishers come in continuously. Each new article gets a structured editorial pass from an LLM, is embedded as vectors and is compared with recent stories from other outlets to find the ones covering the same event. The reader app then surfaces it through theme-based search.

I worked on the front end as well as the pipeline: the reader site is server-rendered Razor on ASP.NET Core MVC.

How it’s built

Structured editorial analysis from the LLM

Each article goes to OpenAI with a custom prompt that returns strict JSON:

  • an editorial summary
  • a normalised title that cleans up the publisher’s clickbait headline
  • an additional-context paragraph for stories that need background
  • an SEO title and description
  • sets of topics, tags, named entities and affected countries
  • four perceptual scores: clickbait, vagueness, typos 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, and no “according to the source” boilerplate.

Config-driven multilingual prompting

The prompt template is language-agnostic. Per-country config fills in placeholders for the active language and the local country, so the model never sees the “wrong” language for the deployment. 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 and text clustering

Each article is embedded as four separate 1,536-dimension vectors:

  • the normalised title
  • a packed semantic-metadata blob
  • the raw publisher title
  • a sorted-words signature, which catches near-duplicates with reordered phrasing

A weighted blend of the four (0.45 / 0.25 / 0.15 / 0.10) is combined with text-similarity, shared-tag and theme-overlap signals. Anything with a combined score above 80% 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.

Reader queries are embedded and matched against article themes by cosine similarity. Each query surfaces the top 40 themes from a candidate pool of 500 articles: fast enough to feel live, narrow enough to stay relevant.

Background services and retry semantics

Five hosted services run the pipeline in sequence and talk to each other through bounded channels:

  1. RSS crawl
  2. AI analysis
  3. Vectorisation
  4. Similarity clustering
  5. Theme and trending refresh

Per-row processing locks mean we never process an article twice across replicas. Transient failures retry with exponential backoff, and clustering gracefully requeues an article whose vectors aren’t ready yet. The pipeline keeps moving even when one stage stalls.

Language-specific country classifier

A per-country signal dictionary decides whether a story is local or international before it reaches the reader. It holds place names in the active language, plus a glossary of foreign-country names that is also in the active language. “Italian” looks different in different languages, so the classifier follows whichever language the deployment runs in.

Multi-tenant by deployment

One country is one deployment with one database. Adding a country takes about a 60-line config file, a translation resource file and two dictionary entries, with no 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.

Status

Adios processes real RSS feeds from major Croatian publishers (24sata, Index and Slobodna Dalmacija), with LLM annotation, vectorisation and clustering running 24/7.

On the roadmap:

  • more publishers per country
  • an eval harness for the editorial-voice prompts
  • a tighter feedback loop from reader engagement back into the clickbait and vagueness scoring

In numbers

vectors per article
4 × 1,536-d
clustering threshold
80%
translated keys per language
~1,500
config lines to add a country
~60