Architecture

A query travels from the browser to the Flask orchestrator, which routes it through a three-stage NLP waterfall and assembles the response from typed tool results.

[Browser]
    │ HTTP (JSON)
    ▼
[Flask Orchestrator :5000]
    │  spaCy NLP + regex grammars (en/de/fr/es/hr) + confidence scorer
    │  Entity Resolver (SQLite + rapidfuzz, word-boundary matching)
    │  LLM Planner (Stage 2/3)
    │  Redis cache (per-domain TTL, in-memory LRU fallback)
    │
    ├─ [FastAPI Data API :8000]  ← qalc, chempy, mendeleev, sympy+matplotlib,
    │       Wikidata, Open-Meteo, connector registry (cia/food/hjp/dictionary)
    └─ In-process: temporal_calc, fermi_estimate

Stage 1 — intent classification (< 50 ms)

  1. spaCy tokenises and POS-tags the query.
  2. 32 prioritised regex grammars attempt to match the normalised token sequence (per-language grammar sets via grammars_for(lang)).
  3. The Entity Resolver fuzzy-matches named entities to database primary keys (rapidfuzz, cutoff 75).
  4. A confidence score is computed:
Signal Weight
Grammar base confidence 60%
spaCy NER tag matches domain 20%
Entity Resolver high-confidence hit 20%

Grammars with base_confidence ≥ 0.95 bypass the weighted formula entirely and dispatch directly.

Confidence → action:

  • ≥ 0.85 — Stage 1 fast path, LLM bypassed
  • 0.50 – 0.84 — Stage 2: LLM planner with entity context
  • < 0.50 — Stage 3: LLM planner without pre-resolved context
  • Multi-domain entity hits cap confidence at 0.60 and produce a "Did you mean?" pod — unless a ≥ 0.95 grammar already pinned the intent

If a Stage 1 tool call returns an error, the pipeline automatically falls through to Stage 2 — greedy grammars cannot lock out the planner. The Stage 1 error pod is kept as a last-resort fallback.

Stage 2/3 — LLM planner (< 3–5 s)

The LLM receives the query, entity context (Stage 2 only), the current date and location, and the full tool registry schema. It returns a single JSON array of tool calls, validated against a JSON Schema before dispatch.

The planner uses the Anthropic API when ANTHROPIC_API_KEY is set, otherwise any OpenAI-compatible endpoint (LLM_API_URL). The LLM never produces final answers — only validated tool graphs.

The dispatcher executes tool calls in parallel waves based on declared depends_on dependencies, resolving {stepN.result.field} templates between waves.

Entity resolution

The Entity Resolver sits between the NLP pipeline and all tool calls, preventing the "NYC vs New York City" class of failures:

  • SQLite table entity_aliases(alias, canonical_key, domain, confidence) built at data-ingest time;
  • word-boundary n-gram matching at query time — exact for short aliases, rapidfuzz ratio ≥ 75 for aliases of 4+ characters; substrings never match;
  • the canonical key always overrides grammar-extracted entity text;
  • feeds both Stage 1 confidence scoring and the planner's context.

Tool registry

All ten tools return typed result objects, never raw strings — mandatory for deterministic piping between tools:

Tool Backend
execute_math qalc subprocess (data API)
execute_symbolic qalc via WL→qalc translator
generate_plot sympy + matplotlib
balance_chemistry chempy
element_lookup mendeleev + qalc
fetch_weather Open-Meteo + geocoding
query_database Connector registry: cia, food_facts, hjp, dictionary
query_wikidata Wikidata API + Wikipedia
temporal_calc python-dateutil (in-process)
fermi_estimate static lookup table (in-process)

Key source files

File Purpose
app/nlp/pipeline.py Pipeline entry point — run()
app/nlp/stage1/grammars.py Grammar dataclasses + grammars_for(lang)
app/nlp/stage1/classifier.py spaCy NER + confidence scorer
app/nlp/entity_resolver.py Entity → canonical key
app/nlp/stage2/planner.py LLM planner
app/nlp/stage2/dispatcher.py Wave-based parallel tool executor
app/tools/registry.py TOOL_REGISTRY dispatch table
app/assembler/pod_builder.py Tool results → Pod list
app/schemas/validators.py JSON Schema for planner output
data_api/routers/ FastAPI compute routers
data_api/connectors/ One DataConnector subclass per data source