luckiest-co 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +1 -1
- package/LICENSE +24 -0
- package/bin/install.js +7 -5
- package/commands/{luckiest/finish.md → finish.md} +2 -0
- package/commands/go.md +53 -0
- package/commands/plan.md +66 -0
- package/package.json +16 -5
- package/skills/brainstorm/ATTRIBUTION.md +27 -0
- package/skills/brainstorm/SKILL.md +139 -0
- package/skills/luckiest-ab-testing/ATTRIBUTION.md +35 -0
- package/skills/luckiest-ab-testing/CHANGELOG.md +27 -0
- package/skills/luckiest-ab-testing/SKILL.md +379 -0
- package/skills/luckiest-ab-testing/evals/evals.json +118 -0
- package/skills/luckiest-ab-testing/references/sample-size-guide.md +290 -0
- package/skills/luckiest-ab-testing/references/test-templates.md +284 -0
- package/skills/luckiest-ad-creative/ATTRIBUTION.md +35 -0
- package/skills/luckiest-ad-creative/CHANGELOG.md +23 -0
- package/skills/luckiest-ad-creative/SKILL.md +393 -0
- package/skills/luckiest-ad-creative/evals/evals.json +90 -0
- package/skills/luckiest-ad-creative/references/generative-tools.md +637 -0
- package/skills/luckiest-ad-creative/references/platform-specs.md +213 -0
- package/skills/luckiest-ads/ATTRIBUTION.md +36 -0
- package/skills/luckiest-ads/CHANGELOG.md +24 -0
- package/skills/luckiest-ads/LICENSE +21 -0
- package/skills/luckiest-ads/SKILL.md +434 -0
- package/skills/luckiest-ads/evals/evals.json +90 -0
- package/skills/luckiest-ads/references/ad-copy-templates.md +207 -0
- package/skills/luckiest-ads/references/audience-targeting.md +243 -0
- package/skills/luckiest-ads/references/conversion-tracking.md +361 -0
- package/skills/luckiest-ads/references/platform-setup-checklists.md +277 -0
- package/skills/luckiest-advisors/ATTRIBUTION.md +16 -0
- package/skills/luckiest-advisors/CHANGELOG.md +99 -0
- package/skills/luckiest-advisors/SKILL.md +151 -0
- package/skills/luckiest-aeo-grader/ATTRIBUTION.md +19 -0
- package/skills/luckiest-aeo-grader/CHANGELOG.md +52 -0
- package/skills/luckiest-aeo-grader/SKILL.md +200 -0
- package/skills/luckiest-aeo-grader/evals/evals.json +19 -0
- package/skills/luckiest-aeo-grader/references/rubric.md +72 -0
- package/skills/luckiest-aeo-grader/scripts/scan.py +265 -0
- package/skills/luckiest-ai-seo/ATTRIBUTION.md +38 -0
- package/skills/luckiest-ai-seo/CHANGELOG.md +23 -0
- package/skills/luckiest-ai-seo/SKILL.md +510 -0
- package/skills/luckiest-ai-seo/evals/evals.json +90 -0
- package/skills/luckiest-ai-seo/references/content-patterns.md +285 -0
- package/skills/luckiest-ai-seo/references/content-types.md +71 -0
- package/skills/luckiest-ai-seo/references/okf.md +104 -0
- package/skills/luckiest-ai-seo/references/platform-ranking-factors.md +154 -0
- package/skills/luckiest-analytics/ATTRIBUTION.md +42 -0
- package/skills/luckiest-analytics/CHANGELOG.md +36 -0
- package/skills/luckiest-analytics/SKILL.md +380 -0
- package/skills/luckiest-analytics/evals/evals.json +90 -0
- package/skills/luckiest-analytics/references/event-library.md +260 -0
- package/skills/luckiest-analytics/references/ga4-implementation.md +300 -0
- package/skills/luckiest-analytics/references/gtm-implementation.md +390 -0
- package/skills/luckiest-aso/ATTRIBUTION.md +40 -0
- package/skills/luckiest-aso/CHANGELOG.md +33 -0
- package/skills/luckiest-aso/SKILL.md +327 -0
- package/skills/luckiest-aso/evals/evals.json +91 -0
- package/skills/luckiest-aso/references/apple-specs.md +107 -0
- package/skills/luckiest-aso/references/benchmarks.md +129 -0
- package/skills/luckiest-aso/references/google-play-specs.md +131 -0
- package/skills/luckiest-aso/references/report-template.md +213 -0
- package/skills/luckiest-aso/references/scoring-criteria.md +213 -0
- package/skills/luckiest-churn-prevention/ATTRIBUTION.md +41 -0
- package/skills/luckiest-churn-prevention/CHANGELOG.md +24 -0
- package/skills/luckiest-churn-prevention/SKILL.md +445 -0
- package/skills/luckiest-churn-prevention/evals/evals.json +93 -0
- package/skills/luckiest-churn-prevention/references/cancel-flow-patterns.md +316 -0
- package/skills/luckiest-churn-prevention/references/dunning-playbook.md +408 -0
- package/skills/luckiest-co-marketing/ATTRIBUTION.md +38 -0
- package/skills/luckiest-co-marketing/CHANGELOG.md +25 -0
- package/skills/luckiest-co-marketing/SKILL.md +331 -0
- package/skills/luckiest-co-marketing/evals/evals.json +84 -0
- package/skills/luckiest-coder/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder/CHANGELOG.md +30 -0
- package/skills/luckiest-coder/SKILL.md +77 -0
- package/skills/luckiest-coder/references/antigravity-tools.md +23 -0
- package/skills/luckiest-coder/references/codex-tools.md +39 -0
- package/skills/luckiest-coder/references/pi-tools.md +16 -0
- package/skills/luckiest-coder-brainstorming/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-brainstorming/CHANGELOG.md +31 -0
- package/skills/luckiest-coder-brainstorming/SKILL.md +173 -0
- package/skills/luckiest-coder-brainstorming/scripts/frame-template.html +213 -0
- package/skills/luckiest-coder-brainstorming/scripts/helper.js +168 -0
- package/skills/luckiest-coder-brainstorming/scripts/server.cjs +721 -0
- package/skills/luckiest-coder-brainstorming/scripts/start-server.sh +209 -0
- package/skills/luckiest-coder-brainstorming/scripts/stop-server.sh +120 -0
- package/skills/luckiest-coder-brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/skills/luckiest-coder-brainstorming/visual-companion.md +291 -0
- package/skills/luckiest-coder-consistency/ATTRIBUTION.md +30 -0
- package/skills/luckiest-coder-consistency/CHANGELOG.md +26 -0
- package/skills/luckiest-coder-consistency/SKILL.md +98 -0
- package/skills/luckiest-coder-constitution/ATTRIBUTION.md +28 -0
- package/skills/luckiest-coder-constitution/CHANGELOG.md +25 -0
- package/skills/luckiest-coder-constitution/SKILL.md +115 -0
- package/skills/luckiest-coder-debugging/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-debugging/CHANGELOG.md +22 -0
- package/skills/luckiest-coder-debugging/CREATION-LOG.md +119 -0
- package/skills/luckiest-coder-debugging/SKILL.md +309 -0
- package/skills/luckiest-coder-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/luckiest-coder-debugging/condition-based-waiting.md +115 -0
- package/skills/luckiest-coder-debugging/defense-in-depth.md +122 -0
- package/skills/luckiest-coder-debugging/find-polluter.sh +63 -0
- package/skills/luckiest-coder-debugging/root-cause-tracing.md +169 -0
- package/skills/luckiest-coder-debugging/test-academic.md +14 -0
- package/skills/luckiest-coder-debugging/test-pressure-1.md +58 -0
- package/skills/luckiest-coder-debugging/test-pressure-2.md +68 -0
- package/skills/luckiest-coder-debugging/test-pressure-3.md +69 -0
- package/skills/luckiest-coder-dispatching-parallel-agents/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-dispatching-parallel-agents/CHANGELOG.md +23 -0
- package/skills/luckiest-coder-dispatching-parallel-agents/SKILL.md +198 -0
- package/skills/luckiest-coder-executing-plans/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-executing-plans/CHANGELOG.md +22 -0
- package/skills/luckiest-coder-executing-plans/SKILL.md +83 -0
- package/skills/luckiest-coder-finishing-a-branch/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-finishing-a-branch/CHANGELOG.md +22 -0
- package/skills/luckiest-coder-finishing-a-branch/SKILL.md +258 -0
- package/skills/luckiest-coder-git-worktrees/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-git-worktrees/CHANGELOG.md +22 -0
- package/skills/luckiest-coder-git-worktrees/SKILL.md +215 -0
- package/skills/luckiest-coder-guard/ATTRIBUTION.md +29 -0
- package/skills/luckiest-coder-guard/CHANGELOG.md +48 -0
- package/skills/luckiest-coder-guard/LICENSE +21 -0
- package/skills/luckiest-coder-guard/SKILL.md +126 -0
- package/skills/luckiest-coder-guard/bin/check-careful.sh +65 -0
- package/skills/luckiest-coder-guard/bin/check-freeze.sh +48 -0
- package/skills/luckiest-coder-qa/ATTRIBUTION.md +28 -0
- package/skills/luckiest-coder-qa/CHANGELOG.md +49 -0
- package/skills/luckiest-coder-qa/LICENSE +21 -0
- package/skills/luckiest-coder-qa/SKILL.md +160 -0
- package/skills/luckiest-coder-qa/references/issue-taxonomy.md +85 -0
- package/skills/luckiest-coder-qa/references/qa-report-template.md +126 -0
- package/skills/luckiest-coder-receiving-code-review/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-receiving-code-review/CHANGELOG.md +28 -0
- package/skills/luckiest-coder-receiving-code-review/SKILL.md +226 -0
- package/skills/luckiest-coder-requesting-code-review/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-requesting-code-review/CHANGELOG.md +31 -0
- package/skills/luckiest-coder-requesting-code-review/SKILL.md +118 -0
- package/skills/luckiest-coder-requesting-code-review/code-reviewer.md +172 -0
- package/skills/luckiest-coder-shipping/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-shipping/CHANGELOG.md +48 -0
- package/skills/luckiest-coder-shipping/LICENSE +21 -0
- package/skills/luckiest-coder-shipping/SKILL.md +148 -0
- package/skills/luckiest-coder-shipping/references/canary.md +44 -0
- package/skills/luckiest-coder-shipping/references/test-and-coverage.md +91 -0
- package/skills/luckiest-coder-subagent-driven-development/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-subagent-driven-development/CHANGELOG.md +28 -0
- package/skills/luckiest-coder-subagent-driven-development/SKILL.md +439 -0
- package/skills/luckiest-coder-subagent-driven-development/implementer-prompt.md +139 -0
- package/skills/luckiest-coder-subagent-driven-development/scripts/review-package +44 -0
- package/skills/luckiest-coder-subagent-driven-development/scripts/sdd-workspace +22 -0
- package/skills/luckiest-coder-subagent-driven-development/scripts/task-brief +40 -0
- package/skills/luckiest-coder-subagent-driven-development/task-reviewer-prompt.md +188 -0
- package/skills/luckiest-coder-tdd/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-tdd/CHANGELOG.md +26 -0
- package/skills/luckiest-coder-tdd/SKILL.md +384 -0
- package/skills/luckiest-coder-tdd/testing-anti-patterns.md +299 -0
- package/skills/luckiest-coder-verification/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-verification/CHANGELOG.md +22 -0
- package/skills/luckiest-coder-verification/SKILL.md +152 -0
- package/skills/luckiest-coder-writing-plans/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-writing-plans/CHANGELOG.md +22 -0
- package/skills/luckiest-coder-writing-plans/SKILL.md +191 -0
- package/skills/luckiest-coder-writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/skills/luckiest-coder-writing-skills/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-writing-skills/CHANGELOG.md +31 -0
- package/skills/luckiest-coder-writing-skills/SKILL.md +702 -0
- package/skills/luckiest-coder-writing-skills/anthropic-best-practices.md +1150 -0
- package/skills/luckiest-coder-writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/skills/luckiest-coder-writing-skills/graphviz-conventions.dot +172 -0
- package/skills/luckiest-coder-writing-skills/persuasion-principles.md +187 -0
- package/skills/luckiest-coder-writing-skills/render-graphs.js +168 -0
- package/skills/luckiest-coder-writing-skills/testing-skills-with-subagents.md +384 -0
- package/skills/luckiest-cold-email/ATTRIBUTION.md +36 -0
- package/skills/luckiest-cold-email/CHANGELOG.md +24 -0
- package/skills/luckiest-cold-email/SKILL.md +213 -0
- package/skills/luckiest-cold-email/evals/evals.json +94 -0
- package/skills/luckiest-cold-email/references/benchmarks.md +83 -0
- package/skills/luckiest-cold-email/references/follow-up-sequences.md +81 -0
- package/skills/luckiest-cold-email/references/frameworks.md +90 -0
- package/skills/luckiest-cold-email/references/personalization.md +79 -0
- package/skills/luckiest-cold-email/references/subject-lines.md +53 -0
- package/skills/luckiest-community-marketing/ATTRIBUTION.md +41 -0
- package/skills/luckiest-community-marketing/CHANGELOG.md +35 -0
- package/skills/luckiest-community-marketing/SKILL.md +200 -0
- package/skills/luckiest-community-marketing/evals/evals.json +89 -0
- package/skills/luckiest-competitor-profiling/ATTRIBUTION.md +36 -0
- package/skills/luckiest-competitor-profiling/CHANGELOG.md +24 -0
- package/skills/luckiest-competitor-profiling/SKILL.md +443 -0
- package/skills/luckiest-competitor-profiling/evals/evals.json +85 -0
- package/skills/luckiest-competitor-profiling/references/templates.md +167 -0
- package/skills/luckiest-competitor-profiling/references/tool-reference.md +179 -0
- package/skills/luckiest-competitors/ATTRIBUTION.md +36 -0
- package/skills/luckiest-competitors/CHANGELOG.md +25 -0
- package/skills/luckiest-competitors/SKILL.md +171 -0
- package/skills/luckiest-competitors/evals/evals.json +93 -0
- package/skills/luckiest-competitors/references/content-architecture.md +271 -0
- package/skills/luckiest-competitors/references/templates.md +223 -0
- package/skills/luckiest-content-strategy/ATTRIBUTION.md +39 -0
- package/skills/luckiest-content-strategy/CHANGELOG.md +23 -0
- package/skills/luckiest-content-strategy/SKILL.md +395 -0
- package/skills/luckiest-content-strategy/evals/evals.json +90 -0
- package/skills/luckiest-content-strategy/references/headless-cms.md +194 -0
- package/skills/luckiest-copy-editing/ATTRIBUTION.md +41 -0
- package/skills/luckiest-copy-editing/CHANGELOG.md +31 -0
- package/skills/luckiest-copy-editing/SKILL.md +491 -0
- package/skills/luckiest-copy-editing/evals/evals.json +89 -0
- package/skills/luckiest-copy-editing/references/checklist.md +66 -0
- package/skills/luckiest-copy-editing/references/content-refresh.md +38 -0
- package/skills/luckiest-copy-editing/references/plain-english-alternatives.md +394 -0
- package/skills/luckiest-copywriting/ATTRIBUTION.md +35 -0
- package/skills/luckiest-copywriting/CHANGELOG.md +23 -0
- package/skills/luckiest-copywriting/SKILL.md +296 -0
- package/skills/luckiest-copywriting/evals/evals.json +111 -0
- package/skills/luckiest-copywriting/references/copy-frameworks.md +344 -0
- package/skills/luckiest-copywriting/references/natural-transitions.md +272 -0
- package/skills/luckiest-cro/ATTRIBUTION.md +34 -0
- package/skills/luckiest-cro/CHANGELOG.md +23 -0
- package/skills/luckiest-cro/SKILL.md +215 -0
- package/skills/luckiest-cro/evals/evals.json +111 -0
- package/skills/luckiest-cro/references/experiments.md +248 -0
- package/skills/luckiest-cro/references/form.md +422 -0
- package/skills/luckiest-customer-research/ATTRIBUTION.md +40 -0
- package/skills/luckiest-customer-research/CHANGELOG.md +26 -0
- package/skills/luckiest-customer-research/SKILL.md +293 -0
- package/skills/luckiest-customer-research/evals/evals.json +162 -0
- package/skills/luckiest-customer-research/references/source-guides.md +401 -0
- package/skills/luckiest-directory-submissions/ATTRIBUTION.md +40 -0
- package/skills/luckiest-directory-submissions/CHANGELOG.md +33 -0
- package/skills/luckiest-directory-submissions/SKILL.md +419 -0
- package/skills/luckiest-directory-submissions/evals/evals.json +94 -0
- package/skills/luckiest-directory-submissions/references/directory-list.md +477 -0
- package/skills/luckiest-directory-submissions/references/positioning-variations.md +232 -0
- package/skills/luckiest-directory-submissions/references/submission-tracker-template.csv +266 -0
- package/skills/luckiest-emails/ATTRIBUTION.md +42 -0
- package/skills/luckiest-emails/CHANGELOG.md +33 -0
- package/skills/luckiest-emails/SKILL.md +220 -0
- package/skills/luckiest-emails/evals/evals.json +93 -0
- package/skills/luckiest-emails/references/copy-guidelines.md +120 -0
- package/skills/luckiest-emails/references/email-types.md +515 -0
- package/skills/luckiest-emails/references/sequence-templates.md +168 -0
- package/skills/luckiest-extract-design-system/ATTRIBUTION.md +8 -0
- package/skills/luckiest-extract-design-system/CHANGELOG.md +37 -0
- package/skills/luckiest-extract-design-system/SKILL.md +83 -0
- package/skills/luckiest-free-tools/ATTRIBUTION.md +43 -0
- package/skills/luckiest-free-tools/CHANGELOG.md +25 -0
- package/skills/luckiest-free-tools/SKILL.md +220 -0
- package/skills/luckiest-free-tools/evals/evals.json +90 -0
- package/skills/luckiest-free-tools/references/tool-types.md +217 -0
- package/skills/luckiest-image/ATTRIBUTION.md +43 -0
- package/skills/luckiest-image/CHANGELOG.md +23 -0
- package/skills/luckiest-image/SKILL.md +376 -0
- package/skills/luckiest-image/evals/evals.json +89 -0
- package/skills/luckiest-image/references/ai-image-prompting.md +229 -0
- package/skills/luckiest-launch/ATTRIBUTION.md +40 -0
- package/skills/luckiest-launch/CHANGELOG.md +43 -0
- package/skills/luckiest-launch/SKILL.md +385 -0
- package/skills/luckiest-launch/evals/evals.json +91 -0
- package/skills/luckiest-lead-magnets/ATTRIBUTION.md +35 -0
- package/skills/luckiest-lead-magnets/CHANGELOG.md +23 -0
- package/skills/luckiest-lead-magnets/SKILL.md +331 -0
- package/skills/luckiest-lead-magnets/evals/evals.json +87 -0
- package/skills/luckiest-lead-magnets/references/benchmarks.md +129 -0
- package/skills/luckiest-lead-magnets/references/format-guide.md +196 -0
- package/skills/luckiest-marketing-ideas/ATTRIBUTION.md +39 -0
- package/skills/luckiest-marketing-ideas/CHANGELOG.md +33 -0
- package/skills/luckiest-marketing-ideas/SKILL.md +191 -0
- package/skills/luckiest-marketing-ideas/evals/evals.json +90 -0
- package/skills/luckiest-marketing-ideas/references/ideas-by-category.md +366 -0
- package/skills/luckiest-marketing-plan/ATTRIBUTION.md +40 -0
- package/skills/luckiest-marketing-plan/CHANGELOG.md +23 -0
- package/skills/luckiest-marketing-plan/SKILL.md +285 -0
- package/skills/luckiest-marketing-plan/evals/evals.json +96 -0
- package/skills/luckiest-marketing-plan/references/aarrr-framework.md +180 -0
- package/skills/luckiest-marketing-plan/references/budget-planning.md +168 -0
- package/skills/luckiest-marketing-plan/references/client-types.md +373 -0
- package/skills/luckiest-marketing-plan/references/current-state-rubric.md +255 -0
- package/skills/luckiest-marketing-plan/references/example-quietude.md +972 -0
- package/skills/luckiest-marketing-plan/references/funding-stage-unlocks.md +230 -0
- package/skills/luckiest-marketing-plan/references/growth-patterns.md +148 -0
- package/skills/luckiest-marketing-plan/references/idea-cross-reference.md +265 -0
- package/skills/luckiest-marketing-plan/references/measurement-framework.md +213 -0
- package/skills/luckiest-marketing-plan/references/methodology.md +363 -0
- package/skills/luckiest-marketing-plan/references/ops-stack-mapping.md +197 -0
- package/skills/luckiest-marketing-plan/references/plan-template.md +494 -0
- package/skills/luckiest-marketing-plan/references/team-and-agency-model.md +278 -0
- package/skills/luckiest-marketing-psychology/ATTRIBUTION.md +31 -0
- package/skills/luckiest-marketing-psychology/CHANGELOG.md +32 -0
- package/skills/luckiest-marketing-psychology/SKILL.md +202 -0
- package/skills/luckiest-marketing-psychology/evals.json +88 -0
- package/skills/luckiest-model-router/ATTRIBUTION.md +11 -0
- package/skills/luckiest-model-router/CHANGELOG.md +45 -0
- package/skills/luckiest-model-router/SKILL.md +74 -0
- package/skills/luckiest-model-router/scripts/route.py +145 -0
- package/skills/luckiest-offers/ATTRIBUTION.md +39 -0
- package/skills/luckiest-offers/CHANGELOG.md +23 -0
- package/skills/luckiest-offers/SKILL.md +167 -0
- package/skills/luckiest-offers/references/bonus-stacking.md +150 -0
- package/skills/luckiest-offers/references/examples.md +215 -0
- package/skills/luckiest-offers/references/guarantee-design.md +172 -0
- package/skills/luckiest-offers/references/offer-anatomy.md +203 -0
- package/skills/luckiest-offers/references/offer-formats.md +240 -0
- package/skills/luckiest-offers/references/scarcity-urgency.md +175 -0
- package/skills/luckiest-offers/references/value-equation.md +134 -0
- package/skills/luckiest-onboarding/ATTRIBUTION.md +43 -0
- package/skills/luckiest-onboarding/CHANGELOG.md +31 -0
- package/skills/luckiest-onboarding/SKILL.md +243 -0
- package/skills/luckiest-onboarding/references/experiments.md +258 -0
- package/skills/luckiest-paywalls/ATTRIBUTION.md +39 -0
- package/skills/luckiest-paywalls/CHANGELOG.md +25 -0
- package/skills/luckiest-paywalls/SKILL.md +256 -0
- package/skills/luckiest-paywalls/evals/evals.json +93 -0
- package/skills/luckiest-paywalls/references/experiments.md +164 -0
- package/skills/luckiest-popups/ATTRIBUTION.md +35 -0
- package/skills/luckiest-popups/CHANGELOG.md +27 -0
- package/skills/luckiest-popups/SKILL.md +477 -0
- package/skills/luckiest-popups/evals/evals.json +107 -0
- package/skills/luckiest-pricing/ATTRIBUTION.md +35 -0
- package/skills/luckiest-pricing/CHANGELOG.md +31 -0
- package/skills/luckiest-pricing/SKILL.md +283 -0
- package/skills/luckiest-pricing/evals/evals.json +90 -0
- package/skills/luckiest-pricing/references/research-methods.md +152 -0
- package/skills/luckiest-pricing/references/tier-structure.md +232 -0
- package/skills/luckiest-product-marketing/ATTRIBUTION.md +38 -0
- package/skills/luckiest-product-marketing/CHANGELOG.md +23 -0
- package/skills/luckiest-product-marketing/SKILL.md +256 -0
- package/skills/luckiest-product-marketing/evals/evals.json +85 -0
- package/skills/luckiest-programmatic-seo/ATTRIBUTION.md +35 -0
- package/skills/luckiest-programmatic-seo/CHANGELOG.md +24 -0
- package/skills/luckiest-programmatic-seo/SKILL.md +286 -0
- package/skills/luckiest-programmatic-seo/evals/evals.json +95 -0
- package/skills/luckiest-programmatic-seo/references/playbooks.md +308 -0
- package/skills/luckiest-prompt-rewrite/ATTRIBUTION.md +39 -0
- package/skills/luckiest-prompt-rewrite/CHANGELOG.md +70 -0
- package/skills/luckiest-prompt-rewrite/LICENSE +21 -0
- package/skills/luckiest-prompt-rewrite/SKILL.md +494 -0
- package/skills/luckiest-prompt-rewrite/references/patterns.md +82 -0
- package/skills/luckiest-prompt-rewrite/references/templates.md +454 -0
- package/skills/luckiest-prospecting/ATTRIBUTION.md +29 -0
- package/skills/luckiest-prospecting/CHANGELOG.md +24 -0
- package/skills/luckiest-prospecting/SKILL.md +275 -0
- package/skills/luckiest-prospecting/evals/evals.json +107 -0
- package/skills/luckiest-prospecting/references/b2b-prospecting.md +106 -0
- package/skills/luckiest-prospecting/references/compliance.md +123 -0
- package/skills/luckiest-prospecting/references/data-sources.md +287 -0
- package/skills/luckiest-prospecting/references/local-prospecting.md +165 -0
- package/skills/luckiest-prospecting/references/saas-prospecting.md +123 -0
- package/skills/luckiest-public-relations/ATTRIBUTION.md +34 -0
- package/skills/luckiest-public-relations/CHANGELOG.md +23 -0
- package/skills/luckiest-public-relations/SKILL.md +154 -0
- package/skills/luckiest-public-relations/references/journalist-pitching.md +368 -0
- package/skills/luckiest-public-relations/references/media-outlets.md +207 -0
- package/skills/luckiest-public-relations/references/newsjacking.md +218 -0
- package/skills/luckiest-public-relations/references/press-platforms.md +188 -0
- package/skills/luckiest-referrals/ATTRIBUTION.md +41 -0
- package/skills/luckiest-referrals/CHANGELOG.md +23 -0
- package/skills/luckiest-referrals/SKILL.md +292 -0
- package/skills/luckiest-referrals/evals/evals.json +90 -0
- package/skills/luckiest-referrals/references/affiliate-programs.md +167 -0
- package/skills/luckiest-referrals/references/program-examples.md +143 -0
- package/skills/luckiest-revops/ATTRIBUTION.md +40 -0
- package/skills/luckiest-revops/CHANGELOG.md +24 -0
- package/skills/luckiest-revops/SKILL.md +362 -0
- package/skills/luckiest-revops/evals/evals.json +91 -0
- package/skills/luckiest-revops/references/automation-playbooks.md +290 -0
- package/skills/luckiest-revops/references/lifecycle-definitions.md +278 -0
- package/skills/luckiest-revops/references/routing-rules.md +203 -0
- package/skills/luckiest-revops/references/scoring-models.md +247 -0
- package/skills/luckiest-sales-enablement/ATTRIBUTION.md +39 -0
- package/skills/luckiest-sales-enablement/CHANGELOG.md +23 -0
- package/skills/luckiest-sales-enablement/SKILL.md +379 -0
- package/skills/luckiest-sales-enablement/evals/evals.json +91 -0
- package/skills/luckiest-sales-enablement/references/deck-frameworks.md +263 -0
- package/skills/luckiest-sales-enablement/references/demo-scripts.md +355 -0
- package/skills/luckiest-sales-enablement/references/objection-library.md +270 -0
- package/skills/luckiest-sales-enablement/references/one-pager-templates.md +208 -0
- package/skills/luckiest-schema/ATTRIBUTION.md +39 -0
- package/skills/luckiest-schema/CHANGELOG.md +25 -0
- package/skills/luckiest-schema/SKILL.md +209 -0
- package/skills/luckiest-schema/evals/evals.json +88 -0
- package/skills/luckiest-schema/references/schema-examples.md +398 -0
- package/skills/luckiest-seo-audit/ATTRIBUTION.md +39 -0
- package/skills/luckiest-seo-audit/CHANGELOG.md +23 -0
- package/skills/luckiest-seo-audit/SKILL.md +529 -0
- package/skills/luckiest-seo-audit/evals/evals.json +136 -0
- package/skills/luckiest-seo-audit/references/ai-writing-detection.md +200 -0
- package/skills/luckiest-seo-audit/references/international-seo.md +230 -0
- package/skills/luckiest-session-handoff/ATTRIBUTION.md +14 -0
- package/skills/luckiest-session-handoff/CHANGELOG.md +30 -0
- package/skills/luckiest-session-handoff/SKILL.md +96 -0
- package/skills/luckiest-signup/ATTRIBUTION.md +48 -0
- package/skills/luckiest-signup/CHANGELOG.md +24 -0
- package/skills/luckiest-signup/SKILL.md +382 -0
- package/skills/luckiest-signup/evals/evals.json +88 -0
- package/skills/luckiest-site-architecture/ATTRIBUTION.md +38 -0
- package/skills/luckiest-site-architecture/CHANGELOG.md +23 -0
- package/skills/luckiest-site-architecture/SKILL.md +383 -0
- package/skills/luckiest-site-architecture/evals/evals.json +88 -0
- package/skills/luckiest-site-architecture/references/mermaid-templates.md +216 -0
- package/skills/luckiest-site-architecture/references/navigation-patterns.md +305 -0
- package/skills/luckiest-site-architecture/references/site-type-templates.md +293 -0
- package/skills/luckiest-sms/ATTRIBUTION.md +27 -0
- package/skills/luckiest-sms/CHANGELOG.md +24 -0
- package/skills/luckiest-sms/SKILL.md +400 -0
- package/skills/luckiest-sms/evals/evals.json +100 -0
- package/skills/luckiest-sms/references/compliance.md +202 -0
- package/skills/luckiest-sms/references/platforms.md +318 -0
- package/skills/luckiest-sms/references/sequence-templates.md +282 -0
- package/skills/luckiest-social/ATTRIBUTION.md +27 -0
- package/skills/luckiest-social/CHANGELOG.md +24 -0
- package/skills/luckiest-social/SKILL.md +430 -0
- package/skills/luckiest-social/evals/evals.json +92 -0
- package/skills/luckiest-social/references/listening-sources-template.md +123 -0
- package/skills/luckiest-social/references/listening.md +283 -0
- package/skills/luckiest-social/references/platform-limits.md +110 -0
- package/skills/luckiest-social/references/platforms.md +170 -0
- package/skills/luckiest-social/references/post-templates.md +177 -0
- package/skills/luckiest-social/references/reverse-engineering.md +195 -0
- package/skills/luckiest-social/references/short-form-video.md +237 -0
- package/skills/luckiest-thinker/ATTRIBUTION.md +16 -0
- package/skills/luckiest-thinker/CHANGELOG.md +20 -0
- package/skills/luckiest-thinker/LICENSE +21 -0
- package/skills/luckiest-thinker/SKILL.md +92 -0
- package/skills/luckiest-thinker/references/distribution-calibration.md +106 -0
- package/skills/luckiest-thinker/references/research-foundations.md +214 -0
- package/skills/luckiest-thinker/references/thinking-patterns.md +138 -0
- package/skills/luckiest-trends/.skillignore +10 -0
- package/skills/luckiest-trends/ATTRIBUTION.md +43 -0
- package/skills/luckiest-trends/CHANGELOG.md +86 -0
- package/skills/luckiest-trends/LICENSE +21 -0
- package/skills/luckiest-trends/SKILL.md +194 -0
- package/skills/luckiest-trends/agents/openai.yaml +8 -0
- package/skills/luckiest-trends/references/engine-contract.md +2041 -0
- package/skills/luckiest-trends/references/save-html-brief.md +206 -0
- package/skills/luckiest-trends/scripts/briefing.py +272 -0
- package/skills/luckiest-trends/scripts/build-skill.sh +39 -0
- package/skills/luckiest-trends/scripts/compare.sh +61 -0
- package/skills/luckiest-trends/scripts/evaluate_search_quality.py +573 -0
- package/skills/luckiest-trends/scripts/last30days.py +1467 -0
- package/skills/luckiest-trends/scripts/lib/__init__.py +1 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/arxiv.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/bird_x.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/bluesky.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/categories.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/cjk.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/cluster.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/dates.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/dedupe.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/digg.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/entity_extract.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/env.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/fusion.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/github.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/grounding.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/hackernews.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/hiring_signals.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/html_render.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/http.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/instagram.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/jobs.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/linkedin.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/log.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/normalize.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/permission_preflight.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/perplexity.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/pinterest.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/pipeline.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/planner.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/polymarket.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/preflight.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/providers.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/quality_nudge.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/query.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_arctic.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_enrich.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_keyless.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_listing.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_public.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_rss.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_shreddit.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/relevance.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/render.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/rerank.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/resolve.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/schema.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/signals.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/skill_meta.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/snippet.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/subproc.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/techmeme.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/threads.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/tiktok.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/trustpilot.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/truthsocial.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/ui.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/web_search_keyless.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/xai_x.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/xiaohongshu_api.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/xquik.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/xurl_x.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/youtube_yt.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/arxiv.py +290 -0
- package/skills/luckiest-trends/scripts/lib/bird_x.py +639 -0
- package/skills/luckiest-trends/scripts/lib/bluesky.py +323 -0
- package/skills/luckiest-trends/scripts/lib/categories.py +283 -0
- package/skills/luckiest-trends/scripts/lib/chrome_cookies.py +414 -0
- package/skills/luckiest-trends/scripts/lib/cjk.py +107 -0
- package/skills/luckiest-trends/scripts/lib/cluster.py +271 -0
- package/skills/luckiest-trends/scripts/lib/competitors.py +198 -0
- package/skills/luckiest-trends/scripts/lib/cookie_extract.py +508 -0
- package/skills/luckiest-trends/scripts/lib/dates.py +169 -0
- package/skills/luckiest-trends/scripts/lib/dedupe.py +145 -0
- package/skills/luckiest-trends/scripts/lib/digg.py +440 -0
- package/skills/luckiest-trends/scripts/lib/entity_extract.py +127 -0
- package/skills/luckiest-trends/scripts/lib/env.py +1073 -0
- package/skills/luckiest-trends/scripts/lib/fanout.py +84 -0
- package/skills/luckiest-trends/scripts/lib/fusion.py +207 -0
- package/skills/luckiest-trends/scripts/lib/github.py +996 -0
- package/skills/luckiest-trends/scripts/lib/grounding.py +331 -0
- package/skills/luckiest-trends/scripts/lib/hackernews.py +357 -0
- package/skills/luckiest-trends/scripts/lib/health.py +92 -0
- package/skills/luckiest-trends/scripts/lib/hiring_signals.py +326 -0
- package/skills/luckiest-trends/scripts/lib/html_publish.py +73 -0
- package/skills/luckiest-trends/scripts/lib/html_render.py +738 -0
- package/skills/luckiest-trends/scripts/lib/http.py +372 -0
- package/skills/luckiest-trends/scripts/lib/instagram.py +521 -0
- package/skills/luckiest-trends/scripts/lib/jobs.py +708 -0
- package/skills/luckiest-trends/scripts/lib/linkedin.py +363 -0
- package/skills/luckiest-trends/scripts/lib/log.py +35 -0
- package/skills/luckiest-trends/scripts/lib/normalize.py +759 -0
- package/skills/luckiest-trends/scripts/lib/permission_preflight.py +242 -0
- package/skills/luckiest-trends/scripts/lib/perplexity.py +745 -0
- package/skills/luckiest-trends/scripts/lib/pinterest.py +154 -0
- package/skills/luckiest-trends/scripts/lib/pipeline.py +1720 -0
- package/skills/luckiest-trends/scripts/lib/planner.py +763 -0
- package/skills/luckiest-trends/scripts/lib/polymarket.py +794 -0
- package/skills/luckiest-trends/scripts/lib/preflight.py +119 -0
- package/skills/luckiest-trends/scripts/lib/providers.py +380 -0
- package/skills/luckiest-trends/scripts/lib/quality_nudge.py +383 -0
- package/skills/luckiest-trends/scripts/lib/query.py +202 -0
- package/skills/luckiest-trends/scripts/lib/reddit.py +731 -0
- package/skills/luckiest-trends/scripts/lib/reddit_arctic.py +92 -0
- package/skills/luckiest-trends/scripts/lib/reddit_enrich.py +322 -0
- package/skills/luckiest-trends/scripts/lib/reddit_keyless.py +329 -0
- package/skills/luckiest-trends/scripts/lib/reddit_listing.py +188 -0
- package/skills/luckiest-trends/scripts/lib/reddit_public.py +275 -0
- package/skills/luckiest-trends/scripts/lib/reddit_rss.py +224 -0
- package/skills/luckiest-trends/scripts/lib/reddit_shreddit.py +184 -0
- package/skills/luckiest-trends/scripts/lib/relevance.py +200 -0
- package/skills/luckiest-trends/scripts/lib/render.py +2130 -0
- package/skills/luckiest-trends/scripts/lib/rerank.py +708 -0
- package/skills/luckiest-trends/scripts/lib/resolve.py +345 -0
- package/skills/luckiest-trends/scripts/lib/safari_cookies.py +193 -0
- package/skills/luckiest-trends/scripts/lib/schema.py +319 -0
- package/skills/luckiest-trends/scripts/lib/setup_wizard.py +843 -0
- package/skills/luckiest-trends/scripts/lib/signals.py +334 -0
- package/skills/luckiest-trends/scripts/lib/skill_meta.py +33 -0
- package/skills/luckiest-trends/scripts/lib/snippet.py +51 -0
- package/skills/luckiest-trends/scripts/lib/subproc.py +116 -0
- package/skills/luckiest-trends/scripts/lib/techmeme.py +239 -0
- package/skills/luckiest-trends/scripts/lib/threads.py +207 -0
- package/skills/luckiest-trends/scripts/lib/tiktok.py +596 -0
- package/skills/luckiest-trends/scripts/lib/transcribe.py +235 -0
- package/skills/luckiest-trends/scripts/lib/trustpilot.py +277 -0
- package/skills/luckiest-trends/scripts/lib/truthsocial.py +162 -0
- package/skills/luckiest-trends/scripts/lib/ui.py +626 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/LICENSE +21 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/bird-search.mjs +147 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/cookies.js +220 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/features.json +17 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/paginate-cursor.js +37 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/query-ids.json +20 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/runtime-features.js +151 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/runtime-query-ids.js +264 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-base.js +129 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-constants.js +50 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-features.js +347 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-search.js +157 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-types.js +2 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-utils.js +511 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/package.json +13 -0
- package/skills/luckiest-trends/scripts/lib/web_fetch_keyless.py +105 -0
- package/skills/luckiest-trends/scripts/lib/web_search_keyless.py +158 -0
- package/skills/luckiest-trends/scripts/lib/xai_x.py +236 -0
- package/skills/luckiest-trends/scripts/lib/xiaohongshu_api.py +162 -0
- package/skills/luckiest-trends/scripts/lib/xquik.py +404 -0
- package/skills/luckiest-trends/scripts/lib/xurl_x.py +171 -0
- package/skills/luckiest-trends/scripts/lib/youtube_yt.py +1365 -0
- package/skills/luckiest-trends/scripts/setup-keychain.sh +123 -0
- package/skills/luckiest-trends/scripts/setup-pass.sh +132 -0
- package/skills/luckiest-trends/scripts/store.py +1020 -0
- package/skills/luckiest-trends/scripts/test-v1-vs-v2.sh +220 -0
- package/skills/luckiest-trends/scripts/test_device_auth.py +128 -0
- package/skills/luckiest-trends/scripts/verify_v3.py +195 -0
- package/skills/luckiest-trends/scripts/watchlist.py +318 -0
- package/skills/luckiest-video/ATTRIBUTION.md +27 -0
- package/skills/luckiest-video/CHANGELOG.md +23 -0
- package/skills/luckiest-video/SKILL.md +371 -0
- package/skills/luckiest-video/evals/evals.json +88 -0
- package/skills/luckiest-video/references/ai-video-prompting.md +175 -0
- package/skills/remix/SKILL.md +32 -0
- package/commands/luckiest/go.md +0 -45
- package/commands/luckiest/plan.md +0 -55
- /package/commands/{luckiest/charms.md → charms.md} +0 -0
- /package/commands/{luckiest/helpers.md → helpers.md} +0 -0
- /package/commands/{luckiest/home.md → home.md} +0 -0
- /package/commands/{luckiest/leaderboard.md → leaderboard.md} +0 -0
- /package/commands/{luckiest/merge.md → merge.md} +0 -0
- /package/commands/{luckiest/overlaps.md → overlaps.md} +0 -0
- /package/commands/{luckiest/skills.md → skills.md} +0 -0
- /package/commands/{luckiest/start.md → start.md} +0 -0
- /package/commands/{luckiest/status.md → status.md} +0 -0
- /package/commands/{luckiest/updates.md → updates.md} +0 -0
- /package/commands/{luckiest/vouch.md → vouch.md} +0 -0
- /package/commands/{luckiest/wishes.md → wishes.md} +0 -0
|
@@ -0,0 +1,1720 @@
|
|
|
1
|
+
"""v3.0.0 orchestration pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from shutil import which
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from . import (
|
|
15
|
+
arxiv,
|
|
16
|
+
bird_x,
|
|
17
|
+
bluesky,
|
|
18
|
+
dates,
|
|
19
|
+
dedupe,
|
|
20
|
+
digg,
|
|
21
|
+
entity_extract,
|
|
22
|
+
env,
|
|
23
|
+
github,
|
|
24
|
+
grounding,
|
|
25
|
+
hackernews,
|
|
26
|
+
hiring_signals,
|
|
27
|
+
instagram,
|
|
28
|
+
jobs,
|
|
29
|
+
linkedin,
|
|
30
|
+
normalize,
|
|
31
|
+
permission_preflight,
|
|
32
|
+
perplexity,
|
|
33
|
+
pinterest,
|
|
34
|
+
planner,
|
|
35
|
+
polymarket,
|
|
36
|
+
providers,
|
|
37
|
+
query,
|
|
38
|
+
reddit,
|
|
39
|
+
reddit_public,
|
|
40
|
+
relevance,
|
|
41
|
+
rerank,
|
|
42
|
+
schema,
|
|
43
|
+
signals,
|
|
44
|
+
snippet,
|
|
45
|
+
techmeme,
|
|
46
|
+
threads,
|
|
47
|
+
tiktok,
|
|
48
|
+
truthsocial,
|
|
49
|
+
trustpilot,
|
|
50
|
+
xai_x,
|
|
51
|
+
xiaohongshu_api,
|
|
52
|
+
xquik,
|
|
53
|
+
xurl_x,
|
|
54
|
+
youtube_yt,
|
|
55
|
+
)
|
|
56
|
+
from .cluster import cluster_candidates
|
|
57
|
+
from .fusion import weighted_rrf
|
|
58
|
+
|
|
59
|
+
DEPTH_SETTINGS = {
|
|
60
|
+
"quick": {"per_stream_limit": 6, "pool_limit": 15, "rerank_limit": 12},
|
|
61
|
+
"default": {"per_stream_limit": 12, "pool_limit": 40, "rerank_limit": 40},
|
|
62
|
+
"deep": {"per_stream_limit": 20, "pool_limit": 60, "rerank_limit": 60},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
SEARCH_ALIAS = {
|
|
66
|
+
"hn": "hackernews",
|
|
67
|
+
"bsky": "bluesky",
|
|
68
|
+
"truth": "truthsocial",
|
|
69
|
+
"web": "grounding",
|
|
70
|
+
"xhs": "xiaohongshu",
|
|
71
|
+
"xquik": "x", # xquik is a backend of the single "x" source, not its own source
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
MAX_SOURCE_FETCHES: dict[str, int] = {"x": 2, "jobs": 1, "linkedin": 1}
|
|
75
|
+
|
|
76
|
+
# Per-handle result caps for the X handle-search lanes. The FROM lane (the
|
|
77
|
+
# subject's own timeline) is the single best source for a person topic, so it
|
|
78
|
+
# gets the highest cap; the ABOUT (mention) and related-handle lanes stay
|
|
79
|
+
# modest so total volume and request budget don't balloon.
|
|
80
|
+
FROM_LANE_COUNT_PER = 8
|
|
81
|
+
MENTION_LANE_COUNT_PER = 5
|
|
82
|
+
RELATED_HANDLE_COUNT_PER = 3
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _has_perplexity_provider(config: dict[str, Any]) -> bool:
|
|
86
|
+
return bool(config.get("PERPLEXITY_API_KEY") or config.get("OPENROUTER_API_KEY"))
|
|
87
|
+
|
|
88
|
+
MOCK_AVAILABLE_SOURCES = [
|
|
89
|
+
"reddit",
|
|
90
|
+
"x",
|
|
91
|
+
"youtube",
|
|
92
|
+
"tiktok",
|
|
93
|
+
"instagram",
|
|
94
|
+
"hackernews",
|
|
95
|
+
"bluesky",
|
|
96
|
+
"truthsocial",
|
|
97
|
+
"polymarket",
|
|
98
|
+
"grounding",
|
|
99
|
+
"xiaohongshu",
|
|
100
|
+
"github",
|
|
101
|
+
"perplexity",
|
|
102
|
+
"threads",
|
|
103
|
+
"pinterest",
|
|
104
|
+
"digg",
|
|
105
|
+
"arxiv",
|
|
106
|
+
"techmeme",
|
|
107
|
+
"trustpilot",
|
|
108
|
+
"jobs",
|
|
109
|
+
"linkedin",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def normalize_requested_sources(sources: list[str] | None) -> list[str] | None:
|
|
114
|
+
if not sources:
|
|
115
|
+
return None
|
|
116
|
+
normalized = []
|
|
117
|
+
for source in sources:
|
|
118
|
+
key = SEARCH_ALIAS.get(source.lower(), source.lower())
|
|
119
|
+
if key not in normalized:
|
|
120
|
+
normalized.append(key)
|
|
121
|
+
return normalized
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def available_sources(
|
|
125
|
+
config: dict[str, Any],
|
|
126
|
+
requested_sources: list[str] | None = None,
|
|
127
|
+
*,
|
|
128
|
+
x_pending: bool | None = None,
|
|
129
|
+
) -> list[str]:
|
|
130
|
+
available: list[str] = []
|
|
131
|
+
# reddit_public needs no API key - always available
|
|
132
|
+
available.append("reddit")
|
|
133
|
+
if config.get("SCRAPECREATORS_API_KEY"):
|
|
134
|
+
available.extend(["tiktok", "instagram"])
|
|
135
|
+
if env.get_x_source(config):
|
|
136
|
+
available.append("x")
|
|
137
|
+
else:
|
|
138
|
+
# Safe inspection (--diagnose/--preflight) skips browser-cookie
|
|
139
|
+
# extraction, so get_x_source is None even though a real run would
|
|
140
|
+
# authenticate X via FROM_BROWSER. Report it as available so consumers
|
|
141
|
+
# of available_sources (SKILL.md ACTIVE_SOURCES_LIST) don't under-report.
|
|
142
|
+
# diagnose() precomputes the predicate and passes it via x_pending to
|
|
143
|
+
# avoid evaluating it twice in one diagnose() call.
|
|
144
|
+
if x_pending is None:
|
|
145
|
+
x_pending = env.x_pending_browser_auth(config)
|
|
146
|
+
if x_pending:
|
|
147
|
+
available.append("x")
|
|
148
|
+
if which("yt-dlp") or env.is_youtube_sc_available(config):
|
|
149
|
+
available.append("youtube")
|
|
150
|
+
available.extend(["hackernews", "polymarket"])
|
|
151
|
+
# GitHub is reachable via the unauthenticated REST tier too, so it is
|
|
152
|
+
# available even without a token/gh CLI (a token only raises rate limits).
|
|
153
|
+
available.append("github")
|
|
154
|
+
if which("digg-pp-cli"):
|
|
155
|
+
available.append("digg")
|
|
156
|
+
# arXiv is default-on when its Printing Press CLI is installed (zero auth).
|
|
157
|
+
# The adapter relevance-and-recency gates so it stays quiet off-topic.
|
|
158
|
+
if which("arxiv-pp-cli"):
|
|
159
|
+
available.append("arxiv")
|
|
160
|
+
# Techmeme is default-on when its CLI is installed (zero auth; sub-second
|
|
161
|
+
# local sync before each run's first search).
|
|
162
|
+
if which("techmeme-pp-cli"):
|
|
163
|
+
available.append("techmeme")
|
|
164
|
+
if env.is_bluesky_available(config):
|
|
165
|
+
available.append("bluesky")
|
|
166
|
+
if env.is_truthsocial_available(config):
|
|
167
|
+
available.append("truthsocial")
|
|
168
|
+
# Grounding (general web) is available when a paid backend is configured OR
|
|
169
|
+
# the keyless floor is permitted (i.e. the host has no native search). On a
|
|
170
|
+
# native-search host with no paid key, keyless_web_allowed is False and the
|
|
171
|
+
# engine leaves general web to the model's own search.
|
|
172
|
+
if (config.get("BRAVE_API_KEY") or config.get("EXA_API_KEY")
|
|
173
|
+
or config.get("SERPER_API_KEY") or config.get("PARALLEL_API_KEY")
|
|
174
|
+
or env.keyless_web_allowed(config)):
|
|
175
|
+
available.append("grounding")
|
|
176
|
+
if requested_sources and "jobs" in requested_sources:
|
|
177
|
+
available.append("jobs")
|
|
178
|
+
# Perplexity Sonar: opt-in additive source via INCLUDE_SOURCES=perplexity
|
|
179
|
+
include_sources = (config.get("INCLUDE_SOURCES") or "").lower().split(",")
|
|
180
|
+
if _has_perplexity_provider(config) and (
|
|
181
|
+
"perplexity" in include_sources or (requested_sources and "perplexity" in requested_sources)
|
|
182
|
+
):
|
|
183
|
+
available.append("perplexity")
|
|
184
|
+
# LinkedIn: opt-in additive source via INCLUDE_SOURCES=linkedin (same
|
|
185
|
+
# consent pattern as Perplexity). Unlike tiktok/instagram, which are
|
|
186
|
+
# offered during SKILL.md Step 0 onboarding, LinkedIn is power-user-only
|
|
187
|
+
# and must not silently activate for existing SCRAPECREATORS_API_KEY
|
|
188
|
+
# holders.
|
|
189
|
+
if config.get("SCRAPECREATORS_API_KEY") and (
|
|
190
|
+
"linkedin" in include_sources or (requested_sources and "linkedin" in requested_sources)
|
|
191
|
+
):
|
|
192
|
+
available.append("linkedin")
|
|
193
|
+
# Trustpilot: opt-in additive source via INCLUDE_SOURCES=trustpilot (same
|
|
194
|
+
# consent pattern as Perplexity/LinkedIn). Off by default -- unlike arXiv and
|
|
195
|
+
# Techmeme, which are zero-auth, it can spawn a one-time headless-Chrome WAF
|
|
196
|
+
# cookie harvest on a brand topic, so activating it is the user's choice.
|
|
197
|
+
if which("trustpilot-pp-cli") and (
|
|
198
|
+
"trustpilot" in include_sources or (requested_sources and "trustpilot" in requested_sources)
|
|
199
|
+
):
|
|
200
|
+
available.append("trustpilot")
|
|
201
|
+
if requested_sources and "xiaohongshu" in requested_sources and env.is_xiaohongshu_available(config):
|
|
202
|
+
available.append("xiaohongshu")
|
|
203
|
+
if env.is_threads_available(config):
|
|
204
|
+
available.append("threads")
|
|
205
|
+
if requested_sources and "pinterest" in requested_sources and env.is_pinterest_available(config):
|
|
206
|
+
available.append("pinterest")
|
|
207
|
+
# xquik is a backend of the single "x" source (see env.x_backend_chain),
|
|
208
|
+
# not a separate parallel source — registered via the "x" entry above.
|
|
209
|
+
exclude = {s.strip().lower() for s in (config.get("EXCLUDE_SOURCES") or "").split(",") if s.strip()}
|
|
210
|
+
if exclude:
|
|
211
|
+
available = [s for s in available if s not in exclude]
|
|
212
|
+
return available
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def diagnose(
|
|
216
|
+
config: dict[str, Any],
|
|
217
|
+
requested_sources: list[str] | None = None,
|
|
218
|
+
*,
|
|
219
|
+
safe: bool = False,
|
|
220
|
+
) -> dict[str, Any]:
|
|
221
|
+
requested_sources = normalize_requested_sources(requested_sources)
|
|
222
|
+
google_key = _google_key(config)
|
|
223
|
+
x_status = env.get_x_source_status(config, probe=not safe)
|
|
224
|
+
# Compute once and reuse for both the diag flag and available_sources below.
|
|
225
|
+
x_pending = env.x_pending_browser_auth(config)
|
|
226
|
+
native_web_backend = None
|
|
227
|
+
if config.get("BRAVE_API_KEY"):
|
|
228
|
+
native_web_backend = "brave"
|
|
229
|
+
elif config.get("EXA_API_KEY"):
|
|
230
|
+
native_web_backend = "exa"
|
|
231
|
+
elif config.get("SERPER_API_KEY"):
|
|
232
|
+
native_web_backend = "serper"
|
|
233
|
+
elif config.get("PARALLEL_API_KEY"):
|
|
234
|
+
native_web_backend = "parallel"
|
|
235
|
+
providers_status = {
|
|
236
|
+
"google": bool(google_key),
|
|
237
|
+
"openai": bool(config.get("OPENAI_API_KEY")) and config.get("OPENAI_AUTH_STATUS") == env.AUTH_STATUS_OK,
|
|
238
|
+
"xai": bool(config.get("XAI_API_KEY")),
|
|
239
|
+
"openrouter": bool(config.get("OPENROUTER_API_KEY")),
|
|
240
|
+
"perplexity": bool(config.get("PERPLEXITY_API_KEY")),
|
|
241
|
+
}
|
|
242
|
+
reasoning_provider_available = any(
|
|
243
|
+
providers_status[name] for name in ("google", "openai", "xai", "openrouter")
|
|
244
|
+
)
|
|
245
|
+
external_commands = {
|
|
246
|
+
"yt-dlp": bool(which("yt-dlp")),
|
|
247
|
+
"digg-pp-cli": bool(which("digg-pp-cli")),
|
|
248
|
+
"arxiv-pp-cli": bool(which("arxiv-pp-cli")),
|
|
249
|
+
"techmeme-pp-cli": bool(which("techmeme-pp-cli")),
|
|
250
|
+
"trustpilot-pp-cli": bool(which("trustpilot-pp-cli")),
|
|
251
|
+
"gh": bool(which("gh")),
|
|
252
|
+
}
|
|
253
|
+
credential_destinations = {
|
|
254
|
+
"global_env": str(env.CONFIG_FILE) if env.CONFIG_FILE else None,
|
|
255
|
+
}
|
|
256
|
+
browser_cookies = {
|
|
257
|
+
"mode": config.get("_BROWSER_COOKIE_MODE", "off"),
|
|
258
|
+
"browsers": list(config.get("_BROWSER_COOKIE_BROWSERS") or []),
|
|
259
|
+
"reads_values": False if safe else config.get("_BROWSER_COOKIE_MODE") == "read",
|
|
260
|
+
}
|
|
261
|
+
ignored_project_keys = list(config.get("_IGNORED_PROJECT_CONFIG_KEYS") or [])
|
|
262
|
+
ignored_endpoint_overrides = [
|
|
263
|
+
key for key in ignored_project_keys if key in permission_preflight.ENDPOINT_OVERRIDE_KEYS
|
|
264
|
+
]
|
|
265
|
+
local_writes: list[dict[str, str]] = []
|
|
266
|
+
if config.get("LAST30DAYS_MEMORY_DIR"):
|
|
267
|
+
local_writes.append({"kind": "report", "path": str(config.get("LAST30DAYS_MEMORY_DIR"))})
|
|
268
|
+
diag = {
|
|
269
|
+
"providers": providers_status,
|
|
270
|
+
"local_mode": not reasoning_provider_available,
|
|
271
|
+
"reasoning_provider": (config.get("LAST30DAYS_REASONING_PROVIDER") or "auto").lower(),
|
|
272
|
+
"x_backend": x_status["source"],
|
|
273
|
+
"bird_installed": x_status["bird_installed"],
|
|
274
|
+
"bird_authenticated": x_status["bird_authenticated"],
|
|
275
|
+
"bird_username": x_status["bird_username"],
|
|
276
|
+
"x_pending_browser_auth": x_pending,
|
|
277
|
+
"xquik_available": x_status.get("xquik_available", False),
|
|
278
|
+
"xquik_working": x_status.get("xquik_working"),
|
|
279
|
+
"xquik_status": x_status.get("xquik_status", ""),
|
|
280
|
+
"native_web_backend": native_web_backend,
|
|
281
|
+
"native_search": env.is_native_search(config),
|
|
282
|
+
"has_scrapecreators": bool(config.get("SCRAPECREATORS_API_KEY")),
|
|
283
|
+
"has_github": bool(config.get("GITHUB_TOKEN") or which("gh")),
|
|
284
|
+
"available_sources": available_sources(config, requested_sources, x_pending=x_pending),
|
|
285
|
+
"safe": safe,
|
|
286
|
+
"config_source": config.get("_CONFIG_SOURCE"),
|
|
287
|
+
"ignored_project_config": config.get("_IGNORED_PROJECT_CONFIG"),
|
|
288
|
+
"ignored_project_config_keys": ignored_project_keys,
|
|
289
|
+
"ignored_endpoint_overrides": ignored_endpoint_overrides,
|
|
290
|
+
"browser_cookies": browser_cookies,
|
|
291
|
+
"external_commands": external_commands,
|
|
292
|
+
"credential_destinations": credential_destinations,
|
|
293
|
+
"local_writes": local_writes,
|
|
294
|
+
}
|
|
295
|
+
diag["permission_preflight"] = permission_preflight.build(config, diag)
|
|
296
|
+
return diag
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _inner_max_workers(stream_count: int, *, internal_subrun: bool) -> int:
|
|
300
|
+
"""Worker-pool size for the per-stream fanout inside a single pipeline run.
|
|
301
|
+
|
|
302
|
+
Top-level runs use up to 16 workers. Subruns of ``run_competitor_fanout``
|
|
303
|
+
cap the inner pool to 4 so a six-way competitor fan-out stays below
|
|
304
|
+
roughly 30 worker threads in aggregate instead of ~96.
|
|
305
|
+
"""
|
|
306
|
+
if internal_subrun:
|
|
307
|
+
return max(2, min(4, stream_count or 1))
|
|
308
|
+
return max(4, min(16, stream_count or 1))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def run(
|
|
312
|
+
*,
|
|
313
|
+
topic: str,
|
|
314
|
+
config: dict[str, Any],
|
|
315
|
+
depth: str,
|
|
316
|
+
requested_sources: list[str] | None = None,
|
|
317
|
+
mock: bool = False,
|
|
318
|
+
x_handle: str | None = None,
|
|
319
|
+
x_related: list[str] | None = None,
|
|
320
|
+
web_backend: str = "auto",
|
|
321
|
+
external_plan: dict | None = None,
|
|
322
|
+
subreddits: list[str] | None = None,
|
|
323
|
+
tiktok_hashtags: list[str] | None = None,
|
|
324
|
+
tiktok_creators: list[str] | None = None,
|
|
325
|
+
ig_creators: list[str] | None = None,
|
|
326
|
+
lookback_days: int = 30,
|
|
327
|
+
as_of_date: str | None = None,
|
|
328
|
+
github_user: str | None = None,
|
|
329
|
+
github_repos: list[str] | None = None,
|
|
330
|
+
hiring_signals_mode: bool = False,
|
|
331
|
+
internal_subrun: bool = False,
|
|
332
|
+
) -> schema.Report:
|
|
333
|
+
settings = DEPTH_SETTINGS[depth]
|
|
334
|
+
requested_sources = normalize_requested_sources(requested_sources)
|
|
335
|
+
from_date, to_date = dates.get_date_range(lookback_days, as_of_date=as_of_date)
|
|
336
|
+
|
|
337
|
+
if mock:
|
|
338
|
+
runtime = providers.mock_runtime(config, depth)
|
|
339
|
+
reasoning_provider = None
|
|
340
|
+
available = list(requested_sources or MOCK_AVAILABLE_SOURCES)
|
|
341
|
+
if not requested_sources and not hiring_signals_mode and not _company_topic_likely(topic):
|
|
342
|
+
available = [source for source in available if source != "jobs"]
|
|
343
|
+
else:
|
|
344
|
+
runtime, reasoning_provider = providers.resolve_runtime(config, depth)
|
|
345
|
+
available = available_sources(config, requested_sources)
|
|
346
|
+
if requested_sources:
|
|
347
|
+
available = [source for source in available if source in requested_sources]
|
|
348
|
+
if web_backend == "none":
|
|
349
|
+
available = [s for s in available if s != "grounding"]
|
|
350
|
+
elif web_backend in ("brave", "exa", "serper", "parallel", "keyless") and "grounding" not in available:
|
|
351
|
+
available.append("grounding")
|
|
352
|
+
if (hiring_signals_mode or _company_topic_likely(topic)) and "jobs" not in available:
|
|
353
|
+
available.append("jobs")
|
|
354
|
+
if hiring_signals_mode:
|
|
355
|
+
config = dict(config)
|
|
356
|
+
config["_hiring_signals_mode"] = True
|
|
357
|
+
if not requested_sources:
|
|
358
|
+
available = ["jobs"]
|
|
359
|
+
if not available:
|
|
360
|
+
raise RuntimeError("No sources are available for this run.")
|
|
361
|
+
|
|
362
|
+
planner_requested_sources = requested_sources
|
|
363
|
+
if hiring_signals_mode and not planner_requested_sources:
|
|
364
|
+
planner_requested_sources = ["jobs"]
|
|
365
|
+
|
|
366
|
+
if external_plan:
|
|
367
|
+
# External plan provided (e.g., from Claude Code via --plan flag).
|
|
368
|
+
# Parse it through the same sanitizer to validate structure.
|
|
369
|
+
plan = planner._sanitize_plan(
|
|
370
|
+
external_plan, topic, available, planner_requested_sources, depth,
|
|
371
|
+
)
|
|
372
|
+
plan_source = "external"
|
|
373
|
+
else:
|
|
374
|
+
plan = planner.plan_query(
|
|
375
|
+
topic=topic,
|
|
376
|
+
available_sources=available,
|
|
377
|
+
requested_sources=planner_requested_sources,
|
|
378
|
+
depth=depth,
|
|
379
|
+
provider=None if mock else reasoning_provider,
|
|
380
|
+
model=None if mock else runtime.planner_model,
|
|
381
|
+
context=config.get("_auto_resolve_context", ""),
|
|
382
|
+
internal_subrun=internal_subrun,
|
|
383
|
+
)
|
|
384
|
+
# Source labelling: the fallback path annotates notes with "fallback-plan"
|
|
385
|
+
# or "deterministic-comparison-plan"; anything else came from the LLM.
|
|
386
|
+
if any("fallback" in note or "deterministic" in note for note in (plan.notes or [])):
|
|
387
|
+
plan_source = "deterministic"
|
|
388
|
+
elif not mock and reasoning_provider and runtime.planner_model:
|
|
389
|
+
plan_source = "llm"
|
|
390
|
+
else:
|
|
391
|
+
plan_source = "deterministic"
|
|
392
|
+
|
|
393
|
+
# Safety net: ensure grounding appears in all subqueries even if the planner
|
|
394
|
+
# omits it. This is redundant when the planner includes grounding via
|
|
395
|
+
# SOURCE_CAPABILITIES, but kept as a fallback.
|
|
396
|
+
if web_backend != "none" and "grounding" in available:
|
|
397
|
+
for sq in plan.subqueries:
|
|
398
|
+
if "grounding" not in sq.sources:
|
|
399
|
+
sq.sources.append("grounding")
|
|
400
|
+
_ensure_jobs_in_plan(plan, available, explicit=hiring_signals_mode, topic=topic)
|
|
401
|
+
|
|
402
|
+
# Always-on planner trace. Emits one summary line plus one per subquery
|
|
403
|
+
# so retrieval-breadth failures like the 2026-04-19 Hermes Agent Use Cases
|
|
404
|
+
# disaster are visible without --debug. Stderr only; does not leak into
|
|
405
|
+
# the user-facing stdout synthesis.
|
|
406
|
+
print(
|
|
407
|
+
f"[Planner] Plan: intent={plan.intent}, freshness={plan.freshness_mode}, "
|
|
408
|
+
f"cluster_mode={plan.cluster_mode}, subqueries={len(plan.subqueries)}, "
|
|
409
|
+
f"source={plan_source}",
|
|
410
|
+
file=sys.stderr,
|
|
411
|
+
)
|
|
412
|
+
if plan.subqueries:
|
|
413
|
+
for index, sq in enumerate(plan.subqueries, start=1):
|
|
414
|
+
sources_str = ",".join(sq.sources) if sq.sources else "(none)"
|
|
415
|
+
print(
|
|
416
|
+
f"[Planner] sq{index} label={sq.label} "
|
|
417
|
+
f'search="{sq.search_query}" sources=[{sources_str}]',
|
|
418
|
+
file=sys.stderr,
|
|
419
|
+
)
|
|
420
|
+
else:
|
|
421
|
+
print("[Planner] (no subqueries in plan)", file=sys.stderr)
|
|
422
|
+
|
|
423
|
+
bundle = schema.RetrievalBundle(artifacts={"grounding": []})
|
|
424
|
+
# Expose plan_source to the renderer so render_compact can emit the
|
|
425
|
+
# DEGRADED RUN banner when a named-entity topic was invoked bare
|
|
426
|
+
# (source=deterministic AND no pre-research flags). LAW 7 backstop.
|
|
427
|
+
bundle.artifacts["plan_source"] = plan_source
|
|
428
|
+
# Hiring-signals is deliberately jobs-only with no multi-source --plan, so
|
|
429
|
+
# the LAW 7 degraded-run and Step 0.55 pre-research banners do not apply -
|
|
430
|
+
# they would contradict the documented jobs-scoped flow. Suppress them.
|
|
431
|
+
bundle.artifacts["hiring_signals_mode"] = hiring_signals_mode
|
|
432
|
+
|
|
433
|
+
# Project-mode or person-mode GitHub: run once before the main subquery loop
|
|
434
|
+
_github_custom_done = False
|
|
435
|
+
_github_enriched_repos: set[str] = set()
|
|
436
|
+
|
|
437
|
+
# Project mode takes priority over person mode
|
|
438
|
+
if github_repos and "github" in available:
|
|
439
|
+
try:
|
|
440
|
+
project_items = github.search_github_project(
|
|
441
|
+
github_repos, from_date, to_date,
|
|
442
|
+
depth=depth, token=config.get("GITHUB_TOKEN"),
|
|
443
|
+
)
|
|
444
|
+
if project_items:
|
|
445
|
+
normalized = _normalize_score_dedupe(
|
|
446
|
+
"github", project_items, from_date, to_date,
|
|
447
|
+
freshness_mode=plan.freshness_mode,
|
|
448
|
+
ranking_query=f"What are {', '.join(github_repos)} doing on GitHub?",
|
|
449
|
+
)
|
|
450
|
+
primary_label = plan.subqueries[0].label if plan.subqueries else "primary"
|
|
451
|
+
bundle.add_items(primary_label, "github", normalized)
|
|
452
|
+
_github_custom_done = True
|
|
453
|
+
_github_enriched_repos = {r.lower() for r in github_repos}
|
|
454
|
+
except Exception as exc:
|
|
455
|
+
bundle.errors_by_source["github"] = f"Project-mode failed: {exc}"
|
|
456
|
+
|
|
457
|
+
_github_person_done = False
|
|
458
|
+
if github_user and "github" in available and not _github_custom_done:
|
|
459
|
+
try:
|
|
460
|
+
person_items = github.search_github_person(
|
|
461
|
+
github_user, from_date, to_date,
|
|
462
|
+
depth=depth, token=config.get("GITHUB_TOKEN"),
|
|
463
|
+
)
|
|
464
|
+
if person_items:
|
|
465
|
+
normalized = _normalize_score_dedupe(
|
|
466
|
+
"github", person_items, from_date, to_date,
|
|
467
|
+
freshness_mode=plan.freshness_mode,
|
|
468
|
+
ranking_query=f"What is @{github_user} doing on GitHub?",
|
|
469
|
+
)
|
|
470
|
+
# Use the first subquery's label so RRF can look up the weight
|
|
471
|
+
primary_label = plan.subqueries[0].label if plan.subqueries else "primary"
|
|
472
|
+
bundle.add_items(primary_label, "github", normalized)
|
|
473
|
+
_github_person_done = True
|
|
474
|
+
except Exception as exc:
|
|
475
|
+
bundle.errors_by_source["github"] = f"Person-mode failed: {exc}"
|
|
476
|
+
|
|
477
|
+
# Thread-safe set prevents redundant fetches after a source returns 429
|
|
478
|
+
rate_limited_sources: set[str] = set()
|
|
479
|
+
rate_limit_lock = threading.Lock()
|
|
480
|
+
|
|
481
|
+
futures = {}
|
|
482
|
+
# Per-source fetch budget prevents redundant API calls
|
|
483
|
+
source_fetch_count: dict[str, int] = {}
|
|
484
|
+
stream_count = sum(
|
|
485
|
+
1
|
|
486
|
+
for subquery in plan.subqueries
|
|
487
|
+
for source in subquery.sources
|
|
488
|
+
if source in available
|
|
489
|
+
)
|
|
490
|
+
max_workers = _inner_max_workers(stream_count, internal_subrun=internal_subrun)
|
|
491
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
492
|
+
for subquery in plan.subqueries:
|
|
493
|
+
for source in subquery.sources:
|
|
494
|
+
if source not in available:
|
|
495
|
+
continue
|
|
496
|
+
# Skip GitHub keyword search if person-mode already ran
|
|
497
|
+
if source == "github" and (_github_person_done or _github_custom_done):
|
|
498
|
+
continue
|
|
499
|
+
# Enforce per-source fetch cap
|
|
500
|
+
cap = MAX_SOURCE_FETCHES.get(source)
|
|
501
|
+
if cap is not None:
|
|
502
|
+
current = source_fetch_count.get(source, 0)
|
|
503
|
+
if current >= cap:
|
|
504
|
+
continue
|
|
505
|
+
source_fetch_count[source] = current + 1
|
|
506
|
+
futures[
|
|
507
|
+
executor.submit(
|
|
508
|
+
_retrieve_stream,
|
|
509
|
+
topic=topic,
|
|
510
|
+
subquery=subquery,
|
|
511
|
+
source=source,
|
|
512
|
+
config=config,
|
|
513
|
+
depth=depth,
|
|
514
|
+
date_range=(from_date, to_date),
|
|
515
|
+
runtime=runtime,
|
|
516
|
+
mock=mock,
|
|
517
|
+
rate_limited_sources=rate_limited_sources,
|
|
518
|
+
rate_limit_lock=rate_limit_lock,
|
|
519
|
+
web_backend=web_backend,
|
|
520
|
+
raw_topic=topic,
|
|
521
|
+
subreddits=subreddits,
|
|
522
|
+
tiktok_hashtags=tiktok_hashtags,
|
|
523
|
+
tiktok_creators=tiktok_creators,
|
|
524
|
+
ig_creators=ig_creators,
|
|
525
|
+
)
|
|
526
|
+
] = (subquery, source)
|
|
527
|
+
|
|
528
|
+
for future in as_completed(futures):
|
|
529
|
+
subquery, source = futures[future]
|
|
530
|
+
try:
|
|
531
|
+
raw_items, artifact = future.result()
|
|
532
|
+
except Exception as exc:
|
|
533
|
+
# Share 429 signal so pending futures skip this source
|
|
534
|
+
if _is_rate_limit_error(exc):
|
|
535
|
+
with rate_limit_lock:
|
|
536
|
+
rate_limited_sources.add(source)
|
|
537
|
+
bundle.errors_by_source[source] = str(exc)
|
|
538
|
+
continue
|
|
539
|
+
# Retry once for transient 5xx errors
|
|
540
|
+
if _is_transient_error(exc):
|
|
541
|
+
time.sleep(3)
|
|
542
|
+
try:
|
|
543
|
+
raw_items, artifact = _retrieve_stream(
|
|
544
|
+
topic=topic, subquery=subquery, source=source,
|
|
545
|
+
config=config, depth=depth, date_range=(from_date, to_date),
|
|
546
|
+
runtime=runtime, mock=mock,
|
|
547
|
+
rate_limited_sources=rate_limited_sources,
|
|
548
|
+
rate_limit_lock=rate_limit_lock,
|
|
549
|
+
web_backend=web_backend,
|
|
550
|
+
raw_topic=topic,
|
|
551
|
+
subreddits=subreddits,
|
|
552
|
+
tiktok_hashtags=tiktok_hashtags,
|
|
553
|
+
tiktok_creators=tiktok_creators,
|
|
554
|
+
ig_creators=ig_creators,
|
|
555
|
+
)
|
|
556
|
+
except Exception as retry_exc:
|
|
557
|
+
bundle.errors_by_source[source] = f"{exc} (retried once, still failed: {retry_exc})"
|
|
558
|
+
continue
|
|
559
|
+
else:
|
|
560
|
+
bundle.errors_by_source[source] = str(exc)
|
|
561
|
+
continue
|
|
562
|
+
normalized = _normalize_score_dedupe(
|
|
563
|
+
source, raw_items, from_date, to_date,
|
|
564
|
+
freshness_mode=plan.freshness_mode,
|
|
565
|
+
ranking_query=subquery.ranking_query,
|
|
566
|
+
)
|
|
567
|
+
# Jobs is exempt from per_stream_limit: a careers board is a complete
|
|
568
|
+
# snapshot of open roles, and truncating it to the default 12 drops
|
|
569
|
+
# strategic postings (the whole point of hiring-signals coverage).
|
|
570
|
+
if source != "jobs":
|
|
571
|
+
normalized = normalized[: settings["per_stream_limit"]]
|
|
572
|
+
bundle.add_items(subquery.label, source, normalized)
|
|
573
|
+
if artifact:
|
|
574
|
+
bundle.artifacts.setdefault("grounding", []).append(artifact)
|
|
575
|
+
|
|
576
|
+
# Phase 2: supplemental entity-based searches
|
|
577
|
+
_run_supplemental_searches(
|
|
578
|
+
topic=topic,
|
|
579
|
+
bundle=bundle,
|
|
580
|
+
plan=plan,
|
|
581
|
+
config=config,
|
|
582
|
+
depth=depth,
|
|
583
|
+
date_range=(from_date, to_date),
|
|
584
|
+
runtime=runtime,
|
|
585
|
+
mock=mock,
|
|
586
|
+
rate_limited_sources=rate_limited_sources,
|
|
587
|
+
rate_limit_lock=rate_limit_lock,
|
|
588
|
+
x_handle=x_handle,
|
|
589
|
+
x_related=x_related,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# Phase 2b: retry thin sources with simplified query
|
|
593
|
+
# Note: _github_skip_sources tells the retry to not re-run GitHub keyword search
|
|
594
|
+
# when project-mode or person-mode already provided authoritative data.
|
|
595
|
+
_github_skip_retry = {"github"} if (_github_person_done or _github_custom_done) else set()
|
|
596
|
+
_retry_thin_sources(
|
|
597
|
+
topic=topic,
|
|
598
|
+
bundle=bundle,
|
|
599
|
+
plan=plan,
|
|
600
|
+
config=config,
|
|
601
|
+
depth=depth,
|
|
602
|
+
date_range=(from_date, to_date),
|
|
603
|
+
runtime=runtime,
|
|
604
|
+
mock=mock,
|
|
605
|
+
rate_limited_sources=rate_limited_sources,
|
|
606
|
+
rate_limit_lock=rate_limit_lock,
|
|
607
|
+
settings=settings,
|
|
608
|
+
web_backend=web_backend,
|
|
609
|
+
skip_sources=_github_skip_retry,
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# Reclassify partial failures as DEGRADED instead of silently dropping them.
|
|
613
|
+
# A source that 429'd on one subquery but succeeded on another is not a hard
|
|
614
|
+
# failure, but it is not healthy either: it likely returned fewer results
|
|
615
|
+
# than it should have. Move it out of errors_by_source (so it isn't reported
|
|
616
|
+
# as "failed") and into degraded_by_source (so it survives into warnings),
|
|
617
|
+
# rather than deleting the signal outright as the engine used to.
|
|
618
|
+
degraded_by_source: dict[str, str] = {}
|
|
619
|
+
for source in list(bundle.errors_by_source):
|
|
620
|
+
if bundle.items_by_source.get(source):
|
|
621
|
+
degraded_by_source[source] = bundle.errors_by_source[source]
|
|
622
|
+
del bundle.errors_by_source[source]
|
|
623
|
+
|
|
624
|
+
hiring_summary = _apply_hiring_signal_gate(
|
|
625
|
+
bundle,
|
|
626
|
+
explicit=hiring_signals_mode,
|
|
627
|
+
topic=topic,
|
|
628
|
+
)
|
|
629
|
+
if hiring_summary:
|
|
630
|
+
bundle.artifacts["hiring_signals"] = hiring_summary
|
|
631
|
+
|
|
632
|
+
items_by_source = _finalize_items_by_source(
|
|
633
|
+
bundle.items_by_source, topic=topic, config=config, depth=depth, mock=mock,
|
|
634
|
+
)
|
|
635
|
+
candidates = weighted_rrf(bundle.items_by_source_and_query, plan, pool_limit=settings["pool_limit"])
|
|
636
|
+
# Normalized set of handles this run resolved for the topic. A candidate
|
|
637
|
+
# authored by one of these is first-party and is exempted from the
|
|
638
|
+
# entity-miss demotion in rerank (a post never repeats its own author's
|
|
639
|
+
# name, so the body-text grounding check would otherwise zero out the
|
|
640
|
+
# subject's own highest-signal posts).
|
|
641
|
+
resolved_handles = {
|
|
642
|
+
h.lstrip("@").strip().lower()
|
|
643
|
+
for h in ([x_handle, github_user, *(x_related or [])])
|
|
644
|
+
if h and h.strip()
|
|
645
|
+
}
|
|
646
|
+
ranked_candidates = rerank.rerank_candidates(
|
|
647
|
+
topic=topic,
|
|
648
|
+
plan=plan,
|
|
649
|
+
candidates=candidates,
|
|
650
|
+
provider=None if mock else reasoning_provider,
|
|
651
|
+
model=None if mock else runtime.rerank_model,
|
|
652
|
+
shortlist_size=settings["rerank_limit"],
|
|
653
|
+
resolved_handles=resolved_handles,
|
|
654
|
+
)
|
|
655
|
+
rerank.score_fun(
|
|
656
|
+
topic=topic,
|
|
657
|
+
candidates=ranked_candidates,
|
|
658
|
+
provider=None if mock else reasoning_provider,
|
|
659
|
+
model=None if mock else runtime.rerank_model,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
# Phase 3: post-rerank GitHub star enrichment
|
|
663
|
+
if "github" in available and not mock:
|
|
664
|
+
github.enrich_candidates_with_stars(
|
|
665
|
+
ranked_candidates,
|
|
666
|
+
token=config.get("GITHUB_TOKEN"),
|
|
667
|
+
already_enriched=_github_enriched_repos,
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
clusters = cluster_candidates(ranked_candidates, plan)
|
|
671
|
+
warnings = _warnings(items_by_source, ranked_candidates, bundle.errors_by_source, degraded_by_source)
|
|
672
|
+
|
|
673
|
+
return schema.Report(
|
|
674
|
+
topic=topic,
|
|
675
|
+
range_from=from_date,
|
|
676
|
+
range_to=to_date,
|
|
677
|
+
generated_at=datetime.now(timezone.utc).isoformat(),
|
|
678
|
+
provider_runtime=runtime,
|
|
679
|
+
query_plan=plan,
|
|
680
|
+
clusters=clusters,
|
|
681
|
+
ranked_candidates=ranked_candidates,
|
|
682
|
+
items_by_source=items_by_source,
|
|
683
|
+
errors_by_source=bundle.errors_by_source,
|
|
684
|
+
warnings=warnings,
|
|
685
|
+
artifacts=bundle.artifacts,
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _normalize_score_dedupe(
|
|
690
|
+
source: str,
|
|
691
|
+
raw_items: list[dict],
|
|
692
|
+
from_date: str,
|
|
693
|
+
to_date: str,
|
|
694
|
+
freshness_mode: str,
|
|
695
|
+
ranking_query: str,
|
|
696
|
+
) -> list[schema.SourceItem]:
|
|
697
|
+
"""Normalize, annotate, prune, dedupe, and extract snippets for a batch of raw items."""
|
|
698
|
+
normalized = normalize.normalize_source_items(
|
|
699
|
+
source, raw_items, from_date, to_date,
|
|
700
|
+
freshness_mode=freshness_mode,
|
|
701
|
+
)
|
|
702
|
+
prepared_query = relevance.PreparedQuery(ranking_query)
|
|
703
|
+
lookback_window_days = (
|
|
704
|
+
datetime.strptime(to_date, "%Y-%m-%d").date()
|
|
705
|
+
- datetime.strptime(from_date, "%Y-%m-%d").date()
|
|
706
|
+
).days
|
|
707
|
+
normalized = signals.annotate_stream(
|
|
708
|
+
normalized,
|
|
709
|
+
prepared_query,
|
|
710
|
+
freshness_mode,
|
|
711
|
+
reference_date=to_date,
|
|
712
|
+
max_days=lookback_window_days,
|
|
713
|
+
)
|
|
714
|
+
if source != "jobs":
|
|
715
|
+
normalized = signals.prune_low_relevance(normalized)
|
|
716
|
+
normalized = dedupe.dedupe_items(normalized)
|
|
717
|
+
for item in normalized:
|
|
718
|
+
item.snippet = snippet.extract_best_snippet(item, prepared_query)
|
|
719
|
+
return normalized
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _finalize_items_by_source(
|
|
723
|
+
items_by_source_raw: dict[str, list[schema.SourceItem]],
|
|
724
|
+
topic: str = "",
|
|
725
|
+
config: dict | None = None,
|
|
726
|
+
depth: str = "default",
|
|
727
|
+
mock: bool = False,
|
|
728
|
+
) -> dict[str, list[schema.SourceItem]]:
|
|
729
|
+
finalized = {}
|
|
730
|
+
for source, items in items_by_source_raw.items():
|
|
731
|
+
items = sorted(items, key=lambda item: item.local_rank_score or 0.0, reverse=True)
|
|
732
|
+
items = dedupe.dedupe_items(items)
|
|
733
|
+
if source == "youtube" and items and not mock:
|
|
734
|
+
# Same budget-at-the-survivors principle as the digg branch
|
|
735
|
+
# below: retrieval-time transcripts go to each search's
|
|
736
|
+
# top-by-views candidates, while final selection ranks by
|
|
737
|
+
# relevance. Backfill survivors that arrived without one so the
|
|
738
|
+
# transcript budget lands on videos the brief actually shows
|
|
739
|
+
# (#542).
|
|
740
|
+
sc_token = (
|
|
741
|
+
config.get("SCRAPECREATORS_API_KEY")
|
|
742
|
+
if config and env.is_youtube_sc_available(config) else None
|
|
743
|
+
)
|
|
744
|
+
youtube_yt.backfill_transcripts(
|
|
745
|
+
items, topic=topic, depth=depth, token=sc_token,
|
|
746
|
+
)
|
|
747
|
+
# Post-merge topic-relevance filter for Polymarket: comparison queries
|
|
748
|
+
# fan out into per-entity subqueries ("Hermes", "OpenClaw") whose topic
|
|
749
|
+
# is too narrow for Gamma API to filter meaningfully. Re-validating the
|
|
750
|
+
# merged list against the full original topic drops off-topic markets
|
|
751
|
+
# (e.g., WTI crude oil, Elon tweet counts) before footer emission.
|
|
752
|
+
if source == "polymarket" and topic:
|
|
753
|
+
items = polymarket.filter_items_against_topic(topic, items)
|
|
754
|
+
# --polymarket-keywords (via config): additional keyword filter
|
|
755
|
+
# for ambiguous single-token topics (e.g., "Warriors" → nba,gsw).
|
|
756
|
+
keywords = config.get("_polymarket_keywords") if isinstance(config, dict) else None
|
|
757
|
+
if keywords:
|
|
758
|
+
items = polymarket.filter_items_against_keywords(items, keywords)
|
|
759
|
+
if source == "digg" and items:
|
|
760
|
+
# Pull top-ranked X posts only for the survivors that will appear
|
|
761
|
+
# in the brief. Spending the enrichment budget here (rather than
|
|
762
|
+
# at retrieval time) keeps the inline 'via Digg' quotes
|
|
763
|
+
# paired with the clusters dedupe actually kept.
|
|
764
|
+
digg.enrich_source_items(items, top_k=3)
|
|
765
|
+
finalized[source] = items
|
|
766
|
+
return finalized
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _apply_hiring_signal_gate(
|
|
770
|
+
bundle: schema.RetrievalBundle,
|
|
771
|
+
*,
|
|
772
|
+
explicit: bool,
|
|
773
|
+
topic: str,
|
|
774
|
+
) -> dict[str, Any] | None:
|
|
775
|
+
jobs_items = bundle.items_by_source.get("jobs") or []
|
|
776
|
+
if not jobs_items:
|
|
777
|
+
if explicit:
|
|
778
|
+
return hiring_signals.analyze([], explicit=True, topic=topic)
|
|
779
|
+
return None
|
|
780
|
+
|
|
781
|
+
summary = hiring_signals.analyze(jobs_items, explicit=explicit, topic=topic)
|
|
782
|
+
if not explicit and not summary.get("include"):
|
|
783
|
+
bundle.items_by_source.pop("jobs", None)
|
|
784
|
+
for key in list(bundle.items_by_source_and_query):
|
|
785
|
+
if key[1] == "jobs":
|
|
786
|
+
del bundle.items_by_source_and_query[key]
|
|
787
|
+
return summary
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _ensure_jobs_in_plan(
|
|
791
|
+
plan: schema.QueryPlan,
|
|
792
|
+
available: list[str],
|
|
793
|
+
*,
|
|
794
|
+
explicit: bool,
|
|
795
|
+
topic: str,
|
|
796
|
+
) -> None:
|
|
797
|
+
if "jobs" not in available:
|
|
798
|
+
return
|
|
799
|
+
if not (explicit or _company_topic_likely(topic)):
|
|
800
|
+
return
|
|
801
|
+
if "jobs" not in plan.source_weights:
|
|
802
|
+
plan.source_weights["jobs"] = 1.0
|
|
803
|
+
for subquery in plan.subqueries:
|
|
804
|
+
if "jobs" not in subquery.sources:
|
|
805
|
+
subquery.sources.append("jobs")
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _company_topic_likely(topic: str) -> bool:
|
|
809
|
+
text = topic.strip()
|
|
810
|
+
if not text:
|
|
811
|
+
return False
|
|
812
|
+
lower = text.lower()
|
|
813
|
+
if "?" in text or len(text.split()) > 4:
|
|
814
|
+
return False
|
|
815
|
+
generic = {
|
|
816
|
+
"how", "what", "why", "best", "top", "tutorial", "guide", "prompts",
|
|
817
|
+
"news", "latest", "ideas", "examples",
|
|
818
|
+
}
|
|
819
|
+
if any(word in generic for word in lower.split()):
|
|
820
|
+
return False
|
|
821
|
+
known_single_word_companies = {
|
|
822
|
+
"apple", "uber", "google", "microsoft", "amazon", "meta", "netflix",
|
|
823
|
+
"openai", "anthropic", "qualtrics", "stripe", "brex",
|
|
824
|
+
}
|
|
825
|
+
if " vs " in lower or " versus " in lower:
|
|
826
|
+
parts = re.split(r"\s+(?:vs|versus)\s+", text, maxsplit=1, flags=re.IGNORECASE)
|
|
827
|
+
if len(parts) != 2:
|
|
828
|
+
return False
|
|
829
|
+
return _comparison_side_company_like(parts[0], known_single_word_companies) or _comparison_side_company_like(
|
|
830
|
+
parts[1], known_single_word_companies
|
|
831
|
+
)
|
|
832
|
+
return bool(text[:1].isupper() or lower in known_single_word_companies)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def _comparison_side_company_like(side: str, known_companies: set[str]) -> bool:
|
|
836
|
+
token = re.sub(r"[^\w.+#-]", "", side.strip().split()[0] if side.strip() else "")
|
|
837
|
+
if not token:
|
|
838
|
+
return False
|
|
839
|
+
lower = token.lower()
|
|
840
|
+
common_tech_terms = {
|
|
841
|
+
"python", "ruby", "javascript", "typescript", "java", "go", "golang",
|
|
842
|
+
"rust", "php", "swift", "kotlin", "scala", "clojure", "elixir",
|
|
843
|
+
"react", "vue", "angular", "svelte", "node", "django", "rails",
|
|
844
|
+
"postgres", "mysql", "redis", "kubernetes", "docker",
|
|
845
|
+
}
|
|
846
|
+
if lower in common_tech_terms:
|
|
847
|
+
return False
|
|
848
|
+
return bool(token[:1].isupper() or lower in known_companies)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _warnings(
|
|
852
|
+
items_by_source: dict[str, list[schema.SourceItem]],
|
|
853
|
+
candidates: list[schema.Candidate],
|
|
854
|
+
errors_by_source: dict[str, str],
|
|
855
|
+
degraded_by_source: dict[str, str] | None = None,
|
|
856
|
+
) -> list[str]:
|
|
857
|
+
warnings: list[str] = []
|
|
858
|
+
if not candidates:
|
|
859
|
+
warnings.append("No candidates survived retrieval and ranking.")
|
|
860
|
+
if len(candidates) < 5:
|
|
861
|
+
warnings.append("Evidence is thin for this topic.")
|
|
862
|
+
top_sources = {
|
|
863
|
+
source
|
|
864
|
+
for candidate in candidates[:5]
|
|
865
|
+
for source in schema.candidate_sources(candidate)
|
|
866
|
+
}
|
|
867
|
+
if len(top_sources) <= 1 and len(candidates) >= 3:
|
|
868
|
+
warnings.append("Top evidence is highly concentrated in one source.")
|
|
869
|
+
if errors_by_source:
|
|
870
|
+
warnings.append(f"Some sources failed: {', '.join(sorted(errors_by_source))}")
|
|
871
|
+
if degraded_by_source:
|
|
872
|
+
# Partial failures: the source returned some items but errored/timed out
|
|
873
|
+
# on at least one subquery, so its coverage is likely incomplete. Kept
|
|
874
|
+
# distinct from hard failures so the signal is not silently dropped.
|
|
875
|
+
warnings.append(
|
|
876
|
+
f"Some sources returned partial results (degraded): {', '.join(sorted(degraded_by_source))}"
|
|
877
|
+
)
|
|
878
|
+
if not items_by_source:
|
|
879
|
+
warnings.append("No source returned usable items.")
|
|
880
|
+
return warnings
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _is_rate_limit_error(exc: Exception) -> bool:
|
|
884
|
+
"""Detect 429 rate-limit errors by status code or message text."""
|
|
885
|
+
if hasattr(exc, "status_code") and getattr(exc, "status_code", None) == 429:
|
|
886
|
+
return True
|
|
887
|
+
return "429" in str(exc)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _is_transient_error(exc: Exception) -> bool:
|
|
891
|
+
"""Detect 5xx server errors that are worth retrying."""
|
|
892
|
+
status = getattr(exc, "status_code", None)
|
|
893
|
+
if isinstance(status, int) and 500 <= status < 600:
|
|
894
|
+
return True
|
|
895
|
+
msg = str(exc)
|
|
896
|
+
return any(code in msg for code in ("500", "502", "503", "504"))
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _run_supplemental_searches(
|
|
900
|
+
*,
|
|
901
|
+
topic: str,
|
|
902
|
+
bundle: schema.RetrievalBundle,
|
|
903
|
+
plan: schema.QueryPlan,
|
|
904
|
+
config: dict[str, Any],
|
|
905
|
+
depth: str,
|
|
906
|
+
date_range: tuple[str, str],
|
|
907
|
+
runtime: schema.ProviderRuntime,
|
|
908
|
+
mock: bool,
|
|
909
|
+
rate_limited_sources: set[str],
|
|
910
|
+
rate_limit_lock: threading.Lock,
|
|
911
|
+
x_handle: str | None = None,
|
|
912
|
+
x_related: list[str] | None = None,
|
|
913
|
+
) -> None:
|
|
914
|
+
"""Phase 2: extract entities from Phase 1 results, run targeted supplemental searches."""
|
|
915
|
+
if depth == "quick" or mock:
|
|
916
|
+
return
|
|
917
|
+
|
|
918
|
+
from_date, to_date = date_range
|
|
919
|
+
|
|
920
|
+
# Convert SourceItems to dicts for entity_extract. All X items (whatever
|
|
921
|
+
# backend fetched them — bird, xai, xurl, xquik) land under the single "x"
|
|
922
|
+
# slug, so this reads the whole X corpus.
|
|
923
|
+
x_dicts = [
|
|
924
|
+
{"author_handle": item.author or "", "text": item.body or ""}
|
|
925
|
+
for item in bundle.items_by_source.get("x", [])
|
|
926
|
+
]
|
|
927
|
+
reddit_dicts = [
|
|
928
|
+
{
|
|
929
|
+
"subreddit": item.container or "",
|
|
930
|
+
"comment_insights": item.metadata.get("comment_insights", []),
|
|
931
|
+
"top_comments": [
|
|
932
|
+
{"excerpt": c.get("excerpt", c.get("text", ""))}
|
|
933
|
+
for c in (item.metadata.get("top_comments") or [])
|
|
934
|
+
if isinstance(c, dict)
|
|
935
|
+
],
|
|
936
|
+
}
|
|
937
|
+
for item in bundle.items_by_source.get("reddit", [])
|
|
938
|
+
]
|
|
939
|
+
|
|
940
|
+
if not x_dicts and not reddit_dicts and not x_handle and not x_related:
|
|
941
|
+
return
|
|
942
|
+
|
|
943
|
+
entities = entity_extract.extract_entities(
|
|
944
|
+
reddit_dicts, x_dicts,
|
|
945
|
+
max_handles=3, max_subreddits=3,
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
handles = entities.get("x_handles", [])
|
|
949
|
+
|
|
950
|
+
# Add explicit --x-handle if provided
|
|
951
|
+
if x_handle:
|
|
952
|
+
handle_clean = x_handle.lstrip("@").lower()
|
|
953
|
+
if handle_clean not in [h.lower() for h in handles]:
|
|
954
|
+
handles.insert(0, handle_clean)
|
|
955
|
+
|
|
956
|
+
# Collect related handles (searched separately with lower weight)
|
|
957
|
+
related_handles = []
|
|
958
|
+
if x_related:
|
|
959
|
+
primary_lower = x_handle.lstrip("@").lower() if x_handle else ""
|
|
960
|
+
for rh in x_related:
|
|
961
|
+
rh_clean = rh.lstrip("@").lower().strip()
|
|
962
|
+
if rh_clean and rh_clean != primary_lower and rh_clean not in [h.lower() for h in handles]:
|
|
963
|
+
related_handles.append(rh_clean)
|
|
964
|
+
|
|
965
|
+
if not handles and not related_handles:
|
|
966
|
+
return
|
|
967
|
+
|
|
968
|
+
# Pick the X handle-search backend: the first handle-capable backend in the
|
|
969
|
+
# chain (bird or xquik). These supplemental from:/mentions lanes are
|
|
970
|
+
# complementary to the topic search, so when the topic primary can't run
|
|
971
|
+
# them (xai/xurl have no handle-lane implementation) but a capable backend
|
|
972
|
+
# is available, use it rather than skipping Phase 2. bird scrapes X GraphQL
|
|
973
|
+
# with the user's browser cookies; xquik runs the same lanes over its REST
|
|
974
|
+
# API. All items land under the single "x" slug.
|
|
975
|
+
x_slug = "x"
|
|
976
|
+
chain = env.x_backend_chain(config)
|
|
977
|
+
# Trust an explicit runtime backend as the head of the chain.
|
|
978
|
+
pinned = runtime.x_search_backend
|
|
979
|
+
if pinned:
|
|
980
|
+
chain = [pinned] + [b for b in chain if b != pinned]
|
|
981
|
+
primary = next((b for b in chain if b in ("bird", "xquik")), None)
|
|
982
|
+
|
|
983
|
+
if primary == "bird":
|
|
984
|
+
def _from_lane(hs: list, count: int) -> list:
|
|
985
|
+
return bird_x.search_handles(hs, topic, from_date, count_per=count)
|
|
986
|
+
|
|
987
|
+
def _about_lane(hs: list, count: int) -> list:
|
|
988
|
+
return bird_x.search_mentions(hs, from_date, count_per=count)
|
|
989
|
+
elif primary == "xquik":
|
|
990
|
+
xquik_token = env.get_xquik_token(config)
|
|
991
|
+
|
|
992
|
+
def _from_lane(hs: list, count: int) -> list:
|
|
993
|
+
return xquik.search_handles(hs, topic, from_date, to_date, count_per=count, token=xquik_token)
|
|
994
|
+
|
|
995
|
+
def _about_lane(hs: list, count: int) -> list:
|
|
996
|
+
return xquik.search_mentions(hs, from_date, to_date, topic=topic, count_per=count, token=xquik_token)
|
|
997
|
+
else:
|
|
998
|
+
return # primary X backend has no handle-lane support (xai/xurl) or none configured
|
|
999
|
+
|
|
1000
|
+
# Skip if the X source is rate-limited.
|
|
1001
|
+
if x_slug in rate_limited_sources:
|
|
1002
|
+
return
|
|
1003
|
+
|
|
1004
|
+
# Collect existing URLs for deduplication
|
|
1005
|
+
existing_urls = {
|
|
1006
|
+
item.url
|
|
1007
|
+
for items in bundle.items_by_source.values()
|
|
1008
|
+
for item in items
|
|
1009
|
+
if item.url
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
ranking_query = plan.subqueries[0].ranking_query if plan.subqueries else topic
|
|
1013
|
+
primary_label = plan.subqueries[0].label if plan.subqueries else "primary"
|
|
1014
|
+
|
|
1015
|
+
# Search primary handles (full weight): FROM lane (their own tweets) +
|
|
1016
|
+
# ABOUT lane (tweets mentioning them). Both engagement-weighted and deduped
|
|
1017
|
+
# by URL at normalize time.
|
|
1018
|
+
if handles:
|
|
1019
|
+
# Independent try/except per lane so a failure in one does not discard
|
|
1020
|
+
# the other's already-computed results.
|
|
1021
|
+
from_items: list = []
|
|
1022
|
+
about_items: list = []
|
|
1023
|
+
try:
|
|
1024
|
+
from_items = _from_lane(handles, FROM_LANE_COUNT_PER)
|
|
1025
|
+
except Exception as exc:
|
|
1026
|
+
print(f"[Pipeline] Phase 2 FROM-lane search failed: {exc}", file=sys.stderr)
|
|
1027
|
+
if not bundle.items_by_source.get(x_slug):
|
|
1028
|
+
bundle.errors_by_source[x_slug] = f"Phase 2 FROM-lane: {exc}"
|
|
1029
|
+
try:
|
|
1030
|
+
about_items = _about_lane(handles, MENTION_LANE_COUNT_PER)
|
|
1031
|
+
except Exception as exc:
|
|
1032
|
+
print(f"[Pipeline] Phase 2 ABOUT-lane search failed: {exc}", file=sys.stderr)
|
|
1033
|
+
raw_items = from_items + about_items
|
|
1034
|
+
|
|
1035
|
+
if raw_items:
|
|
1036
|
+
normalized = _normalize_score_dedupe(
|
|
1037
|
+
x_slug, raw_items, from_date, to_date,
|
|
1038
|
+
freshness_mode=plan.freshness_mode,
|
|
1039
|
+
ranking_query=ranking_query,
|
|
1040
|
+
)
|
|
1041
|
+
# Deduplicate against Phase 1 URLs
|
|
1042
|
+
normalized = [item for item in normalized if item.url not in existing_urls]
|
|
1043
|
+
if normalized:
|
|
1044
|
+
bundle.add_items(primary_label, x_slug, normalized)
|
|
1045
|
+
# Update existing URLs for related-handle dedup
|
|
1046
|
+
for item in normalized:
|
|
1047
|
+
if item.url:
|
|
1048
|
+
existing_urls.add(item.url)
|
|
1049
|
+
|
|
1050
|
+
# Search related handles with lower weight (0.3)
|
|
1051
|
+
if related_handles:
|
|
1052
|
+
try:
|
|
1053
|
+
raw_items = _from_lane(related_handles, RELATED_HANDLE_COUNT_PER)
|
|
1054
|
+
except Exception as exc:
|
|
1055
|
+
print(f"[Pipeline] Phase 2 related handle search failed: {exc}", file=sys.stderr)
|
|
1056
|
+
raw_items = []
|
|
1057
|
+
|
|
1058
|
+
if raw_items:
|
|
1059
|
+
normalized = _normalize_score_dedupe(
|
|
1060
|
+
x_slug, raw_items, from_date, to_date,
|
|
1061
|
+
freshness_mode=plan.freshness_mode,
|
|
1062
|
+
ranking_query=ranking_query,
|
|
1063
|
+
)
|
|
1064
|
+
# Deduplicate against all existing URLs (Phase 1 + primary handles)
|
|
1065
|
+
normalized = [item for item in normalized if item.url not in existing_urls]
|
|
1066
|
+
if normalized:
|
|
1067
|
+
# Use a separate subquery label with lower weight so RRF
|
|
1068
|
+
# scores related-handle results below primary results.
|
|
1069
|
+
bundle.add_items("supplemental-related", x_slug, normalized)
|
|
1070
|
+
# Register the supplemental-related label in the plan for fusion
|
|
1071
|
+
if not any(sq.label == "supplemental-related" for sq in plan.subqueries):
|
|
1072
|
+
plan.subqueries.append(
|
|
1073
|
+
schema.SubQuery(
|
|
1074
|
+
label="supplemental-related",
|
|
1075
|
+
search_query=", ".join(related_handles),
|
|
1076
|
+
ranking_query=ranking_query,
|
|
1077
|
+
sources=[x_slug],
|
|
1078
|
+
weight=0.3,
|
|
1079
|
+
)
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
def _retry_thin_sources(
|
|
1084
|
+
*,
|
|
1085
|
+
topic: str,
|
|
1086
|
+
bundle: schema.RetrievalBundle,
|
|
1087
|
+
plan: schema.QueryPlan,
|
|
1088
|
+
config: dict[str, Any],
|
|
1089
|
+
depth: str,
|
|
1090
|
+
date_range: tuple[str, str],
|
|
1091
|
+
runtime: schema.ProviderRuntime,
|
|
1092
|
+
mock: bool,
|
|
1093
|
+
rate_limited_sources: set[str],
|
|
1094
|
+
rate_limit_lock: threading.Lock,
|
|
1095
|
+
settings: dict[str, Any],
|
|
1096
|
+
web_backend: str = "auto",
|
|
1097
|
+
skip_sources: set[str] | None = None,
|
|
1098
|
+
) -> None:
|
|
1099
|
+
"""Retry sources with thin results using simplified core subject query."""
|
|
1100
|
+
if depth == "quick":
|
|
1101
|
+
return
|
|
1102
|
+
|
|
1103
|
+
planned_sources: list[str] = []
|
|
1104
|
+
for subquery in plan.subqueries:
|
|
1105
|
+
for source in subquery.sources:
|
|
1106
|
+
if source not in planned_sources:
|
|
1107
|
+
planned_sources.append(source)
|
|
1108
|
+
_skip = skip_sources or set()
|
|
1109
|
+
thin_sources = [
|
|
1110
|
+
source
|
|
1111
|
+
for source in planned_sources
|
|
1112
|
+
if len(bundle.items_by_source.get(source, [])) < 3
|
|
1113
|
+
and source not in bundle.errors_by_source
|
|
1114
|
+
and source not in _skip
|
|
1115
|
+
]
|
|
1116
|
+
|
|
1117
|
+
if not thin_sources:
|
|
1118
|
+
return
|
|
1119
|
+
|
|
1120
|
+
core = query.extract_core_subject(topic, max_words=3)
|
|
1121
|
+
if not core:
|
|
1122
|
+
return
|
|
1123
|
+
# Note: we intentionally do NOT skip when core == topic. For short topics
|
|
1124
|
+
# like "Kanye West", the 3-word core IS the topic — but the planner may
|
|
1125
|
+
# have sent a different (worse) query to the source. Retrying with the
|
|
1126
|
+
# raw core subject is still valuable.
|
|
1127
|
+
|
|
1128
|
+
from_date, to_date = date_range
|
|
1129
|
+
|
|
1130
|
+
# Create a retry subquery with the simplified core subject
|
|
1131
|
+
retry_subquery = schema.SubQuery(
|
|
1132
|
+
label="retry",
|
|
1133
|
+
search_query=core,
|
|
1134
|
+
ranking_query=f"What recent evidence from the last 30 days matters for {core}?",
|
|
1135
|
+
sources=thin_sources,
|
|
1136
|
+
weight=0.3,
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
def _retry_one_source(source: str) -> tuple[str, list[schema.SourceItem]]:
|
|
1140
|
+
raw_items, _artifact = _retrieve_stream(
|
|
1141
|
+
topic=topic,
|
|
1142
|
+
subquery=retry_subquery,
|
|
1143
|
+
source=source,
|
|
1144
|
+
config=config,
|
|
1145
|
+
depth=depth,
|
|
1146
|
+
date_range=date_range,
|
|
1147
|
+
runtime=runtime,
|
|
1148
|
+
mock=mock,
|
|
1149
|
+
rate_limited_sources=rate_limited_sources,
|
|
1150
|
+
rate_limit_lock=rate_limit_lock,
|
|
1151
|
+
web_backend=web_backend,
|
|
1152
|
+
raw_topic=topic,
|
|
1153
|
+
)
|
|
1154
|
+
normalized = _normalize_score_dedupe(
|
|
1155
|
+
source,
|
|
1156
|
+
raw_items,
|
|
1157
|
+
from_date,
|
|
1158
|
+
to_date,
|
|
1159
|
+
freshness_mode=plan.freshness_mode,
|
|
1160
|
+
ranking_query=retry_subquery.ranking_query,
|
|
1161
|
+
)
|
|
1162
|
+
if source == "jobs":
|
|
1163
|
+
return source, normalized
|
|
1164
|
+
return source, normalized[:settings["per_stream_limit"]]
|
|
1165
|
+
|
|
1166
|
+
retryable = [s for s in thin_sources if s not in rate_limited_sources]
|
|
1167
|
+
|
|
1168
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
1169
|
+
with ThreadPoolExecutor(max_workers=min(4, len(retryable) or 1)) as executor:
|
|
1170
|
+
futures = {executor.submit(_retry_one_source, s): s for s in retryable}
|
|
1171
|
+
for future in as_completed(futures):
|
|
1172
|
+
source = futures[future]
|
|
1173
|
+
try:
|
|
1174
|
+
source, normalized = future.result()
|
|
1175
|
+
existing_urls = {item.url for item in bundle.items_by_source.get(source, []) if item.url}
|
|
1176
|
+
new_items = [item for item in normalized if item.url not in existing_urls]
|
|
1177
|
+
|
|
1178
|
+
if new_items:
|
|
1179
|
+
bundle.items_by_source.setdefault(source, []).extend(new_items)
|
|
1180
|
+
primary_label = plan.subqueries[0].label if plan.subqueries else "primary"
|
|
1181
|
+
bundle.items_by_source_and_query.setdefault((primary_label, source), []).extend(new_items)
|
|
1182
|
+
except Exception as exc:
|
|
1183
|
+
print(f"[Pipeline] Retry failed for {source}: {type(exc).__name__}: {exc}", file=sys.stderr)
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def _fetch_x_backend(backend, subquery, from_date, to_date, depth, config):
|
|
1187
|
+
"""Fetch X items from a single backend. Returns (items, error_str).
|
|
1188
|
+
|
|
1189
|
+
Backends are tried in priority order by the caller (env.x_backend_chain);
|
|
1190
|
+
a non-empty error_str signals a hard failure (auth/payment/etc.) so the
|
|
1191
|
+
caller can fail over to the next backend or surface the error honestly.
|
|
1192
|
+
"""
|
|
1193
|
+
query = subquery.search_query
|
|
1194
|
+
if backend == "bird":
|
|
1195
|
+
result = bird_x.search_x(query, from_date, to_date, depth=depth)
|
|
1196
|
+
items = bird_x.parse_bird_response(result, query=query)
|
|
1197
|
+
elif backend == "xai":
|
|
1198
|
+
model = config.get("LAST30DAYS_X_MODEL") or config.get("XAI_MODEL_PIN") or providers.XAI_DEFAULT
|
|
1199
|
+
result = xai_x.search_x(config["XAI_API_KEY"], model, query, from_date, to_date, depth=depth)
|
|
1200
|
+
items = xai_x.parse_x_response(result)
|
|
1201
|
+
elif backend == "xurl":
|
|
1202
|
+
result = xurl_x.search_x(query, depth=depth)
|
|
1203
|
+
items = xurl_x.parse_x_response(result, topic=query)
|
|
1204
|
+
elif backend == "xquik":
|
|
1205
|
+
result = xquik.search_xquik(query, from_date, to_date, depth=depth, token=env.get_xquik_token(config))
|
|
1206
|
+
items = xquik.parse_xquik_response(result)
|
|
1207
|
+
else:
|
|
1208
|
+
return [], f"unknown X backend: {backend}"
|
|
1209
|
+
err = result.get("error") if isinstance(result, dict) else ""
|
|
1210
|
+
return items, (err or "")
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def _reddit_post_key(item: dict) -> str:
|
|
1214
|
+
"""Stable per-thread dedupe key (base36 post id from the url/permalink)."""
|
|
1215
|
+
url = item.get("url") or item.get("permalink") or ""
|
|
1216
|
+
m = re.search(r"/comments/([A-Za-z0-9]+)", url)
|
|
1217
|
+
return m.group(1) if m else url
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def _merge_reddit_items(free: list[dict], sc: list[dict]) -> list[dict]:
|
|
1221
|
+
"""Merge free + ScrapeCreators Reddit items, free first, deduped by post id.
|
|
1222
|
+
|
|
1223
|
+
Used when the thinness-floor trigger backfills a thin free run with SC, so a
|
|
1224
|
+
thread present in both is never double-listed.
|
|
1225
|
+
"""
|
|
1226
|
+
merged = list(free)
|
|
1227
|
+
seen = {_reddit_post_key(it) for it in free}
|
|
1228
|
+
for it in sc:
|
|
1229
|
+
key = _reddit_post_key(it)
|
|
1230
|
+
if key and key not in seen:
|
|
1231
|
+
seen.add(key)
|
|
1232
|
+
merged.append(it)
|
|
1233
|
+
return merged
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
def _retrieve_stream(
|
|
1237
|
+
*,
|
|
1238
|
+
topic: str,
|
|
1239
|
+
subquery: schema.SubQuery,
|
|
1240
|
+
source: str,
|
|
1241
|
+
config: dict[str, Any],
|
|
1242
|
+
depth: str,
|
|
1243
|
+
date_range: tuple[str, str],
|
|
1244
|
+
runtime: schema.ProviderRuntime,
|
|
1245
|
+
mock: bool,
|
|
1246
|
+
rate_limited_sources: set[str] | None = None,
|
|
1247
|
+
rate_limit_lock: threading.Lock | None = None,
|
|
1248
|
+
web_backend: str = "auto",
|
|
1249
|
+
raw_topic: str = "",
|
|
1250
|
+
subreddits: list[str] | None = None,
|
|
1251
|
+
tiktok_hashtags: list[str] | None = None,
|
|
1252
|
+
tiktok_creators: list[str] | None = None,
|
|
1253
|
+
ig_creators: list[str] | None = None,
|
|
1254
|
+
) -> tuple[list[dict], dict]:
|
|
1255
|
+
# Early exit if source was rate-limited by a sibling future
|
|
1256
|
+
if rate_limited_sources is not None and source in rate_limited_sources:
|
|
1257
|
+
return [], {}
|
|
1258
|
+
from_date, to_date = date_range
|
|
1259
|
+
if mock:
|
|
1260
|
+
return _mock_stream_results(source, subquery)
|
|
1261
|
+
if source == "grounding":
|
|
1262
|
+
return grounding.web_search(
|
|
1263
|
+
subquery.search_query, date_range, config, backend=web_backend)
|
|
1264
|
+
if source == "jobs":
|
|
1265
|
+
return jobs.search_jobs(
|
|
1266
|
+
raw_topic or topic or subquery.search_query,
|
|
1267
|
+
date_range,
|
|
1268
|
+
config,
|
|
1269
|
+
depth=depth,
|
|
1270
|
+
web_backend=web_backend,
|
|
1271
|
+
explicit=bool(config.get("_hiring_signals_mode")),
|
|
1272
|
+
)
|
|
1273
|
+
if source == "reddit":
|
|
1274
|
+
# Use raw_topic so expand_reddit_queries() generates diverse variants
|
|
1275
|
+
# from the original user topic, not the planner's narrowed search_query.
|
|
1276
|
+
reddit_query = raw_topic or subquery.search_query
|
|
1277
|
+
dedicated_subreddits = config.get("_dedicated_subreddits") or None
|
|
1278
|
+
has_sc_key = bool(config.get("SCRAPECREATORS_API_KEY"))
|
|
1279
|
+
sc_first = (
|
|
1280
|
+
has_sc_key
|
|
1281
|
+
and (config.get("LAST30DAYS_REDDIT_BACKEND") or "").lower()
|
|
1282
|
+
== "scrapecreators"
|
|
1283
|
+
)
|
|
1284
|
+
if sc_first:
|
|
1285
|
+
# LAST30DAYS_REDDIT_BACKEND=scrapecreators: SC primary, public fallback
|
|
1286
|
+
try:
|
|
1287
|
+
result = reddit.search_and_enrich(
|
|
1288
|
+
reddit_query, from_date, to_date, depth=depth,
|
|
1289
|
+
token=config.get("SCRAPECREATORS_API_KEY"),
|
|
1290
|
+
subreddits=subreddits,
|
|
1291
|
+
)
|
|
1292
|
+
items = reddit.parse_reddit_response(result)
|
|
1293
|
+
if items:
|
|
1294
|
+
return items, {}
|
|
1295
|
+
sys.stderr.write(
|
|
1296
|
+
"[Reddit] ScrapeCreators primary returned no items, "
|
|
1297
|
+
"using public fallback\n"
|
|
1298
|
+
)
|
|
1299
|
+
except Exception as exc:
|
|
1300
|
+
sys.stderr.write(
|
|
1301
|
+
f"[Reddit] ScrapeCreators primary failed "
|
|
1302
|
+
f"({type(exc).__name__}: {exc}), using public fallback\n"
|
|
1303
|
+
)
|
|
1304
|
+
try:
|
|
1305
|
+
public_results = reddit_public.search_reddit_public(
|
|
1306
|
+
reddit_query, from_date, to_date, depth=depth,
|
|
1307
|
+
subreddits=subreddits,
|
|
1308
|
+
)
|
|
1309
|
+
if public_results:
|
|
1310
|
+
return public_results, {}
|
|
1311
|
+
sys.stderr.write(
|
|
1312
|
+
"[Reddit] Public fallback returned no items after "
|
|
1313
|
+
"ScrapeCreators primary miss\n"
|
|
1314
|
+
)
|
|
1315
|
+
except Exception as exc:
|
|
1316
|
+
sys.stderr.write(
|
|
1317
|
+
f"[Reddit] Public fallback also failed "
|
|
1318
|
+
f"({type(exc).__name__}: {exc})\n"
|
|
1319
|
+
)
|
|
1320
|
+
return [], {}
|
|
1321
|
+
|
|
1322
|
+
# Default: public Reddit first (free). ScrapeCreators backfills when the
|
|
1323
|
+
# free path is empty OR returns fewer than the configured thinness floor
|
|
1324
|
+
# (LAST30DAYS_REDDIT_SC_MIN_ITEMS, default 0 = empty-only — today's
|
|
1325
|
+
# behavior, no extra credit spend unless the user opts in).
|
|
1326
|
+
try:
|
|
1327
|
+
min_items = int(config.get("LAST30DAYS_REDDIT_SC_MIN_ITEMS") or 0)
|
|
1328
|
+
except (TypeError, ValueError):
|
|
1329
|
+
min_items = 0
|
|
1330
|
+
public_results: list[dict] = []
|
|
1331
|
+
try:
|
|
1332
|
+
public_results = reddit_public.search_reddit_public(
|
|
1333
|
+
reddit_query, from_date, to_date, depth=depth,
|
|
1334
|
+
subreddits=subreddits, dedicated_subreddits=dedicated_subreddits,
|
|
1335
|
+
) or []
|
|
1336
|
+
except Exception as exc:
|
|
1337
|
+
sys.stderr.write(
|
|
1338
|
+
f"[Reddit] Public search failed ({type(exc).__name__}: {exc})"
|
|
1339
|
+
)
|
|
1340
|
+
if not has_sc_key:
|
|
1341
|
+
sys.stderr.write("\n")
|
|
1342
|
+
return [], {}
|
|
1343
|
+
sys.stderr.write(", using ScrapeCreators backup\n")
|
|
1344
|
+
# Enough free results, or no key to backfill with -> done. max(min_items,
|
|
1345
|
+
# 1) keeps the default (min_items=0) as empty-only AND treats exactly
|
|
1346
|
+
# `min_items` results as acceptable (no backfill) for min_items > 0.
|
|
1347
|
+
if len(public_results) >= max(min_items, 1) or not has_sc_key:
|
|
1348
|
+
return public_results, {}
|
|
1349
|
+
if public_results:
|
|
1350
|
+
sys.stderr.write(
|
|
1351
|
+
f"[Reddit] Free path returned {len(public_results)} "
|
|
1352
|
+
f"(below the {min_items}-item floor); backfilling with ScrapeCreators\n"
|
|
1353
|
+
)
|
|
1354
|
+
try:
|
|
1355
|
+
result = reddit.search_and_enrich(
|
|
1356
|
+
reddit_query, from_date, to_date, depth=depth,
|
|
1357
|
+
token=config.get("SCRAPECREATORS_API_KEY"),
|
|
1358
|
+
subreddits=subreddits,
|
|
1359
|
+
)
|
|
1360
|
+
sc_items = reddit.parse_reddit_response(result)
|
|
1361
|
+
except Exception as exc:
|
|
1362
|
+
sys.stderr.write(
|
|
1363
|
+
f"[Reddit] ScrapeCreators backup also failed "
|
|
1364
|
+
f"({type(exc).__name__}: {exc})\n"
|
|
1365
|
+
)
|
|
1366
|
+
return public_results, {}
|
|
1367
|
+
return _merge_reddit_items(public_results, sc_items), {}
|
|
1368
|
+
if source == "x":
|
|
1369
|
+
# One X source, an ordered chain of interchangeable backends. Try the
|
|
1370
|
+
# primary; fall through to the next only if it returns nothing or errors.
|
|
1371
|
+
chain = env.x_backend_chain(config)
|
|
1372
|
+
# Trust an explicit runtime backend as the primary (already resolved as
|
|
1373
|
+
# available), keeping the rest of the chain as failover backups.
|
|
1374
|
+
pinned = runtime.x_search_backend
|
|
1375
|
+
if pinned:
|
|
1376
|
+
chain = [pinned] + [b for b in chain if b != pinned]
|
|
1377
|
+
if not chain:
|
|
1378
|
+
raise RuntimeError("No X backend is available.")
|
|
1379
|
+
last_error = ""
|
|
1380
|
+
for i, backend in enumerate(chain):
|
|
1381
|
+
items, err = _fetch_x_backend(backend, subquery, from_date, to_date, depth, config)
|
|
1382
|
+
if items:
|
|
1383
|
+
if i > 0:
|
|
1384
|
+
print(f"[X] primary backend(s) returned nothing; used fallback '{backend}'", file=sys.stderr)
|
|
1385
|
+
return items, {}
|
|
1386
|
+
if err:
|
|
1387
|
+
last_error = f"{backend}: {err}"
|
|
1388
|
+
print(f"[X] backend '{backend}' failed ({err}); trying next", file=sys.stderr)
|
|
1389
|
+
if last_error:
|
|
1390
|
+
raise RuntimeError(f"All X backends failed — {last_error}")
|
|
1391
|
+
return [], {}
|
|
1392
|
+
if source == "youtube":
|
|
1393
|
+
# Use raw_topic so expand_youtube_queries() generates diverse variants
|
|
1394
|
+
# from the original user topic, not the planner's narrowed search_query.
|
|
1395
|
+
yt_query = raw_topic or subquery.search_query
|
|
1396
|
+
result = None
|
|
1397
|
+
# ScrapeCreators key (when present) is the default-on backup tier: it
|
|
1398
|
+
# powers the per-video transcript fallback, the SC search fallback, and
|
|
1399
|
+
# comment enrichment. None when no key, which keeps everything keyless.
|
|
1400
|
+
sc_token = (
|
|
1401
|
+
config.get("SCRAPECREATORS_API_KEY", "")
|
|
1402
|
+
if env.is_youtube_sc_available(config) else None
|
|
1403
|
+
)
|
|
1404
|
+
# Try yt-dlp first; the SC transcript fallback covers per-video failures.
|
|
1405
|
+
if which("yt-dlp"):
|
|
1406
|
+
try:
|
|
1407
|
+
result = youtube_yt.search_and_transcribe(
|
|
1408
|
+
yt_query, from_date, to_date, depth=depth, token=sc_token,
|
|
1409
|
+
)
|
|
1410
|
+
except Exception:
|
|
1411
|
+
result = None
|
|
1412
|
+
# Fall back to SC YouTube search if yt-dlp failed or isn't installed.
|
|
1413
|
+
if (result is None or not result.get("items")) and sc_token:
|
|
1414
|
+
result = youtube_yt.search_youtube_sc(
|
|
1415
|
+
yt_query, from_date, to_date, depth=depth, token=sc_token,
|
|
1416
|
+
)
|
|
1417
|
+
if result is None:
|
|
1418
|
+
result = {"items": []}
|
|
1419
|
+
# Enrich top videos with comments (default-on when a key is present).
|
|
1420
|
+
items = youtube_yt.parse_youtube_response(result)
|
|
1421
|
+
if items and env.is_youtube_comments_available(config):
|
|
1422
|
+
youtube_yt.enrich_with_comments(
|
|
1423
|
+
items, token=config.get("SCRAPECREATORS_API_KEY", ""),
|
|
1424
|
+
)
|
|
1425
|
+
return items, {}
|
|
1426
|
+
if source == "tiktok":
|
|
1427
|
+
# Use raw_topic so expand_tiktok_queries() generates diverse variants
|
|
1428
|
+
# from the original user topic, not the planner's narrowed search_query.
|
|
1429
|
+
tiktok_query = raw_topic or subquery.search_query
|
|
1430
|
+
result = tiktok.search_and_enrich(
|
|
1431
|
+
tiktok_query,
|
|
1432
|
+
from_date,
|
|
1433
|
+
to_date,
|
|
1434
|
+
depth=depth,
|
|
1435
|
+
token=env.get_tiktok_token(config),
|
|
1436
|
+
hashtags=tiktok_hashtags,
|
|
1437
|
+
creators=tiktok_creators,
|
|
1438
|
+
)
|
|
1439
|
+
items = tiktok.parse_tiktok_response(result)
|
|
1440
|
+
if items and env.is_tiktok_comments_available(config):
|
|
1441
|
+
sc_token = config.get("SCRAPECREATORS_API_KEY", "")
|
|
1442
|
+
tiktok.enrich_with_comments(items, token=sc_token)
|
|
1443
|
+
return items, {}
|
|
1444
|
+
if source == "instagram":
|
|
1445
|
+
# Use raw_topic so expand_instagram_queries() generates diverse variants
|
|
1446
|
+
# from the original user topic, not the planner's narrowed search_query.
|
|
1447
|
+
ig_query = raw_topic or subquery.search_query
|
|
1448
|
+
result = instagram.search_and_enrich(
|
|
1449
|
+
ig_query,
|
|
1450
|
+
from_date,
|
|
1451
|
+
to_date,
|
|
1452
|
+
depth=depth,
|
|
1453
|
+
token=env.get_instagram_token(config),
|
|
1454
|
+
ig_creators=ig_creators,
|
|
1455
|
+
)
|
|
1456
|
+
return instagram.parse_instagram_response(result), {}
|
|
1457
|
+
if source == "linkedin":
|
|
1458
|
+
token = config.get("SCRAPECREATORS_API_KEY", "")
|
|
1459
|
+
result = linkedin.search_linkedin(
|
|
1460
|
+
subquery.search_query,
|
|
1461
|
+
from_date,
|
|
1462
|
+
to_date,
|
|
1463
|
+
depth=depth,
|
|
1464
|
+
token=token,
|
|
1465
|
+
)
|
|
1466
|
+
items = linkedin.parse_linkedin_response(
|
|
1467
|
+
result, from_date=from_date, to_date=to_date
|
|
1468
|
+
)
|
|
1469
|
+
# Articles never appear in post search — surface them (high signal)
|
|
1470
|
+
# via a bounded profile-enrichment lane on person topics.
|
|
1471
|
+
items += linkedin.enrich_articles(
|
|
1472
|
+
items, raw_topic or topic, token, from_date=from_date, to_date=to_date
|
|
1473
|
+
)
|
|
1474
|
+
return items, {}
|
|
1475
|
+
if source == "hackernews":
|
|
1476
|
+
result = hackernews.search_hackernews(subquery.search_query, from_date, to_date, depth=depth)
|
|
1477
|
+
return hackernews.parse_hackernews_response(result, query=subquery.search_query), {}
|
|
1478
|
+
if source == "digg":
|
|
1479
|
+
result = digg.search_digg(subquery.search_query, from_date, to_date, depth=depth)
|
|
1480
|
+
items = digg.parse_digg_response(result, query=subquery.search_query)
|
|
1481
|
+
# Enrichment with attached X posts is deferred to
|
|
1482
|
+
# _finalize_items_by_source so it runs on the items that actually
|
|
1483
|
+
# survive dedupe rather than on top-K of the raw fanout.
|
|
1484
|
+
return items, {}
|
|
1485
|
+
if source == "arxiv":
|
|
1486
|
+
result = arxiv.search_arxiv(subquery.search_query, from_date, to_date, depth=depth)
|
|
1487
|
+
# Relevance keys off the stable research topic, not the per-subquery
|
|
1488
|
+
# search_query, so off-topic narrowing does not let weak matches through.
|
|
1489
|
+
relevance_topic = raw_topic or topic or subquery.search_query
|
|
1490
|
+
return arxiv.parse_arxiv_response(result, query=relevance_topic), {}
|
|
1491
|
+
if source == "techmeme":
|
|
1492
|
+
result = techmeme.search_techmeme(subquery.search_query, from_date, to_date, depth=depth)
|
|
1493
|
+
relevance_topic = raw_topic or topic or subquery.search_query
|
|
1494
|
+
return techmeme.parse_techmeme_response(result, query=relevance_topic), {}
|
|
1495
|
+
if source == "trustpilot":
|
|
1496
|
+
# Brand-shape gate keys off the stable research topic, not the narrowed
|
|
1497
|
+
# per-subquery search_query, so the company is detected consistently.
|
|
1498
|
+
relevance_topic = raw_topic or topic or subquery.search_query
|
|
1499
|
+
result = trustpilot.search_trustpilot(
|
|
1500
|
+
relevance_topic, from_date, to_date, depth=depth, config=config
|
|
1501
|
+
)
|
|
1502
|
+
return trustpilot.parse_trustpilot_response(result, query=relevance_topic), {}
|
|
1503
|
+
if source == "bluesky":
|
|
1504
|
+
result = bluesky.search_bluesky(subquery.search_query, from_date, to_date, depth=depth, config=config)
|
|
1505
|
+
return bluesky.parse_bluesky_response(result), {}
|
|
1506
|
+
if source == "threads":
|
|
1507
|
+
result = threads.search_threads(
|
|
1508
|
+
subquery.search_query, from_date, to_date,
|
|
1509
|
+
depth=depth,
|
|
1510
|
+
token=config.get("SCRAPECREATORS_API_KEY"),
|
|
1511
|
+
)
|
|
1512
|
+
return threads.parse_threads_response(result), {}
|
|
1513
|
+
if source == "truthsocial":
|
|
1514
|
+
result = truthsocial.search_truthsocial(subquery.search_query, from_date, to_date, depth=depth, config=config)
|
|
1515
|
+
return truthsocial.parse_truthsocial_response(result), {}
|
|
1516
|
+
if source == "polymarket":
|
|
1517
|
+
result = polymarket.search_polymarket(subquery.search_query, from_date, to_date, depth=depth)
|
|
1518
|
+
# Relevance filtering keys off the stable original research topic, not the
|
|
1519
|
+
# per-subquery search_query (which narrows differently on each fanout pass
|
|
1520
|
+
# and would let off-topic markets through on broad subqueries while dropping
|
|
1521
|
+
# everything on narrow ones).
|
|
1522
|
+
relevance_topic = raw_topic or topic or subquery.search_query
|
|
1523
|
+
return polymarket.parse_polymarket_response(result, topic=relevance_topic), {}
|
|
1524
|
+
if source == "github":
|
|
1525
|
+
# Resolve once at the pipeline boundary so search and enrich
|
|
1526
|
+
# share the result; otherwise each call would re-run the env
|
|
1527
|
+
# lookup and gh-CLI subprocess fallback (up to 5s timeout each).
|
|
1528
|
+
token = github.resolve_token(config.get("GITHUB_TOKEN"))
|
|
1529
|
+
response = github.search_github(subquery.search_query, from_date, to_date, depth=depth, token=token)
|
|
1530
|
+
items = github.parse_github_response(response)
|
|
1531
|
+
# Note: an unauth rate-limit (response["error"]) is expected on the
|
|
1532
|
+
# tokenless anon tier and returns empty here rather than raising — github
|
|
1533
|
+
# is now always eligible, so raising would spam "github failed" on every
|
|
1534
|
+
# tokenless run. The condition is logged in github.search_github.
|
|
1535
|
+
items = github.enrich_with_comments(items, depth=depth, token=token)
|
|
1536
|
+
return items, {}
|
|
1537
|
+
if source == "pinterest":
|
|
1538
|
+
result = pinterest.search_pinterest(
|
|
1539
|
+
subquery.search_query, from_date, to_date,
|
|
1540
|
+
depth=depth,
|
|
1541
|
+
token=env.get_pinterest_token(config),
|
|
1542
|
+
)
|
|
1543
|
+
return pinterest.parse_pinterest_response(result), {}
|
|
1544
|
+
if source == "xiaohongshu":
|
|
1545
|
+
return xiaohongshu_api.search_feeds(
|
|
1546
|
+
subquery.search_query,
|
|
1547
|
+
from_date,
|
|
1548
|
+
to_date,
|
|
1549
|
+
env.get_xiaohongshu_api_base(config),
|
|
1550
|
+
depth=depth,
|
|
1551
|
+
), {}
|
|
1552
|
+
if source == "perplexity":
|
|
1553
|
+
return perplexity.search(subquery.search_query, date_range, config, deep=config.get("_deep_research", False))
|
|
1554
|
+
raise RuntimeError(f"Unsupported source: {source}")
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
def _google_key(config: dict[str, Any]) -> str | None:
|
|
1558
|
+
return config.get("GOOGLE_API_KEY") or config.get("GEMINI_API_KEY") or config.get("GOOGLE_GENAI_API_KEY")
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
|
|
1562
|
+
|
|
1563
|
+
def _mock_stream_results(source: str, subquery: schema.SubQuery) -> tuple[list[dict], dict]:
|
|
1564
|
+
payloads = {
|
|
1565
|
+
"reddit": [
|
|
1566
|
+
{
|
|
1567
|
+
"id": "R1",
|
|
1568
|
+
"title": f"{subquery.search_query} discussion thread",
|
|
1569
|
+
"url": "https://reddit.com/r/example/comments/1",
|
|
1570
|
+
"subreddit": "example",
|
|
1571
|
+
"date": dates.get_date_range(5)[0],
|
|
1572
|
+
"engagement": {"score": 120, "num_comments": 48, "upvote_ratio": 0.91},
|
|
1573
|
+
"selftext": f"Community discussion about {subquery.search_query}.",
|
|
1574
|
+
"top_comments": [{"excerpt": "Strong firsthand feedback from users."}],
|
|
1575
|
+
"relevance": 0.82,
|
|
1576
|
+
"why_relevant": "Mock Reddit result",
|
|
1577
|
+
}
|
|
1578
|
+
],
|
|
1579
|
+
"x": [
|
|
1580
|
+
{
|
|
1581
|
+
"id": "X1",
|
|
1582
|
+
"text": f"People on X are discussing {subquery.search_query} right now.",
|
|
1583
|
+
"url": "https://x.com/example/status/1",
|
|
1584
|
+
"author_handle": "example",
|
|
1585
|
+
"date": dates.get_date_range(2)[0],
|
|
1586
|
+
"engagement": {"likes": 200, "reposts": 35, "replies": 18, "quotes": 4},
|
|
1587
|
+
"relevance": 0.79,
|
|
1588
|
+
"why_relevant": "Mock X result",
|
|
1589
|
+
}
|
|
1590
|
+
],
|
|
1591
|
+
"grounding": [
|
|
1592
|
+
{
|
|
1593
|
+
"id": "WB1",
|
|
1594
|
+
"title": f"{subquery.search_query} article",
|
|
1595
|
+
"url": "https://example.com/article",
|
|
1596
|
+
"source_domain": "example.com",
|
|
1597
|
+
"snippet": f"Recent web reporting about {subquery.search_query}.",
|
|
1598
|
+
"date": dates.get_date_range(7)[0],
|
|
1599
|
+
"relevance": 0.88,
|
|
1600
|
+
"why_relevant": "Brave web search",
|
|
1601
|
+
}
|
|
1602
|
+
],
|
|
1603
|
+
"digg": [
|
|
1604
|
+
{
|
|
1605
|
+
"id": "mock1abc",
|
|
1606
|
+
"title": f"Digg cluster about {subquery.search_query}",
|
|
1607
|
+
"url": "https://di.gg/ai/mock1abc",
|
|
1608
|
+
"tldr": f"Curated cluster summarizing recent {subquery.search_query} discussion across the AI 1000.",
|
|
1609
|
+
"author": "",
|
|
1610
|
+
"date": dates.get_date_range(3)[0],
|
|
1611
|
+
"engagement": {"postCount": 8, "uniqueAuthors": 5, "rank": 2, "rank_score": 49.0},
|
|
1612
|
+
"first_post_age": "3d",
|
|
1613
|
+
"posts": [
|
|
1614
|
+
{
|
|
1615
|
+
"username": "exampledev",
|
|
1616
|
+
"display_name": "Example Dev",
|
|
1617
|
+
"category": "Engineer",
|
|
1618
|
+
"rank": 142,
|
|
1619
|
+
"body": f"Quote from the AI 1000 about {subquery.search_query}.",
|
|
1620
|
+
"post_type": "tweet",
|
|
1621
|
+
"x_url": "https://x.com/exampledev/status/1",
|
|
1622
|
+
"posted_at": dates.get_date_range(3)[0],
|
|
1623
|
+
},
|
|
1624
|
+
],
|
|
1625
|
+
"relevance": 0.84,
|
|
1626
|
+
"why_relevant": "Mock Digg cluster",
|
|
1627
|
+
},
|
|
1628
|
+
{
|
|
1629
|
+
"id": "mock2def",
|
|
1630
|
+
"title": f"Second Digg cluster on {subquery.search_query}",
|
|
1631
|
+
"url": "https://di.gg/ai/mock2def",
|
|
1632
|
+
"tldr": f"Another angle on {subquery.search_query}.",
|
|
1633
|
+
"author": "",
|
|
1634
|
+
"date": dates.get_date_range(8)[0],
|
|
1635
|
+
"engagement": {"postCount": 3, "uniqueAuthors": 2, "rank": 18, "rank_score": 33.0},
|
|
1636
|
+
"first_post_age": "8d",
|
|
1637
|
+
"posts": [],
|
|
1638
|
+
"relevance": 0.71,
|
|
1639
|
+
"why_relevant": "Mock Digg cluster",
|
|
1640
|
+
},
|
|
1641
|
+
],
|
|
1642
|
+
"arxiv": [
|
|
1643
|
+
{
|
|
1644
|
+
"id": "http://arxiv.org/abs/2606.00001v1",
|
|
1645
|
+
"title": f"A Survey of {subquery.search_query}",
|
|
1646
|
+
"url": "https://arxiv.org/abs/2606.00001v1",
|
|
1647
|
+
"summary": f"We present a comprehensive study of {subquery.search_query} and its recent advances.",
|
|
1648
|
+
"author": "Ada Lovelace et al.",
|
|
1649
|
+
"authors": ["Ada Lovelace", "Alan Turing"],
|
|
1650
|
+
"date": dates.get_date_range(20)[0],
|
|
1651
|
+
"engagement": {},
|
|
1652
|
+
"relevance": 0.86,
|
|
1653
|
+
"why_relevant": "Mock arXiv paper",
|
|
1654
|
+
},
|
|
1655
|
+
],
|
|
1656
|
+
"techmeme": [
|
|
1657
|
+
{
|
|
1658
|
+
"id": "https://www.techmeme.com/260627/p1",
|
|
1659
|
+
"title": f"Major development in {subquery.search_query} reshapes the industry",
|
|
1660
|
+
"url": "https://www.techmeme.com/260627/p1",
|
|
1661
|
+
"source_name": "techcrunch.com",
|
|
1662
|
+
"date": dates.get_date_range(1)[0],
|
|
1663
|
+
"engagement": {},
|
|
1664
|
+
"relevance": 0.83,
|
|
1665
|
+
"why_relevant": "Mock Techmeme headline",
|
|
1666
|
+
},
|
|
1667
|
+
],
|
|
1668
|
+
"trustpilot": [
|
|
1669
|
+
{
|
|
1670
|
+
"id": "example.com",
|
|
1671
|
+
"title": f"{subquery.search_query}: TrustScore 3.4",
|
|
1672
|
+
"url": "https://www.trustpilot.com/review/example.com",
|
|
1673
|
+
"summary": f"Across recent reviews, customers were split on {subquery.search_query}: some praised support, others cited delays.",
|
|
1674
|
+
"name": subquery.search_query,
|
|
1675
|
+
"trustScore": 3.4,
|
|
1676
|
+
"reviewCount": 128,
|
|
1677
|
+
"date": dates.get_date_range(1)[0],
|
|
1678
|
+
"engagement": {"reviews": 128, "trustScore": 3.4},
|
|
1679
|
+
"relevance": 0.8,
|
|
1680
|
+
"why_relevant": "Mock Trustpilot sentiment",
|
|
1681
|
+
},
|
|
1682
|
+
],
|
|
1683
|
+
"jobs": [
|
|
1684
|
+
{
|
|
1685
|
+
"id": "J1",
|
|
1686
|
+
"title": "Founding Enterprise Solutions Engineer",
|
|
1687
|
+
"url": "https://boards.greenhouse.io/example/jobs/1",
|
|
1688
|
+
"description": (
|
|
1689
|
+
f"Work with enterprise customers on SSO, SOC 2, security, "
|
|
1690
|
+
f"and procurement workflows for {subquery.search_query}."
|
|
1691
|
+
),
|
|
1692
|
+
"department": "Sales",
|
|
1693
|
+
"location": "San Francisco, CA",
|
|
1694
|
+
"date": dates.get_date_range(4)[0],
|
|
1695
|
+
"provider": "mock",
|
|
1696
|
+
"relevance": 0.8,
|
|
1697
|
+
"why_relevant": "Mock public job posting",
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
"id": "J2",
|
|
1701
|
+
"title": "Security Platform Engineer",
|
|
1702
|
+
"url": "https://boards.greenhouse.io/example/jobs/2",
|
|
1703
|
+
"description": "Build enterprise security, audit, and admin workflows.",
|
|
1704
|
+
"department": "Engineering",
|
|
1705
|
+
"location": "Remote",
|
|
1706
|
+
"date": dates.get_date_range(6)[0],
|
|
1707
|
+
"provider": "mock",
|
|
1708
|
+
"relevance": 0.78,
|
|
1709
|
+
"why_relevant": "Mock public job posting",
|
|
1710
|
+
},
|
|
1711
|
+
],
|
|
1712
|
+
}
|
|
1713
|
+
if source == "grounding":
|
|
1714
|
+
return payloads.get(source, []), {
|
|
1715
|
+
"label": subquery.label,
|
|
1716
|
+
"mock": True,
|
|
1717
|
+
"webSearchQueries": [subquery.search_query],
|
|
1718
|
+
"resultCount": 1,
|
|
1719
|
+
}
|
|
1720
|
+
return payloads.get(source, []), {}
|