# AgentForms Architecture — Technical Reference

## System Design

AgentForms is a three-tier service:

| Component | Technology | Port | Responsibility |
|---|---|---|---|
| **Relay** | Flask + Gunicorn | 5060 | API surface, form logic, LLM client, auth |
| **Worker** | Python + Redis queue | — | Async action execution, email delivery |
| **LLM** | llama.cpp (Qwen3.5-4B) | 8086 | Form generation from natural language |

All three run in Docker Compose. SQLite is the default database with a Postgres migration planned.

## Agent Integration Pattern

The primary use case is an agent calling AgentForms on behalf of a user. The flow:

```
User → Agent → AgentForms API → Form Created → Form Published
                                          ↓
                                    User fills form → Submission → Actions Fire
```

### Step 1: Agent creates a form

```python
import requests

# Agent asks user: "What kind of form do you need?"
# User says: "A client intake form for our consulting business"

resp = requests.post(
    "https://agentforms.io/api/agent/build",
    headers={"Authorization": "Bearer <api_key>"},
    json={
        "prompt": "Client intake form for consulting business: client name, company, email, phone, project description, budget range, timeline, existing contracts, referral source",
        "form_type": "form"
    }
)

site = resp.json()
token = site["token"]  # Embed/publish token
fields = site["fields"]  # Generated field config
```

### Step 2: Agent configures actions

```python
# Webhook to client's system
requests.post(
    f"https://agentforms.io/api/sites/{site_id}/actions",
    headers={"Authorization": "Bearer <api_key>"},
    json={
        "type": "webhook",
        "config": {"url": "https://client-crm.com/api/lead"},
        "execution_order": 1
    }
)

# Email notification
requests.post(
    f"https://agentforms.io/api/sites/{site_id}/actions",
    headers={"Authorization": "Bearer <api_key>"},
    json={
        "type": "email",
        "config": {
            "recipients": ["sales@example.com"],
            "subject": "New client inquiry: {{client_name}}"
        },
        "execution_order": 2
    }
)
```

### Step 3: Agent returns embed URL to user

The user shares the embed link or the agent embeds it on their own site. No login required on the form-filler's side.

## Field Configuration Format

Every form is defined by a JSON array of field objects:

```json
[
  {
    "key": "client_name",
    "type": "text",
    "label": "Client Name",
    "required": true,
    "placeholder": "John Smith",
    "validation": null,
    "condition": null,
    "calculation": null,
    "step": 1
  },
  {
    "key": "budget",
    "type": "select",
    "label": "Budget Range",
    "required": true,
    "options": ["Under $5K", "$5K-$15K", "$15K-$50K", "$50K+"],
    "validation": null,
    "condition": null,
    "calculation": null,
    "step": 1
  }
]
```

### Supported Field Types

| Type | Description |
|---|---|
| `text` | Single-line text input |
| `email` | Email with validation |
| `phone` | Phone number input |
| `number` | Numeric input with min/max |
| `textarea` | Multi-line text |
| `select` | Dropdown with options |
| `radio` | Radio button group |
| `checkbox` | Single checkbox |
| `date` | Date picker |
| `url` | URL input |
| `computed` | Calculated field (read-only) |
| `file` | File upload (Phase 5) |

### Condition Operators

11 operators for conditional field visibility:

| Operator | Description | Example |
|---|---|---|
| `eq` | Equals | `{"field": "marital_status", "operator": "eq", "value": "married"}` |
| `neq` | Not equals | `{"field": "choice", "operator": "neq", "value": "none"}` |
| `filled` | Field has value | `{"field": "name", "operator": "filled"}` |
| `empty` | Field is empty | `{"field": "result", "operator": "empty"}` |
| `gt` | Greater than (numeric) | `{"field": "total", "operator": "gt", "value": "100"}` |
| `gte` | Greater than or equal | `{"field": "total", "operator": "gte", "value": "100"}` |
| `lt` | Less than (numeric) | `{"field": "age", "operator": "lt", "value": "18"}` |
| `lte` | Less than or equal | `{"field": "age", "operator": "lte", "value": "18"}` |
| `contains` | String contains | `{"field": "message", "operator": "contains", "value": "urgent"}` |
| `not_contains` | String does not contain | `{"field": "message", "operator": "not_contains", "value": "spam"}` |
| `in` | Value in list | `{"field": "product", "operator": "in", "value": "laptop,phone,tablet"}` |
| `not_in` | Value not in list | `{"field": "product", "operator": "not_in", "value": "laptop,phone,tablet"}` |

### Calculation Engine

Calculated fields use `{{key}}` template syntax:

```json
{
  "key": "total",
  "type": "computed",
  "calculation": {
    "formula": "{{price}} * {{quantity}}",
    "round": 2
  }
}
```

Formula engine supports basic arithmetic: `+`, `-`, `*`, `/`, parentheses.

## Action Pipeline

### Action Types

**Webhook**
```json
{
  "type": "webhook",
  "config": {
    "url": "https://example.com/webhook",
    "method": "POST",
    "headers": {"Authorization": "Bearer ..."},
    "body_template": "{{ submission_data }}"
  }
}
```

**Email**
```json
{
  "type": "email",
  "config": {
    "recipients": ["user@example.com"],
    "subject": "New submission: {{client_name}}",
    "body_template": "..."
  }
}
```

**Log**
```json
{"type": "log", "config": {"message_template": "..."}}
```

**Redirect**
```json
{
  "type": "redirect",
  "config": {
    "url": "https://example.com/thanks",
    "delay": 3
  }
}
```

**Document**
```json
{
  "type": "document",
  "config": {
    "template_id": "contract_v1",
    "output_format": "pdf"
  }
}
```

### Execution Model

Actions execute in order of `execution_order` (ascending). Each action can be independently enabled/disabled. Failed actions don't block subsequent actions — they log and continue.

## Workflow Builder

The LLM client sends the user's prompt to a Qwen3.5-4B model running via llama.cpp. The model returns a structured JSON response that is parsed into `field_config`.

### Request Format

```python
requests.post(
    f"{LLM_HOST}/v1/chat/completions",
    json={
        "model": "Qwen3.5-4B",
        "messages": [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_prompt}
        ],
        "temperature": 0.3,
        "max_tokens": 2048
    }
)
```

The system prompt instructs the model to generate valid field_config JSON with proper types, labels, and optional validation/condition/calculation.

### LLM Configuration

| Variable | Default | Description |
|---|---|---|
| `BUILDER_LLM_HOST` | `http://10.0.0.68:8086` | llama.cpp server address |

Overridable via Docker `.env` in the relay container.

## Database Schema

### Users
```sql
CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    email TEXT UNIQUE,
    password_hash TEXT,
    name TEXT,
    tier TEXT DEFAULT 'free',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

### Sites
```sql
CREATE TABLE sites (
    id INTEGER PRIMARY KEY,
    token TEXT UNIQUE,
    name TEXT,
    owner_email TEXT,
    user_id INTEGER REFERENCES users(id),
    field_config TEXT,  -- JSON array
    theme_config TEXT,  -- JSON object
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

### Actions
```sql
CREATE TABLE actions (
    id INTEGER PRIMARY KEY,
    site_id INTEGER REFERENCES sites(id),
    type TEXT,          -- webhook/email/log/redirect/document
    config TEXT,        -- JSON object
    trigger_event TEXT DEFAULT 'submission',
    enabled BOOLEAN DEFAULT 1,
    execution_order INTEGER DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

### Submissions
```sql
CREATE TABLE submissions (
    id INTEGER PRIMARY KEY,
    site_id INTEGER REFERENCES sites(id),
    data TEXT,          -- JSON object of submitted fields
    submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

### Form Versions
```sql
CREATE TABLE form_versions (
    id INTEGER PRIMARY KEY,
    site_id INTEGER REFERENCES sites(id),
    field_config TEXT,  -- Snapshot of field_config at this version
    version_number INTEGER,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

## Security

- **API key auth** — `X-API-Key` or `Bearer` token for all agent-facing endpoints
- **CSRF protection** — Exempted for JSON API, required for HTML forms
- **Rate limiting** — Planned for Phase 5
- **Field sanitization** — `bleach` library for XSS prevention
- **PII encryption** — AES-256-GCM for sensitive fields (Phase 5)

## Deployment

### Docker Compose

```yaml
services:
  relay:
    build: .
    ports: ["5060:5060"]
    env_file: data/.env
    volumes:
      - ./app:/app/app
      - ./data:/app/data

  worker:
    build: .
    env_file: data/.env
    volumes:
      - ./app:/app/app
      - ./data:/app/data

  redis:
    image: redis:7-alpine
```

### Environment Variables (data/.env)

| Variable | Purpose |
|---|---|
| `SECRET_KEY` | Flask session signing |
| `DATABASE_URL` | Database connection string |
| `BUILDER_LLM_HOST` | LLM server address |
| `RESEND_API_KEY` | Email provider |
| `STRIPE_*` | Billing integration |

## Error Handling

| Status | Meaning |
|---|---|
| `200` | Success |
| `201` | Created |
| `400` | Bad request / validation error |
| `401` | Unauthorized |
| `403` | Forbidden |
| `404` | Not found |
| `422` | Validation errors on submission |
| `500` | Internal server error |

Response format:
```json
{
  "success": true/false,
  "error": "message (if failed)",
  "data": {...}  // or "fields": [...] for submission errors
}
```