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

276 Commits

Folders and files

Repository files navigation

🛒 E-Commerce Agent (React + FastAPI)

An AI shopping assistant over a scraped Flipkart catalogue, with a React frontend and FastAPI backend.


🚀 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

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 agent (create_agent) picks one tool per step; 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

Saved items and orders are separate tools so the model doesn't confuse "which saved shoe is best?" with "buy it".

Design points:

  • Tool output is shown verbatim. Product lists, ratings and prices stream straight from the tool. The model may add one linking sentence, dropped if it restates prices, ratings or links. Single-request messages stop after one tool with no extra model call.
  • user_id comes from runtime context, never a tool argument, so the model can't invent or swap it. The raw message and transcript ride there too, so "save 2" resolves against the list on screen.
  • A tool is optional. Greetings and small talk get a direct answer; zero-tool turns are logged to spot missing capabilities.
  • Routing is cached in middleware. A cache hit skips the model; the tool still runs and streams as usual.

Offline scripts (not in the request path): refresh_products.py, discover_products.py, admin_ingest_faqs.py, evals/run_eval_v2.py and load_test.py.


🔧 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), 4 @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

From backend/, copy app/.env.example to 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) and are kept in tab-scoped sessionStorage. Closing the tab doesn't revoke the token 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.
  • 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, multi-part messages and safety. gemini-2.5-flash answers (gemini-2.5-pro as SQL fallback, gemini-3.1-flash-lite for follow-up rewrites and splitting); a gemini-3.5-flash-lite DeepEval GEval judge scores them, and code checks PII leakage. The figures merge 157 cases from the 2026-10-18 run with three newer cases on the same models, so they are not a fresh 160-case run after the compare-results tool was removed.

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 measures in-process execution, including model and database calls but excluding the job queue and client-to-server transit. Scores are LLM-judge, not human-calibrated. Run sources are listed in 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 workers × 20 job slots; the model is replaced by a 7.3s wait. Each virtual user logs in, browses every 5–10s, submits one message and polls it. Each level ramps 30s and holds 180s; k6 times out requests at 60s. Run from a laptop against Neon with about 0.8 GB free RAM, so these are not production capacity numbers.

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 + compileall + a no-network unit test (tests/test_logging.py); no API keys or DB needed.
  • frontend: npm ci + eslint + vite build.
  • docker: builds both images (cached, no push).
  • deploy: on push to main after the other three pass, triggers the Render deploy hooks (RENDER_DEPLOY_HOOK_* secrets; skipped 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