Skip to content

Commit c32ad3a

Browse files
committed
fix: harden identity query retrieval
1 parent e33aeb9 commit c32ad3a

4 files changed

Lines changed: 71 additions & 16 deletions

File tree

‎ai_filter.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5687,6 +5687,8 @@ def _valid(definition: str, *, reverse: bool = False) -> str:
56875687
forward_patterns = (
56885688
rf"\b{escaped}\s*:\s*(?P<definition>[^.;:]+)",
56895689
rf"\b{escaped}\s+(?:is|was|are|were)\s+(?P<definition>[^.;:]+)",
5690+
rf"\b{escaped},\s+(?:which|who)\s+(?:is|was|are|were)\s+(?P<definition>[^.;:]+)",
5691+
rf"\b{escaped},\s+(?:the|a|an)\s+(?P<definition>[^,.;:]+)",
56905692
rf"\b{escaped},\s+(?P<definition>[^.;:]+)",
56915693
)
56925694
for pattern in forward_patterns:
@@ -5739,11 +5741,15 @@ def _render_local_nlp_query_answer(
57395741
if lead:
57405742
parts.append(sanitize_telegram_html(lead))
57415743
seen = {_digest_line_key(lead)}
5744+
lead_plain = normalize_space(strip_telegram_html(lead)).lower()
57425745
max_bullets = 4 if detailed else 2
57435746
for sentence in sentences[1:]:
57445747
key = _digest_line_key(sentence)
57455748
if not key or key in seen:
57465749
continue
5750+
sentence_plain = normalize_space(strip_telegram_html(sentence)).lower()
5751+
if lead_plain and SequenceMatcher(None, lead_plain, sentence_plain).ratio() > 0.85:
5752+
continue
57475753
seen.add(key)
57485754
parts.append(f"• {sanitize_telegram_html(sentence)}")
57495755
if len(parts) >= 2 + max_bullets:
@@ -7396,6 +7402,7 @@ async def generate_answer_from_context_result(
73967402
output_language=output_language,
73977403
detailed=detailed,
73987404
strategic_trend=strategic_trend_query,
7405+
identity_query=_query_is_identity_question(question),
73997406
)
74007407
retry_issue = ""
74017408

‎main.py‎

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@
174174
extract_query_numbers,
175175
sanitize_telegram_html,
176176
search_entity_web_summary,
177+
query_entity_title,
177178
search_recent_news_web,
178179
search_recent_messages,
179180
query_prefers_direct_answer,
@@ -1734,6 +1735,10 @@ def _query_window_strategy(*, requested_hours: int, explicit_time_filter: bool)
17341735
return min(primary_hours, 24), max(primary_hours, _query_expanded_window_hours())
17351736

17361737

1738+
def _query_identity_window_hours() -> int:
1739+
return 24 * 14
1740+
1741+
17371742
def _query_needs_expanded_window(
17381743
query: str,
17391744
results: Sequence[Dict[str, object]],
@@ -9805,10 +9810,15 @@ async def _handle_query_request(
98059810

98069811
plan = build_query_plan(text, default_hours=_query_default_hours_back())
98079812
parsed_hours = plan.hours_back
9813+
explicit_time_filter = bool(getattr(plan, "explicit_time_filter", False))
98089814
primary_hours, expanded_hours = _query_window_strategy(
98099815
requested_hours=parsed_hours,
9810-
explicit_time_filter=bool(getattr(plan, "explicit_time_filter", False)),
9816+
explicit_time_filter=explicit_time_filter,
98119817
)
9818+
identity_query = bool(query_entity_title(plan.original_query or text))
9819+
if identity_query and not explicit_time_filter:
9820+
primary_hours = max(primary_hours, _query_identity_window_hours())
9821+
expanded_hours = max(expanded_hours, primary_hours)
98129822

98139823
progress = await _safe_reply_markdown(
98149824
event_ref, # type: ignore[arg-type]
@@ -9844,6 +9854,16 @@ async def _handle_query_request(
98449854
force=True,
98459855
)
98469856

9857+
entity_web_results: list[dict[str, object]] = []
9858+
entity_web_summary_used = False
9859+
should_run_web_search = bool(_is_query_web_crosscheck_required())
9860+
if should_run_web_search and identity_query:
9861+
entity_web_results = await _search_entity_web_summary_bounded(
9862+
effective_query,
9863+
logger=LOGGER,
9864+
)
9865+
entity_web_summary_used = bool(entity_web_results)
9866+
98479867
queue_results: list[dict[str, object]] = []
98489868
archive_results: list[dict[str, object]] = []
98499869

@@ -9927,9 +9947,7 @@ async def _handle_query_request(
99279947
web_results: list[dict[str, object]] = []
99289948
web_fallback_used = False
99299949
ran_web_search = False
9930-
entity_web_summary_used = False
9931-
9932-
should_run_web_search = bool(_is_query_web_crosscheck_required())
9950+
web_results = list(entity_web_results)
99339951

99349952
if should_run_web_search:
99359953
ran_web_search = True
@@ -9951,17 +9969,11 @@ async def _handle_query_request(
99519969
progress_cb=progress_tracker.handle_search_progress,
99529970
logger=LOGGER,
99539971
)
9954-
entity_rows = await _search_entity_web_summary_bounded(
9955-
effective_query,
9956-
logger=LOGGER,
9972+
web_results = _merge_query_context(
9973+
entity_web_results,
9974+
web_results,
9975+
limit=max(_query_web_max_results(), 3),
99579976
)
9958-
if entity_rows:
9959-
entity_web_summary_used = True
9960-
web_results = _merge_query_context(
9961-
entity_rows,
9962-
web_results,
9963-
limit=max(_query_web_max_results(), 3),
9964-
)
99659977

99669978
if should_run_web_search and web_results and not entity_web_summary_used:
99679979
required_sources = _query_web_require_min_sources()
@@ -9981,8 +9993,8 @@ async def _handle_query_request(
99819993
web_fallback_used = bool(web_results)
99829994

99839995
results = _merge_query_context(
9984-
pre_web_results,
99859996
web_results,
9997+
pre_web_results,
99869998
limit=max(context_limit, _query_max_messages()),
99879999
)
998810000

‎prompts.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ def build_query_system_prompt(
220220
output_language: str,
221221
detailed: bool,
222222
strategic_trend: bool = False,
223+
identity_query: bool = False,
223224
) -> str:
224225
mode_line = (
225226
"Detail mode enabled: still lead with a sharp direct answer, then use no more than 4 short support bullets."
@@ -231,12 +232,18 @@ def build_query_system_prompt(
231232
if strategic_trend
232233
else ""
233234
)
235+
identity_line = (
236+
"Identity query: use an Anchor + Delta structure. Line 1 must define the entity as 'X is [entity type] from/connected to [place/org]'. Line 2 must say why it is in the news now. Bullets may add numbers or official claims. Never define X from a report lead like 'initial reports indicate'."
237+
if identity_query
238+
else ""
239+
)
234240
base = QUERY_SYSTEM_PROMPT.replace("NO_MATCH_SENTINEL", QUERY_NO_MATCH_TEXT)
235241
return (
236242
f"{base}\n\n"
237243
f"Output language must be {output_language}. Translate source text when needed.\n"
238244
f"{HUMAN_NEWSROOM_VOICE}\n"
239245
f"{HTML_RULES}\n"
240246
f"{mode_line}\n"
241-
f"{strategic_line}".rstrip()
247+
f"{strategic_line}\n"
248+
f"{identity_line}".rstrip()
242249
)

‎tests/test_feed_quality.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,31 @@ def test_local_query_answer_does_not_define_entity_from_report_lead():
902902
assert "€1.7 billion" in plain
903903

904904

905+
def test_local_query_answer_handles_appositive_identity_and_dedupes_support():
906+
answer = ai_filter._local_nlp_query_answer(
907+
"what is Lufthansa?",
908+
[
909+
{
910+
"text": "Lufthansa, the German flag carrier airline, announced losses after fuel prices rose.",
911+
"source": "Web:Article",
912+
"timestamp": 1778150000,
913+
"is_web": True,
914+
},
915+
{
916+
"text": "Lufthansa, the German flag carrier airline, announced losses after fuel prices rose.",
917+
"source": "Web:Article",
918+
"timestamp": 1778150000,
919+
"is_web": True,
920+
},
921+
],
922+
detailed=False,
923+
)
924+
plain = ai_filter.strip_telegram_html(answer)
925+
926+
assert "Lufthansa is German flag carrier airline" in plain
927+
assert plain.count("German flag carrier airline") == 1
928+
929+
905930
def test_local_query_answer_defines_entity_from_wikipedia_style_context():
906931
answer = ai_filter._local_nlp_query_answer(
907932
"what or who is Lufthansa?",
@@ -2024,11 +2049,13 @@ async def fake_generate_answer_from_context(*, query, context_messages, auth_man
20242049
async def test_handle_query_request_adds_entity_web_summary_for_identity_query(monkeypatch):
20252050
main.query_last_request_ts.clear()
20262051
answer_contexts: list[list[dict[str, object]]] = []
2052+
search_hours: list[int] = []
20272053

20282054
async def fake_safe_reply_markdown(_event, text, *, edit_message=None, reply_to=None, prefer_bot_identity=False, bot_chat_id=None):
20292055
return edit_message or {"message_id": 711}
20302056

20312057
async def fake_search_recent_messages(*_args, **_kwargs):
2058+
search_hours.append(int(_kwargs.get("default_hours_back") or 0))
20322059
return [
20332060
{
20342061
"text": "Initial reports indicate Lufthansa has lost €1.7 billion due to rising jet fuel prices.",
@@ -2079,6 +2106,8 @@ async def fake_generate_answer_from_context(*, query, context_messages, auth_man
20792106
)
20802107

20812108
assert answer_contexts
2109+
assert search_hours == [24 * 14]
2110+
assert answer_contexts[0][0]["source"] == "Web:Wikipedia"
20822111
assert any(row["source"] == "Web:Wikipedia" for row in answer_contexts[0])
20832112

20842113

0 commit comments

Comments
 (0)