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.
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
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
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", ""))]
+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
)
_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
View File
@@ -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
+10 -1
View File
@@ -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):