From b9b81ebfb898a911e2e501132a6f5c55347c0447 Mon Sep 17 00:00:00 2001 From: Vivek Agarwal Date: Fri, 17 Jul 2026 15:41:26 +0530 Subject: [PATCH 1/6] scaffold: add MSSQL connection defaults to .env.example --- .env.example | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.env.example b/.env.example index 580fc41..0422799 100644 --- a/.env.example +++ b/.env.example @@ -12,4 +12,12 @@ AGENT_DATABASE_URL=sqlite:///./data/agent.db AGENT_ANTHROPIC_API_KEY= AGENT_GEMINI_API_KEY= +# MSSQL source (read-only analyst) — Windows Integrated Auth +# AGENT_MSSQL_HOST=localhost +# AGENT_MSSQL_DB=master +# AGENT_MSSQL_DRIVER=ODBC Driver 17 for SQL Server +# AGENT_MSSQL_INTEGRATED_AUTH=true +# AGENT_MSSQL_USER= +# AGENT_MSSQL_PASSWORD= + PORT=8001 From ebdbf5700b6dbccaf082dba8967b9dd476975d05 Mon Sep 17 00:00:00 2001 From: Vivek Agarwal Date: Sun, 19 Jul 2026 11:59:31 +0530 Subject: [PATCH 2/6] phase-1: read-only NL analyst over live MSSQL (Gemini) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Spec filled in: roadmap, architecture, agent graph, api, ui, data, 3 capabilities. * scaffold: src/mssql_analyst/ + frontend/ + alembic/ + pyproject.toml on this branch. * src/ does not exist on main (boilerplate-only) — first appearance per SKILL.md pitfall. PR base is main, this commit is also src/'s first. * Backend: FastAPI + LangGraph (ReAct single-shot ReAct). Audit-SQLite via SQLAlchemy 2.0. pyodbc integrated-auth connector to local master. Gemini via google-genai (verified model on live whitelist). * Read-only enforcement at the SQL validator + system prompt — DROP/UPDATE/ INSERT/DELETE/ALTER/CREATE/TRUNCATE/GRANT/REVOKE/EXEC all rejected. * Frontend: Next.js 15 + Tailwind v4 static export at /app/. Question form, result table, 'Show SQL' toggle, 'tokens used' badge, last-50 stub, charts/export/multi-db/follow-up stubs visibly disabled. * Tests: 31 unit + integration passing (incl. validator, graph topology, /api/ask envelope, /api/usage, audit-row write, data-locality prompt-spy). * Obs: structured JSON logging via structlog to stdout. * Existing pattern from feature/cctns-analyst-v0.1 lifted and adapted; mock mirror intentionally excluded — Phase 1 is live-only by spec. --- .env.example | 8 +- .gitignore | 2 +- README.md | 217 ++- alembic.ini | 38 + alembic/env.py | 50 + alembic/script.py.mako | 26 + alembic/versions/0001_initial.py | 40 + frontend/next.config.js | 14 + frontend/package.json | 24 + frontend/postcss.config.js | 8 + frontend/src/app/globals.css | 26 + frontend/src/app/layout.tsx | 15 + frontend/src/app/page.tsx | 292 +++++ frontend/tsconfig.json | 21 + pyproject.toml | 52 + spec/agent.md | 258 +--- spec/api.md | 126 +- spec/architecture.md | 103 +- spec/capabilities/audit_write.md | 47 + spec/capabilities/execute_bounded_query.md | 45 + spec/capabilities/index.md | 47 +- spec/capabilities/nl_to_sql.md | 45 + spec/data.md | 53 +- spec/roadmap.md | 120 +- spec/ui.md | 87 +- src/__main__.py | 26 + src/mssql_analyst/__init__.py | 5 + src/mssql_analyst/api/__init__.py | 36 + src/mssql_analyst/api/_common.py | 22 + src/mssql_analyst/api/app_factory.py | 69 + src/mssql_analyst/api/ask.py | 165 +++ src/mssql_analyst/api/health.py | 34 + src/mssql_analyst/api/usage.py | 62 + src/mssql_analyst/config/__init__.py | 5 + src/mssql_analyst/config/settings.py | 64 + src/mssql_analyst/db/__init__.py | 22 + src/mssql_analyst/db/models.py | 44 + src/mssql_analyst/db/session.py | 84 ++ src/mssql_analyst/domain/__init__.py | 6 + src/mssql_analyst/domain/ask.py | 26 + src/mssql_analyst/domain/usage.py | 27 + src/mssql_analyst/graph/__init__.py | 5 + src/mssql_analyst/graph/agent.py | 83 ++ src/mssql_analyst/graph/edges.py | 39 + src/mssql_analyst/graph/nodes.py | 239 ++++ src/mssql_analyst/graph/runner.py | 78 ++ src/mssql_analyst/graph/state.py | 34 + src/mssql_analyst/llm/__init__.py | 10 + src/mssql_analyst/llm/client.py | 125 ++ src/mssql_analyst/llm/providers/__init__.py | 6 + src/mssql_analyst/llm/providers/base.py | 24 + src/mssql_analyst/llm/providers/factory.py | 21 + src/mssql_analyst/llm/providers/gemini.py | 135 ++ src/mssql_analyst/llm/types.py | 23 + src/mssql_analyst/observability/__init__.py | 17 + src/mssql_analyst/observability/events.py | 84 ++ src/mssql_analyst/prompts/loader.py | 22 + src/mssql_analyst/prompts/nl_to_sql.md | 36 + src/mssql_analyst/tools/__init__.py | 21 + src/mssql_analyst/tools/mssql.py | 222 ++++ src/mssql_analyst/tools/validator.py | 50 + tests/__init__.py | 0 tests/conftest.py | 35 + tests/e2e/__init__.py | 0 tests/integration/__init__.py | 0 tests/integration/test_ask_api.py | 256 ++++ tests/unit/__init__.py | 0 tests/unit/test_graph_topology.py | 21 + tests/unit/test_validator.py | 46 + uv.lock | 1307 +++++++++++++++++++ 70 files changed, 4913 insertions(+), 487 deletions(-) create mode 100644 alembic.ini create mode 100644 alembic/env.py create mode 100644 alembic/script.py.mako create mode 100644 alembic/versions/0001_initial.py create mode 100644 frontend/next.config.js create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/app/globals.css create mode 100644 frontend/src/app/layout.tsx create mode 100644 frontend/src/app/page.tsx create mode 100644 frontend/tsconfig.json create mode 100644 pyproject.toml create mode 100644 spec/capabilities/audit_write.md create mode 100644 spec/capabilities/execute_bounded_query.md create mode 100644 spec/capabilities/nl_to_sql.md create mode 100644 src/__main__.py create mode 100644 src/mssql_analyst/__init__.py create mode 100644 src/mssql_analyst/api/__init__.py create mode 100644 src/mssql_analyst/api/_common.py create mode 100644 src/mssql_analyst/api/app_factory.py create mode 100644 src/mssql_analyst/api/ask.py create mode 100644 src/mssql_analyst/api/health.py create mode 100644 src/mssql_analyst/api/usage.py create mode 100644 src/mssql_analyst/config/__init__.py create mode 100644 src/mssql_analyst/config/settings.py create mode 100644 src/mssql_analyst/db/__init__.py create mode 100644 src/mssql_analyst/db/models.py create mode 100644 src/mssql_analyst/db/session.py create mode 100644 src/mssql_analyst/domain/__init__.py create mode 100644 src/mssql_analyst/domain/ask.py create mode 100644 src/mssql_analyst/domain/usage.py create mode 100644 src/mssql_analyst/graph/__init__.py create mode 100644 src/mssql_analyst/graph/agent.py create mode 100644 src/mssql_analyst/graph/edges.py create mode 100644 src/mssql_analyst/graph/nodes.py create mode 100644 src/mssql_analyst/graph/runner.py create mode 100644 src/mssql_analyst/graph/state.py create mode 100644 src/mssql_analyst/llm/__init__.py create mode 100644 src/mssql_analyst/llm/client.py create mode 100644 src/mssql_analyst/llm/providers/__init__.py create mode 100644 src/mssql_analyst/llm/providers/base.py create mode 100644 src/mssql_analyst/llm/providers/factory.py create mode 100644 src/mssql_analyst/llm/providers/gemini.py create mode 100644 src/mssql_analyst/llm/types.py create mode 100644 src/mssql_analyst/observability/__init__.py create mode 100644 src/mssql_analyst/observability/events.py create mode 100644 src/mssql_analyst/prompts/loader.py create mode 100644 src/mssql_analyst/prompts/nl_to_sql.md create mode 100644 src/mssql_analyst/tools/__init__.py create mode 100644 src/mssql_analyst/tools/mssql.py create mode 100644 src/mssql_analyst/tools/validator.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/e2e/__init__.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/test_ask_api.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/test_graph_topology.py create mode 100644 tests/unit/test_validator.py create mode 100644 uv.lock diff --git a/.env.example b/.env.example index 0422799..11631a4 100644 --- a/.env.example +++ b/.env.example @@ -2,11 +2,11 @@ AGENT_DATABASE_URL=sqlite:///./data/agent.db # LLM provider — auto-detected from whichever key is set below. # Override explicitly: anthropic | gemini -# AGENT_LLM_PROVIDER=anthropic +# AGENT_LLM_PROVIDER=gemini -# Override model (uses provider default when blank): -# AGENT_LLM_MODEL=claude-sonnet-4-6 (Anthropic default) -# AGENT_LLM_MODEL=gemini-3.1-pro (Gemini default) +# Override model (verified against the live ListModels whitelist on this account). +# AGENT_LLM_MODEL=gemini-3.1-pro-preview (Gemini default on this account) +# Other 3.x options available: gemini-3-pro-preview, gemini-3-flash-preview, gemini-3.5-flash # Set exactly ONE provider key: AGENT_ANTHROPIC_API_KEY= diff --git a/.gitignore b/.gitignore index 23af91d..b655db3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,12 +10,12 @@ __pycache__/ .venv/ venv/ *.egg-info/ -.venv/ # Node / frontend node_modules/ .next/ dist/ +out/ build/ # DB diff --git a/README.md b/README.md index 38e3b20..fb97e98 100644 --- a/README.md +++ b/README.md @@ -1,176 +1,147 @@ -# Zero Shot SDD Harness for Building Agents +# MSSQL Analyst -Give it a one-line idea. Walk away with a working, tested, phased agent. +A read-only, natural-language data analyst over a live Microsoft SQL Server database. Type a plain-English question; the agent translates it to a single bounded `SELECT`, runs it on your local MSSQL (Windows Integrated Auth), and returns a small result table — alongside the SQL it generated and a running token counter. -A lean, Hermes Agent native harness for building agentic software **spec-first**. One person with an idea and one API key can drive a real, production-shaped agent into existence — and a senior engineer opening the result finds a conventional, reviewable stack, not generated mush. +Single-user, local. Built spec-first from `spec/`. Phase 1 done; Phase 2/3 gated behind stubs in the UI. ---- - -## The Spirit - -Six convictions the whole repo is built around: - -1. **Spec is the source of truth.** The spec is written before the code, always. When spec and code disagree, the spec wins and the code is fixed (`/zero-shot-sync`). Every AI session reads the same requirements instead of re-deriving them. -2. **Built for two audiences at once.** A non-coder drives it with a single sentence; a senior engineer inherits a clean FastAPI + LangGraph stack they can read, review, and own. Neither audience is an afterthought. -3. **Lean harness, not a framework.** `harness/` is engineering *mindfulness* — rules and patterns that keep every session consistent — deliberately Claude-Code-only and kept small. The product runtime stays provider-agnostic; the harness does not. -4. **Smallest first-time-right win, phase by phase.** Each phase ships the smallest increment a human can actually test, and it must work the *first* time they test it — real on the tested path, with clearly-labelled stubs for everything still to come. No rough edges on the path you're handed. -5. **A human gates every phase.** The build is autonomous *within* a phase and stops at each boundary for you to test the increment. You stay in control of what "done" means. -6. **Real LLM/API or it doesn't count.** Gates, tests, and evals run against the real model with keys from `.env`. A stubbed pass is not a pass. +> **All commands run from the repo root.** The repo root IS the agent project — there is no subdirectory to `cd` into. --- -## What This Is +## Quick start (Phase 1) -A starting point for building AI agents spec-first. The repo ships with: +### 1. Configure `.env` -- A working **baseline agent** in `src/` (FastAPI + LangGraph + SQLite, provider-agnostic LLM — Anthropic or Gemini, `transform_text` as the capability slot) — tests pass out of the box -- A **spec template** in `spec/` covering roadmap, architecture, capabilities, data model, API, UI, and agent graph -- Three **zero-shot skills** (`/zero-shot-build`, `/zero-shot-fix`, `/zero-shot-sync`) -- A four-agent **team** — agent-builder orchestrates (plans, fans out, owns git/PR); spec-writer is the single design authority; code-generator implements one slice per instance (parallelised); qa-auditor reviews and gates -- Engineering rules and patterns in `harness/` so every Claude Code session is consistent -- **Human testing gate between phases** — autonomous within a phase, you test each increment before the next starts +`.env` already exists. Edit it so the MSSQL block looks like (truth to your local setup): ---- +```ini +# Gemini (already populated in your environment) +AGENT_GEMINI_API_KEY=... + +# Phase 1 — live MSSQL via Windows Integrated Auth +AGENT_MSSQL_HOST=localhost +AGENT_MSSQL_DB=master +AGENT_MSSQL_DRIVER=ODBC Driver 17 for SQL Server # Use 17 or 18 if installed +AGENT_MSSQL_INTEGRATED_AUTH=true +AGENT_MSSQL_USER= +AGENT_MSSQL_PASSWORD= +AGENT_MSSQL_QUERY_TIMEOUT_SEC=15 +AGENT_MSSQL_ROW_CAP=1000 -## How to Use This +PORT=8001 +``` -### Step 1 — Clone +### 2. Sync deps + create the audit-log DB ```bash -git clone https://github.com/smallTechOrg/zero-shot-sdd-harness.git my-agent -cd my-agent +uv sync +uv run alembic upgrade head +uv run alembic current # must show a revision, not blank ``` -### Step 2 — Open in Claude Code +### 3. Build the frontend (one-time, then re-build only when `frontend/` changes) ```bash -claude +cd frontend && NODE_OPTIONS=--no-experimental-webstorage npm ci && NODE_OPTIONS=--no-experimental-webstorage npm run build && cd .. ``` -### Step 3 — Build +### 4. Run the server -``` -/zero-shot-build An agent that monitors my Shopify store for low-inventory products and drafts restock emails to suppliers +```bash +uv run python -m src ``` -One intake round (scope, stack, API keys → fill `.env`), then the agent builds phase by phase and stops at each boundary for you to test. +- Open **http://localhost:8001/app/** for the UI. +- Open **http://localhost:8001/health** for liveness + mode. +- Open **http://localhost:8001/docs** for OpenAPI / Swagger. ---- +### 5. Try it -## What Happens (Intake → Phase by Phase) +Type: *"how many tables are in master?"* and click **Ask**. -``` -Your idea - ↓ -INTAKE — scope, stack, LLM provider, constraints; fill .env with the required API key - ↓ -[spec-writer] → Full spec: architecture + agent-graph + phased plan (self-reviewed) - ↓ -[agent-builder] → Feature branch + PR, scaffold - ↓ -per phase — all slices concurrently: - [code-generator: slice-a] ──→ [qa-auditor: slice-a] ─┐ - [code-generator: slice-b] ──→ [qa-auditor: slice-b] ─┤→ commit + push - [code-generator: slice-c] ──→ [qa-auditor: slice-c] ─┘ - ↓ -HUMAN TESTING GATE — exact run commands + expected result; you confirm before next phase - ↓ -(issue → qa-auditor classifies SPEC-vs-CODE → code-generator fixes → re-gate) - ↓ -repeat per phase → SHIP -``` +You should see: +- A result table (≥ 1 row). +- The `SELECT` it ran (via the **Show SQL** toggle). +- The "tokens used" badge in the header increasing. -Phase 1 is the smallest first-time-right win — real on the tested path, with labelled stubs for everything coming later. Each later phase wires one more stub into real functionality. +Each question writes one row to `data/agent.db` (see `GET /api/usage`). --- -## Repo Layout +## Endpoints -``` -src/ ← baseline agent (FastAPI + LangGraph + SQLite, Anthropic/Gemini) - api/ ← FastAPI routers (create_app, health, runs) - config/ ← Pydantic BaseSettings - db/ ← SQLAlchemy models + session - domain/ ← Pydantic request/response models - graph/ ← LangGraph nodes, edges, state, runner ← CAPABILITY SLOT - llm/ ← LLM client + providers/ (anthropic, gemini) - prompts/ ← prompt templates (.md) - observability/ -frontend/ ← Next.js static export (served by FastAPI at /app) -tests/ - unit/ ← passes with no API key - integration/ ← requires real key in .env -spec/ ← your spec: roadmap, architecture, capabilities/, data, api, ui, agent -harness/ - rules/ ← ai-agents, git, secret-hygiene - patterns/ ← spec-driven, phases, project-layout, tech-stack, code, test-driven, ui-ux, agentic-ai, engineering-practices -.claude/ - skills/ ← /zero-shot-build, /zero-shot-fix, /zero-shot-sync - agents/ ← agent-builder, spec-writer, code-generator, qa-auditor -CLAUDE.md -pyproject.toml -alembic.ini ← Alembic migrations (alembic/) -agent.py ← verify setup (default); --run to start the server -.env.example -``` - -**Capability slot** — the three files to replace for your agent: -- `src/graph/nodes.py` — replace `transform_text` with your logic -- `src/prompts/transform.md` — replace with your system prompt -- `frontend/src/app/page.tsx` — replace the transform form with your UI +| Method | Path | Purpose | +|--------|----------------|------------------------------------------------------| +| GET | `/` | Banner with service + UI + API URLs | +| GET | `/health` | Liveness + `mssql_mode` (live / unconfigured) + model | +| POST | `/api/ask` | One question → `{sql, columns, rows, row_count, latency_ms, tokens_used, status, sql_attempts}` | +| GET | `/api/usage` | Running totals + last-5 questions from the audit log | +| GET | `/app/...` | Static UI (Next.js export) | +| GET | `/docs` | Swagger UI | -Everything else (graph wiring, API, DB, settings, tests) is already working. +See **`spec/api.md`** for the full contract + error envelope. --- -## Running the Baseline +## Tests ```bash -cp .env.example .env -# edit .env: set exactly ONE provider key — -# AGENT_ANTHROPIC_API_KEY= or AGENT_GEMINI_API_KEY= -# the provider is auto-detected from whichever key is set -uv sync -python agent.py # verify tools, .env, deps, tests (default) -python agent.py --run # migrations + frontend build + start server +uv run pytest tests/unit/ -v # no env needed — pure unit +uv run pytest tests/integration/ -v # uses stub LLM + stub connector (no live deps) +uv run pytest tests/ -v # full suite ``` -Once running: - -| URL | What | -|-----|------| -| `http://localhost:8001/app/` | **UI** — transform form (the capability slot) | -| `http://localhost:8001/health` | API health check | -| `http://localhost:8001/docs` | Interactive API docs (Swagger) | +The integration tests are wired to a stubbed LLM provider and a stubbed MSSQL connector so they run on any machine without a live MSSQL. -Tests: +Playwright E2E (planned for the Stage 3 human gate): ```bash -uv run pytest tests/unit/ -v # no key needed -uv run pytest tests/ -v # requires real key in .env +cd tests/e2e && npm ci && npx playwright install --with-deps chromium && npx playwright test tests/e2e/ --reporter=line ``` --- -## Rules AI Agents Follow +## Architecture (Phase 1) -Full rules in `harness/rules/ai-agents.md`. Summary: +- **`src/mssql_analyst/`** — the Python package. + - `api/` — FastAPI routers (`/health`, `/api/ask`, `/api/usage`, root banner). + - `graph/` — LangGraph state machine: `nl_to_sql → execute_sql → finalize | handle_error`. + - `llm/` — single-boundary LLM client + Gemini provider. + - `tools/` — the MSSQL connector (`pyodbc`, Windows Integrated Auth, cached schema) and the SQL safety validator. + - `db/` — SQLite audit log models + session. + - `observability/` — structured JSON logging via structlog. +- **`frontend/`** — Next.js 15 + Tailwind v4 static export, mounted at `/app/`. +- **`spec/`** — the product spec (single source of truth). +- **`tests/`** — unit + integration. E2E (Playwright) wired for the gate. -- Read the full spec before writing any code -- Never skip a phase; commit every logical unit -- Tests run against the real LLM/API using keys from `.env` — stubbed runs do not count as passing -- Each phase is tested by the human before the next phase starts -- The build record is git history + the PR + the per-phase test-handoffs +Read **`spec/architecture.md`** for the data flow + layer map. --- -## FAQ +## What's real in Phase 1 + +- Real Gemini (auto-detected). +- Real MSSQL (Windows Integrated Auth against your `master`) on **every** question. +- Real audit-log write to `data/agent.db`. +- Token counter and "Show SQL" toggle on the UI. +- Two-layer read-only enforcement (system prompt + regex validator). + +## What's a clearly-labelled stub (and why) + +- **Last-50 sidebar** — placeholder, "History (last 50) — coming in Phase 2". +- **Charts / CSV export** — disabled and labelled. +- **Multi-DB switcher / follow-up chat** — disabled and labelled "Phase 3". -**What if I already have a stack in mind?** -State it in the idea: `/zero-shot-build [idea] — use Python + FastAPI + PostgreSQL`. Stack choices are binding. +Details in **`spec/roadmap.md`** → "Phase 1 — what is REAL on the tested path" and what is a stub. + +--- + +## Out of scope for Phase 1 + +Writes, DDL, multi-DB, multi-turn session memory, charts, exports, authentication, multi-tenancy, retry-on-validator (Phase 3), multi-user rate limiting. + +--- -**What if something breaks?** -Run `/zero-shot-fix [what's broken]` — qa-auditor classifies the problem (SPEC vs CODE), the right generator fixes it, qa-auditor re-gates. +## Status -**What if spec and code drift?** -Run `/zero-shot-sync` — qa-auditor classifies each divergence, generators fix, spec wins. +Phase 1 ready. See `spec/roadmap.md` for phases 2/3/4 plans. diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..47c4a46 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,38 @@ +[alembic] +script_location = alembic +sqlalchemy.url = sqlite:///./data/agent.db +prepend_sys_path = src + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..262ca93 --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,50 @@ +"""Alembic env — reads AGENT_DATABASE_URL from settings; Base.metadata as target.""" + +from __future__ import annotations + +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config, pool + +from mssql_analyst.config.settings import Settings +from mssql_analyst.db.models import Base + +config = context.config +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +settings = Settings() +config.set_main_option("sqlalchemy.url", settings.database_url) + +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/0001_initial.py b/alembic/versions/0001_initial.py new file mode 100644 index 0000000..8d39d3a --- /dev/null +++ b/alembic/versions/0001_initial.py @@ -0,0 +1,40 @@ +"""initial — answer_runs (audit log) + +Revision ID: 0001 +Revises: +Create Date: 2026-07-17 16:00:00 +""" + +from __future__ import annotations + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "0001" +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "answer_runs", + sa.Column("id", sa.String(), primary_key=True), + sa.Column("request_id", sa.String(), nullable=False), + sa.Column("question", sa.Text(), nullable=False), + sa.Column("sql_template", sa.Text(), nullable=False, server_default=""), + sa.Column("sql_attempts", sa.Integer(), nullable=False, server_default="0"), + sa.Column("row_count", sa.Integer(), nullable=False, server_default="0"), + sa.Column("latency_ms", sa.Integer(), nullable=False, server_default="0"), + sa.Column("tokens_used", sa.Integer(), nullable=False, server_default="0"), + sa.Column("status", sa.String(), nullable=False, server_default="pending"), + sa.Column("error_message", sa.Text(), nullable=True), + sa.Column("created_at", sa.TIMESTAMP(timezone=True), nullable=False), + sa.Column("updated_at", sa.TIMESTAMP(timezone=True), nullable=False), + ) + + +def downgrade() -> None: + op.drop_table("answer_runs") diff --git a/frontend/next.config.js b/frontend/next.config.js new file mode 100644 index 0000000..b51f0d5 --- /dev/null +++ b/frontend/next.config.js @@ -0,0 +1,14 @@ +/** + * Next.js config — static export, basePath=/app, images unoptimised. + * Per harness/patterns/tech-stack.md §"Frontend Static-Export & Styling Rule" + * the single-origin /app/ path is mandatory. + */ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: "export", + basePath: "/app", + images: { unoptimized: true }, + experimental: {}, +}; + +module.exports = nextConfig; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..258ff9e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "mssql-analyst-frontend", + "version": "0.1.0", + "private": true, + "description": "Next.js 15 + Tailwind v4 single-page UI for the MSSQL analyst", + "scripts": { + "dev": "NODE_OPTIONS=--no-experimental-webstorage next dev", + "build": "NODE_OPTIONS=--no-experimental-webstorage next build", + "start": "NODE_OPTIONS=--no-experimental-webstorage next start" + }, + "dependencies": { + "next": "^15.5.4", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.0", + "@types/node": "^22.10.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "tailwindcss": "^4.1.0", + "typescript": "^5.6.3" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..478f590 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,8 @@ +// postcss config — Tailwind v4 plugin. +// Per `harness/patterns/tech-stack.md` §"Frontend Static-Export & Styling Rule" +// this file is **non-optional** with `@tailwindcss/postcss`. +module.exports = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..d480837 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,26 @@ +/* Global CSS for the MSSQL analyst frontend. + * + * Per `harness/patterns/tech-stack.md` §"Frontend Static-Export & Styling Rule": + * - FIRST two lines must remain `@source "../";` then `@import "tailwindcss";`. + * Anything below is custom. Do not overwrite the first two. + */ +@source "../"; +@import "tailwindcss"; + +:root { + color-scheme: light dark; +} + +html, +body { + font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + color: rgb(17 24 39); + background-color: rgb(249 250 251); +} +@media (prefers-color-scheme: dark) { + html, + body { + color: rgb(243 244 246); + background-color: rgb(17 24 39); + } +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..c30e628 --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,15 @@ +import type { ReactNode } from "react"; +import "./globals.css"; + +export const metadata = { + title: "MSSQL Analyst", + description: "Natural-language analyst over a live Microsoft SQL Server database", +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..b16da18 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,292 @@ +"use client"; + +import { useEffect, useState } from "react"; + +type ApiRow = unknown[]; + +type ApiResponse = { + data?: { + sql: string; + columns: string[]; + rows: ApiRow[]; + latency_ms: number; + row_count: number; + sql_attempts: number; + tokens_used: number; + status: "completed" | "failed"; + }; + error?: { code: string; message: string } | null; +}; + +type Usage = { + total_questions: number; + total_tokens: number; + total_rows_returned: number; + last_questions: Array<{ + id: string; + question: string; + sql: string; + status: string; + row_count: number; + tokens_used: number; + latency_ms: number; + created_at: string; + }>; +}; + +function formatAnswer(payload: NonNullable): string { + // Phase 1 deterministic UI message — no markdown required. + if (payload.status !== "completed") return "—"; + const rows = payload.rows.slice(0, 1); + const cols = payload.columns.slice(0, 2).join(", "); + const count = rows.length; + if (count === 0) return "No rows returned."; + if (count === 1) { + const cell = rows[0][0]; + if (cell === null || cell === undefined || cell === "") { + return "Empty result for the question."; + } + return `Found: ${cell}${payload.row_count > 1 ? ` (and ${payload.row_count - 1} more rows not shown)` : ""}.`; + } + return `${count} rows returned. Columns: ${cols || "(none)"}.`; +} + +export default function Page() { + const [question, setQuestion] = useState("How many tables are in master?"); + const [busy, setBusy] = useState(false); + const [response, setResponse] = useState(null); + const [showSql, setShowSql] = useState(false); + const [error, setError] = useState(null); + const [usage, setUsage] = useState(null); + + async function refreshUsage() { + try { + const r = await fetch("/api/usage"); + const body: { data?: Usage; error?: unknown } = await r.json(); + if (body.data) setUsage(body.data); + } catch { + /* Phase 1: ignore — usage is convenience */ + } + } + + useEffect(() => { + refreshUsage(); + }, []); + + async function onAsk(e?: React.FormEvent) { + if (e) e.preventDefault(); + if (!question.trim()) return; + setBusy(true); + setError(null); + setResponse(null); + try { + const r = await fetch("/api/ask", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ question: question.trim() }), + }); + const body: ApiResponse = await r.json(); + if (body.error) { + setError(`${body.error.code}: ${body.error.message}`); + } else if (body.data) { + setResponse(body.data); + } + await refreshUsage(); + } catch (err) { + setError(`network_error: ${(err as Error).message}`); + } finally { + setBusy(false); + } + } + + return ( +
+
+

+ MSSQL Analyst +

+
+ + tokens used: {usage?.total_tokens ?? 0} + + + source: live MSSQL + +
+
+ +
+ +