Skip to content

Commit 6886952

Browse files
authored
Merge pull request #23 from i-dot-ai:feat/search-debates-by-title-recency
return most recent debates in search_debate_titles
2 parents d7d949e + 5bc0500 commit 6886952

4 files changed

Lines changed: 87 additions & 54 deletions

File tree

parliament_mcp/mcp_server/api.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,14 @@ async def search_debate_titles(
107107
Only returns the debate ID, title, and date, not the content of the debate.
108108
Useful for finding relevant debates, but must be used in conjunction with search_contributions to get the full text of the debate.
109109
110-
Either query or date range (or both) must be provided. House is optional.
111-
If only date_from is provided, returns debates from that date onwards.
112-
If only date_to is provided, returns debates up to and including that date.
110+
Use date_from and date_to to search for debates in a specific date range.
111+
Use query to search for debates with a keyword or phrase in the title.
113112
114-
Returns a list of debate details (ID, title, date) ranked by relevancy.
113+
Returns a list of debate details (ID, title, date) in order the most recent first.
115114
116115
Common use case for this function:
117-
- Provide a query to search for debates on a specific topic for all time
118-
- Provide the date for date_from and date_to to search for all debates on a specific date
116+
- Provide a query to search for debates on a specific topic
117+
- Provide the date for date_from and date_to to search for debates within a specific date range
119118
- Provide a query and date range to search for debates on a specific topic in a specific date range
120119
121120
Args:

parliament_mcp/mcp_server/members.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ async def get_election_results(
5252
raise ValueError(msg)
5353

5454

55-
# @log_tool_call
55+
@log_tool_call
5656
async def search_members(
5757
Name: str | None = Field(None, description="Member name"),
5858
PartyId: int | None = Field(None, description="Party ID"),

parliament_mcp/mcp_server/qdrant_query_handler.py

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import defaultdict
12
from datetime import datetime
23
from typing import Any, Literal
34

@@ -53,6 +54,46 @@ def build_filters(conditions: list[FieldCondition | None]) -> Filter | None:
5354
return Filter(must=valid_conditions)
5455

5556

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+
5697
class QdrantQueryHandler:
5798
def __init__(
5899
self, qdrant_client: AsyncQdrantClient, openai_client: AsyncAzureOpenAI, settings: ParliamentMCPSettings
@@ -82,7 +123,7 @@ async def search_debate_titles(
82123
date_from: str | None = None,
83124
date_to: str | None = None,
84125
house: str | None = None,
85-
max_results: int = 100,
126+
max_results: int = 50,
86127
) -> list[dict]:
87128
"""
88129
Search debate titles with optional filters.
@@ -116,34 +157,34 @@ async def search_debate_titles(
116157

117158
query_filter = build_filters(filter_conditions)
118159

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()
127161

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+
)
143176

144-
results.append(debate)
177+
if not contributions:
178+
break
145179

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]
147188

148189
async def search_hansard_contributions(
149190
self,
@@ -219,7 +260,7 @@ async def search_hansard_contributions(
219260
query_response = query_response.points
220261
else:
221262
# 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(
223264
collection_name=self.settings.HANSARD_CONTRIBUTIONS_COLLECTION,
224265
scroll_filter=query_filter,
225266
limit=max_results,
@@ -429,13 +470,13 @@ async def search_parliamentary_questions(
429470

430471
else:
431472
# 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(
433474
collection_name=self.settings.PARLIAMENTARY_QUESTIONS_COLLECTION,
434475
scroll_filter=query_filter,
435476
limit=max_results,
436477
with_payload=True,
437478
order_by={
438-
"key": "created_at",
479+
"key": "id",
439480
"direction": "desc",
440481
},
441482
)

parliament_mcp/qdrant_helpers.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ async def create_collection_indicies(client: AsyncQdrantClient, settings: Parlia
182182
field_schema=models.DatetimeIndexParams(
183183
type=models.DatetimeIndexType.DATETIME,
184184
),
185-
wait=False,
185+
wait=True,
186186
)
187187

188188
await client.create_payload_index(
@@ -191,7 +191,7 @@ async def create_collection_indicies(client: AsyncQdrantClient, settings: Parlia
191191
field_schema=models.DatetimeIndexParams(
192192
type=models.DatetimeIndexType.DATETIME,
193193
),
194-
wait=False,
194+
wait=True,
195195
)
196196

197197
await client.create_payload_index(
@@ -200,7 +200,7 @@ async def create_collection_indicies(client: AsyncQdrantClient, settings: Parlia
200200
field_schema=models.KeywordIndexParams(
201201
type=models.KeywordIndexType.KEYWORD,
202202
),
203-
wait=False,
203+
wait=True,
204204
)
205205

206206
await client.create_payload_index(
@@ -209,7 +209,7 @@ async def create_collection_indicies(client: AsyncQdrantClient, settings: Parlia
209209
field_schema=models.IntegerIndexParams(
210210
type=models.IntegerIndexType.INTEGER,
211211
),
212-
wait=False,
212+
wait=True,
213213
)
214214

215215
await client.create_payload_index(
@@ -218,7 +218,7 @@ async def create_collection_indicies(client: AsyncQdrantClient, settings: Parlia
218218
field_schema=models.KeywordIndexParams(
219219
type=models.KeywordIndexType.KEYWORD,
220220
),
221-
wait=False,
221+
wait=True,
222222
)
223223

224224
await client.create_payload_index(
@@ -234,7 +234,7 @@ async def create_collection_indicies(client: AsyncQdrantClient, settings: Parlia
234234
stopwords="english",
235235
stemmer=models.SnowballParams(type=models.Snowball.SNOWBALL, language=models.SnowballLanguage.ENGLISH),
236236
),
237-
wait=False,
237+
wait=True,
238238
)
239239

240240
await client.create_payload_index(
@@ -245,13 +245,6 @@ async def create_collection_indicies(client: AsyncQdrantClient, settings: Parlia
245245
lookup=True,
246246
range=True,
247247
),
248-
wait=False,
249-
)
250-
251-
await client.create_payload_index(
252-
collection_name=settings.PARLIAMENTARY_QUESTIONS_COLLECTION,
253-
field_name="created_at",
254-
field_schema=models.DatetimeIndexParams(type=models.DatetimeIndexType.DATETIME),
255248
wait=True,
256249
)
257250

@@ -263,7 +256,7 @@ async def create_collection_indicies(client: AsyncQdrantClient, settings: Parlia
263256
field_schema=models.DatetimeIndexParams(
264257
type=models.DatetimeIndexType.DATETIME,
265258
),
266-
wait=False,
259+
wait=True,
267260
)
268261

269262
await client.create_payload_index(
@@ -272,7 +265,7 @@ async def create_collection_indicies(client: AsyncQdrantClient, settings: Parlia
272265
field_schema=models.KeywordIndexParams(
273266
type=models.KeywordIndexType.KEYWORD,
274267
),
275-
wait=False,
268+
wait=True,
276269
)
277270

278271
await client.create_payload_index(
@@ -283,7 +276,7 @@ async def create_collection_indicies(client: AsyncQdrantClient, settings: Parlia
283276
lookup=True,
284277
range=True,
285278
),
286-
wait=False,
279+
wait=True,
287280
)
288281

289282
await client.create_payload_index(
@@ -292,7 +285,7 @@ async def create_collection_indicies(client: AsyncQdrantClient, settings: Parlia
292285
field_schema=models.KeywordIndexParams(
293286
type=models.KeywordIndexType.KEYWORD,
294287
),
295-
wait=False,
288+
wait=True,
296289
)
297290

298291
await client.create_payload_index(
@@ -308,5 +301,5 @@ async def create_collection_indicies(client: AsyncQdrantClient, settings: Parlia
308301
stopwords="english",
309302
stemmer=models.SnowballParams(type=models.Snowball.SNOWBALL, language=models.SnowballLanguage.ENGLISH),
310303
),
311-
wait=False,
304+
wait=True,
312305
)

0 commit comments

Comments
 (0)