Skip to content

feat(web): on-device web search for chat - #290

Open
kfaracik wants to merge 57 commits into
mainfrom
feat/web-search
Open

feat(web): on-device web search for chat#290
kfaracik wants to merge 57 commits into
mainfrom
feat/web-search

Conversation

@kfaracik

@kfaracik kfaracik commented Aug 7, 2026

Copy link
Copy Markdown
Member

What

On-device web search for chat: the model can ground answers in live web pages, fully on-device (scrape → fetch → extract → retrieve → rank → converge), with a per-chat Web toggle, an inline search-trace, and a Sources sheet.

Highlights

  • Search pipeline — offscreen WebView scrapes SERPs across engines, pages are fetched/extracted (bot-wall & navigation filtering), passages retrieved in memory, ranked by coverage/similarity/agreement, with a corrective round when evidence is thin.
  • Multilingual — per-language stopwords, script segmentation (CJK/Thai bigrams), language detection, per-model profiles for planner/retrieval tuning.
  • DB — FTS5 bigram keyword index paralleling the vector store; read-state on sources.
  • Chat UI — web-search activity trace & Sources sheet, favicon with retry, keyboard-lift hide guard, source dedup, citation highlighting.
  • Security — scrape/fetch boundary hardened, SERP/article data sanitized at the RN boundary, per-chat toggle validated.
  • Tooling — CI typecheck step (tsc --noEmit).

kfaracik and others added 30 commits August 3, 2026 10:21
…g refusal

The hard fallback ("say I don't know") made the model refuse any question the
context did not cover — including a general question asked mid-chat merely
because a document was attached. Soften the ladder, not the grounding: first
state that the sources (or search results) contain no information about the
question, and only then answer from own knowledge, explicitly marked as such.

The refusal sentence is phrased so looksLikeNoAnswer recognises it. The
previous web wording ("could not find it in the search results") slipped past
that detector, so a refusal after a search was still attributed to sources and
flagged result pages as used.

The scope rule stays absolute but now states its reason — an absent document's
text is genuinely not available to the model — and the grounding hint next to
the question drops its duplicate of that rule, which sat closest to the
question and would have overridden the new fallback. It keeps only the
referent anchor for "this file" / "it".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Before a search, planWebSearch turns the latest message into a JSON plan
{needs_search, intent, queries}: cheap heuristics send already-concise
keyword queries verbatim without touching the model, a gating prompt
skips search for small talk, and anaphoric follow-ups are resolved
against the last conversation turns. Parsing is defensive (think-block
strip, JSON lifted out of prose, per-query sanitising, sub-query cap)
and every failure path falls back to the verbatim message clamped to a
searchable length, so a planner fault can never block a search. An
unterminated <think> block is eaten whole rather than leaking
deliberation into a query or a parsed plan.

When a round fails, reformulateForCorrection climbs a free ladder first
(unused planned sub-query, then intent, then broaden by dropping the
trailing token), deduplicated case- and whitespace-insensitively against
everything already run. reformulateWithEvidence then lets the model read
capped excerpts of the pages the failed round DID find and build one new
query around a lead it spots there, in the topic language, never
translating proper names.

detectTopicLanguage reads that language off result hosts (ccTLDs plus
Wikipedia subdomains, mobile included, plurality of at least two,
English never reported since it is the pivot), and nativeTitleQuery
lifts a native-language page title as a ready-made next query.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
enrichWebResults fetches full article text for the top search results
(the rest keep their SERP snippets) and marks each page usable only when
extraction produced real content: at least WEB_CONTENT_MIN_CHARS of text
and no anti-bot challenge markers.

looksLikeBotWall recognizes challenge pages ("verify you are human",
"checking your browser", "access denied", ...) by title always and by
body only when the text is short — a real article that merely mentions
those phrases stays in. Without this filter an aggressive anti-scraping
page (Cloudflare and friends) would feed its challenge boilerplate into
the model context; with it the page degrades to its honest snippet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ating

- memoize the favicon image source and reset its failed/progress state on
  host change, so a recycled row does not flash the previous site's icon
- guard the collapse animation's onCollapsed behind a mountedRef so runOnJS
  cannot fire after the list unmounts
- gate the generating/processing activity on isLastMessage so only the last
  bubble reflects the in-flight send

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kfaracik and others added 27 commits August 3, 2026 10:34
Without injection, enrichment always performs a real HTTP GET via
extractArticle, so runWebSearch cannot be exercised offline — a
fixture-based eval run would silently hit the live web. Thread an
ArticleFetcher through the runWebSearch input in the same style as
provider/embeddings/generate; the default stays extractArticle, so
production behaviour is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A second utility call while one is still generating hit the native
ExecuTorch module mid-generation and threw code 104 "model is
currently generating", poisoning every subsequent turn. Concurrent
callers now get '' (the callers' existing fallback path) instead.

A hard 8 s interrupt of slow utility generations was considered and
dropped: slow devices will instead be told up front that search takes
longer there (tracked separately).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The offscreen SERP WebView persisted cookies, localStorage and HTTP
cache from every engine it visited for the lifetime of the app install.
Incognito keeps the scrape session ephemeral; cookies still work within
a single challenge flow, but nothing survives the WebView instance.
Trade-off to validate on device: engine challenge clearance no longer
persists across queries, so CAPTCHAs may reappear more often.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The overall abort (90 s cap or user interrupt) settled the search
promise but let the expensive work run on detached: page fetches ran to
their own 8 s timeouts, the embedding loop processed up to 40 chunks to
completion, and the scrape WebView kept loading the SERP. The abort
signal now reaches every stage: fetchHtml/extractArticle take an
external signal wired into their AbortController, enrichWebResults
threads it into each fetch and skips pages once aborted,
retrieveWebPassages bails out of the embedding loop, and the scrape
host resets the WebView to the idle page on abort/cancel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three promptUtils tests encoded the web branch's ratio-based char budget
(CHARS_PER_TOKEN), which main replaced with a finer per-codepoint model in
#246. Under that model latin text is 0.25 tok/char against a 1/3 no-sample
default, so ASCII now earns a *larger* budget than getPromptCharBudget(model)
returns with no sample — the old equality no longer holds by design.

- assert ASCII never drops below the default rather than matching it
- measure the overflow test against the budget the assembled prompt
  actually gets, not the no-sample default
- size the trimming filler at 2x budget, matching main's own convention,
  so it still forces a trim under the larger ASCII budget

llmStore's retry test predated the web branch's inline auto-retry, which
now reduces the prompt budget and regenerates once before surfacing the
error. Exhaust that attempt so the test still exercises what it names:
retryLastGeneration reusing the persisted user message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
private-mind Error Error Aug 7, 2026 1:37pm

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant