|
| 1 | +from collections import defaultdict |
1 | 2 | from datetime import datetime |
2 | 3 | from typing import Any, Literal |
3 | 4 |
|
@@ -53,6 +54,46 @@ def build_filters(conditions: list[FieldCondition | None]) -> Filter | None: |
53 | 54 | return Filter(must=valid_conditions) |
54 | 55 |
|
55 | 56 |
|
| 57 | +class DebateCollection: |
| 58 | + """Collection of debates and their contributions. |
| 59 | + Used to track the contributions for each debate and return the substantial debates. |
| 60 | + """ |
| 61 | + |
| 62 | + def __init__(self): |
| 63 | + self._debates = defaultdict(lambda: {"contribution_ids": set(), "info": None}) |
| 64 | + |
| 65 | + def add_contribution(self, contribution): |
| 66 | + debate_id = contribution.get("DebateSectionExtId") |
| 67 | + contribution_id = contribution.get("ContributionExtId") |
| 68 | + debate = self._debates[debate_id] |
| 69 | + new_data = contribution_id not in debate["contribution_ids"] |
| 70 | + debate["contribution_ids"].add(contribution_id) |
| 71 | + if debate["info"] is None: |
| 72 | + debate["info"] = { |
| 73 | + "debate_id": debate_id, |
| 74 | + "title": contribution.get("DebateSection"), |
| 75 | + "date": contribution.get("SittingDate"), |
| 76 | + "house": contribution.get("House"), |
| 77 | + "debate_parents": contribution.get("debate_parents", []), |
| 78 | + "debate_url": contribution.get("debate_url"), |
| 79 | + } |
| 80 | + return new_data |
| 81 | + |
| 82 | + def get_substantial_debates(self): |
| 83 | + return [ |
| 84 | + debate["info"] |
| 85 | + for debate in self._debates.values() |
| 86 | + if len(debate["contribution_ids"]) >= MINIMUM_DEBATE_HITS |
| 87 | + ] |
| 88 | + |
| 89 | + def get_substantial_debate_ids(self): |
| 90 | + return [ |
| 91 | + debate_id |
| 92 | + for debate_id, debate in self._debates.items() |
| 93 | + if len(debate["contribution_ids"]) >= MINIMUM_DEBATE_HITS |
| 94 | + ] |
| 95 | + |
| 96 | + |
56 | 97 | class QdrantQueryHandler: |
57 | 98 | def __init__( |
58 | 99 | self, qdrant_client: AsyncQdrantClient, openai_client: AsyncAzureOpenAI, settings: ParliamentMCPSettings |
@@ -82,7 +123,7 @@ async def search_debate_titles( |
82 | 123 | date_from: str | None = None, |
83 | 124 | date_to: str | None = None, |
84 | 125 | house: str | None = None, |
85 | | - max_results: int = 100, |
| 126 | + max_results: int = 50, |
86 | 127 | ) -> list[dict]: |
87 | 128 | """ |
88 | 129 | Search debate titles with optional filters. |
@@ -116,34 +157,34 @@ async def search_debate_titles( |
116 | 157 |
|
117 | 158 | query_filter = build_filters(filter_conditions) |
118 | 159 |
|
119 | | - query_response = await self.qdrant_client.query_points_groups( |
120 | | - collection_name=self.settings.HANSARD_CONTRIBUTIONS_COLLECTION, |
121 | | - query_filter=query_filter, |
122 | | - limit=max_results, |
123 | | - with_payload=True, |
124 | | - group_by="DebateSectionExtId", |
125 | | - group_size=MINIMUM_DEBATE_HITS, |
126 | | - ) |
| 160 | + debates = DebateCollection() |
127 | 161 |
|
128 | | - results = [] |
129 | | - for group in query_response.groups: |
130 | | - first_hit = group.hits[0].payload |
131 | | - # If there are less than 2 hits, the debate is not relevant |
132 | | - if len(group.hits) < MINIMUM_DEBATE_HITS: |
133 | | - continue |
134 | | - |
135 | | - debate = { |
136 | | - "debate_id": first_hit.get("DebateSectionExtId"), |
137 | | - "title": first_hit.get("DebateSection"), |
138 | | - "date": first_hit.get("SittingDate"), |
139 | | - "house": first_hit.get("House"), |
140 | | - "debate_parents": first_hit.get("debate_parents", []), |
141 | | - "debate_url": first_hit.get("debate_url"), |
142 | | - } |
| 162 | + while len(substantial_ids := debates.get_substantial_debate_ids()) < max_results: |
| 163 | + # Filter out already found substantial debates |
| 164 | + if substantial_ids: |
| 165 | + query_filter.must_not = [ |
| 166 | + FieldCondition(key="DebateSectionExtId", match=models.MatchAny(any=substantial_ids)), |
| 167 | + ] |
| 168 | + |
| 169 | + contributions, _ = await self.qdrant_client.scroll( |
| 170 | + collection_name=self.settings.HANSARD_CONTRIBUTIONS_COLLECTION, |
| 171 | + scroll_filter=query_filter, |
| 172 | + limit=1000, |
| 173 | + with_payload=True, |
| 174 | + order_by={"key": "SittingDate", "direction": "desc"}, |
| 175 | + ) |
143 | 176 |
|
144 | | - results.append(debate) |
| 177 | + if not contributions: |
| 178 | + break |
145 | 179 |
|
146 | | - return results |
| 180 | + new_data_available = False |
| 181 | + for result in contributions: |
| 182 | + new_data_available |= debates.add_contribution(result.payload) |
| 183 | + |
| 184 | + if not new_data_available: |
| 185 | + break |
| 186 | + |
| 187 | + return debates.get_substantial_debates()[:max_results] |
147 | 188 |
|
148 | 189 | async def search_hansard_contributions( |
149 | 190 | self, |
@@ -219,7 +260,7 @@ async def search_hansard_contributions( |
219 | 260 | query_response = query_response.points |
220 | 261 | else: |
221 | 262 | # If no query, use scroll to get results with filters only |
222 | | - query_response, offset = await self.qdrant_client.scroll( |
| 263 | + query_response, _ = await self.qdrant_client.scroll( |
223 | 264 | collection_name=self.settings.HANSARD_CONTRIBUTIONS_COLLECTION, |
224 | 265 | scroll_filter=query_filter, |
225 | 266 | limit=max_results, |
@@ -429,13 +470,13 @@ async def search_parliamentary_questions( |
429 | 470 |
|
430 | 471 | else: |
431 | 472 | # If no query, use scroll to get results with filters only |
432 | | - query_response, offset = await self.qdrant_client.scroll( |
| 473 | + query_response, _ = await self.qdrant_client.scroll( |
433 | 474 | collection_name=self.settings.PARLIAMENTARY_QUESTIONS_COLLECTION, |
434 | 475 | scroll_filter=query_filter, |
435 | 476 | limit=max_results, |
436 | 477 | with_payload=True, |
437 | 478 | order_by={ |
438 | | - "key": "created_at", |
| 479 | + "key": "id", |
439 | 480 | "direction": "desc", |
440 | 481 | }, |
441 | 482 | ) |
|
0 commit comments