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:
Jeffrey Sperling
2026-03-11 18:32:37 -07:00
parent 588cff3e00
commit ce8e289692
5 changed files with 37 additions and 25 deletions
+3 -3
View File
@@ -1,7 +1,7 @@
"""Brave Search web search for last30days skill. """Brave Search web search for last30days skill.
Uses the Brave Search API as a web search backend. 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: Two modes:
- Standard: /res/v1/web/search — returns URLs + snippets (default) - Standard: /res/v1/web/search — returns URLs + snippets (default)
@@ -191,7 +191,7 @@ def _normalize_results(
continue continue
if domain.startswith("www."): if domain.startswith("www."):
domain = domain[4:] domain = domain[4:]
except Exception: except (ValueError, TypeError):
domain = "" domain = ""
title = _clean_html(str(result.get("title", "")).strip()) 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 continue
if domain.startswith("www."): if domain.startswith("www."):
domain = domain[4:] domain = domain[4:]
except Exception: except (ValueError, TypeError):
domain = "" domain = ""
title = str(result.get("title", "")).strip() title = str(result.get("title", "")).strip()
+8 -11
View File
@@ -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 at ~3-5x lower cost. We prefer the newest-generation mini model, falling
back to mainline only when mini isn't available. back to mainline only when mini isn't available.
OpenAI cost per Reddit search call (web_search tool + JSON output): xAI non-reasoning variant preferred: same pricing as reasoning, but
gpt-4.1-mini: ~$0.014 (fixed 8K search token block) faster (skips thinking phase, saves reasoning token output costs).
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.
""" """
import re import re
import sys
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from . import cache, http, env from . import cache, http, env
@@ -119,7 +112,11 @@ def select_openai_model(
headers = {"Authorization": f"Bearer {api_key}"} headers = {"Authorization": f"Bearer {api_key}"}
response = http.get(OPENAI_MODELS_URL, headers=headers) response = http.get(OPENAI_MODELS_URL, headers=headers)
models = response.get("data", []) 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] return OPENAI_FALLBACK_MODELS[0]
candidates = [m for m in models if is_search_capable_model(m.get("id", ""))] candidates = [m for m in models if is_search_capable_model(m.get("id", ""))]
+5 -4
View File
@@ -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 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( _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( _BREAKING_PATTERNS = re.compile(
r"\b(latest|breaking|just announced|launched|released|new|update|news|happened|today|this week)\b", re.I r"\b(latest|breaking|just announced|launched|released|new|update|news|happened|today|this week)\b", re.I
) )
_PREDICTION_PATTERNS = re.compile( _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: def detect_query_type(topic: str) -> QueryType:
"""Classify a query into a type using pattern matching. """Classify a query into a type using pattern matching.
Returns the most specific match. When multiple patterns match, Returns the first match in priority order:
priority order is: comparison > how_to > product > opinion > prediction > concept > breaking_news. comparison > how_to > product > opinion > prediction > concept > breaking_news.
""" """
# Most specific first # Most specific first
if _COMPARISON_PATTERNS.search(topic): if _COMPARISON_PATTERNS.search(topic):
@@ -69,6 +69,7 @@ SOURCE_TIERS = {
} }
# WebSearch penalty adjustment by query type. # 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. # Concept/how_to queries benefit from authoritative web sources.
WEBSEARCH_PENALTY_BY_TYPE = { WEBSEARCH_PENALTY_BY_TYPE = {
"product": 15, # default: social discussion > blog posts "product": 15, # default: social discussion > blog posts
+11 -6
View File
@@ -11,10 +11,12 @@ WEIGHT_RELEVANCE = 0.45
WEIGHT_RECENCY = 0.25 WEIGHT_RECENCY = 0.25
WEIGHT_ENGAGEMENT = 0.30 WEIGHT_ENGAGEMENT = 0.30
# WebSearch weights (no engagement, reweighted to 100%) # WebSearch weights (no engagement data available)
WEBSEARCH_WEIGHT_RELEVANCE = 0.55 WEBSEARCH_WEIGHT_RELEVANCE = 0.55
WEBSEARCH_WEIGHT_RECENCY = 0.45 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 date confidence adjustments
WEBSEARCH_VERIFIED_BONUS = 10 # Bonus for URL-verified recent date (high confidence) WEBSEARCH_VERIFIED_BONUS = 10 # Bonus for URL-verified recent date (high confidence)
@@ -706,16 +708,19 @@ _ITEM_SOURCE_MAP = {
schema.TikTokItem: "tiktok", schema.TikTokItem: "tiktok",
schema.InstagramItem: "instagram", schema.InstagramItem: "instagram",
schema.HackerNewsItem: "hn", schema.HackerNewsItem: "hn",
schema.BlueskyItem: "bluesky",
schema.TruthSocialItem: "truthsocial",
schema.PolymarketItem: "polymarket", 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: 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, Tiebreaker (tertiary sort key, after score and date): source priority
X ranks first for breaking_news, Polymarket ranks first for prediction. varies by query type. YouTube ranks first for how_to, X ranks first
for breaking_news, Polymarket ranks first for prediction.
Args: Args:
items: List of items to sort items: List of items to sort
+10 -1
View File
@@ -46,8 +46,9 @@ class TestDetectQueryType(unittest.TestCase):
self.assertEqual(detect_query_type("OpenAI just announced GPT-6"), "breaking_news") self.assertEqual(detect_query_type("OpenAI just announced GPT-6"), "breaking_news")
def test_prediction_queries(self): def test_prediction_queries(self):
self.assertEqual(detect_query_type("will Trump win 2028 election"), "prediction")
self.assertEqual(detect_query_type("odds of Fed rate cut"), "prediction") self.assertEqual(detect_query_type("odds of Fed rate cut"), "prediction")
self.assertEqual(detect_query_type("predict the next recession"), "prediction")
self.assertEqual(detect_query_type("election outcome 2028"), "prediction")
def test_default_is_breaking_news(self): def test_default_is_breaking_news(self):
self.assertEqual(detect_query_type("tariffs"), "breaking_news") self.assertEqual(detect_query_type("tariffs"), "breaking_news")
@@ -61,6 +62,14 @@ class TestDetectQueryType(unittest.TestCase):
"""How-to is more specific than concept.""" """How-to is more specific than concept."""
self.assertEqual(detect_query_type("how to explain transformers"), "how_to") self.assertEqual(detect_query_type("how to explain transformers"), "how_to")
def test_will_alone_not_prediction(self):
"""Bare 'will' should not trigger prediction classification."""
self.assertNotEqual(detect_query_type("Will React 19 support concurrent mode"), "prediction")
def test_or_for_not_comparison(self):
"""'or X for Y' should not trigger comparison classification."""
self.assertNotEqual(detect_query_type("best tools or libraries for Python"), "comparison")
class TestIsSourceEnabled(unittest.TestCase): class TestIsSourceEnabled(unittest.TestCase):