Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "zero-shot-agent"
version = "0.1.0"
description = "Baseline agent skeleton — FastAPI + LangGraph + SQLite, provider-agnostic LLM. The capability slot is transform_text."
description = "Baseline agent skeleton — FastAPI + LangGraph + MSSQL, provider-agnostic LLM. The capability slot is for police data analysis."
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
Expand All @@ -14,11 +14,14 @@ dependencies = [
"langgraph>=0.2.28",
"httpx>=0.27",
"structlog>=24.1",
"pyodbc>=5.0",
"aioodbc>=0.16.0",
]

[dependency-groups]
dev = [
"pytest>=8.2",
"docker>=7.0.0",
]

[tool.pytest.ini_options]
Expand All @@ -27,4 +30,4 @@ pythonpath = ["."]
addopts = "-q"

[tool.uv]
package = false
package = false
312 changes: 154 additions & 158 deletions spec/agent.md

Large diffs are not rendered by default.

106 changes: 91 additions & 15 deletions spec/api.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,117 @@
# API

> Fill in this section — see comments below.

---

## API Style

<!-- FILL IN: REST / GraphQL / CLI / webhook / none -->
REST

## Endpoints / Commands
### `POST /api/ask`
**Purpose:** Accept a natural-language question from an officer, run it through the agent pipeline, and return the answer with optional SQL and chart.

<!-- FILL IN: One section per endpoint or command. -->
**Request:**
```json
{
"question": "string",
"officer_id": "string (optional, from header in Phase 3)"
}
```

### `<!-- METHOD /path or command name -->`
**Response:**
```json
{
"answer": "string (natural-language response)",
"sql": "string (the executed SQL, for transparency)",
"execution_time_ms": "integer",
"row_count": "integer",
"chart_spec": "object (optional Vega-Lite specification)",
"status": "string: "success" | "clarification_needed" | "error"
}
```

**Purpose:** <!-- what this endpoint does -->
**Error cases:**
| Status | Condition |
|--------|-----------|
| 400 | Missing or invalid `question` in request body. |
| 401 | Missing or invalid `officer_id` (when required by phase). |
| 429 | Agent rate-limited (upstream LLM or DB); retry after delay. |
| 500 | Internal agent error (see `answer` field for message). |
### `POST /api/pin`
**Purpose:** Save a question and its result as a pinned report for quick reuse.

**Request:**
```json
```json
{
"<!-- field -->": "<!-- type and description -->"
"question": "string",
"officer_id": "string"
1": "{
"answer": "string": "integer",
"string: "string thetime_ms": integer",
"row_count": "integer",
"created_at": "ISO timestamp"
}'
}
}
```

**Response:**
```json
{
"<!-- field -->": "<!-- type and description -->"
"id": "string (uuid)",
"message": "Report pinned successfully"
}
```

**Error cases:**
| Status | Condition |
|--------|-----------|
| 400 | <!-- bad input --> |
| 500 | <!-- internal error --> |
| 400 | Missing required fields. |
| 401 | Invalid or missing officer_id. |
| 500 | Database error while saving. |
### `GET /api/reports`
**Purpose:** List recent and pinned reports for an officer.

## Authentication
**Request:** None (officer_id from header or query)

<!-- FILL IN: How are API callers authenticated? -->
**Response:**
```json
{
"recent": [
{
"id": "string",
"question": "string",
"answer": "string",
"created_at": "ISO timestamp"
}
],
"pinned": [
{
"id": "string",
"question": "string",
"answer": "string",
"pinned_at": "ISO timestamp"
}
]
}
```

**Error cases:**
| Status | Condition |
|--------|-----------|
| 401 | Invalid or missing officer_id. |
| 500 | Database error while fetching. |
### `GET /api/health`
**Purpose:** Simple health check; returns 200 if the service is up and can reach the DB and LLM API (without validating keys).

**Response:**
```json
{
"status": "ok",
"timestamp": "ISO timestamp",
"services": {
"database": "reachable" | "unreachable",
"llm_api": "reachable" | "unreachable"
}
}
```
## Authentication
In Phase 1, the officer_id is accepted as an optional header (`X-Officer-Id`) or query parameter for audit logging; no real authentication is performed. In Phase 3, this will be replaced with proper JWT validation against an identity provider.
95 changes: 50 additions & 45 deletions spec/architecture.md
Original file line number Diff line number Diff line change
@@ -1,68 +1,73 @@
# Architecture

> Fill in this section — see comments below.

---

## System Overview

<!-- FILL IN: One paragraph describing the system at a high level. Who/what interacts with it? -->
The UP Police Data Analyst agent is a read-only interface over a Microsoft SQL Server database containing FIR/CCTNS, HR/personnel, and logistics/property data. Authorized officers interact via a chat-based web UI to ask natural-language questions about crime, personnel, or logistics data. The agent translates questions into safe SQL queries, executes them against the database, and returns answers with the executed SQL, timing, and optional visualizations. All interactions are logged immutably for audit and compliance.

## Component Map

<!-- FILL IN: List the major components and what each does. -->

```
[Component A]
[Component B] ←→ [External Service]
[Component C]
```mermaid
graph TD
A[Officer's Browser] -->|HTTPS/WSS| B(FastAPI Backend)
B -->|SQLAlchemy + pyodbc| C[Microsoft SQL Server]
B -->|HTTP (JSON)| D[OpenRouter LLM API]
B -->|Append-only| E[Audit Table (SQL Server)]
B -->|Serve at /app| F[Static Frontend (HTML/JS)]
```

## Layers

<!-- FILL IN: Describe the layers of the system (e.g., API → Agent Loop → Tools → Storage). -->

| Layer | Responsibility |
|-------|----------------|
| <!-- layer --> | <!-- responsibility --> |
| Presentation | Static HTML/JS frontend served at `/app`; handles user input, displays answers with collapsible SQL/chart/table panels, and shows recent/pinned reports sidebar. |
| API | FastAPI endpoints (`/api/ask`, `/api/pin`, `/api/recent`) that validate input, invoke the agent loop, and return structured responses. |
| Agent Loop | LangGraph-based reasoning loop: Planner → SQL Writer → Validator → Executor → Answer Writer. Each step is a node with conditional edges for error handling and retries. |
| Tools & Storage | SQLAlchemy ORM with MSSQL driver (`pyodbc`/`aioodbc`) for schema introspection and query execution; audit table for immutable logging. |
| External | OpenRouter LLM API (primary: Anthropic Claude Sonnet 3.5, fallback: Claude Haiku) for reasoning and SQL generation. |

## Data Flow

<!-- FILL IN: Walk through the main data flow from trigger to output. -->

1. Trigger: <!-- how does the agent start? (cron, webhook, user input, etc.) -->
2. <!-- step 2 -->
3. <!-- step 3 -->
4. Output: <!-- what does the agent produce? -->
1. Trigger: Officer types a question in the chat input and presses Enter.
2. Frontend sends POST `/api/ask` with `{ question, officer_id }` (officer_id from headers or login context).
3. API validates input, creates an agent run record, and invokes the LangGraph agent loop.
4. Planner node refines the question (if ambiguous) and outputs a refined question or clarification request.
5. SQL Writer node generates a parameterized SQL query using the cached schema (tables/columns only, no row data).
6. Validator node checks the SQL for safety: ensures read-only, mandates `LIMIT/TOP`, blocks `SELECT *`, and validates against a read-only DB user policy.
7. Executor node runs the SQL against the MSSQL database via SQLAlchemy, capturing row count and latency.
8. Answer Writer node formats the result into a natural-language answer, optionally generating a chart/table if the data is suitable.
9. The run record is updated with the answer, SQL, timing, and an immutable audit log entry is written.
10. API returns the answer, SQL, timing, and optional chart spec to the frontend.
11. Frontend displays the answer with collapsible panels for SQL and chart/table, and updates the recent/pinned sidebar.

## External Dependencies

<!-- FILL IN: APIs, services, databases the agent depends on. -->

| Dependency | Purpose | Failure Mode |
|------------|---------|--------------|
| <!-- name --> | <!-- what it does --> | <!-- what happens if it's down --> |
| Microsoft SQL Server | Stores FIR/CCTNS, HR/personnel, and logistics/property data; source of truth for all queries. | Database downtime causes 503 errors; agent returns "Database unavailable" error. |
| OpenRouter LLM API | Provides reasoning and SQL generation via Anthropic Claude models. | API downtime or rate limiting causes fallback to cheaper model or explicit error; agent may ask user to rephrase or try later. |
| Officer Auth System (future) | Provides JWT tokens for officer identification (Phase 3). | Missing/invalid token results in 401 error; Phase 1 uses stubbed officer_id from header. |

## Stack

> This project's concrete technology choices (captured at intake, filled by the spec-writer). The generic, every-project rules — model-naming, DB driver, dev port, test environment — live in `harness/patterns/tech-stack.md`; this section is only what **this** project picked.

- **Language:** <!-- FILL IN: e.g., Python 3.12 -->
- **Agent framework:** <!-- FILL IN: e.g., LangGraph / custom / none -->
- **LLM provider + model:** <!-- FILL IN: e.g., Anthropic / claude-sonnet-4-6 -->
- **Backend:** <!-- FILL IN: e.g., FastAPI / none -->
- **Database + ORM:** <!-- FILL IN: e.g., PostgreSQL + SQLAlchemy 2.0 / none -->
- **Frontend:** <!-- FILL IN: e.g., Next.js / none -->
- **Dependency management:** <!-- FILL IN: e.g., uv + pyproject.toml -->
- **Language:** Python 3.11
- **Agent framework:** LangGraph
- **LLM provider + model:** OpenRouter (Anthropic Claude Sonnet 3.5 for planning, Claude Haiku for cheap fallback)
- **Backend:** FastAPI
- **Database + ORM:** Microsoft SQL Server + SQLAlchemy 2.0 (with `pyodbc`/`aioodbc` driver)
- **Frontend:** Zero-build static HTML/CSS/JS (served at `/app`)
- **Dependency management:** uv + pyproject.toml

| Key library | Version | Purpose |
|-------------|---------|---------|
| <!-- name --> | <!-- ver --> | <!-- purpose --> |

**Avoid:** <!-- FILL IN: libraries/patterns explicitly off-limits, and why -->
| langgraph | 0.1.x | Agent reasoning loop |
| fastapi | 0.109.x | Web API and server |
| sqlalchemy | 2.0.x | ORM and DB abstraction |
| pyodbc | 5.x | MSSQL driver (sync) |
| aioodbc | 1.x | Async MSSQL driver (if needed) |
| pydantic | 2.x | Data validation and settings |
| python-dotenv | 1.x | Load environment variables from `.env` |
| structlog | 23.x | Structured logging |
| alembic | 1.13.x | Database migrations |

**Avoid:**
- Raw SQL string concatenation (use ORM or parameterized queries only).
- Storing secrets in code or logs (use `.env` and secret hygiene rules).
- Using SQLite as a substitute for production PostgreSQL/MSSQL in tests (tests must use the real driver).
- Hardcoding model names or DB connection strings (load from config/env).

## Deployment Model

<!-- FILL IN: How does this run? (local script, cloud function, long-running service, etc.) -->
The agent runs as a long-running service via `uv run python -m src` (which starts Uvicorn on port 8001). It is designed for deployment behind a reverse proxy (NGINX/Apache) with TLS termination in production. In development, it runs locally on `http://localhost:8001`.
45 changes: 45 additions & 0 deletions spec/capabilities/ask_question.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Capability: ask_question
## What It Does
Converts a natural-language question about police data into a safe SQL query, executes it against the read-only MSSQL database, and returns an answer with the executed SQL, timing, and optional visualizations.

## Inputs
| Input | Type | Source | Required |
|-------|------|--------|----------|
| question | string | user (chat input) | yes |
| officer_id | string | header or auth (Phase 3) | no (Phase 1: optional for audit) |

## Outputs
| Output | Type | Destination |
|--------|------|-------------|
| answer | string | agent message bubble |
| sql | string | collapsible "View SQL" panel |
| row_count | integer | part of answer text |
| latency_ms | integer | part of answer text |
| chart_spec | dict (optional) | collapsible "View Chart" panel |
| status | string ("success", "clarification_needed", "error") | determines message bubble type |

## External Calls
| System | Operation | On Failure |
|--------|-----------|------------|
| MSSQL (read-only) | introspect_schema (tables/columns only) | set error, route to handle_error |
| MSSQL (read-only) | execute_sql (parameterized SELECT) | capture error, set state.error |
| OpenRouter LLM API | planner (question refinement) | retry 2x with backoff, then error |
| OpenRouter LLM API | sql_writer (SQL generation) | retry 2x with backoff, then error |
| OpenRouter LLM API | validator (SQL safety check) | retry 2x with backoff, then error |
| OpenRouter LLM API | answer_writer (natural-language summary) | retry 2x with backoff, then error |
| AuditLog table | insert immutable audit log | log error but do not fail run |

## Business Rules
- The agent must never include raw row data in any LLM prompt (schema-only by default).
- All SQL queries must be read-only (SELECT only) and include a LIMIT or TOP clause.
- The agent must use parameterized queries; never concatenate user input into SQL.
- If the planner determines the question is ambiguous, it must ask for clarification before proceeding.
- Every query must be logged immutably in the audit table for compliance.
- The officer_id (when available) must be included in the audit log for accountability.

## Success Criteria
- [ ] Given a clear natural-language question, the agent returns a correct answer with SQL and timing within 15 seconds p50.
- [ ] Given an ambiguous question, the agent asks for clarification instead of guessing.
- [ ] Given a potentially unsafe question (e.g., attempting INSERT), the agent rejects it with an error.
- [ ] The audit log contains an entry for every query with officer_id, question, SQL, row count, latency, and result hash.
- [ ] Raw data never appears in LLM prompts or logs (verified via observability).
9 changes: 4 additions & 5 deletions spec/capabilities/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ A capability is a single, discrete action or behavior the agent performs. Exampl
- "Draft a personalized email given a lead profile"
- "Send a Slack notification when a threshold is crossed"

## Capabilities in This Project

<!-- FILL IN: List capabilities here as they are defined. Each entry links to its spec file (no number prefix). -->
## Capabilities In This Project

| Capability | File |
|-----------|------|
| <!-- name --> | [name.md](name.md) |
| ask_question | [ask_question.md](ask_question.md) |
| pin_report | [pin_report.md](pin_report.md) |

## How to Add a New Capability

Expand All @@ -35,4 +34,4 @@ Each capability file should answer:
- **Outputs** (what it produces)
- **External calls** (APIs, LLMs, databases it touches)
- **Error cases** (what can go wrong and how it's handled)
- **Success criteria** (how we test it)
- **Success criteria** (how we test it)
38 changes: 38 additions & 0 deletions spec/capabilities/pin_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Capability: pin_report
## What It Does
Allows an officer to save a question and its result (or a link to re-run it) for quick access later, pinned to their sidebar.

## Inputs
| Input | Type | Source | Required |
|-------|------|--------|----------|
| question | string | from a prior ask_question turn | yes |
| officer_id | string | header or auth | yes |
| nickname | string | user-defined label for the pin | no |

## Outputs
| Output | Type | Destination |
|--------|------|-------------|
| pin_id | integer | returned to frontend |
| nickname | string | displayed in sidebar |
| pinned_at | timestamp | stored in database |
| status | string | "success" or "error" |

## External Calls
| System | Operation | On Failure |
|--------|-----------|------------|
| OfficerReport table | INSERT or UPDATE | set error, return error response |
| AuditLog table | INSERT (pin event) | log error but do not fail pin |

## Business Rules
- A pinned report stores the original question (not the result set) so it can be re-run against fresh data.
- Officers can pin the same question multiple times with different nicknames.
- Pinned reports are private to the officer unless sharing is enabled (Phase 2+).
- The pin does not store the result set to avoid stale data; re-running gets current data.
- Each pin action is logged in the audit trail for accountability.

## Success Criteria
- [ ] After a successful ask_question, the officer can click a pin icon to save the query.
- [ ] The pinned query appears in the sidebar under "Pinned" with the given nickname (or a default).
- [ ] Clicking the pinned query re-runs the question and displays the current result.
- [ ] The audit log records the pin action with officer_id and question.
- [ ] Pins persist across page refreshes and sessions.
Loading