Improve signal quality (Exa news category) and remove mock fallback
Exa harvester: - EXA_CATEGORY=news (default) restricts to news articles, excluding evergreen careers/marketing landing pages that were producing irrelevant signals. - Event-focused query (acquisition/earnings/launch/leadership), dropped hiring/career terms that pulled recruiting pages. - Domain scoping now OFF by default (broad-web news about the account beats first-party careers pages); add EXA_EXCLUDE_DOMAINS and loop --domain-filter. LLM layer: - Remove the mock reasoner entirely (no provider => loop refuses to run). - On HTTP 429, WAIT (LLM_RATELIMIT_WAIT_SECONDS, default 900s; honors Retry-After) and retry up to LLM_RATELIMIT_MAX_RETRIES (24) instead of degrading to mock output. - If the LLM is ultimately unavailable for an account, skip it and leave its signals unconsumed (retried next run) — never fabricate briefs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -28,12 +28,13 @@ def _load_dotenv() -> None:
|
||||
_load_dotenv()
|
||||
|
||||
# --- LLM ---
|
||||
# Provider abstraction: openai | gemini | anthropic | mock. If LLM_PROVIDER is
|
||||
# unset we auto-detect from whichever key is present (OpenAI-compatible first),
|
||||
# falling back to a deterministic mock reasoner so the loop still runs offline.
|
||||
# Provider abstraction: openai | gemini | anthropic. If LLM_PROVIDER is unset we
|
||||
# auto-detect from whichever key is present (OpenAI-compatible first). There is NO
|
||||
# mock fallback: the loop refuses to run without a configured provider, and on
|
||||
# rate limits it WAITS and retries rather than emitting low-quality output.
|
||||
#
|
||||
# "openai" is any OpenAI-compatible /chat/completions endpoint — OpenAI, OpenRouter,
|
||||
# Together, Groq, vLLM, Ollama, LM Studio, etc. — configured by three env vars:
|
||||
# Together, Groq, NVIDIA, vLLM, Ollama, LM Studio, etc. — configured by:
|
||||
# OPENAI_BASE_URL (e.g. https://api.openai.com/v1)
|
||||
# OPENAI_API_KEY
|
||||
# OPENAI_MODEL (e.g. gpt-4o-mini)
|
||||
@@ -50,8 +51,13 @@ OPENAI_TEMPERATURE = float(os.environ.get("OPENAI_TEMPERATURE", "0.5"))
|
||||
OPENAI_FREQUENCY_PENALTY = float(os.environ.get("OPENAI_FREQUENCY_PENALTY", "0.4"))
|
||||
OPENAI_PRESENCE_PENALTY = float(os.environ.get("OPENAI_PRESENCE_PENALTY", "0.0"))
|
||||
# Retries when a model returns degenerate/unparseable content (stochastic — a
|
||||
# retry usually succeeds). Distinct from the network/429 backoff.
|
||||
# retry usually succeeds).
|
||||
LLM_CONTENT_RETRIES = int(os.environ.get("LLM_CONTENT_RETRIES", "3"))
|
||||
# On HTTP 429 (rate limit) we WAIT this long and retry (no mock fallback). Honors
|
||||
# a Retry-After response header when present. Lets the loop grind slowly but
|
||||
# correctly through a tight rate limit instead of degrading quality.
|
||||
LLM_RATELIMIT_WAIT_SECONDS = int(os.environ.get("LLM_RATELIMIT_WAIT_SECONDS", "900"))
|
||||
LLM_RATELIMIT_MAX_RETRIES = int(os.environ.get("LLM_RATELIMIT_MAX_RETRIES", "24"))
|
||||
|
||||
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "").strip()
|
||||
LLM_MODEL = os.environ.get("LLM_MODEL", "claude-sonnet-4-6")
|
||||
@@ -68,24 +74,22 @@ if not _provider:
|
||||
elif ANTHROPIC_API_KEY:
|
||||
_provider = "anthropic"
|
||||
else:
|
||||
_provider = "mock"
|
||||
# A forced USE_MOCK_LLM=1, or selecting a provider whose key is missing, => mock.
|
||||
if os.environ.get("USE_MOCK_LLM", "").strip() == "1":
|
||||
_provider = "mock"
|
||||
elif _provider == "openai" and not OPENAI_API_KEY:
|
||||
_provider = "mock"
|
||||
_provider = "none"
|
||||
# Selecting a provider whose key is missing => no usable provider.
|
||||
if _provider == "openai" and not OPENAI_API_KEY:
|
||||
_provider = "none"
|
||||
elif _provider == "gemini" and not GEMINI_API_KEY:
|
||||
_provider = "mock"
|
||||
_provider = "none"
|
||||
elif _provider == "anthropic" and not ANTHROPIC_API_KEY:
|
||||
_provider = "mock"
|
||||
_provider = "none"
|
||||
|
||||
LLM_PROVIDER = _provider
|
||||
USE_MOCK_LLM = LLM_PROVIDER == "mock"
|
||||
LLM_CONFIGURED = LLM_PROVIDER != "none"
|
||||
|
||||
|
||||
def llm_label() -> str:
|
||||
if LLM_PROVIDER == "mock":
|
||||
return "MOCK reasoner (no LLM key)"
|
||||
if LLM_PROVIDER == "none":
|
||||
return "NONE (no LLM provider configured)"
|
||||
if LLM_PROVIDER == "openai":
|
||||
host = OPENAI_BASE_URL.split("//")[-1].split("/")[0]
|
||||
return f"OpenAI-compatible ({OPENAI_MODEL} @ {host})"
|
||||
@@ -111,10 +115,18 @@ EXA_MONTHLY_REQUEST_CAP = int(os.environ.get("EXA_MONTHLY_REQUEST_CAP", "1000"))
|
||||
EXA_PER_RUN_REQUEST_CAP = int(os.environ.get("EXA_PER_RUN_REQUEST_CAP", "25"))
|
||||
EXA_RESULTS_PER_QUERY = int(os.environ.get("EXA_RESULTS_PER_QUERY", "5"))
|
||||
EXA_LOOKBACK_DAYS = int(os.environ.get("EXA_LOOKBACK_DAYS", "45"))
|
||||
# Scope news search to each account's own domain (newsroom/press/IR) to cut
|
||||
# third-party ticker/analyst noise. Toggle off in .env or per-run with
|
||||
# `loop --no-domain-filter` to include broad third-party coverage.
|
||||
EXA_DOMAIN_FILTER = os.environ.get("EXA_DOMAIN_FILTER", "1").strip() == "1"
|
||||
# Exa content category. "news" restricts results to news articles (press releases,
|
||||
# coverage) and keeps out evergreen pages like careers/marketing landing pages —
|
||||
# the single biggest signal-quality lever. Set to "" to disable category filtering.
|
||||
EXA_CATEGORY = os.environ.get("EXA_CATEGORY", "news").strip()
|
||||
# Domain scoping is OFF by default now: with category=news, broad-web search finds
|
||||
# actual news ABOUT the account from outlets, whereas scoping to the company's own
|
||||
# domain surfaces careers/marketing pages. Turn on for first-party newsroom only.
|
||||
EXA_DOMAIN_FILTER = os.environ.get("EXA_DOMAIN_FILTER", "0").strip() == "1"
|
||||
# Hosts to always exclude (comma-separated) — e.g. careers/talent subdomains.
|
||||
EXA_EXCLUDE_DOMAINS = [
|
||||
d.strip() for d in os.environ.get("EXA_EXCLUDE_DOMAINS", "").split(",") if d.strip()
|
||||
]
|
||||
|
||||
# --- Dedup / suppression windows ---
|
||||
SEMANTIC_DEDUP_DAYS = 90
|
||||
|
||||
Reference in New Issue
Block a user