Address review feedback: fix tiebreaker map, error handling, regex patterns
- Add BlueskyItem/TruthSocialItem to _ITEM_SOURCE_MAP (wrong tiebreaker) - Add bluesky/truthsocial to _DEFAULT_TIEBREAKER - Log HTTPError in select_openai_model instead of silent fallback - Remove overly broad 'or.*for' from comparison regex (false positives) - Remove bare 'will' from prediction regex (misclassifies feature queries) - Narrow brave_search except clauses to ValueError/TypeError - Fix stale comments: pricing table, docstrings, penalty descriptions
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""Brave Search web search for last30days skill.
|
||||
|
||||
Uses the Brave Search API as a web search backend.
|
||||
Requires a paid Brave Search subscription (free tier eliminated Feb 2026).
|
||||
Requires a paid Brave Search subscription.
|
||||
|
||||
Two modes:
|
||||
- Standard: /res/v1/web/search — returns URLs + snippets (default)
|
||||
@@ -191,7 +191,7 @@ def _normalize_results(
|
||||
continue
|
||||
if domain.startswith("www."):
|
||||
domain = domain[4:]
|
||||
except Exception:
|
||||
except (ValueError, TypeError):
|
||||
domain = ""
|
||||
|
||||
title = _clean_html(str(result.get("title", "")).strip())
|
||||
@@ -247,7 +247,7 @@ def _normalize_llm_context(response: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
continue
|
||||
if domain.startswith("www."):
|
||||
domain = domain[4:]
|
||||
except Exception:
|
||||
except (ValueError, TypeError):
|
||||
domain = ""
|
||||
|
||||
title = str(result.get("title", "")).strip()
|
||||
|
||||
+8
-11
@@ -6,19 +6,12 @@ reasoning-heavy or creative work — mini models handle it equally well
|
||||
at ~3-5x lower cost. We prefer the newest-generation mini model, falling
|
||||
back to mainline only when mini isn't available.
|
||||
|
||||
OpenAI cost per Reddit search call (web_search tool + JSON output):
|
||||
gpt-4.1-mini: ~$0.014 (fixed 8K search token block)
|
||||
gpt-5-mini: ~$0.015
|
||||
gpt-4.1: ~$0.044
|
||||
gpt-5.2: ~$0.043
|
||||
gpt-4o: ~$0.053
|
||||
|
||||
xAI: grok-4-1-fast reasoning vs non-reasoning have identical token
|
||||
pricing ($0.20/1M in, $0.50/1M out). Non-reasoning skips the thinking
|
||||
phase, saving latency and reasoning token output costs.
|
||||
xAI non-reasoning variant preferred: same pricing as reasoning, but
|
||||
faster (skips thinking phase, saves reasoning token output costs).
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from . import cache, http, env
|
||||
@@ -119,7 +112,11 @@ def select_openai_model(
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
response = http.get(OPENAI_MODELS_URL, headers=headers)
|
||||
models = response.get("data", [])
|
||||
except http.HTTPError:
|
||||
except http.HTTPError as e:
|
||||
sys.stderr.write(f"[Models] Failed to fetch OpenAI models: {e}")
|
||||
if hasattr(e, 'status_code') and e.status_code in (401, 403):
|
||||
sys.stderr.write(" — API key may be invalid or lack permissions")
|
||||
sys.stderr.write(f", using fallback {OPENAI_FALLBACK_MODELS[0]}\n")
|
||||
return OPENAI_FALLBACK_MODELS[0]
|
||||
|
||||
candidates = [m for m in models if is_search_capable_model(m.get("id", ""))]
|
||||
|
||||
@@ -19,21 +19,21 @@ _HOWTO_PATTERNS = re.compile(
|
||||
r"\b(how to|tutorial|step by step|setup|install|configure|deploy|migrate|implement|build a|create a)\b", re.I
|
||||
)
|
||||
_COMPARISON_PATTERNS = re.compile(
|
||||
r"\b(vs\.?|versus|compared to|comparison|better than|or\b.*\bfor\b|difference between|switch from)\b", re.I
|
||||
r"\b(vs\.?|versus|compared to|comparison|better than|difference between|switch from)\b", re.I
|
||||
)
|
||||
_BREAKING_PATTERNS = re.compile(
|
||||
r"\b(latest|breaking|just announced|launched|released|new|update|news|happened|today|this week)\b", re.I
|
||||
)
|
||||
_PREDICTION_PATTERNS = re.compile(
|
||||
r"\b(will|predict|forecast|odds|chance|probability|election|outcome|bet on|market for)\b", re.I
|
||||
r"\b(predict|forecast|odds|chance|probability|election|outcome|bet on|market for)\b", re.I
|
||||
)
|
||||
|
||||
|
||||
def detect_query_type(topic: str) -> QueryType:
|
||||
"""Classify a query into a type using pattern matching.
|
||||
|
||||
Returns the most specific match. When multiple patterns match,
|
||||
priority order is: comparison > how_to > product > opinion > prediction > concept > breaking_news.
|
||||
Returns the first match in priority order:
|
||||
comparison > how_to > product > opinion > prediction > concept > breaking_news.
|
||||
"""
|
||||
# Most specific first
|
||||
if _COMPARISON_PATTERNS.search(topic):
|
||||
@@ -69,6 +69,7 @@ SOURCE_TIERS = {
|
||||
}
|
||||
|
||||
# WebSearch penalty adjustment by query type.
|
||||
# Points subtracted from websearch score (0-100 scale). 0 = no penalty, 15 = full penalty.
|
||||
# Concept/how_to queries benefit from authoritative web sources.
|
||||
WEBSEARCH_PENALTY_BY_TYPE = {
|
||||
"product": 15, # default: social discussion > blog posts
|
||||
|
||||
+11
-6
@@ -11,10 +11,12 @@ WEIGHT_RELEVANCE = 0.45
|
||||
WEIGHT_RECENCY = 0.25
|
||||
WEIGHT_ENGAGEMENT = 0.30
|
||||
|
||||
# WebSearch weights (no engagement, reweighted to 100%)
|
||||
# WebSearch weights (no engagement data available)
|
||||
WEBSEARCH_WEIGHT_RELEVANCE = 0.55
|
||||
WEBSEARCH_WEIGHT_RECENCY = 0.45
|
||||
WEBSEARCH_SOURCE_PENALTY = 15 # Points deducted for lacking engagement
|
||||
# Default web search penalty (fallback when query_type is not provided).
|
||||
# Per-type penalties in query_type.WEBSEARCH_PENALTY_BY_TYPE.
|
||||
WEBSEARCH_SOURCE_PENALTY = 15
|
||||
|
||||
# WebSearch date confidence adjustments
|
||||
WEBSEARCH_VERIFIED_BONUS = 10 # Bonus for URL-verified recent date (high confidence)
|
||||
@@ -706,16 +708,19 @@ _ITEM_SOURCE_MAP = {
|
||||
schema.TikTokItem: "tiktok",
|
||||
schema.InstagramItem: "instagram",
|
||||
schema.HackerNewsItem: "hn",
|
||||
schema.BlueskyItem: "bluesky",
|
||||
schema.TruthSocialItem: "truthsocial",
|
||||
schema.PolymarketItem: "polymarket",
|
||||
}
|
||||
_DEFAULT_TIEBREAKER = {"reddit": 0, "x": 1, "youtube": 2, "tiktok": 3, "instagram": 4, "hn": 5, "polymarket": 6, "web": 7}
|
||||
_DEFAULT_TIEBREAKER = {"reddit": 0, "x": 1, "youtube": 2, "tiktok": 3, "instagram": 4, "hn": 5, "bluesky": 6, "truthsocial": 7, "polymarket": 8, "web": 9}
|
||||
|
||||
|
||||
def sort_items(items: List[Union[schema.RedditItem, schema.XItem, schema.WebSearchItem, schema.YouTubeItem, schema.TikTokItem, schema.InstagramItem, schema.HackerNewsItem, schema.PolymarketItem]], query_type: QueryType = None) -> List:
|
||||
"""Sort items by score (descending), then date, then source priority.
|
||||
"""Sort items by score (descending), then date, then source tiebreaker.
|
||||
|
||||
Source priority varies by query type: YouTube ranks first for how_to,
|
||||
X ranks first for breaking_news, Polymarket ranks first for prediction.
|
||||
Tiebreaker (tertiary sort key, after score and date): source priority
|
||||
varies by query type. YouTube ranks first for how_to, X ranks first
|
||||
for breaking_news, Polymarket ranks first for prediction.
|
||||
|
||||
Args:
|
||||
items: List of items to sort
|
||||
|
||||
Reference in New Issue
Block a user