Skip to content

About

An AI-powered e-commerce agent that can handle complex user queries about products, inventory, and order status. Built with a FastAPI backend and a React (Vite) frontend, featuring integration with LLMs for natural language understanding and automated task execution.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

269 Commits

Folders and files

Repository files navigation

πŸ›’ E-Commerce Agent (React + FastAPI)

An intelligent AI-powered e-commerce assistant built with a modern React frontend and FastAPI backend. Features agentic reasoning, secure authentication, and a clean light UI shared by the landing page and the app.


πŸš€ Key Features

  • Agentic reasoning β€” a LangChain agent routes each message to four tools: product search (text-to-SQL), FAQ (RAG), saved items, and cart/orders. It can chain several tools in one turn or reply without any.
  • Swappable LLM provider β€” Gemini 2.5 Flash or Cloudflare Workers AI via LLM_MODEL; shoppers can also bring their own key in Settings (stored encrypted).
  • Shop by photo β€” upload a shoe photo and get similar products. Non-shoe photos are refused, not answered with invented results.
  • Cart & simulated orders β€” add to cart, place and cancel orders from chat. Out-of-stock items block the order instead of being dropped silently.
  • Long-term memory β€” preferences like "I prefer Puma under 3000" are stored per user in Supermemory and recalled in later chats.
  • Multi-part questions β€” "return policy and Nike under 3000" or "4 Nike and 5 Puma" are split and each part is answered.
  • Honest answers β€” recommends only in-stock items, refuses filters the data can't support, and stays on shopping topics.
  • Streaming & resilient jobs β€” answers stream over SSE from a queued job, so closing the tab or redeploying doesn't lose the answer. Users are scheduled fairly under load.
  • Provider failover β€” a circuit breaker switches to the other provider after repeated failures.
  • Bounded spend β€” daily token budget and per-user rate limits.
  • Live product data β€” scripts refresh prices/stock and discover new listings from Flipkart's schema.org data.
  • Quality tracking β€” a 160-question evaluation (evals/run_eval_v2.py, DeepEval GEval) (see Evaluation Results).

πŸ—οΈ Architecture

Five layers, read top to bottom. Each arrow is a hand-off between layers; the shared services (data, external AI) are reached once per layer rather than by every tool, so the flow stays legible. Caching and observability are cross-cutting.

graph TD
    User(["πŸ‘€ Shopper"])

    subgraph CLIENT ["1 Β· Client layer β€” React / Vite"]
        UI["πŸ’¬ Chat UI Β· streamed answers Β· save Β· compare"]
        Land["πŸ›¬ Landing page"]
    end

    subgraph APP ["2 Β· Application layer β€” FastAPI (main.py)"]
        Auth["πŸ” Auth Β· bcrypt Β· JWT Β· login lockout Β· identity rate-limit"]
        REST["πŸ—‚οΈ POST /message β†’ 202 + job_id Β· GET /jobs/{id}/events (SSE)<br>poll Β· cancel Β· saved products"]
        WORK["βš™οΈ Worker Β· claims from the Postgres queue<br>lease Β· reaper Β· token metering (worker.py)"]
        REST -->|enqueue job| WORK
    end

    subgraph AI ["3 Β· Reasoning layer β€” LangChain agent"]
        Mem["🧠 Memory · rewrite query from history (memory.py)"]
        Route["🧭 create_agent · LLM picks tools in turn, or none<br>verbatim tool output · runtime ctx (agent.py)"]
        Mem --> Route
        Route --> SQL["πŸ“Š Text-to-SQL Β· read-only engine (sql.py)"]
        Route --> FAQ["πŸ“š FAQ Β· RAG (faq.py)"]
        Route --> CMP["βš–οΈ Compare saved Β· user-scoped (compare.py)"]
    end

    subgraph DATA ["4 Β· Data layer β€” Neon Postgres + Pinecone"]
        PG[("Postgres Β· product Β· chat_sessions/messages<br>ecommerce_accounts Β· saved_products Β· llm_cache<br>jobs Β· job_events")]
        Pine[("Pinecone Β· FAQ vectors")]
    end

    subgraph EXT ["5 Β· External AI services β€” app default or shopper choice"]
        Gem["☁️ Google Gemini · 2.5 Flash<br>routing / SQL / FAQ / compare"]
        CF["☁️ Cloudflare Workers AI · gpt-oss-20b<br>routing / SQL / FAQ / compare"]
        Emb["πŸ”’ Gemini embeddings Β· always, either way"]
    end

    Cache["πŸ—„οΈ Caching Β· cross-cutting<br>Postgres llm_cache Β· SQL / FAQ / routing"]
    OBS["πŸ“ˆ Observability Β· cross-cutting<br>Langfuse LLM traces + request logs"]

    User --> CLIENT
    CLIENT -->|HTTP + JWT| APP
    APP -->|auth Β· sessions Β· saved| DATA
    WORK -->|optimized query| AI
    SQL -->|read-only SQL| PG
    FAQ -->|semantic search| Pine
    CMP -->|saved + live prices| PG
    Route -->|generate Β· embed| EXT
    AI -.->|hit / miss| Cache
    APP -.->|request logs| OBS
    AI -.->|LLM traces| OBS
Loading

The reasoning layer is a LangChain agent (create_agent) that routes each message to four tools via the LLM, one at a time, (no rule-based routing β€” the model chooses, which is what keeps it an agent). The tool docstrings are the routing prompt:

Tool Module Routed when
Product search (text-to-SQL) sql.py shopper asks about products β€” price, brand, rating, stock, "cheaper than X"
FAQ (RAG) faq.py shopper asks about store policy β€” delivery, returns, payment, cancellation
Saved items (add / remove / compare) compare.py anything about their shortlist β€” "save 2", "remove the Puma", "which of my saved is best"
Cart & orders (add_results_to_cart / add_to_cart / place / cancel / view) orders.py the cart, placing or cancelling an order, or what they have ordered before
(no tool) β€” greetings, thanks, opinions, questions about themselves β€” ordinary conversation

Actions are grouped into one tool each because they are the same kind of action on the same data. Saved items and orders stay separate on purpose: merging them is what makes a model confuse "which saved shoe is best?" with "buy it".

Four design points worth knowing:

  • Tool output is shown verbatim; the model never re-types it. Product lists, rating counts, price-age and unsupported-filter notes stream straight to the shopper. The model only decides the next tool, one at a time, and may add one short linking sentence, which is dropped if it restates prices, ratings or links. A single request stops after its tool with no extra model call. A cart action in the same message resolves only against that message's results, and asks when there are none.
  • user_id travels in the agent's runtime context, never as a tool argument β€” so the model cannot hallucinate one, or be talked into supplying someone else's, and read a stranger's shortlist. The raw message and the transcript ride there too: "save 2" means the second row of the list on screen, and neither the number nor the list survives the history-aware rewrite.
  • A tool is optional. When nothing fits, the model answers directly. Forcing a tool on every message is what made greetings get answered out of the returns policy; a zero-tool turn is logged instead, because a missing capability shows up as the model improvising rather than as an error.
  • The routing decision is cached in middleware. A cache hit returns a synthetic tool call, so the model is never invoked, but the tool still executes and streams exactly as it otherwise would.

Offline/ops scripts sit alongside the request path, not in it: refresh_products.py / discover_products.py (keep the catalogue live), admin_ingest_faqs.py (push FAQ vectors), evals/run_eval_v2.py (quality), and load_test.py (responsiveness) β€” see Project Structure.


πŸ”§ Tech Stack

Layer Technology
Frontend React 19 + Vite, Axios, React Markdown, Lucide Icons
Backend FastAPI, Uvicorn, Pydantic, SlowAPI
Jobs Postgres-backed queue (FOR UPDATE SKIP LOCKED), fair per-user claiming, depth-scaled concurrency
Agent LangChain 1.x create_agent (LangGraph-backed), 3 @tools
AI Models Gemini 2.5 Flash or Cloudflare Workers AI @cf/openai/gpt-oss-20b, with LLM_MODEL as the app-paid default and a per-user BYOK choice; Gemini 2.5 Pro as SQL fallback
Auth JWT (python-jose), bcrypt
Database PostgreSQL (Neon Cloud), SQLAlchemy ORM
Vector DB Pinecone (gemini-embedding-001, 1024-dim)
Logging Python logging module (structured, leveled)

πŸ› οΈ Setup & Execution

Prerequisites

  • Node.js (v18+) for frontend
  • Python 3.10+ for backend

1. Backend Setup

cd backend
pip install -r requirements.txt

Copy backend/app/.env.example to backend/app/.env and fill in your credentials.

Shoppers can save their own Gemini key or Cloudflare credentials in Settings (encrypted, never returned by the API); a selected user-paid provider lifts the daily caps.

If setting up your own Neon database, run backend/migrations.sql in its SQL editor.

Run the server:

uvicorn main:app --port 8000

2. Frontend Setup

cd frontend
npm install
npm run dev

Create frontend/.env (the deployed URL is the fallback in api.js, so this is only for local dev):

VITE_API_BASE_URL=http://localhost:8000/api

Open http://localhost:5173 in your browser.

3. Keeping product data fresh

The catalogue is scraped, so prices/stock drift. Both scripts are plain CLI, need no browser, and are safe to re-run (they process oldest-first):

python -m app.refresh_products --limit 500   # re-check prices/ratings/stock (2 workers)
python -m app.discover_products --query "running shoes for men" --pages 10   # find new products

.github/workflows/refresh.yml runs the refresh nightly at 00:00 IST (--limit 500), and can be triggered by hand. It needs one repo secret: DATABASE_URL.


πŸ“‘ API Endpoints

Key endpoints (all under /api, JWT except auth and health; full list in backend/main.py or /docs):

Method Endpoint Description
POST /auth/signup, /auth/login Create account / login (5/min, 10/min per username)
POST /chats/{id}/message Submit a message β†’ 202 + job_id (30/min, Idempotency-Key supported)
GET /jobs/{id}/events Stream the answer as SSE; ?after=<seq> resumes
GET/POST /saved, /cart Saved items / cart, joined to live prices
POST /orders Place an order from the cart; 409 if anything is out of stock
PUT /account/provider Choose FREE, GEMINI or CLOUDFLARE (own credentials for paid)

The message endpoint returns a job, not the answer; reconnect to /events?after=<seq> to resume. Photos go to the same endpoint as image (base64), image_mime and optional image_thumb. Every 429 carries Retry-After.


πŸ”’ Security

  • SQL Injection Prevention: LLM-generated SQL runs on a read-only PostgreSQL engine (SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY)
  • Password Security: bcrypt hashing with auto-migration from legacy SHA-256
  • JWT Auth: HS256 tokens expire after 12 hours (JWT_EXPIRY_HOURS). Login state uses tab-scoped sessionStorage: reload keeps the login, but closing the tab normally requires signing in again. Browser session restore can preserve tab storage; closing a tab does not revoke an issued JWT server-side.
  • Login Lockout: 5 failed logins within 15 minutes locks the username (DB-backed, holds across instances)
  • Concurrency-Safe Writes: chat updates take a row-level lock so simultaneous messages can't overwrite each other
  • Rate Limiting: signup 5/min and login 10/min per username; chat messages 30/min per user; provider selection and credential validation 10/min per user. Failed-login lockout remains separate.
  • Input Validation: Pydantic validators reject malformed/oversized inputs before they reach business logic
  • CORS: Whitelisted origins only

πŸ“ˆ Evaluation Results

backend/evals/run_eval_v2.py runs 160 shopper-style questions (53 easy, 54 medium, 53 hard) in-process: product search, store policy, saved items, cart and orders, follow-ups, photos, two requests in one message, and safety cases. DeepEval GEval with a gemini-3.5-flash-lite judge scores the answers; code checks PII leakage. App answers use gemini-2.5-flash (rewrite on gemini-3.1-flash-lite). The figures below merge 157 cases from the 2026-10-18 run with three new cases run separately. They are not a fresh 160-case run after the compare-results tool was removed.

All 160 cases:

Latency (s)TTFT mean (s)Cost per query ($)
p50p95p99meanp50p95p99mean
9.521.441.210.79.60.00110.00580.01210.0019
MetricCases scoredAbove thresholdRateMean score
Answer completeness16014791.9%0.92
Answer relevancy16015395.6%0.97
Faithfulness (answers with product, FAQ or saved-item evidence)979395.9%0.97
Toxicity (non-toxic)88100%β€”
Scope adherence1010100%β€”
PII leakage (leak-free)66100%β€”

Latency is in-process (no queue or network). Scores are LLM-judge, not human-calibrated. Source runs: backend/evals/results_v2/run_2026-10-18_160_merged_sources.json.

Where one query's time goes β€” "Puma running shoes under 3000", third of three runs in one process, caches off:

Step Time
Off-topic check (keyword fast path) 0.00s
Query rewrite (no history, skipped) 0.00s
Memory recall (Supermemory) 1.27s
Agent routing (model picks the tool) 2.35s
SQL generation (model) 1.85s
Postgres query (Neon) 0.89s
Formatting + streaming 0.94s
Total 7.3s

The first query in a fresh process took 16.2s: opening the Neon connection and the model clients. A route or SQL cache hit removes most of the two model steps.

cd backend
python evals/run_eval_v2.py --all            # all 160 cases
python evals/run_eval_v2.py --ids Q001,Q151  # selected cases
python evals/run_eval_v2.py --all --trace    # one Langfuse trace per case (environment=eval)

Load testing & capacity

Commands (run only with Render suspended for any job mode):

cd backend
python load_test_worker.py --render-suspended --jobs 40 --workers 2  # queue drain

Local k6 tests (2026-10-19, Render suspended). Real FastAPI, Neon queue and three separate workers Γ— 20 job slots; the AI is replaced by a 7.3s wait, so no model is called. Each virtual user logs in once with the correct password, then browses chats, saved items, cart, orders and credits every 5–10s, submits one message and polls it. Stepped levels ramp for 30s and hold for 180s. k6's default request timeout is 60s. These numbers include laptop-to-Neon latency and were measured with about 0.8 GB of free RAM; they are not production capacity claims.

Load: 25–150 users.

UsersRequestsErrorsLogin (s)Chats (s)Submit (s)
p50p95p99p50p95p99p50p95p99
252,8720%3.54.14.21.65.16.66.87.88.1
504,8010%2.64.04.04.16.69.210.013.814.2
755,0860%3.54.85.48.610.310.930.836.837.7
1004,8710%4.15.66.312.215.416.248.655.756.2
1505,1202.13%2.84.14.218.428.031.840.160.060.0
UsersJobsJob ID receivedQueue wait (s)
CreatedSucceededErroredClientsExpectedp50p95p99
252525025253.318.718.8
505050050502.727.849.3
757575075752.815.442.4
10010010001001002.916.219.5
15015015001501503.326.251.6

Before any job mode: suspend Render and stop other workers on the shared Neon queue (--render-suspended acknowledges this; the harness also refuses an active queue). Reports go to ignored backend/load_test_results/. Clean up an interrupted load_test.py run with --cleanup --cleanup-user.


🐳 CI/CD & Docker

GitHub Actions (.github/workflows/ci.yml) β€” four gated jobs on every push/PR:

  • backend: ruff lint + compileall syntax check + a no-network unit test (tests/test_logging.py). No API keys or DB needed, so it's fast and honest.
  • frontend: npm ci + eslint + vite build.
  • docker: builds both images (no push) with GitHub Actions cache, so a broken COPY or dependency breaks here rather than at compose up.
  • deploy: only on push to main, only after the other three succeed β€” triggers the Render deploy hooks (RENDER_DEPLOY_HOOK_* secrets; skips cleanly if unset).

Docker β€” docker compose up --build runs the API (backend/Dockerfile, uvicorn) and the frontend (frontend/Dockerfile, Vite build β†’ nginx) locally; Neon and Pinecone stay remote. Images are non-root, healthchecked against /api/health, and carry no secrets β€” credentials are injected at runtime via env_file.


πŸ“‚ Project Structure

β”œβ”€β”€ .github/workflows/ci.yml      # CI: backend + frontend + docker build + deploy
β”œβ”€β”€ docker-compose.yml            # Local stack: API + frontend (Neon/Pinecone remote)
β”œβ”€β”€ .dockerignore
β”œβ”€β”€ ruff.toml                     # Lint config (pinned default rule set; E402 for load-dotenv-before-import)
β”œβ”€β”€ tests/
β”‚   └── test_logging.py           # No-network unit test (request_id JSON logging)
β”‚
β”œβ”€β”€ frontend/
β”‚   β”œβ”€β”€ Dockerfile                # Vite build -> nginx static serve
β”‚   β”œβ”€β”€ nginx.conf                # SPA routing + asset caching
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ LandingPage.jsx   # Marketing landing page (entry point)
β”‚   β”‚   β”‚   β”œβ”€β”€ landing.css       # Landing page styles
β”‚   β”‚   β”‚   β”œβ”€β”€ Auth.jsx          # Login/Signup with password requirements
β”‚   β”‚   β”‚   β”œβ”€β”€ Sidebar.jsx       # Chats / Saved tabs, search, price-drop badge
β”‚   β”‚   β”‚   └── ChatArea.jsx      # Chat interface (SSE streaming, save buttons)
β”‚   β”‚   β”œβ”€β”€ api.js                # Axios config with JWT interceptor
β”‚   β”‚   β”œβ”€β”€ App.jsx               # Main app with session persistence
β”‚   β”‚   └── index.css             # Design tokens (:root) + app styles
β”‚   └── package.json
β”‚
β”œβ”€β”€ backend/
β”‚   β”œβ”€β”€ Dockerfile                # python:3.12-slim, uvicorn, non-root, healthcheck
β”‚   β”œβ”€β”€ main.py                   # FastAPI app, auth, chat + saved endpoints
β”‚   β”œβ”€β”€ migrations.sql            # Product indexes, scraped_at/availability/pid; drops legacy discount + index cols
β”‚   β”œβ”€β”€ evals/                    # 160-question eval (DeepEval GEval judge + code checks)
β”‚   β”œβ”€β”€ load_test.py              # API browse ramp, saturation and real calibration
β”‚   β”œβ”€β”€ load_test_worker.py       # stubbed queue drain and multi-process claims
β”‚   β”œβ”€β”€ requirements.txt
β”‚   β”œβ”€β”€ app/
β”‚   β”‚   β”œβ”€β”€ agent.py              # LangChain create_agent, 4 tools, multi-step routing + route-cache middleware
β”‚   β”‚   β”œβ”€β”€ llm_provider.py       # Provider switch (LLM_MODEL): Gemini or Cloudflare chat model
β”‚   β”‚   β”œβ”€β”€ jobs.py               # Postgres job queue: claim, lease, events, usage, reaper
β”‚   β”‚   β”œβ”€β”€ worker.py             # The worker; runs the agent outside the request, scales with queue depth
β”‚   β”‚   β”œβ”€β”€ memory.py             # Context-aware query optimization
β”‚   β”‚   β”œβ”€β”€ sql.py                # Text-to-SQL pipeline (gemini-2.5-flash, Pro fallback)
β”‚   β”‚   β”œβ”€β”€ compare.py            # Saved items: add from results, remove, compare; reference resolution
β”‚   β”‚   β”œβ”€β”€ orders.py             # Cart + simulated orders; place/cancel shared with the HTTP endpoints
β”‚   β”‚   β”œβ”€β”€ order_history.py      # Order summary (one query, totals and spend)
β”‚   β”‚   β”œβ”€β”€ memory_store.py       # Long-term memory via Supermemory, scoped per user, fail-open
β”‚   β”‚   β”œβ”€β”€ cache.py              # Postgres-backed LLM response cache
β”‚   β”‚   β”œβ”€β”€ observability.py      # Optional Langfuse LLM tracing, fail-open
β”‚   β”‚   β”œβ”€β”€ logging_setup.py      # Structured JSON logs correlated by request_id
β”‚   β”‚   β”œβ”€β”€ llm_utils.py          # Retry/backoff for transient LLM errors
β”‚   β”‚   β”œβ”€β”€ refresh_products.py   # Re-check prices/stock via JSON-LD
β”‚   β”‚   β”œβ”€β”€ discover_products.py  # Find newly listed products
β”‚   β”‚   β”œβ”€β”€ faq.py                # RAG pipeline with Pinecone (gemini-2.5-flash)
β”‚   β”‚   β”œβ”€β”€ admin_ingest_faqs.py  # FAQ vector ingestion script
β”‚   β”‚   └── db/
β”‚   β”‚       β”œβ”€β”€ database.py       # SQLAlchemy engines (read-write + read-only)
β”‚   β”‚       └── models.py         # ORM models (EcommerceAccount, Chat, Message, SavedProduct, LLMCache, LoginFailure)
β”‚   └── app/resources/
β”‚       β”œβ”€β”€ faq_data.csv          # FAQ knowledge base
β”‚       └── ecommerce_data_final.csv
β”‚
└── web-scrapping/                # Flipkart data collection scripts

πŸ“œ License

MIT License β€” see LICENSE. Copyright (c) 2026 Vivek P Marakumbi.

About

An AI-powered e-commerce agent that can handle complex user queries about products, inventory, and order status. Built with a FastAPI backend and a React (Vite) frontend, featuring integration with LLMs for natural language understanding and automated task execution.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages