This is Part 3 of a 5-part series documenting my MSSE Capstone project at Quantic: a deployed, agentic RAG system that answers natural-language questions about European electricity markets. Part 2 covered the two architectural pivots — this one covers what actually goes into the system, and the unglamorous work that makes it trustworthy.


Three kinds of source, none of which talk to each other

The whole premise of this project is that the answer to “why did prices move” is scattered across sources that were never designed to be read together. So the first real engineering problem wasn’t retrieval or generation — it was just getting a coherent corpus in the first place, out of three genuinely different kinds of source:

  • Structured outage data from the ENTSO-E Transparency Platform — the closest thing to ground truth on what’s actually offline and when.
  • Regulatory disclosures — REMIT/UMM notices, pulled from Nord Pool’s UMM RSS feed and the IIP Atom feed. These are terse, legally worded, and precise in a way news never is.
  • News — Google News RSS, the Guardian API, and Clean Energy Wire, which is where the why behind an outage usually shows up in language a human actually wrote.

None of these were built to cross-reference each other. A UMM notice identifies an asset by an EIC code. A news article about the same asset never mentions that code — it says “the plant” or the operator’s name. Getting these to line up in a way the retrieval layer can actually use turned out to be most of the real work.

The unglamorous fix that mattered most: stripping codes journalists never write

Early on, news retrieval for a given outage event was quietly bad, and it took a while to see why. The query-construction step was building search queries straight out of the asset name in the structured data — which meant EIC-code prefixes and zone suffixes were riding along into a search engine that was never going to match them against a Guardian headline. No journalist writes “10T-DE-RE-Brunsbuttel-01” — they write “Brunsbüttel.”

The fix was simple once it was diagnosed: strip the EIC-code prefix and the zone suffix before building the news query, so it reads the way a human would actually write it. This is not a sophisticated fix. It’s the kind of bug that’s invisible in a demo and quietly kills your recall in production. It’s also exactly the kind of thing that separates a corpus with real citations from one that just looks like it has them.

Making retrieval trust more than one kind of match

Once the corpus was clean enough to search, the next question was what “search” should even mean here. Pure semantic similarity is good at surfacing related content and bad at surfacing the specific event a question is asking about. So retrieval ended up hybrid: semantic search over Cohere multilingual embeddings, combined with structured filtering — zone, date range, event type — so a question like “what happened in DK1 last week” doesn’t rely on an embedding model’s fuzzy sense of “last week.”

Sitting on top of that is a three-band guardrail system with an LLM relevance gate — the layer that decides whether the system has actually found something worth answering with, or should say so honestly instead of stretching a weak match into an answer. Getting that gate to behave correctly turned out to be its own long story, including a bug where a user-selected zone from the dropdown never actually reached the gate — worth its own mention later, since it silently made zone-scoped answers worse for months before an eval run caught it.

Closing the loop: linking events to the news that explains them

The corpus started as three separate streams sitting next to each other. The piece that actually made it feel like a coherent analyst rather than three search indexes was event-to-news enrichment — a step that looks at a structured outage event and finds the news articles that plausibly explain it, then stores that link explicitly rather than hoping retrieval finds the connection at query time.

This came together as enrich_events_with_news(), feeding an event_news_links table, wired into the ingestion run so it happens automatically rather than as a manual step. It’s a small addition on paper, but it’s the difference between “here’s an outage” and “here’s an outage, and here’s the reporting that tells you what it means.”

What’s ahead

With a corpus that’s clean, hybrid-searchable, and cross-linked, the next problem is making the system do more than retrieve — reaching past its own static corpus to pull live numbers when a question calls for them. That’s the agent layer, and where most of the real debugging stories in this project come from.

Leave a Reply

Trending

Discover more from Convergence Point

Subscribe now to keep reading and get access to the full archive.

Continue reading