Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Process Intelligence Engine

Modus Enterprise AI Build Challenge — Assignment 2 (100-Process AI Research & Intelligence Engine)

An AI application that researches and analyses ~100 business processes for a fictional retailer (Northfield Retail Group), one by one, through the same pipeline every time — so that adding process #101 live works identically to how processes #1–100 were built.


Architecture

USER INTERFACE        frontend/index.html   -- single-page dashboard, vanilla JS
        ↕
APPLICATION/API        backend/main.py       -- FastAPI routes
        ↕
AI INTELLIGENCE        backend/intelligence.py, query_engine.py, llm_client.py
        ↕
DATA & KNOWLEDGE       backend/database.py (SQLite)  +  backend/vector_store.py (ChromaDB)
        ↕
EXTERNAL RESEARCH      backend/research.py            -- DuckDuckGo web search

Why this shape, not a simpler one: the brief's "surprise record" test and its final judging question ("if we give your application 1,000 processes tomorrow, what happens?") are really the same question twice. The answer depends on two design choices made up front:

  1. One pipeline function, no special-casing. intelligence.analyze_process() is the only code path that turns a process name into a full analysis. It runs identically whether called from the seed script, the "add process" box in the UI, or a live evaluator input. There's no separate "demo mode."
  2. Two data layers, not one. SQLite holds structured fields for anything that needs exact filtering or ranking (top-10 by score, human-led filter). ChromaDB holds embeddings for anything that needs semantic retrieval (open-ended questions). This matters at scale specifically: a "stuff everything into one prompt" design gets linearly more expensive and eventually breaks the context window as the corpus grows. Retrieval-based answering stays roughly constant-cost because only the top-k relevant records are ever sent to the LLM, regardless of whether the corpus has 100 or 100,000 rows.

Setup

python -m venv venv
source venv/bin/activate        # Windows: venv\Scripts\activate
pip install -r requirements.txt

cp .env.example .env
# Edit .env and add a free Groq API key from https://console.groq.com/keys
# (no credit card required). See "LLM provider" below for the fully-local
# alternative that needs no key at all.

uvicorn backend.main:app --reload

Open http://localhost:8000 — the dashboard is served directly by the backend, no separate frontend build step.

To seed the ~100 retail processes and analyse all of them:

python data/seed_processes.py --analyze

This takes a while — each process is one real web search plus one real LLM call, deliberately not shortcut. Watch progress live in the dashboard (stats poll every 4 seconds) or via GET /api/stats.

Live demo / "surprise record" test

Type any new process name into the "Add & analyse a process live" box at the top of the dashboard. It POSTs to /api/processes, which runs the exact same analyze_process() pipeline as every seeded process, and the result appears in the grid within seconds. This is the literal mechanism the brief's Process 101 test is checking for.

The four example queries from the brief, plus the open-ended demonstration question

All five are handled by POST /api/query (also exposed as clickable chips in the dashboard's query bar):

Question How it's answered
"Analyse all processes." SQL count/rollup — no LLM call
"Show the 10 processes with highest AI potential." SQL ORDER BY ai_potential_score DESC LIMIT 10 — no LLM call
"Which processes should remain predominantly human-led?" SQL WHERE filter — no LLM call
"Show me the research supporting Process 37." SQL lookup by id, returns stored evidence — no LLM call
"What should this organisation transform, why, what evidence supports it, and what should be done first?" ChromaDB semantic search narrows to the ~10 most relevant processes, then one scoped LLM call reasons over just that subset

Only the last row touches the LLM. See backend/query_engine.py for the routing logic — the point of splitting it this way (rather than one prompt that tries to handle every question) is that four of the five answers are correct by construction (SQL doesn't get ranking wrong), and the fifth is where real reasoning is actually needed.

LLM provider — and what happens if it goes away

backend/llm_client.py wraps an OpenAI-compatible client, not a provider-specific SDK. Groq, Ollama, and OpenAI itself all speak the same /chat/completions format, so switching providers is a .env change only:

# Free, hosted (default) — get a key at https://console.groq.com/keys
LLM_BASE_URL=https://api.groq.com/openai/v1
LLM_MODEL=llama-3.3-70b-versatile

# Free, fully local/offline — no key, no internet needed at inference time
# (requires: ollama pull llama3.1, then run `ollama serve`)
LLM_BASE_URL=http://localhost:11434/v1
LLM_MODEL=llama3.1
GROQ_API_KEY=ollama   # value is ignored by Ollama but must be non-empty

This is the direct answer to the brief's "what happens if the free-tier service becomes paid or unavailable" requirement: nothing in the codebase changes, because every caller only ever imports chat_json() from this one module.

What I tested, and what I could not test in my own build environment

In the interest of not overstating what's verified — this was built and tested inside a sandboxed tool environment with a restricted network allowlist (only PyPI/npm/GitHub-family domains reachable). Everything below was actually run, not just written:

  • Full backend + API: server starts, every route wired correctly, confirmed via live curl requests against a running instance.
  • Deduplication logic: caught and fixed a real bug during testing where SQLAlchemy's autoflush=False meant a duplicate name submitted twice in the same bulk request wasn't caught by a mid-loop query — fixed with an explicit in-batch set check. (Left the original comment in main.py explaining why, since it's a genuinely non-obvious SQLAlchemy behaviour worth knowing.)
  • All four structured query routes (ranked list, human-led filter, evidence lookup, corpus status): tested against seeded data with real HTTP requests, correct results confirmed.
  • The analysis pipeline itself: tested with the LLM and web search mocked out, covering (a) a normal successful response, (b) a malformed response — bad enum casing, non-numeric score, missing fields — confirmed it degrades to safe defaults instead of crashing, and (c) a complete LLM failure, confirmed the process is marked FAILED without taking down the rest of the batch.
  • Frontend: served correctly by the backend, byte-identical to source, inline JavaScript syntax-checked with Node.

Not fully exercised end-to-end: the live Groq API call and the ChromaDB default embedding model's first-run download. Groq needs a real key I don't have. Chroma's embedding function downloads a small ONNX model from chroma-onnx-models.s3.amazonaws.com on first use — that domain sits outside my sandbox's allowlist, so I confirmed why it fails here (a truncated download, not a code defect) but couldn't complete the download myself. This is a very standard, widely-used path that should complete without issue on a normal residential or office connection — but run python data/seed_processes.py --analyze once, well before any live demo, specifically so this one-time ~90MB download happens on your own schedule rather than during a timed presentation.

Known limitation: DuckDuckGo rate limits

Confirmed directly during testing, not a hypothetical: DuckDuckGo's search rate-limits fairly aggressively from shared or cloud-hosted IPs — a 403 is common, not an edge case. Seeding ~100 processes back-to-back is exactly the access pattern that triggers it. Two mitigations are already built in (backend/research.py): a short delay between calls during bulk runs, and a retry with backoff specifically on rate-limit errors. Neither is a guarantee. If a process still comes back with no evidence, the pipeline degrades to reasoning from general knowledge rather than blocking the batch — by design, not as an unhandled failure. If this is a real problem on your network, swap research.py's search call for the Tavily or Brave free-tier APIs (both need a free key but are far less prone to rate limiting than scraping DDG's HTML endpoint).

Project structure

backend/
  database.py       SQLAlchemy models (SQLite) -- the structured source of truth
  vector_store.py    ChromaDB wrapper -- semantic retrieval layer
  research.py        DuckDuckGo search -- the evidence/citation source
  llm_client.py      Provider-agnostic LLM call, JSON-mode enforced
  intelligence.py    The per-process analysis pipeline (the "surprise record" path)
  query_engine.py     Routes natural-language questions to SQL or semantic+LLM
  schemas.py          Pydantic request/response models
  main.py             FastAPI app and route definitions
frontend/
  index.html           Dashboard: process grid, query bar, live add-process, evidence drawer
data/
  seed_processes.py   ~100 retail process names + seeding script

Extending past 100 processes

Nothing in the schema or pipeline assumes a fixed count. To point this at a different industry: edit ORG_NAME / ORG_INDUSTRY in .env, and replace the PROCESSES list in data/seed_processes.py with process names relevant to that industry. Everything downstream — analysis, ranking, semantic search, the query router — works unchanged, because none of it hard-codes retail-specific logic; industry context only ever enters as a prompt variable in intelligence.py.

About

AI application that researches and analyses 100+ retail business processes end-to-end — FastAPI, ChromaDB, SQLite, and Groq LLM, with live process ingestion and natural-language querying. Built for the Modus Enterprise AI Build Challenge.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages