Comment votent les partis?
Python · React · Firebase · Firestore · Gemini
"Comment votent les partis?" compares how French parties vote on laws, at the European Parliament and at the French National Assembly, with what they promised in their programs. It reads the bills and the programs, uses an LLM to sort them by topic, and stores them as vectors so anyone can search a topic and see how each party voted on it.
Genesis
Living outside of France, I find it hard to follow French politics. I also see a lot of claims from politicians that do not always match what they actually vote when it comes to laws. And some parties vote against the government purely for political reasons, even when the law itself might be a good one.
I decided to build a small application to help me understand this, and to get a quick view and comparison across the whole French political landscape.
How it works
"Comment votent les partis?" is a partial RAG (retrieval-augmented generation): it has the ingestion and vector search half of a RAG, but no chat. Instead of answering questions in a conversation, it uses the same building blocks to classify and compare content.
Every day, the application:
- Finds the bills that were voted in the last 30 days, using the official open data of the National Assembly and the European Parliament.
- Saves how each member of parliament voted, and which party they belong to.
- Downloads the full text of each new bill and cuts it into small passages (about 1,800 characters each).
- Asks an LLM which topics the bill affects, and in which direction (for example, does it make immigration easier or stricter?).
- Turns each passage into a vector (a list of numbers that places a text by its meaning) and stores it.
Party programs from the last elections go through the same steps: the LLM reads each program and lists its concrete promises, with the exact quote and page, and each promise is stored as a vector too.
flowchart LR
od[Open data: Assembly and European Parliament] --> sync[Daily sync]
sync --> votes[(Votes)]
sync --> ingest[Bill ingestion]
ingest --> classify[Classify by topic with Gemini]
ingest --> embed[Embed passages with Gemini]
programs[Party programs PDF] --> pledges[Extract promises with Gemini]
classify --> db[(Firestore with vector search)]
embed --> db
pledges --> db
votes --> db
db --> api[API]
api --> web[Web app]
Classifying bills by category
"Comment votent les partis?" sorts every bill into a fixed list of categories: women's rights, LGBTQ+ rights, environment, workers' rights, immigration, civil liberties, public security, social welfare, taxation, agriculture and education.
Each category has a clear definition of what "+1" and "-1" mean. The LLM gets the bill's text and these definitions, and answers in a fixed JSON format: which categories the bill touches, how much, in which direction, and how confident it is. The LLM never sees the votes, so it can't be influenced by who voted for what.
A party's score on a category then comes from its votes: voting for a bill that moves immigration towards "+1" counts as +1, voting against counts as -1. Only bills that are clearly about the category count, otherwise a big budget law tagged with every topic would blur everything.
Each party's score per category, from its votes
Comparing programs and votes
"Comment votent les partis?" also compares what each party promised with what its members voted. Promises extracted from the programs use the same categories and the same +1 / -1 scale as bills, so both can be compared directly.
For each party and category, the site shows how many of the party's votes went the way its program promised. The score is always shown next to a baseline (what the party would get by voting for everything), because most bills go in the same direction, and a party voting "yes" to everything would otherwise look very consistent.
To check the LLM was not making things up, I verified the quotes it extracted against the program PDFs: 94% of the 2,412 quotes were found word for word on the page given, and none of the others were invented (mostly two sentences joined together, or missing accents). Every promise links to its page, so anyone can check.
Searching any topic with vectors
On "Comment votent les partis?", a visitor can type any topic, like "retraites" or "nucléaire", not only the fixed categories. This is where the vector search comes in: the topic is turned into a vector, and Firestore returns the bill passages and program promises closest in meaning. The site then shows how each party voted on those bills, and what they promised on that topic.
Searching a free topic: retraite
Below the scores, the site lists the bills that were found, with how each party's members voted on them and a link to the official page. This way anyone can check what a score is made of.
The bills behind a score, and each party's votes
My first version gave wrong results. Searching "immigration" showed LFI as "against immigration", because most bills found made immigration stricter, and LFI voted against them. Now, when a typed topic is close enough to one of the categories, the search uses that category's direction for each bill. When it is not, the site only shows "support for the bills about this topic", without pretending to know which side is which.
Architecture choices
The goal of "Comment votent les partis?" was to stay close to zero cost.
- Firebase for almost everything: Firestore stores bills, votes, promises and vectors, and has vector search built in, so there was no need for a separate vector database. Hosting serves the React web app, and Cloud Functions run the API and the daily sync.
- Gemini on the free tier, both for the embeddings and for the classification. Bills are public documents, so sending them to a free-tier API was fine.
- Python for the ingestion pipeline, as parsing HTML and PDFs is easy there.
- Cloud Scheduler and Cloud Tasks for the daily sync: each chamber has its own daily job, and each new bill becomes one task in a queue. If one bill fails, it is retried without blocking the others.
The European Parliament API often asks to slow down, so importing 5 years of history did not fit in a cloud function's time limit. The older years were imported once from my laptop, and the daily sync only looks at the last 30 days.
Limits
"Comment votent les partis?" shows votes, not intentions, and some biases are hard to remove:
- Parties vote on a whole law, not on one topic. Big government bills touch many categories, so a party opposing the government can look "against farmers" just because it voted against the budget.
- Most bills add rights or support, so a score mostly says "voted for most bills in this category". The categories with bills going both ways, like immigration or public security, give the most useful results.
- Some categories have very few bills so far, so I would not draw conclusions from them yet.
This is why every score on the site links back to the bills and the votes behind it.