news: get_news degrades softly on topic miss (news-13) — any-term ranked fallback, recent+note, language hint in schema; idle-impulse default varies form

This commit is contained in:
Oleksandr Kozachuk
2026-07-17 16:26:47 +02:00
parent dc7864efe1
commit f8b9bc75ee
5 changed files with 72 additions and 11 deletions
+17 -2
View File
@@ -198,7 +198,12 @@ GET_NEWS_TOOL = {
"parameters": {
"type": "object",
"properties": {
"topic": {"type": "string", "description": "Optional keywords to filter by, e.g. 'Nordland', 'football', 'weather'."},
"topic": {
"type": "string",
"description": "Optional filter: one or two keywords, in the language the feeds are written in "
"(e.g. Norwegian for Norwegian news: 'Nordland', 'fotball', 'trafikkulykke'). If nothing matches "
"exactly, related or recent items come back with a `note` saying so.",
},
"source": {"type": "string", "description": "Optional source label, e.g. 'NRK', 'Aftenposten', 'Verden', 'Sport'."},
"limit": {"type": "integer", "description": "How many items to return (default 10, max 30)."},
},
@@ -220,8 +225,15 @@ def query_news(
limit = max(1, min(int(limit or 10), 30))
src = (str(source).strip() or None) if source else None
terms = _news_terms(topic)
note = None
try:
rows = store.search_news(terms, limit, src) if terms else store.recent_news(limit, src)
if terms and not rows: # NEWS-13: soft degradation, never empty-handed
rows = store.search_news(terms, limit, src, match_any=True)
note = "no item matches all keywords; showing items matching some of them"
if terms and not rows:
rows = store.recent_news(limit, src)
note = "nothing matches the topic; showing the newest stored items instead"
except Exception as err:
logging.warning(f"news: query failed: {err!r}")
return {"error": "news lookup failed"}
@@ -234,7 +246,10 @@ def query_news(
}
for row in rows
]
return {"topic": topic or "", "source": src or "", "results": results}
payload = {"topic": topic or "", "source": src or "", "results": results}
if note:
payload["note"] = note
return payload
def _open_store(config: Dict[str, Any]) -> Any:
+21 -8
View File
@@ -152,20 +152,33 @@ class PersistentStore:
rows = conn.execute(sql, params).fetchall()
return [{"source": r[0], "title": r[1], "link": r[2], "summary": r[3]} for r in rows]
def search_news(self, terms: List[str], limit: int = 20, source: Optional[str] = None) -> List[Dict[str, Any]]:
"""Rows where every term appears in title or summary; optional source filter (NEWS-11)."""
def search_news(self, terms: List[str], limit: int = 20, source: Optional[str] = None, match_any: bool = False) -> List[Dict[str, Any]]:
"""Rows where every term appears in title/summary/source; match_any ranks by how many terms hit (NEWS-11/13)."""
params: List[Any] = []
clauses = []
for term in terms:
clauses.append("(title LIKE ? OR summary LIKE ? OR source LIKE ?)")
like = f"%{term}%"
params += [like, like, like]
where = " AND ".join(clauses) if clauses else "1=1"
if source:
where = f"({where}) AND source = ?"
params.append(source)
params.append(int(limit))
sql = f"SELECT source, title, link, summary FROM news WHERE {where} ORDER BY id DESC LIMIT ?" # nosec B608 - fixed templates; values parameterised
if match_any and clauses:
hits = " + ".join(clauses)
where = "hits > 0"
if source:
where += " AND source = ?"
params.append(source)
params.append(int(limit))
sql = (
f"SELECT source, title, link, summary FROM " # nosec B608 - fixed templates; values parameterised
f"(SELECT id, source, title, link, summary, {hits} AS hits FROM news) "
f"WHERE {where} ORDER BY hits DESC, id DESC LIMIT ?"
)
else:
where = " AND ".join(clauses) if clauses else "1=1"
if source:
where = f"({where}) AND source = ?"
params.append(source)
params.append(int(limit))
sql = f"SELECT source, title, link, summary FROM news WHERE {where} ORDER BY id DESC LIMIT ?" # nosec B608 - fixed templates; values parameterised
with closing(self._connect()) as conn:
rows = conn.execute(sql, params).fetchall()
return [{"source": r[0], "title": r[1], "link": r[2], "summary": r[3]} for r in rows]
+3 -1
View File
@@ -20,7 +20,9 @@ DEFAULT_TASKGEN_INTERVAL_HOURS = 6.0
DEFAULT_BORENESS_PROMPT = (
"A thought just occurred to you. Anchor it to something real you know — recent news (use get_news), a game "
"releasing soon, the weather, or a regular you remember — not a generic musing. Share it briefly, in your own "
"voice, as an observation, a gentle question, or a joke; never an advertisement. Read the room and stay in character."
"voice, as an observation, a gentle question, or a joke; never an advertisement. Read the room and stay in character. "
"Check your own recent posts in the history first: pick a subject you have not touched lately and a different form "
"than last time, and never open with a fixed label or heading — just start mid-thought."
)
ExecuteCallback = Callable[[str, str], Awaitable[None]]