Legal database search tool with Files-and-Sources per-message picker

nwhitehouse added a `legal_search` tool fanning out across four open U.S. legal databases, hard-gated behind a per-message source picker in the chat input. When no sources are selected, the tool isn't in the schema at all.

searchchat-ui

backend/src/lib/legalSearch.ts (270 LOC, new) fans out to CourtListener, GovInfo, Federal Register, and eCFR via Promise.allSettled with 12s per-source timeouts. Sources with missing API keys return zero results with a logged warning rather than failing the whole call. formatLegalResultsForModel() produces markdown links so the model can cite sources inline.

The hard-gate pattern is worth noting: LEGAL_TOOLS is a separate array from the always-on TOOLS. It's only appended to the active tool schema when sources.legal is non-empty. Inside runToolCalls, the model's sources argument is clamped to the user's actual selection - intersection of what the model requested and what the user enabled. If the model tries to widen scope, it's silently constrained. A system-prompt line reinforces the same constraint.

The UI rename is AddDocButton.tsxFilesAndSourcesButton.tsx, which grows the button into a sectioned popover: existing upload/browse actions, plus a new "U.S. Legal Sources" section with four checkboxes defaulting to off. The architecture is explicitly designed for additional sections to drop in without rework. MikeMessage.sources is per-message transient state, reset after submit.

Two of the four sources require API keys: CourtListener (COURTLISTENER_API_TOKEN) and GovInfo (GOVINFO_API_KEY). Both are free. Federal Register and eCFR are open. All four were smoke-tested against live endpoints.

So what Worth importing if you want U.S. legal research grounding. The `Promise.allSettled` + timeout + missing-key tolerance pattern is a solid template for any multi-source tool - legal search, external APIs, or anything else that fans out across heterogeneous sources. The hard-gate-on-disabled-sources pattern is the right default and straightforward to extend. Note that CourtListener and GovInfo require free API keys; the others don't.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

1 commit from nwhitehouse/mike, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
6a4ea15e [feat-002] Legal database search with Files and Sources picker Nick Whitehouse 2026-05-03 ↗ GitHub
commit body
Adds a `legal_search` tool that fans out across four open US legal databases
(CourtListener, GovInfo, Federal Register, eCFR), gated by a new "Files and
Sources" picker on the chat input. Hard gate: when no sources are selected,
the tool isn't in the schema at all.

Backend
- backend/src/lib/legalSearch.ts (new): TS port of work___'s
  legal_search_service.py. Promise.allSettled fan-out, 12s per-source
  timeouts, graceful degradation when an API key is missing or a source
  errors. Includes formatLegalResultsForModel() that produces markdown
  links so the model can cite inline.
- chatTools.ts: new LEGAL_TOOLS array (kept separate from always-on TOOLS
  so it can be conditionally appended). Dispatch case in runToolCalls
  defensively clamps the model's `sources` arg to the user's selection.
  runLLMStream takes new `sources?: { legal?: string[] }` param;
  conditionally appends LEGAL_TOOLS and a system-prompt line restricting
  the model's `sources` arg to the user's selection.
- routes/chat.ts: reads `sources` from request body, passes through.
- .env.example: documents COURTLISTENER_API_TOKEN, GOVINFO_API_KEY (FR +
  eCFR are open APIs, no auth).

Frontend
- AddDocButton.tsx → FilesAndSourcesButton.tsx (rename + extend). Trigger
  label "Files and sources"; count = attached files + selected sources.
  Sectioned popover: existing Upload / Browse + new "US Legal Sources"
  section with 4 checkboxes (Court Opinions, Federal Legislation, Federal
  Register, Regulations CFR). Default off. Architected so future sections
  (Knowledge Base, Integrations, EU/UK Law) drop in without rework.
- ChatInput.tsx: tracks selectedLegalSources, passes to button, includes
  in submitted MikeMessage's `sources` field, resets to [] after submit
  (per-message-only - no chat-level persistence in v1).
- MikeMessage.sources (transient field, not persisted in history).
- useAssistantChat.ts + mikeApi.streamChat: thread `sources` through to
  POST /chat body.
- Renamed prop `hideAddDocButton` → `hideFilesAndSourcesButton`; one
  caller updated (project chat page).

Smoke-tested both open APIs (Federal Register + eCFR) end-to-end against
real endpoints - both return real results in <1.5s.

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

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-115.md from inside the repo you want the changes in.

⬇ Download capture-thread-115.md