SPB Git forge

spb/qc26

Public
10commits 1branches 0releases
2.8 MBsize
maindefault branch
17 days agolast push
Python 51.6% TypeScript 46.7% CSS 1.7%

Boussole : élargissement lexical des propositions candidates ; PLQ : pages d'amorçage engagements/communiqués ; 80 documents par parti

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 18 days ago (Sep 6, 2026) parent 3e2d0ec

2 changed files +25 −4

modified engine/qc26/compass/positions.py +23 −2
@@ -59,12 +59,33 @@ Si les propositions ne permettent PAS de conclure (sujet absent, ambigu), répon
59 59 Réponds : {"value": -2|-1|0|1|2|null, "confidence": 0-1, "proposal_ids": [ids utilisés], "rationale": "1-2 phrases citant les propositions"}"""
60 60
61 61
62 −def _proposals_for(s: Session, party_id: str, topic: str, limit: int = 40) -> list[PolicyProposal]:
62 +_STOP = {"les", "des", "une", "pour", "dans", "avec", "plus", "être", "devrait", "devraient", "leur", "leurs", "cette", "sont", "que", "qui", "par", "sur", "aux", "est", "pas", "même", "sans", "tout", "tous", "afin", "cela", "quitte", "aussi", "entre"}
63 +
64 +
65 +def _keywords(text: str) -> set[str]:
66 + import re
67 + return {w for w in re.findall(r"[a-zàâçéèêëîïôûùüÿœ\-]{4,}", text.lower()) if w not in _STOP}
68 +
69 +
70 +def _proposals_for(s: Session, party_id: str, topic: str, limit: int = 40, question_text: str | None = None) -> list[PolicyProposal]:
63 71 topics = RELATED.get(topic, [topic])
64 72 rows = s.scalars(select(PolicyProposal).where(PolicyProposal.party_id == party_id, PolicyProposal.topic.in_(topics),
65 73 PolicyProposal.status == "accepted").order_by(PolicyProposal.confidence.desc())).all()
66 74 # sujet exact d'abord
67 75 rows.sort(key=lambda p: (0 if p.topic == topic else 1, -p.confidence))
76 + if len(rows) < 8 and question_text:
77 + # peu de propositions classées dans les sujets voisins : on élargit par recouvrement lexical avec l'énoncé
78 + kw = _keywords(question_text)
79 + others = s.scalars(select(PolicyProposal).where(PolicyProposal.party_id == party_id, PolicyProposal.status == "accepted",
80 + PolicyProposal.topic.not_in(topics))).all()
81 + scored = []
82 + for p in others:
83 + words = _keywords(f"{p.proposal} {p.position or ''} {p.subtopic or ''} {p.quote}")
84 + score = len(kw & words)
85 + if score >= 2:
86 + scored.append((score, p))
87 + scored.sort(key=lambda z: -z[0])
88 + rows += [p for _, p in scored[: max(0, 25 - len(rows))]]
68 89 return rows[:limit]
69 90
70 91
@@ -79,7 +100,7 @@ def compute_positions(s: Session, party_ids: list[str] | None = None, only_missi
79 100 if pos and only_missing and pos.status in ("extracted", "manual"):
80 101 skipped += 1
81 102 continue
82 − props = _proposals_for(s, pid, q.topic)
103 + props = _proposals_for(s, pid, q.topic, question_text=q.text)
83 104 if not pos:
84 105 pos = PartyPosition(question_id=q.id, party_id=pid)
85 106 s.add(pos)
modified engine/qc26/connectors/parties/documents.py +2 −2
@@ -62,7 +62,7 @@ SITES: dict[str, PartySite] = {
62 62 ("plateforme 2026", "engagements", "cadre financier", "communiqué de presse"),
63 63 re.compile(r"plq\.org/(?!.*(don|boutique|adhesion|nous-joindre|recherche|logo))", re.I),
64 64 re.compile(r"(\.(jpg|jpeg|png|gif|svg|webp|mp4)|/en/|/page/\d|wp-json|/feed|#)", re.I),
65 − ("https://plq.org/", "https://plq.org/fr/nouvelles/", "https://plq.org/fr/plateforme/")),
65 + ("https://plq.org/", "https://plq.org/engagements", "https://plq.org/communiques", "https://plq.org/fr/communiques", "https://plq.org/fr/nouvelles/", "https://plq.org/fr/plateforme/")),
66 66 "pq": PartySite("pq", "https://pq.org/",
67 67 ("programme", "plateforme électorale 2026", "engagements", "cadre financier", "communiqué"),
68 68 re.compile(r"pq\.org/(?!.*(don|boutique|adhesion|ressources|nous-joindre|recherche))", re.I),
@@ -331,7 +331,7 @@ def analyze_version(s: Session, doc: PartyDocument, v: DocumentVersion, max_chun
331 331 return n
332 332
333 333
334 −def sync_party(s: Session, party_id: str, max_docs: int = 40, analyze: bool = True) -> dict:
334 +def sync_party(s: Session, party_id: str, max_docs: int = 80, analyze: bool = True) -> dict:
335 335 site = SITES[party_id]
336 336 job = IngestionJob(kind=f"party-docs-{party_id}")
337 337 s.add(job)
338 338