SPB Git

spb/qc26

Public

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 (Sep 6, 2026) parent 3e2d0ec Browse files

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 Réponds : {"value": -2|-1|0|1|2|null, "confidence": 0-1, "proposal_ids": [ids utilisés], "rationale": "1-2 phrases citant les propositions"}""" 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 topics = RELATED.get(topic, [topic]) 71 topics = RELATED.get(topic, [topic])
64 rows = s.scalars(select(PolicyProposal).where(PolicyProposal.party_id == party_id, PolicyProposal.topic.in_(topics), 72 rows = s.scalars(select(PolicyProposal).where(PolicyProposal.party_id == party_id, PolicyProposal.topic.in_(topics),
65 PolicyProposal.status == "accepted").order_by(PolicyProposal.confidence.desc())).all() 73 PolicyProposal.status == "accepted").order_by(PolicyProposal.confidence.desc())).all()
66 # sujet exact d'abord 74 # sujet exact d'abord
67 rows.sort(key=lambda p: (0 if p.topic == topic else 1, -p.confidence)) 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 return rows[:limit] 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 if pos and only_missing and pos.status in ("extracted", "manual"): 100 if pos and only_missing and pos.status in ("extracted", "manual"):
80 skipped += 1 101 skipped += 1
81 continue 102 continue
82− props = _proposals_for(s, pid, q.topic) 103+ props = _proposals_for(s, pid, q.topic, question_text=q.text)
83 if not pos: 104 if not pos:
84 pos = PartyPosition(question_id=q.id, party_id=pid) 105 pos = PartyPosition(question_id=q.id, party_id=pid)
85 s.add(pos) 106 s.add(pos)
modified engine/qc26/connectors/parties/documents.py +2 −2
@@ -62,7 +62,7 @@ SITES: dict[str, PartySite] = {
62 ("plateforme 2026", "engagements", "cadre financier", "communiqué de presse"), 62 ("plateforme 2026", "engagements", "cadre financier", "communiqué de presse"),
63 re.compile(r"plq\.org/(?!.*(don|boutique|adhesion|nous-joindre|recherche|logo))", re.I), 63 re.compile(r"plq\.org/(?!.*(don|boutique|adhesion|nous-joindre|recherche|logo))", re.I),
64 re.compile(r"(\.(jpg|jpeg|png|gif|svg|webp|mp4)|/en/|/page/\d|wp-json|/feed|#)", re.I), 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 "pq": PartySite("pq", "https://pq.org/", 66 "pq": PartySite("pq", "https://pq.org/",
67 ("programme", "plateforme électorale 2026", "engagements", "cadre financier", "communiqué"), 67 ("programme", "plateforme électorale 2026", "engagements", "cadre financier", "communiqué"),
68 re.compile(r"pq\.org/(?!.*(don|boutique|adhesion|ressources|nous-joindre|recherche))", re.I), 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 return n 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 site = SITES[party_id] 335 site = SITES[party_id]
336 job = IngestionJob(kind=f"party-docs-{party_id}") 336 job = IngestionJob(kind=f"party-docs-{party_id}")
337 s.add(job) 337 s.add(job)
338 338
Menu

Theme