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)
- spaCy tokenises and POS-tags the query.
- 32 prioritised regex grammars attempt to match the normalised token
sequence (per-language grammar sets via
grammars_for(lang)). - The Entity Resolver fuzzy-matches named entities to database primary keys (rapidfuzz, cutoff 75).
- 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 bypassed0.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 |