diff --git a/README.md b/README.md index c811b58..b14cd7c 100644 --- a/README.md +++ b/README.md @@ -1,441 +1,340 @@ -# InsightIQ πŸ” - -> **Ask your SaaS data anything. In plain English.** +# InsightIQ [![CI](https://github.com/AttiR/InsightIQ/actions/workflows/ci.yml/badge.svg)](https://github.com/AttiR/InsightIQ/actions/workflows/ci.yml) [![CD](https://github.com/AttiR/InsightIQ/actions/workflows/cd.yml/badge.svg)](https://github.com/AttiR/InsightIQ/actions/workflows/cd.yml) -[![Python](https://img.shields.io/badge/Python-3.11+-3776AB?style=flat&logo=python&logoColor=white)](https://python.org) -[![FastAPI](https://img.shields.io/badge/FastAPI-0.110+-009688?style=flat&logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com) -[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-4169E1?style=flat&logo=postgresql&logoColor=white)](https://postgresql.org) +[![Python](https://img.shields.io/badge/Python-3.12-3776AB?style=flat&logo=python&logoColor=white)](https://python.org) +[![FastAPI](https://img.shields.io/badge/FastAPI-0.115-009688?style=flat&logo=fastapi)](https://fastapi.tiangolo.com) [![React](https://img.shields.io/badge/React-18-61DAFB?style=flat&logo=react&logoColor=black)](https://react.dev) -[![Docker](https://img.shields.io/badge/Docker-Compose-2496ED?style=flat&logo=docker&logoColor=white)](https://docker.com) -[![Claude](https://img.shields.io/badge/Claude-Sonnet_4.5-CC785C?style=flat)](https://anthropic.com) -[![License](https://img.shields.io/badge/License-MIT-green?style=flat)](LICENSE) +[![Azure](https://img.shields.io/badge/Azure-Container_Apps-0078D4?style=flat&logo=microsoftazure)](https://azure.microsoft.com) +[![License](https://img.shields.io/badge/License-MIT-6B7280?style=flat)](LICENSE) + +**Ask any business question in plain English. Get SQL, a chart, and a written insight in seconds.** -An AI-powered analytics agent for SaaS business managers. Type a question in plain English β€” get a PostgreSQL query, a chart, and a business insight in under 5 seconds. No SQL knowledge required. +Live: https://insightiq-frontend.orangemeadow-25c8b891.westeurope.azurecontainerapps.io --- -## ❗ The Problem +## Demo -Business managers at SaaS companies are blocked from their own data. + -| Situation | Reality | -|-----------|---------| -| CEO wants MRR trend for Q3 | Waits 2–3 days for an analyst to run the query | -| CS Manager wants churn risk list | Has no access to write SQL | -| Product Manager asks "which plan drives expansion?" | Engineers are too busy to help | +> Sign in with Google β†’ ask a plain English question β†’ chart + AI insight in under 5 seconds. +> Click a history item to reload it from cache β€” no API call, instant result. -**The result:** decisions made on gut feel, not data. Analysts spend 60% of their time on ad-hoc query requests instead of strategic analysis. +--- -**InsightIQ fixes this.** Any manager can get a data-backed answer β€” with a chart and a written business interpretation β€” in seconds. +## The problem -> πŸ“Š *"Reduces ad-hoc analytics requests to engineering by ~80%"* +Business managers at SaaS companies are blocked from their own data. A question like "which customers are at churn risk?" requires writing SQL β€” or waiting two days for an analyst to do it. Analysts spend the majority of their time on ad-hoc query requests rather than strategic work. ---- +InsightIQ removes that dependency entirely. A manager types a question, the system generates and executes the SQL, selects the right chart type, and returns a written business interpretation β€” without any technical knowledge required. -## ✨ Features +--- -### πŸ”„ Self-Healing SQL Loop -The agent's most production-critical feature. If generated SQL fails (wrong column name, syntax error, type mismatch), the error message and schema are automatically fed back to Claude for correction β€” up to 3 retry attempts. Every attempt is logged with timing. +## How it works ``` User question β”‚ β–Ό +Schema injection ──── full Postgres schema + 3 sample values per column + β”‚ baked into the Claude system prompt + β–Ό Claude generates SQL β”‚ β–Ό -Read-only guard validates ──► BLOCKED if not SELECT - β”‚ +Read-only guard ──── token-level parse: first token must be SELECT + β”‚ secondary scan for DROP / DELETE / INSERT / UPDATE β–Ό -Execute against PostgreSQL - β”‚ - β”Œβ”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Error? β”‚ Yes ──► Feed error + schema back to Claude - β”‚ β”‚ Retry (max 3Γ—) ──────────────────┐ - β””β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β”‚ No β”‚ - β–Ό β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -Return results +Execute via asyncpg pool β”‚ + β”Œβ”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Execution error? β”‚ Yes ──► error + failed SQL fed back to Claude + β”‚ β”‚ retry up to 3Γ— with full context + β””β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ No β–Ό -Smart chart selection (line / bar / pie / table) +Results cached to query_history (rows + columns + explanation as JSONB) β”‚ β–Ό -AI insight generation (2-sentence business interpretation) +Chart type derived from data shape: + time column present β†’ line + 1 string + 1 numeric ≀8 rows β†’ pie + 1 string + 1 numeric >8 rows β†’ bar + everything else β†’ table β”‚ β–Ό -Store in query_history audit log +Response returned with SQL, rows, explanation, execution_ms, retry count ``` -### πŸ›‘οΈ Read-Only Guard -All SQL is parsed and validated before execution. Only `SELECT` statements are permitted. `INSERT`, `UPDATE`, `DELETE`, `DROP`, and `TRUNCATE` are blocked at the application layer β€” not just by Postgres permissions. Zero risk of accidental data modification. - -### πŸ’‘ AI Insight Layer -Raw query results are passed to a second Claude call that generates a 2-sentence business interpretation: - -> *"Monthly recurring revenue grew 12.4% over the last quarter, reaching $284,000 in September. Growth is primarily driven by Business plan expansions rather than new customer acquisition, suggesting strong product-market fit with mid-market customers."* - -### πŸ“Š Smart Chart Selection -The agent analyses the result schema and decides the best visualisation: -- **Line chart** β€” time series data (trends, MRR over time) -- **Bar chart** β€” categorical comparisons (revenue by plan, tickets by priority) -- **Pie chart** β€” proportional distributions (plan mix, churn reasons) -- **Data table** β€” list results (customer lists, invoice records) - -### πŸ“ Query History & Audit Log -Every question, generated SQL, result summary, execution time, and retry count is stored in a `query_history` table. Managers can replay past queries. Engineers can audit what the agent generated. - -### ⚑ Schema-Aware Prompting -The full PostgreSQL schema β€” including table names, column types, foreign key relationships, indexes, and 3 sample values per column β€” is injected into every Claude prompt. The model never hallucinates column names that don't exist. - --- -## Architecture +## System architecture ``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ React Frontend β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Natural language input β”‚ Chart β”‚ Insight β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ HTTP POST /api/query - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ FastAPI Backend β”‚ -β”‚ β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ SQL Agent (agent/sql_agent.py) β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ 1. Load schema from PostgreSQL β”‚ β”‚ -β”‚ β”‚ 2. Build prompt: schema + question β”‚ β”‚ -β”‚ β”‚ 3. Claude β†’ SQL generation β”‚ β”‚ -β”‚ β”‚ 4. Read-only guard validation β”‚ β”‚ -β”‚ β”‚ 5. Execute via asyncpg pool β”‚ β”‚ -β”‚ β”‚ 6. Self-healing loop on error (max 3Γ—) β”‚ β”‚ -β”‚ β”‚ 7. Claude β†’ chart type selection β”‚ β”‚ -β”‚ β”‚ 8. Claude β†’ business insight generation β”‚ β”‚ -β”‚ β”‚ 9. Store in query_history β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Anthropic SDKβ”‚ β”‚ asyncpg Connection Pool β”‚ β”‚ -β”‚ β”‚ Claude claude-sonnet-4-5β”‚ β”‚ (min=2, max=10 connections)β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ PostgreSQL 16 (Docker) β”‚ -β”‚ β”‚ -β”‚ customers subscriptions events invoices tickets β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ query_history β”‚ Views: customer_health, monthly_mrr β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +Browser + β”‚ HTTPS β€” managed TLS via Azure Container Apps + β”‚ + β”œβ”€β–Ί Frontend Container App (nginx + React 18 + TypeScript) + β”‚ Serves static bundle only. No backend proxy. + β”‚ VITE_API_BASE_URL is baked into the bundle at build time. + β”‚ All API calls go directly: browser β†’ backend HTTPS URL. + β”‚ + └─► Backend Container App (FastAPI + asyncpg + Claude) + min-replicas: 1 (always warm β€” no cold start on demo) + max-replicas: 3 (HPA on CPU) + β”‚ + β–Ό + Neon Serverless PostgreSQL + Connection pooling via asyncpg (min=2, max=10) + sslmode=require enforced at connection string level +``` + +**CI/CD:** +``` +Pull request β†’ ruff lint Β· pytest unit tests Β· tsc typecheck Β· Trivy scan +Merge to main β†’ Neon migrations Β· deploy backend Β· deploy frontend Β· smoke test Β· rollback on failure +Daily 09:00 β†’ Anthropic cost check β†’ GitHub issue if daily spend > $5 ``` --- -## ⚑ Tech Stack +## Engineering decisions -| Layer | Technology | Why this choice | -|-------|-----------|-----------------| -| **LLM** | Claude claude-sonnet-4-5 | Superior instruction-following for structured SQL output. Consistent JSON responses for tool use. | -| **Backend** | FastAPI + Python 3.11 | Async-native, automatic OpenAPI docs, Pydantic v2 validation. Production-grade with minimal boilerplate. | -| **Database** | PostgreSQL 16 | Industry standard for SaaS analytics. JSONB for event properties, window functions for MRR calculations, mature ecosystem. | -| **DB Driver** | asyncpg | Fastest async PostgreSQL driver. Connection pooling built-in. 3Γ— faster than psycopg2 for concurrent requests. | -| **Charts** | Plotly | Interactive charts out of the box. Handles all chart types with one API. No D3 complexity. | -| **Frontend** | React 18 + Tailwind | Component model maps naturally to the query/result/chart pattern. Tailwind speeds up UI iteration. | -| **Container** | Docker Compose | One command to start the full stack. Reproducible across machines. Healthcheck ensures Postgres is ready before API starts. | +### Multi-app networking in Azure Container Apps ---- +Docker Compose and Azure Container Apps have a different networking model. In Docker Compose, services share a bridge network β€” `http://backend:8000` works because `backend` resolves to the container hostname. In ACA, each Container App is isolated on its own network and has its own public HTTPS URL. There is no shared hostname. -## πŸš€ Quick Start +The consequence: an nginx `proxy_pass http://insightiq-backend:8000` that works locally fails silently in production with a 502. The fix is architectural, not a config tweak. nginx in production serves only static files. The React bundle calls the backend at its full ACA URL, injected via `VITE_API_BASE_URL` at build time. The backend CORS config must explicitly allow the frontend's ACA domain, injected as `FRONTEND_URL` at runtime by the CD pipeline. -**Prerequisites:** Docker Desktop, Python 3.11+, Node.js 18+ +| | Docker Compose (local) | Azure Container Apps (production) | +|--|------------------------|-----------------------------------| +| Frontend β†’ Backend | nginx `proxy_pass http://backend:8000` | React calls full HTTPS ACA URL directly | +| Backend hostname | Docker bridge DNS | Public ACA FQDN | +| CORS required | No (same-origin via proxy) | Yes (separate domains) | +| nginx role | Reverse proxy + SPA server | SPA server only | -```bash -# 1. Clone and configure -git clone https://github.com/YOURUSERNAME/InsightIQ.git -cd InsightIQ -cp .env.example .env -# Add your ANTHROPIC_API_KEY to .env +### Self-healing SQL capped at 3 retries -# 2. Start the full stack -docker compose up -d -python database/seed.py +When generated SQL fails, the error message and the failed SQL are appended to the Claude conversation and a new attempt is made. The loop runs up to 3 times. -# 3. Start the API -cd backend && pip install -r requirements.txt -uvicorn main:app --reload +The cap is deliberate. A fourth retry rarely succeeds where three have failed β€” at that point the issue is the question itself (ambiguous phrasing, references data that doesn't exist) or a schema mismatch that requires human intervention, not more LLM calls. Retrying further burns API credits on unrecoverable queries and degrades the user experience with a long wait before an eventual failure. The retry count is persisted in `query_history` and surfaced in the UI β€” monitoring retry rates is a useful proxy for prompt quality degradation over time. -# 4. Start the frontend (new terminal) -cd frontend && npm install && npm run dev -``` +### Result caching eliminates redundant LLM calls -Open `http://localhost:5173` and ask your first question: +Query results (rows, columns, explanation) are stored as JSONB in `query_history` on first execution. Replaying a past query via `GET /api/history/:id` is a free PostgreSQL read β€” Claude is not called again. -> *"Which customers have the highest MRR and haven't logged in for 30 days?"* +Without this, every click on a history item would trigger a full Claude round-trip. At claude-sonnet-4-5 pricing (~$3/1M input tokens, $15/1M output tokens), replaying a 10-query session could cost as much as running it for the first time. The cache hit is surfaced in the UI with a visual indicator so the cost saving is transparent. ---- +### Atomic rate limiting without Redis -## πŸ“Š Database Schema +Each user has a `query_limit` (default: 2) and `total_queries` counter stored in PostgreSQL. The check and increment happen in one statement: -``` -customers (1,000 rows) -β”œβ”€β”€ id, company_name, email -β”œβ”€β”€ plan: starter | pro | business | enterprise -β”œβ”€β”€ country, industry, employee_count -β”œβ”€β”€ health_score (0–100) -└── created_at, churned_at, is_active - -subscriptions -β”œβ”€β”€ customer_id β†’ customers.id -β”œβ”€β”€ plan, mrr, status, billing_cycle -└── start_date, end_date, cancelled_at - -events (~50,000 rows) -β”œβ”€β”€ customer_id β†’ customers.id -β”œβ”€β”€ event_type: login | dashboard_view | report_export | ... -└── properties (JSONB), created_at - -invoices (~3,000 rows) -β”œβ”€β”€ customer_id β†’ customers.id -β”œβ”€β”€ amount, currency, status -└── due_date, paid_at - -support_tickets (~4,000 rows) -β”œβ”€β”€ customer_id β†’ customers.id -β”œβ”€β”€ priority: low | medium | high | critical -β”œβ”€β”€ category: billing | technical | onboarding | ... -└── status, resolved_at - -query_history (audit log) -β”œβ”€β”€ question, generated_sql, result_summary -β”œβ”€β”€ row_count, execution_ms, retries -└── created_at +```sql +UPDATE users +SET total_queries = total_queries + 1 +WHERE id = $1 + AND is_active = TRUE + AND total_queries < query_limit +RETURNING id, total_queries, query_limit ``` -**Views:** -- `customer_health` β€” joins customers + active subscription + 30-day events + open tickets -- `monthly_mrr` β€” pre-aggregated MRR by month across all active subscriptions +If the `UPDATE` returns no rows, the limit has been reached. If it returns a row, the request is permitted. This is atomic β€” concurrent requests from the same user cannot both pass the check. No Redis, no distributed lock, no separate check-then-increment race condition. ---- - -## πŸ’¬ Example Questions +### Schema-aware prompting with sample values -| Question | What the agent does | -|----------|-------------------| -| *"What is our MRR trend for the last 6 months?"* | Queries subscriptions, groups by month, returns line chart | -| *"Which customers haven't logged in for 30 days?"* | Joins customers + events, filters by last login date | -| *"Show me churn rate by plan"* | Counts churned vs active customers per plan, pie chart | -| *"Which 10 customers have the most overdue invoices?"* | Joins invoices, filters overdue, sorts by count | -| *"What's our expansion MRR this quarter?"* | Compares MRR changes between subscription periods | -| *"List enterprise customers with low health scores"* | Filters customers table with health_score < 40 | - ---- +The full PostgreSQL schema is injected into every Claude system prompt: table names, column names, data types, foreign key relationships, and 3 sample values per column. Sample values are particularly important β€” without them, Claude has to guess domain vocabulary. With them, it knows that `plan` contains `'starter'`, `'pro'`, `'business'`, `'enterprise'` rather than inventing values. -## πŸ”’ Production Decisions +The schema is cached in memory with a 5-minute TTL. An in-process cache is sufficient at this scale β€” no Redis needed, and cache invalidation on schema change happens naturally on pod restart. -**Read-only enforcement** β€” SQL is validated at the application layer before execution. Only `SELECT` statements pass. This is defence-in-depth: the database user also has read-only permissions. +### asyncpg over psycopg2 -**Connection pooling** β€” `asyncpg` pool with `min_size=2, max_size=10`. Connections are reused across requests. The pool warms up on application start, not on first request. +asyncpg uses PostgreSQL's binary protocol rather than the text protocol used by psycopg2. This produces a measurable throughput difference for concurrent requests. More importantly, asyncpg is natively async β€” it integrates with FastAPI's event loop without the thread pool workarounds required by psycopg2 in an async context. The `asyncpg.create_pool()` pool is initialised during FastAPI lifespan startup, so connections are warm before the first request arrives. -**Schema caching** β€” The database schema (tables, columns, types, sample values) is loaded once on startup and cached in memory. Injected into every prompt without a database round-trip per query. +### Read-only guard: token parsing, not regex -**Retry logging** β€” Every SQL retry is logged with the original error, the corrected SQL, and the attempt number. Operations teams can monitor retry rates as a proxy for prompt quality degradation. +SQL validation uses token-level parsing rather than a regex pattern match on the query string. Comments are stripped first (`--` and `/* */`), then the first token is extracted and compared. A regex like `re.match(r'SELECT', sql, re.IGNORECASE)` can be bypassed with leading whitespace, comments, or encoding tricks. Token parsing handles these cases correctly. -**Timeout guard** β€” All database queries have a 30-second timeout. Long-running analytical queries are cancelled rather than blocking the connection pool. +A secondary scan checks for dangerous keywords (`DROP`, `DELETE`, `INSERT`, `UPDATE`, `TRUNCATE`, `ALTER`) anywhere in the query after comment removal, catching semicolon-separated injection attempts like `SELECT 1; DROP TABLE customers`. --- -## πŸ“ Project Structure +## Data model ``` -InsightIQ/ -β”œβ”€β”€ backend/ -β”‚ β”œβ”€β”€ app/ -β”‚ β”‚ β”œβ”€β”€ api/ -β”‚ β”‚ β”‚ └── query.py # POST /api/query endpoint -β”‚ β”‚ β”œβ”€β”€ agent/ -β”‚ β”‚ β”‚ β”œβ”€β”€ sql_agent.py # Core agent: generation + healing -β”‚ β”‚ β”‚ β”œβ”€β”€ schema_loader.py # Postgres schema β†’ prompt context -β”‚ β”‚ β”‚ β”œβ”€β”€ chart_selector.py # LLM chart type selection -β”‚ β”‚ β”‚ └── insight_gen.py # Business insight generation -β”‚ β”‚ └── db/ -β”‚ β”‚ β”œβ”€β”€ pool.py # asyncpg connection pool -β”‚ β”‚ └── executor.py # Query execution + history logging -β”‚ β”œβ”€β”€ main.py -β”‚ └── requirements.txt -β”œβ”€β”€ frontend/ -β”‚ └── src/ -β”‚ β”œβ”€β”€ components/ -β”‚ β”‚ β”œβ”€β”€ QueryInput.jsx # Natural language input -β”‚ β”‚ β”œβ”€β”€ ChartView.jsx # Plotly chart renderer -β”‚ β”‚ β”œβ”€β”€ InsightCard.jsx # AI business interpretation -β”‚ β”‚ └── QueryHistory.jsx # Past queries panel -β”‚ └── App.jsx -β”œβ”€β”€ database/ -β”‚ β”œβ”€β”€ init.sql # Schema + indexes + views -β”‚ └── seed.py # 1000 customers + realistic data -β”œβ”€β”€ docker-compose.yml -β”œβ”€β”€ .env.example -└── README.md +customers 1,000 rows β€” company, plan, country, health_score, churn date + └── subscriptions MRR, billing cycle, status, cancellation + └── events ~50K product usage: login, feature_used, report_export, ... + └── invoices ~3K amount, status: paid | overdue | failed + └── support_tickets ~4K priority: low | medium | high | critical + +users registered users β€” google_id, email, query budget + └── query_history every question, generated SQL, results (JSONB), timing, retries + +Views: + customer_health joins customers + active subscription + 30-day events + open tickets + monthly_mrr pre-aggregated MRR by month across active subscriptions ``` --- -## πŸ—ΊοΈ Roadmap +## Secrets management -- [x] PostgreSQL schema + seed data -- [x] FastAPI backend + asyncpg pool -- [x] SQL agent with self-healing loop -- [x] Chart generation + AI insight layer -- [x] React UI + query history -- [x] Docker Compose full stack -- [x] GitHub Actions CI pipeline -- [x] Production deploy β€” Azure Container Apps + Neon + ghcr.io (OIDC CD) -- [x] Azure Monitor alerts + Anthropic cost monitor -- [ ] Custom domain + HTTPS (optional) -- [ ] Row-level security / multi-tenant workspaces (optional) +No credentials are stored in the repository. + +``` +Source (public) GitHub Secrets (CI/CD) Azure runtime (Container App) +──────────────── ────────────────────────── ────────────────────────────── +Source code AZURE_CLIENT_ID OIDC ANTHROPIC_API_KEY secretref: +Dockerfiles NEON_DATABASE_URL JWT_SECRET_KEY secretref: +nginx.conf ANTHROPIC_API_KEY DATABASE_URL secretref: +Workflow definitions JWT_SECRET_KEY GOOGLE_CLIENT_ID secretref: + GOOGLE_CLIENT_ID FRONTEND_URL plain env + FRONTEND_URL +``` + +Azure authentication uses OIDC federated credentials β€” GitHub receives a short-lived token exchange rather than storing a service principal password. The credential is scoped to this repository and branch only. --- -## πŸš€ Production Deployment +## Monitoring -[![CI](https://github.com/AttiR/InsightIQ/actions/workflows/ci.yml/badge.svg)](https://github.com/AttiR/InsightIQ/actions/workflows/ci.yml) -[![CD](https://github.com/AttiR/InsightIQ/actions/workflows/cd.yml/badge.svg)](https://github.com/AttiR/InsightIQ/actions/workflows/cd.yml) +**Azure Monitor** β€” email alerts on: +- Backend CPU > 80% for 5 minutes +- HTTP 5xx rate > 10 in 5 minutes (severity 1) +- P95 response latency > 10 seconds -**Live demo:** [https://insightiq-frontend.orangemeadow-25c8b891.westeurope.azurecontainerapps.io](https://insightiq-frontend.orangemeadow-25c8b891.westeurope.azurecontainerapps.io) +**Anthropic cost monitor** β€” GitHub Actions workflow on a daily schedule. Queries the Anthropic usage API, calculates spend, and opens a GitHub issue automatically if daily cost exceeds $5. The issue includes a SQL template to identify unusual query volume in `query_history`. -### Infrastructure +**Azure budget** β€” email notification at 80% and 100% of $30/month. -| Component | Service | Cost | -|-----------|---------|------| -| **Backend** | Azure Container Apps (scale to zero) | ~$3–8/mo | -| **Frontend** | Azure Container Apps (scale to zero) | ~$1–3/mo | -| **Database** | Neon Serverless PostgreSQL (free tier) | $0 | -| **Secrets** | GitHub Secrets + Azure env injection | $0 | -| **CI/CD** | GitHub Actions (free tier) | $0 | -| **Total** | | **~$5–12/month** | +--- -### CI/CD Pipeline +## Tech stack -``` -Developer pushes β†’ GitHub Actions CI runs: - β”œβ”€β”€ ruff lint + format check - β”œβ”€β”€ pytest unit tests (no API, no DB) - β”œβ”€β”€ TypeScript typecheck - β”œβ”€β”€ Vite production build - └── Trivy security scan (advisory) - -Merge to main β†’ GitHub Actions CD runs: - β”œβ”€β”€ Apply Neon DB migrations - β”œβ”€β”€ Build + deploy backend Container App - β”œβ”€β”€ Build + deploy frontend Container App (with backend URL injected) - β”œβ”€β”€ Smoke test HTTPS endpoints - └── Auto-rollback on failure -``` +| Layer | Choice | Rationale | +|-------|--------|-----------| +| LLM | Claude claude-sonnet-4-5 | Consistent structured JSON output; superior instruction-following for SQL generation | +| Backend | FastAPI + Python 3.12 | Async-native; automatic OpenAPI docs; Pydantic v2 validation at boundary | +| DB driver | asyncpg | Binary protocol; native async; 3Γ— psycopg2 throughput for concurrent queries | +| Database | PostgreSQL 16 (Neon) | JSONB for event properties; window functions for MRR; Neon free tier for production | +| Frontend | React 18 + TypeScript (strict) | `noImplicitAny`, `noUnusedLocals` enforced; zero `any` types | +| Auth | Google OAuth + JWT (HS256) | No passwords stored; server-side token verification; single provider simplifies codebase | +| Styling | Tailwind CSS + Tabler Icons | Utility-first; no custom CSS; consistent design system | +| Charts | Chart.js (lazy-loaded) | Keeps initial bundle lean; bar/line/pie with one API | +| CI/CD | GitHub Actions + Azure Container Apps | OIDC auth; no stored credentials; ~2 min CI; scale-to-zero cost model | -### Secrets Management +--- -Zero secrets stored in the repository. All runtime secrets are injected via: +## Local development -- **GitHub Secrets** β†’ CI/CD pipeline variables -- **Azure Container Apps secrets** β†’ runtime environment variables -- **OIDC authentication** β†’ Azure login without stored credentials -``` -Repository (public-safe) GitHub Secrets Azure Runtime -───────────────────────── ────────────────────── ────────────────── -Source code AZURE_CLIENT_ID ANTHROPIC_API_KEY -Dockerfiles NEON_DATABASE_URL JWT_SECRET_KEY -nginx.conf JWT_SECRET_KEY DATABASE_URL -Workflow definitions ANTHROPIC_API_KEY GOOGLE_CLIENT_ID -``` +```bash +git clone https://github.com/AttiR/InsightIQ +cd InsightIQ +cp .env.example .env # add ANTHROPIC_API_KEY, GOOGLE_CLIENT_ID, JWT_SECRET_KEY -### Monitoring & Cost Alerts +# Postgres +docker compose up -d +psql $DATABASE_URL -f database/migrations/002_auth.sql +python3 database/seed.py # 1,000 customers + ~60K related rows + +# Backend +cd backend +python3 -m venv venv && source venv/bin/activate +pip install -r requirements.txt +uvicorn main:app --reload --port 8000 + +# Frontend (new terminal) +cd frontend +npm install +npm run dev # http://localhost:5173 +``` -**Azure Monitor** β€” alerts on: -- Backend CPU > 80% for 5 minutes β†’ email -- HTTP 5xx errors > 10 in 5 minutes β†’ email (severity 1) -- Response latency > 10s for 5 minutes β†’ email -**Anthropic cost monitor** β€” GitHub Actions workflow runs daily: -- Checks Anthropic API daily spend -- Creates a GitHub issue automatically if spend exceeds $5/day -- SQL query template included to investigate unusual usage -**Azure budget alert** β€” email at 80% and 100% of $30/month cap. +--- -### Deployment Commands +## Production deployment ```bash -# One-time infrastructure setup -chmod +x infra/provision.sh -./infra/provision.sh +# One-time Azure provisioning +chmod +x infra/provision.sh && ./infra/provision.sh -# Set up monitoring alerts -chmod +x infra/alerts.sh -./infra/alerts.sh your@email.com +# Monitoring alerts +chmod +x infra/alerts.sh && ./infra/alerts.sh your@email.com -# Check what GitHub Secrets to configure -./scripts/secrets-reference.sh - -# Manual deploy (normally triggered by git push) +# Deploy (triggered automatically on merge to main) git push origin main ``` -### Architecture Diagram +All deploys are zero-downtime rolling updates. The CD pipeline applies Neon migrations before updating either Container App. Smoke test failure triggers automatic rollback via `az containerapp revision deactivate`. -``` -Internet - β”‚ HTTPS (managed TLS) - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Azure Container Apps Environment β”‚ -β”‚ β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Frontend β”‚ β”‚ Backend β”‚ β”‚ -β”‚ β”‚ nginx + React SPA β”‚ β”‚ FastAPI + asyncpg β”‚ β”‚ -β”‚ β”‚ /api/* β†’ backend β”‚ β”‚ Claude SQL agent β”‚ β”‚ -β”‚ β”‚ scale: 0β†’2 replicas β”‚ β”‚ scale: 0β†’3 replicas β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ - β”‚ β–Ό - β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ β”‚ Neon Serverless β”‚ - β”‚ β”‚ PostgreSQL β”‚ - β”‚ β”‚ (free tier, scaleβ†’0) β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - GitHub Actions CI/CD - β”œβ”€β”€ CI: test + lint + typecheck (every PR) - β”œβ”€β”€ CD: build + deploy (every merge to main) - └── Cost monitor: daily Anthropic spend check -``` --- -## Engineering Decisions Log - -> *"Why did you choose X over Y?"*. - -**Claude over GPT-4o for SQL generation** β€” Claude's instruction-following produces more consistently structured JSON tool-call responses. In testing across 200 sample questions, Claude produced valid SQL on the first attempt 87% of the time vs 79% for GPT-4o-mini. The self-healing loop handles the remaining cases. +## Monthly cost -**asyncpg over psycopg2** β€” asyncpg is 3Γ— faster for concurrent requests due to its binary protocol implementation. For an analytics agent that may receive multiple simultaneous queries, async connection pooling is essential. psycopg2 would require threading workarounds. +| Service | Cost | +|---------|------| +| Azure Container Apps β€” backend (min 1 replica) | ~$5–8 | +| Azure Container Apps β€” frontend (scale to zero) | ~$1–3 | +| Neon Serverless PostgreSQL | $0 (free tier) | +| GitHub Actions | $0 (free tier) | +| **Total infrastructure** | **~$6–12/month** | -**PostgreSQL views for common aggregations** β€” `customer_health` and `monthly_mrr` are pre-defined views that Claude discovers via schema injection. This reduces query complexity for the most common manager questions and ensures consistent aggregation logic across all queries. - -**Self-healing loop capped at 3 retries** β€” More than 3 retries indicates a fundamental prompt or schema issue, not a fixable query error. Failing fast after 3 attempts surfaces the real problem rather than burning API credits on unrecoverable queries. +Anthropic API cost scales with query volume. With the 2-query lifetime cap per user and JSONB result caching, the marginal cost per unique visitor is bounded at approximately $0.01–0.03. --- -## πŸ‘€ Author - -Built by Atti Rehman (Full-Stack β†’ AI Systems Engineer). -Part of a portfolio demonstrating production-grade AI systems engineering. +## Project structure ---- - -*InsightIQ is a portfolio project demonstrating AI engineering skills: LLM tool use, self-healing agents, production PostgreSQL integration, and full-stack AI application deployment.* +``` +InsightIQ/ +β”œβ”€β”€ .github/ +β”‚ └── workflows/ +β”‚ β”œβ”€β”€ ci.yml ruff Β· pytest Β· tsc Β· trivy (every PR) +β”‚ β”œβ”€β”€ cd.yml migrate Β· deploy Β· smoke test Β· rollback (main) +β”‚ └── cost-monitor.yml daily Anthropic spend check +β”œβ”€β”€ backend/ +β”‚ β”œβ”€β”€ app/ +β”‚ β”‚ β”œβ”€β”€ agent/ +β”‚ β”‚ β”‚ └── sql_agent.py schema injection Β· generation Β· guard Β· healing Β· cache +β”‚ β”‚ β”œβ”€β”€ api/ +β”‚ β”‚ β”‚ └── query.py POST /api/query Β· GET /api/history Β· GET /api/schema +β”‚ β”‚ β”œβ”€β”€ auth/ +β”‚ β”‚ β”‚ β”œβ”€β”€ router.py POST /api/auth/google Β· GET /api/auth/me +β”‚ β”‚ β”‚ β”œβ”€β”€ service.py Google token verify Β· JWT Β· rate limit (atomic UPDATE) +β”‚ β”‚ β”‚ └── dependencies.py get_current_user FastAPI dependency +β”‚ β”‚ β”œβ”€β”€ core/ +β”‚ β”‚ β”‚ └── config.py Pydantic Settings Β· CORS Β· FRONTEND_URL +β”‚ β”‚ └── db/ +β”‚ β”‚ β”œβ”€β”€ pool.py asyncpg pool Β· type codecs Β· timeout guard +β”‚ β”‚ └── schema_loader.py schema + sample values Β· 5-min TTL cache +β”‚ β”œβ”€β”€ Dockerfile multi-stage Β· non-root (UID 1001) Β· healthcheck +β”‚ └── requirements.txt +β”œβ”€β”€ frontend/ +β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”œβ”€β”€ api/client.ts typed fetch Β· Authorization header Β· abort signals +β”‚ β”‚ β”œβ”€β”€ context/ AuthContext β€” Google sign-in Β· JWT Β· refreshUser +β”‚ β”‚ β”œβ”€β”€ hooks/ useQuery Β· useHistory (with loadFromHistory) +β”‚ β”‚ β”œβ”€β”€ components/ TopNav Β· Sidebar Β· ChartView Β· DataTable Β· SQLViewer +β”‚ β”‚ └── pages/LoginPage.tsx split-screen Β· Google Identity Services SDK +β”‚ β”œβ”€β”€ Dockerfile multi-stage Node build Β· nginx runtime Β· non-root +β”‚ β”œβ”€β”€ nginx.conf SPA routing Β· security headers Β· no backend proxy +β”‚ └── tsconfig.json strict Β· noImplicitAny Β· noUnusedLocals +β”œβ”€β”€ database/ +β”‚ β”œβ”€β”€ migrations/ +β”‚ β”‚ └── 002_auth.sql users Β· query_history user_id + result_json +β”‚ └── seed.py 1,000 customers Β· ~60K events Β· faker Β· reproducible (seed=42) +β”œβ”€β”€ infra/ +β”‚ β”œβ”€β”€ provision.sh ACA environment Β· OIDC federated credentials +β”‚ └── alerts.sh Azure Monitor Β· CPU Β· 5xx Β· latency Β· budget +β”œβ”€β”€ tests/ +β”‚ β”œβ”€β”€ test_sql_agent.py read-only guard (12 cases) Β· serialisation Β· Pydantic +β”‚ └── test_auth.py JWT unit tests Β· rate limit (atomic mock) Β· auth 401 +β”œβ”€β”€ docker-compose.yml local: postgres + backend + frontend +└── .env.example +``` diff --git a/assets/ insightiq-demo.mp4 b/assets/ insightiq-demo.mp4 new file mode 100644 index 0000000..235fd39 Binary files /dev/null and b/assets/ insightiq-demo.mp4 differ