Overhaul docs retrieval and web search quality

Replace the abandoned llms-txt-mcp/Chroma docs backend with an in-repo
MCP service: SQLite WAL + FTS5 + sentence-transformer embeddings,
transactional source replacement, persisted state across restarts,
singleflight refresh with conditional requests, hybrid lexical/semantic
ranking with exact-duplicate collapse, source/host filters, and
explicit-by-default content retrieval. Add docs_rebuild and a
docs-rebuild CLI command.

Add deterministic llms-full.txt snapshot generation for machine-local
menus with hash-validated provenance manifests; lifecycle commands
promote a local menu to its snapshot only when the manifest validates.
Switch public source profiles to content-bearing llms-full.txt feeds.

Improve web search: bounded provider fallback with per-attempt
diagnostics and cancellation, an optional Brave Search API provider,
strict SearXNG engine selection, capped link/media extraction, and a
real engine=browser renderer that routes every request through the
existing SSRF vetting while blocking WebSockets, non-GET traffic, and
private destinations.

Extend release checks with offline unit suites and isolated candidate
container tests for both images.
This commit is contained in:
2026-07-25 08:49:26 -07:00
parent 29bcb123fa
commit 51dceee224
60 changed files with 3207 additions and 107 deletions

View File

@@ -0,0 +1,51 @@
from __future__ import annotations
import httpx
from .models import FetchResponse, SourceState
class SourceFetcher:
def __init__(self, timeout_seconds: float = 30, max_bytes: int = 20_000_000):
self.timeout_seconds = timeout_seconds
self.max_bytes = max_bytes
self._client = httpx.AsyncClient(
follow_redirects=True,
timeout=httpx.Timeout(timeout_seconds),
headers={"User-Agent": "context-kit-docs/1.0"},
)
async def close(self) -> None:
await self._client.aclose()
async def fetch(self, source_url: str, state: SourceState | None = None) -> FetchResponse:
headers: dict[str, str] = {}
if state and state.etag:
headers["If-None-Match"] = state.etag
if state and state.last_modified:
headers["If-Modified-Since"] = state.last_modified
async with self._client.stream("GET", source_url, headers=headers) as response:
if response.status_code == 304:
return FetchResponse(
status=304,
requested_url=source_url,
resolved_url=str(response.url),
etag=response.headers.get("etag"),
last_modified=response.headers.get("last-modified"),
)
chunks: list[bytes] = []
size = 0
async for chunk in response.aiter_bytes():
size += len(chunk)
if size > self.max_bytes:
raise RuntimeError(f"source exceeds {self.max_bytes} byte limit")
chunks.append(chunk)
body = b"".join(chunks).decode(response.encoding or "utf-8", errors="replace")
return FetchResponse(
status=response.status_code,
requested_url=source_url,
resolved_url=str(response.url),
body=body,
etag=response.headers.get("etag"),
last_modified=response.headers.get("last-modified"),
)