Poomchaio/LLM-UI

★ 0Forks 0TypeScriptGitHub ↗Compare

README

Financial Chat App

A chat web app where signed-in users ask questions about revenue/income of US public companies, grounded in a Postgres database via OpenAI tool-calling (no hallucinated numbers), with SSE streaming, visible tool-call rendering, Redis-backed usage limits, and conversation management.

Prerequisites

  • Node.js 20+
  • pnpm 9+ (corepack enable will install the pinned version from package.json's packageManager field)
  • Docker (for Postgres + Redis via docker-compose.yml)
  • An OpenAI API key

Setup

  1. Install dependencies:

    pnpm install
  2. Copy the env file and fill in your OpenAI key:

    cp apps/backend/.env.example apps/backend/.env

    Edit apps/backend/.env and set OPENAI_API_KEY. Defaults for everything else (DATABASE_URL, REDIS_URL, JWT_SECRET, OPENAI_MODEL=gpt-4.1-nano, USAGE_LIMIT_USD=1, USAGE_RESET_SECONDS=3600) work out of the box with the Docker Compose services below.

  3. Bring up Postgres and Redis:

    docker compose up -d
  4. Run database migrations, then load the provided financial data:

    pnpm --filter @financial-chat/backend migrate
    pnpm --filter @financial-chat/backend load-data

    load-data runs data/financial_data.sql directly against Postgres (it drops and recreates the financial_data table from the dump, unmodified) — safe to re-run at any time.

  5. Start the backend and frontend (in separate terminals):

    pnpm dev:backend    # http://localhost:4000
    pnpm dev:frontend   # http://localhost:5173
  6. Open http://localhost:5173, register an account, and start chatting.

Running tests

pnpm test                                    # every workspace
pnpm --filter @financial-chat/backend test   # backend only (requires docker compose up -d)
pnpm --filter @financial-chat/frontend test  # frontend only
pnpm --filter @financial-chat/shared test    # shared types only

Backend tests hit real Postgres and Redis instances via DATABASE_URL/REDIS_URL, so docker compose up -d must be running first.

Configuring usage limits

Two env vars in apps/backend/.env control the per-window spend gate (Redis key usage:{userId}:{windowKey}, windowKey = floor(now / 1000 / USAGE_RESET_SECONDS)):

  • USAGE_LIMIT_USD (default 1) — dollar ceiling per window before requests are rejected with a friendly SSE error event.
  • USAGE_RESET_SECONDS (default 3600) — window length in seconds; windows are clock-aligned, not per-user-rolling.

For fast manual testing of the limit-exceeded flow (assignment scenario S4), temporarily set:

USAGE_LIMIT_USD=0.001
USAGE_RESET_SECONDS=180

then restart the backend, send a couple of chat messages, and observe the rejection — then restore the defaults.

Model configuration

OPENAI_MODEL defaults to gpt-4.1-nano (cheapest tier, function-calling capable). gpt-4o-mini is a documented alternative — set OPENAI_MODEL=gpt-4o-mini in apps/backend/.env to switch; pricing for both is hardcoded in apps/backend/src/pricing/pricing-table.ts.

Architecture

/apps/backend    — Express + TypeScript + Drizzle ORM + ioredis + openai + jsonwebtoken + bcrypt + tiktoken
/apps/frontend   — React + TypeScript (Vite) + Tailwind + shadcn/ui + recharts + react-router-dom + React Query
/packages/shared — shared types (SSE event shapes, message part shapes)
docker-compose.yml — Postgres 16 + Redis 7

Two-tool pattern for grounding: query_financials (whitelisted Drizzle query builder, no LLM-authored SQL) and render_answer (structured { text, table?, chart? } output).

Known simplifications

  • No CSRF token (sameSite=lax cookie only) — acceptable for a local single-origin dev app.
  • No login rate limiting / brute-force protection.
  • No lifetime/historical usage view beyond the current-window badge (Postgres stores cost_usd per message if this is ever needed later).
  • Fixed clock-aligned usage windows, not per-user-rolling.

Contributors

Poomchaio

Issues