feat: v3.0.0 - intelligent search, GitHub person/project mode, ELI5, 13+ sources
v3 rewrites the search engine from the ground up: - Intelligent pre-research: resolves X handles, GitHub repos, subreddits, TikTok hashtags, and YouTube channels before searching - GitHub person-mode: PR velocity, top repos by stars, release notes - GitHub project-mode: live star counts, README, releases, top issues - ELI5 mode: plain language synthesis, no jargon - 13+ sources: Reddit, X, YouTube, TikTok, Instagram, HN, Polymarket, GitHub, Threads, Pinterest, Perplexity, Bluesky, Web - Free Reddit comments via public JSON (no API key needed) - Fun judge v2: humor scoring baked into narrative - Cookie consent before browser scanning - 10,000 free ScrapeCreators calls - 1,012 tests Thank you to the community contributors whose issues and PRs shaped v3: @uppinote20 (#143), @zerone0x (#134, #136), @thinkun (#116), @thomasmktong (#124), @fanispoulinakisai-boop (#100), @pejmanjohn (#78), @zl190 (#115), @hnshah (#84, #85, #86) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+114
-8
@@ -12,8 +12,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from typing import Any, Dict, List, Optional
|
||||
from urllib.parse import quote_plus, urlencode
|
||||
|
||||
from . import http
|
||||
from .query_type import detect_query_type
|
||||
from . import http, log
|
||||
from .relevance import LOW_SIGNAL_QUERY_TOKENS, token_overlap_relevance
|
||||
|
||||
GAMMA_SEARCH_URL = "https://gamma-api.polymarket.com/public-search"
|
||||
@@ -34,10 +33,7 @@ RESULT_CAP = {
|
||||
|
||||
|
||||
def _log(msg: str):
|
||||
"""Log to stderr (only in TTY mode to avoid cluttering Claude Code output)."""
|
||||
if sys.stderr.isatty():
|
||||
sys.stderr.write(f"[PM] {msg}\n")
|
||||
sys.stderr.flush()
|
||||
log.source_log("PM", msg)
|
||||
|
||||
|
||||
def _extract_core_subject(topic: str) -> str:
|
||||
@@ -75,7 +71,7 @@ def _expand_queries(topic: str) -> List[str]:
|
||||
words = core.split()
|
||||
if len(words) >= 2:
|
||||
for word in words:
|
||||
if len(word) > 1 and word.lower() not in LOW_SIGNAL_QUERY_TOKENS:
|
||||
if len(word) > 1 and word.lower() not in LOW_SIGNAL_QUERY_TOKENS and word.lower() not in _NOISE_WORDS:
|
||||
queries.append(word)
|
||||
|
||||
# Add the full topic if different from core
|
||||
@@ -95,6 +91,79 @@ def _expand_queries(topic: str) -> List[str]:
|
||||
|
||||
_GENERIC_TAGS = frozenset({"sports", "politics", "crypto", "science", "culture", "pop culture"})
|
||||
|
||||
# Words that are too generic to serve as the sole topic-match signal.
|
||||
# If ALL core words from the topic are in this set, we skip filtering (can't meaningfully filter).
|
||||
# But if some words are informative and some are generic, we require at least one informative word.
|
||||
_NOISE_WORDS = frozenset({
|
||||
# Articles, prepositions, conjunctions
|
||||
"the", "a", "an", "in", "on", "at", "of", "for", "and", "or", "to", "is", "are",
|
||||
"was", "were", "will", "be", "by", "with", "from", "as", "it", "its", "not", "no",
|
||||
"but", "if", "so", "do", "has", "had", "have", "this", "that", "what", "who",
|
||||
# Directional / geographic terms that cause false matches
|
||||
"west", "east", "north", "south", "central", "southern", "northern", "eastern", "western",
|
||||
# Common sports / category terms
|
||||
"champion", "championship", "league", "division", "conference", "cup", "series",
|
||||
"team", "game", "match", "season", "win", "winner", "finals",
|
||||
# Common geographic / place nouns that cause false matches
|
||||
# "club" -> Athletic Club, Racing Club; "island" -> Epstein's Island, Rhode Island
|
||||
"club", "island", "city", "park", "hill", "lake", "bay", "beach", "valley",
|
||||
"river", "mountain", "county", "state", "village", "town", "point", "creek",
|
||||
"springs", "heights", "ridge", "bridge", "harbor", "port", "station", "center",
|
||||
"square", "field", "forest", "garden", "tower", "school", "church", "camp",
|
||||
"ranch", "crossing", "shore", "rock", "summit", "falls", "grove", "haven",
|
||||
# Generic tech terms that match too broadly on Polymarket
|
||||
# "cli" -> any CLI tool market; "mcp" -> protocol markets; "ai" -> every AI market
|
||||
"cli", "mcp", "protocol", "tool", "app", "code", "model", "ai", "api",
|
||||
"software", "plugin", "skill", "agent", "bot", "search", "research",
|
||||
# Generic prediction market terms
|
||||
"market", "odds", "prediction", "forecast", "chance", "probability",
|
||||
})
|
||||
|
||||
|
||||
def _passes_topic_filter(topic: str, event_title: str) -> bool:
|
||||
"""Check if event title contains enough informative words from the topic.
|
||||
|
||||
Prevents noise like "Meek Mill" matching "Mill.com food recycler" by requiring
|
||||
proportional word overlap. For topics with 3+ informative words, at least 2 must
|
||||
match. For shorter topics, 1 match suffices (existing behavior).
|
||||
|
||||
Returns True if the event should be kept, False if it should be filtered out.
|
||||
"""
|
||||
core = _extract_core_subject(topic).lower()
|
||||
core_words = [w for w in re.sub(r"[^\w\s]", " ", core).split() if len(w) > 1]
|
||||
|
||||
if not core_words:
|
||||
return True # No words to check against
|
||||
|
||||
# Split into informative vs generic
|
||||
informative = [w for w in core_words if w not in _NOISE_WORDS]
|
||||
|
||||
# If ALL words are generic, we can't meaningfully filter — keep everything
|
||||
if not informative:
|
||||
return True
|
||||
|
||||
# Normalize the title for matching
|
||||
title_lower = " ".join(re.sub(r"[^\w\s]", " ", event_title.lower()).split())
|
||||
title_words = set(title_lower.split())
|
||||
|
||||
# Count how many informative words appear in the title
|
||||
match_count = 0
|
||||
for word in informative:
|
||||
# Check as whole word in the title word set
|
||||
if word in title_words:
|
||||
match_count += 1
|
||||
continue
|
||||
# Also check as substring for compound words (e.g., "kanye" in "kanyewest")
|
||||
if len(word) >= 4 and word in title_lower:
|
||||
match_count += 1
|
||||
|
||||
# For topics with 3+ informative words, require at least 2 matches.
|
||||
# This prevents single-word false positives like "mill" in "Meek Mill"
|
||||
# when the topic is "Mill.com food recycler" (3 informative words).
|
||||
min_matches = 2 if len(informative) >= 3 else 1
|
||||
|
||||
return match_count >= min_matches
|
||||
|
||||
|
||||
def _extract_domain_queries(topic: str, events: List[Dict]) -> List[str]:
|
||||
"""Extract domain-indicator search terms from first-pass event tags.
|
||||
@@ -130,6 +199,14 @@ def _extract_domain_queries(topic: str, events: List[Dict]) -> List[str]:
|
||||
return domain_queries
|
||||
|
||||
|
||||
def _infer_query_intent(topic: str) -> str:
|
||||
"""Tiny local fallback for Polymarket search tuning only."""
|
||||
text = topic.lower().strip()
|
||||
if re.search(r"\b(predict|prediction|odds|forecast|chance|probability|will .* win)\b", text):
|
||||
return "prediction"
|
||||
return "breaking_news"
|
||||
|
||||
|
||||
def _search_single_query(query: str, page: int = 1) -> Dict[str, Any]:
|
||||
"""Run a single search query against Gamma API."""
|
||||
params = {
|
||||
@@ -328,7 +405,7 @@ def _compute_text_similarity(topic: str, title: str, outcomes: List[str] = None)
|
||||
if core in title_lower:
|
||||
return 1.0
|
||||
|
||||
query_type = detect_query_type(topic)
|
||||
query_type = _infer_query_intent(topic)
|
||||
title_score = token_overlap_relevance(core, title)
|
||||
best_score = title_score
|
||||
|
||||
@@ -392,6 +469,7 @@ def parse_polymarket_response(response: Dict[str, Any], topic: str = "") -> List
|
||||
events = response.get("events", [])
|
||||
items = []
|
||||
|
||||
filtered_count = 0
|
||||
for i, event in enumerate(events):
|
||||
event_id = event.get("id", "")
|
||||
title = event.get("title", "")
|
||||
@@ -403,6 +481,12 @@ def parse_polymarket_response(response: Dict[str, Any], topic: str = "") -> List
|
||||
if not event.get("active", True):
|
||||
continue
|
||||
|
||||
# Filter: skip events that don't match the topic's core subject
|
||||
# This prevents "NFC West" from matching a "Kanye West" search
|
||||
if topic and not _passes_topic_filter(topic, title):
|
||||
filtered_count += 1
|
||||
continue
|
||||
|
||||
# Get markets for this event
|
||||
markets = event.get("markets", [])
|
||||
if not markets:
|
||||
@@ -574,7 +658,29 @@ def parse_polymarket_response(response: Dict[str, Any], topic: str = "") -> List
|
||||
"why_relevant": f"Prediction market: {title[:60]}",
|
||||
})
|
||||
|
||||
if filtered_count:
|
||||
_log(f"Filtered {filtered_count} noise events (topic: '{topic}')")
|
||||
|
||||
# Sort by relevance (quality-signal ranked) and apply cap
|
||||
items.sort(key=lambda x: x["relevance"], reverse=True)
|
||||
|
||||
# Drop ALL results if nothing is genuinely on-topic.
|
||||
# If the best item's relevance is below the threshold, the Gamma API
|
||||
# returned only tangential matches (e.g., "Anthropic best AI model"
|
||||
# for a "CLI vs MCP" query). Better to show 0 than noise.
|
||||
_MIN_RELEVANCE = 0.15
|
||||
if items and items[0]["relevance"] < _MIN_RELEVANCE:
|
||||
_log(f"All {len(items)} Polymarket results below relevance threshold "
|
||||
f"({items[0]['relevance']:.2f} < {_MIN_RELEVANCE}), dropping all")
|
||||
return []
|
||||
|
||||
# Per-item floor: drop individual noise items even if the best item passed
|
||||
_ITEM_MIN_RELEVANCE = 0.10
|
||||
before_count = len(items)
|
||||
items = [i for i in items if i["relevance"] >= _ITEM_MIN_RELEVANCE]
|
||||
dropped = before_count - len(items)
|
||||
if dropped:
|
||||
_log(f"Dropped {dropped} Polymarket items below per-item relevance floor ({_ITEM_MIN_RELEVANCE})")
|
||||
|
||||
cap = response.get("_cap", len(items))
|
||||
return items[:cap]
|
||||
|
||||
Reference in New Issue
Block a user