Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
"""Search provider interface. Answers "where should we look?" - discovery
|
||||
only, never a replacement for the LLM or for a SourceCollector. Every
|
||||
company-discovery query (official website, aliases, competitors, HQ) goes
|
||||
through this Protocol, never a specific vendor SDK - swapping
|
||||
`SEARCH_PROVIDER` changes which class `get_search_provider()` returns and
|
||||
nothing else has to change. See app/services/discovery_service.py for the
|
||||
only caller.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class SearchResult(BaseModel):
|
||||
title: str
|
||||
url: str
|
||||
snippet: str
|
||||
|
||||
|
||||
class SearchProvider(Protocol):
|
||||
provider_name: str
|
||||
|
||||
async def search(self, query: str, *, count: int = 5) -> list[SearchResult]: ...
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Brave Search API provider. A fixed, trusted, first-party integration
|
||||
endpoint (like Twilio/Resend) - calls httpx directly rather than through
|
||||
`safe_fetch`, which exists specifically to guard arbitrary/user-supplied
|
||||
collector targets, not our own known-safe API integrations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core.config import Settings
|
||||
from app.search.base import SearchResult
|
||||
|
||||
_API_URL = "https://api.search.brave.com/res/v1/web/search"
|
||||
|
||||
|
||||
class BraveSearchProvider:
|
||||
provider_name = "brave"
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self._settings = settings
|
||||
|
||||
async def search(self, query: str, *, count: int = 5) -> list[SearchResult]:
|
||||
async with httpx.AsyncClient(timeout=15) as client:
|
||||
response = await client.get(
|
||||
_API_URL,
|
||||
params={"q": query, "count": count},
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"X-Subscription-Token": self._settings.brave_search_api_key,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
results = data.get("web", {}).get("results", [])
|
||||
return [
|
||||
SearchResult(
|
||||
title=r.get("title", ""),
|
||||
url=r.get("url", ""),
|
||||
snippet=r.get("description", ""),
|
||||
)
|
||||
for r in results[:count]
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Resolves `SEARCH_PROVIDER` to a concrete provider instance. Never
|
||||
imported directly by discovery_service - always go through
|
||||
`get_search_provider()` so swapping providers stays a one-line config
|
||||
change."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.search.base import SearchProvider
|
||||
from app.search.mock import MockSearchProvider
|
||||
|
||||
|
||||
def get_search_provider(settings: Settings | None = None) -> SearchProvider:
|
||||
settings = settings or get_settings()
|
||||
|
||||
if settings.search_provider == "brave":
|
||||
from app.search.brave import BraveSearchProvider
|
||||
|
||||
return BraveSearchProvider(settings)
|
||||
|
||||
return MockSearchProvider()
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Deterministic mock search provider - the default (`SEARCH_PROVIDER=mock`)
|
||||
and what every automated test runs against. Never calls a network.
|
||||
|
||||
Unlike `MockLLMProvider` (which fabricates plausible-looking structured
|
||||
output from real evidence), this provider has no real evidence to work
|
||||
from - a company name alone isn't enough to honestly guess an industry,
|
||||
headquarters, or competitor list. So except for the one query type where a
|
||||
name-derived guess is genuinely meaningful (official website - many
|
||||
companies really do live at a domain close to their name), every other
|
||||
query honestly reports "no information available" as its snippet text.
|
||||
That honesty flows through `discovery_service.py` and the company-profile
|
||||
LLM task: fields with no real evidence come back `None`/`[]`, and the
|
||||
wizard's Review step shows them as "not found - fill in yourself" rather
|
||||
than presenting fabricated data as fact.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from app.search.base import SearchResult
|
||||
|
||||
_DOMAIN_STRIP_RE = re.compile(r"[^a-z0-9]+")
|
||||
_WEBSITE_SUFFIX = " official website"
|
||||
|
||||
|
||||
def _guess_domain(name: str) -> str:
|
||||
slug = _DOMAIN_STRIP_RE.sub("", name.lower())
|
||||
return f"{slug}.com" if slug else "example.com"
|
||||
|
||||
|
||||
class MockSearchProvider:
|
||||
provider_name = "mock"
|
||||
|
||||
async def search(self, query: str, *, count: int = 5) -> list[SearchResult]:
|
||||
if query.lower().endswith(_WEBSITE_SUFFIX):
|
||||
name = query[: -len(_WEBSITE_SUFFIX)]
|
||||
domain = _guess_domain(name)
|
||||
results = [
|
||||
SearchResult(
|
||||
title=f"{name} - Official Site",
|
||||
url=f"https://{domain}",
|
||||
snippet=(
|
||||
f"A likely official website for {name}, guessed from its name. "
|
||||
"Mock search - no real web search performed; set SEARCH_PROVIDER=brave "
|
||||
"for real results."
|
||||
),
|
||||
)
|
||||
]
|
||||
else:
|
||||
results = [
|
||||
SearchResult(
|
||||
title=f"Mock search: {query}",
|
||||
url="https://example.com/mock-search",
|
||||
snippet=(
|
||||
"No information available - SEARCH_PROVIDER=mock is the dev/test "
|
||||
"default and performs no real web search. Set SEARCH_PROVIDER=brave "
|
||||
"for real discovery."
|
||||
),
|
||||
)
|
||||
]
|
||||
return results[:count]
|
||||
Reference in New Issue
Block a user