f4f77c5196
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
45 lines
1.5 KiB
Python
Executable File
45 lines
1.5 KiB
Python
Executable File
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Optional
|
|
|
|
|
|
_SEARCH_TRIGGERS = frozenset({
|
|
# Time-sensitive single words
|
|
"news", "weather", "today", "tonight", "yesterday", "price", "stock",
|
|
# Phrases that imply freshness or external lookup
|
|
"latest ", "right now", "this week", "this month", "recently",
|
|
"who is ", "what is ", "when did ", "how much does", "how much is",
|
|
"look up", "search for", "find out", "release date",
|
|
"just released", "just announced", "just launched",
|
|
"current version", "current price",
|
|
})
|
|
|
|
|
|
def needs_web_search(message: str) -> bool:
|
|
lower = message.lower()
|
|
return any(kw in lower for kw in _SEARCH_TRIGGERS)
|
|
|
|
|
|
def web_search(query: str, max_results: int = 4) -> str:
|
|
"""Search DuckDuckGo and return formatted result snippets.
|
|
|
|
Returns an empty string on any failure so callers can treat it as
|
|
optional context — a failed search should never break a chat response.
|
|
"""
|
|
try:
|
|
from duckduckgo_search import DDGS
|
|
with DDGS() as ddgs:
|
|
results = list(ddgs.text(query, max_results=max_results))
|
|
if not results:
|
|
return ""
|
|
parts = []
|
|
for i, r in enumerate(results, 1):
|
|
title = r.get("title", "").strip()
|
|
body = r.get("body", "").strip()
|
|
href = r.get("href", "").strip()
|
|
parts.append(f"{i}. **{title}**\n{body}\nSource: {href}")
|
|
return "\n\n".join(parts)
|
|
except Exception:
|
|
return ""
|