feat: Add WebSearch as third source with zero-config fallback

Add Claude's built-in WebSearch tool as a third research source for
/last30days. This enables the skill to work out of the box with zero
API keys while preserving Reddit/X as the primary sources.

Key changes:
- Add WebSearchItem schema for web results (no engagement metrics)
- Add score_websearch_items() with 55/45 relevance/recency weighting
- Apply -15pt source penalty so WebSearch ranks below Reddit/X
- Add --include-web CLI flag to opt-in to WebSearch
- Return 'web' mode when no API keys configured (zero-config)
- Update render.py with [WEB] source label formatting

When WebSearch is enabled, the script outputs instructions for Claude
to use its built-in WebSearch tool, then synthesize results together.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Matt Van Horn
2026-01-24 10:23:32 -08:00
parent 6d5e4467c2
commit f98a0b9f35
7 changed files with 852 additions and 36 deletions
+63 -19
View File
@@ -38,6 +38,7 @@ from lib import (
schema,
score,
ui,
websearch,
xai_x,
)
@@ -65,7 +66,10 @@ def run_research(
"""Run the research pipeline.
Returns:
Tuple of (reddit_items, x_items, raw_openai, raw_xai, raw_reddit_enriched, reddit_error, x_error)
Tuple of (reddit_items, x_items, web_needed, raw_openai, raw_xai, raw_reddit_enriched, reddit_error, x_error)
Note: web_needed is True when WebSearch should be performed by Claude.
The script outputs a marker and Claude handles WebSearch in its session.
"""
reddit_items = []
x_items = []
@@ -75,8 +79,11 @@ def run_research(
reddit_error = None
x_error = None
# Check if WebSearch is needed
web_needed = sources in ("all", "web", "reddit-web", "x-web")
# Reddit search via OpenAI
if sources in ("both", "reddit"):
if sources in ("both", "reddit", "all", "reddit-web"):
if progress:
progress.start_reddit()
@@ -128,7 +135,7 @@ def run_research(
progress.end_reddit_enrich()
# X search via xAI
if sources in ("both", "x"):
if sources in ("both", "x", "all", "x-web"):
if progress:
progress.start_x()
@@ -161,7 +168,7 @@ def run_research(
if progress:
progress.end_x(len(x_items))
return reddit_items, x_items, raw_openai, raw_xai, raw_reddit_enriched, reddit_error, x_error
return reddit_items, x_items, web_needed, raw_openai, raw_xai, raw_reddit_enriched, reddit_error, x_error
def main():
@@ -197,6 +204,11 @@ def main():
action="store_true",
help="Enable verbose debug logging",
)
parser.add_argument(
"--include-web",
action="store_true",
help="Include general web search alongside Reddit/X (lower weighted)",
)
args = parser.parse_args()
@@ -228,12 +240,6 @@ def main():
# Check available sources
available = env.get_available_sources(config)
if available == "none" and not args.mock:
print("Error: No API keys configured.", file=sys.stderr)
print("Please add at least one key to ~/.config/last30days/.env:", file=sys.stderr)
print(" OPENAI_API_KEY=sk-...", file=sys.stderr)
print(" XAI_API_KEY=xai-...", file=sys.stderr)
sys.exit(1)
# Mock mode can work without keys
if args.mock:
@@ -243,10 +249,14 @@ def main():
sources = args.sources
else:
# Validate requested sources against available
sources, error = env.validate_sources(args.sources, available)
sources, error = env.validate_sources(args.sources, available, args.include_web)
if error:
print(f"Error: {error}", file=sys.stderr)
sys.exit(1)
# If it's a warning about WebSearch fallback, print but continue
if "WebSearch fallback" in error:
print(f"Note: {error}", file=sys.stderr)
else:
print(f"Error: {error}", file=sys.stderr)
sys.exit(1)
# Get date range
from_date, to_date = dates.get_date_range(30)
@@ -272,15 +282,25 @@ def main():
selected_models = models.get_models(config)
# Determine mode string
if sources == "both":
mode = "both"
if sources == "all":
mode = "all" # reddit + x + web
elif sources == "both":
mode = "both" # reddit + x
elif sources == "reddit":
mode = "reddit-only"
else:
elif sources == "reddit-web":
mode = "reddit-web"
elif sources == "x":
mode = "x-only"
elif sources == "x-web":
mode = "x-web"
elif sources == "web":
mode = "web-only"
else:
mode = sources
# Run research
reddit_items, x_items, raw_openai, raw_xai, raw_reddit_enriched, reddit_error, x_error = run_research(
reddit_items, x_items, web_needed, raw_openai, raw_xai, raw_reddit_enriched, reddit_error, x_error = run_research(
args.topic,
sources,
config,
@@ -337,10 +357,17 @@ def main():
progress.show_complete(len(deduped_reddit), len(deduped_x))
# Output result
output_result(report, args.emit)
output_result(report, args.emit, web_needed, args.topic, from_date, to_date)
def output_result(report: schema.Report, emit_mode: str):
def output_result(
report: schema.Report,
emit_mode: str,
web_needed: bool = False,
topic: str = "",
from_date: str = "",
to_date: str = "",
):
"""Output the result based on emit mode."""
if emit_mode == "compact":
print(render.render_compact(report))
@@ -353,6 +380,23 @@ def output_result(report: schema.Report, emit_mode: str):
elif emit_mode == "path":
print(render.get_context_path())
# Output WebSearch instructions if needed
if web_needed:
print("\n" + "="*60)
print("### WEBSEARCH REQUIRED ###")
print("="*60)
print(f"Topic: {topic}")
print(f"Date range: {from_date} to {to_date}")
print("")
print("Claude: Use your WebSearch tool to find 8-15 relevant web pages.")
print("EXCLUDE: reddit.com, x.com, twitter.com (already covered above)")
print("INCLUDE: blogs, docs, news, tutorials from the last 30 days")
print("")
print("After searching, synthesize WebSearch results WITH the Reddit/X")
print("results above. WebSearch items should rank LOWER than comparable")
print("Reddit/X items (they lack engagement metrics).")
print("="*60)
if __name__ == "__main__":
main()