From ce8e289692c091610f0dd185c7d59210fe3f0eb6 Mon Sep 17 00:00:00 2001 From: Jeffrey Sperling Date: Wed, 11 Mar 2026 18:32:37 -0700 Subject: [PATCH] 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 --- scripts/lib/brave_search.py | 6 +++--- scripts/lib/models.py | 19 ++++++++----------- scripts/lib/query_type.py | 9 +++++---- scripts/lib/score.py | 17 +++++++++++------ tests/test_query_type.py | 11 ++++++++++- 5 files changed, 37 insertions(+), 25 deletions(-) diff --git a/scripts/lib/brave_search.py b/scripts/lib/brave_search.py index 2481159..7efd83d 100644 --- a/scripts/lib/brave_search.py +++ b/scripts/lib/brave_search.py @@ -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() diff --git a/scripts/lib/models.py b/scripts/lib/models.py index 8888467..dd25ad0 100644 --- a/scripts/lib/models.py +++ b/scripts/lib/models.py @@ -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", ""))] diff --git a/scripts/lib/query_type.py b/scripts/lib/query_type.py index b542439..36d325d 100644 --- a/scripts/lib/query_type.py +++ b/scripts/lib/query_type.py @@ -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 diff --git a/scripts/lib/score.py b/scripts/lib/score.py index 33317b8..ffd108d 100644 --- a/scripts/lib/score.py +++ b/scripts/lib/score.py @@ -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 diff --git a/tests/test_query_type.py b/tests/test_query_type.py index 4b0f589..c07f343 100644 --- a/tests/test_query_type.py +++ b/tests/test_query_type.py @@ -46,8 +46,9 @@ class TestDetectQueryType(unittest.TestCase): self.assertEqual(detect_query_type("OpenAI just announced GPT-6"), "breaking_news") 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("predict the next recession"), "prediction") + self.assertEqual(detect_query_type("election outcome 2028"), "prediction") def test_default_is_breaking_news(self): self.assertEqual(detect_query_type("tariffs"), "breaking_news") @@ -61,6 +62,14 @@ class TestDetectQueryType(unittest.TestCase): """How-to is more specific than concept.""" 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):