IDENTITY

title: "TICKET_044: BEP/EIR Generation Engine — Phase 1 (FastAPI + UK Template)"
type: ticket
subtype: execution
purpose: "Build the core revenue-generating capability: a FastAPI service that takes wizard input data and generates ISO 19650-compliant BEP/EIR documents using deterministic rules + LLM assembly."

GOVERNANCE

status: done
priority: P0
sprint: S001_base_and_mvp1
assignee: "Claude Code (Sonnet 4.6) — scaffold + templates. Architect review required before merge."
estimated_hours: 6
depends_on: [TICKET_043]
blocks: [TICKET_038, TICKET_039, TICKET_040]
last_updated: "2026-03-16"

TICKET_044: BEP/EIR Generation Engine — Phase 1

Objective

Build and deploy a FastAPI service on Hetzner that:

  1. Accepts project wizard data as JSON input (POST /api/v1/generate)
  2. Maps input to ISO 19650 template sections using a deterministic rules database
  3. Routes to Gemini Flash via LiteLLM for natural language document assembly
  4. Returns structured Markdown (BEP + EIR) as response
  5. Serves as the backend for the 15-minute wizard (TICKET_038) and the free trial watermarked PDF (TICKET_039)

This is the CORE PRODUCT CAPABILITY. Everything else (wizard, landing page, Stripe, CDE provisioning) depends on this engine working.

Context

Constraints

Architecture

Odoo Website (wizard form)
    │
    ▼ POST JSON
┌─────────────────────────────────────────┐
│  FastAPI: iso19650-engine (port 8010)   │
│                                         │
│  /api/v1/generate                       │
│    1. Validate input (Pydantic)         │
│    2. Load national_annex rules (YAML)  │
│    3. Map wizard data → template slots  │
│    4. LiteLLM → Gemini Flash: assemble  │
│    5. Return structured Markdown        │
│                                         │
│  /api/v1/preview (free trial)           │
│    Same as above but adds watermark     │
│                                         │
│  /api/v1/health                         │
│    Returns: { status: ok, version: x }  │
└─────────────────────────────────────────┘
    │
    ▼ HTTP via Docker network
┌─────────────────────────────────────────┐
│  LiteLLM Proxy (devxio-litellm:4000)   │
│  Model: gemini-3-flash                  │
└─────────────────────────────────────────┘

Execution Plan

Task 1: Create the Rules Database (STRUXIO_Logic) — 60 min

Create the ISO 19650 knowledge base that the engine queries deterministically.

File: STRUXIO_Logic/skills/iso_19650/national_annexes/uk_bim_framework.yaml

# UK BIM Framework — National Annex Rules
# Source: BS EN ISO 19650-2:2018 UK National Annex + PAS 1192 series
annex_id: uk
annex_name: "UK BIM Framework"
standard_ref: "BS EN ISO 19650-2:2018 + UK National Annex"

naming_convention:
  format: "{project}-{originator}-{functional}-{spatial}-{form}-{discipline}-{number}"
  example: "PRJ1-ARC-ZZ-01-M3-A-0001"
  fields:
    project: "Project code (e.g., PRJ1)"
    originator: "3-letter company code (e.g., ARC)"
    functional: "Functional breakdown (ZZ = not applicable)"
    spatial: "Spatial breakdown / floor (01 = Level 01)"
    form: "M3 = 3D Model, DR = Drawing, SH = Schedule, SP = Specification"
    discipline: "A = Architecture, S = Structural, M = Mechanical, E = Electrical, C = Civil"
    number: "Sequential (0001, 0002, ...)"

status_codes:
  wip:
    - code: S0
      name: "Work In Progress"
      description: "Unchecked data, visible only to originating task team"
  shared:
    - code: S1
      name: "Suitable for Coordination"
      description: "Checked by originator, shared for spatial coordination"
    - code: S2
      name: "Suitable for Information"
      description: "Shared as reference information"
    - code: S3
      name: "Suitable for Review and Comment"
      description: "Shared for formal review and comment"
    - code: S4
      name: "Suitable for Stage Approval"
      description: "Shared for stage gate approval"
  published:
    - code: A1
      name: "Accepted — Stage 1"
      description: "Authorized by appointing party for use"
    - code: A2
      name: "Accepted — Stage 2"
    - code: A3
      name: "Accepted — Stage 3"
    - code: A4
      name: "Accepted — Stage 4"
    - code: A5
      name: "Accepted — Stage 5"
    - code: A6
      name: "Accepted — Stage 6"
  as_built:
    - code: CR
      name: "As-Constructed Record"
      description: "Verified as-built information"

cde_state_transitions:
  - from: S0
    to: S1
    gate: "Originator self-check + task team manager approval"
  - from: S1
    to: S2
    gate: "Lead appointed party coordination review"
  - from: S2
    to: A1
    gate: "Appointing party formal acceptance"

classification_system: "Uniclass 2015"

bep_required_sections:
  - id: info_mgmt_functions
    title: "Information Management Functions and Roles"
    iso_ref: "ISO 19650-2 §5.3.1"
    description: "Define the information management roles: BIM Manager, Lead BIM Coordinator, Task Team BIM Coordinators"
  - id: delivery_strategy
    title: "Information Delivery Strategy"
    iso_ref: "ISO 19650-2 §5.3.2"
    description: "How information will be produced, exchanged, and managed across the delivery team"
  - id: loin
    title: "Level of Information Need (LOIN)"
    iso_ref: "ISO 19650-2 §5.3.3"
    description: "Specify geometric detail, alphanumeric data, and documentation requirements per element type and project stage"
  - id: software_hardware
    title: "Software and Hardware"
    iso_ref: "ISO 19650-2 §5.3.4"
    description: "Approved software platforms, versions, file formats, and hardware requirements"
  - id: it_security
    title: "IT Security Approach"
    iso_ref: "ISO 19650-5"
    description: "Data security measures, access controls, and sensitivity classification"
  - id: methods_procedures
    title: "Methods and Procedures"
    iso_ref: "ISO 19650-2 §5.3.5"
    description: "Coordination procedures, clash detection workflow, design review process"
  - id: responsibility_matrix
    title: "Responsibility Matrix"
    iso_ref: "ISO 19650-2 Annex A"
    description: "RACI matrix mapping information containers to responsible parties"
  - id: midp
    title: "Master Information Delivery Plan (MIDP)"
    iso_ref: "ISO 19650-2 §5.4"
    description: "Timeline of all information deliverables with milestone dates"

eir_required_sections:
  - id: information_requirements
    title: "Exchange Information Requirements"
    iso_ref: "ISO 19650-2 §5.2"
  - id: acceptance_criteria
    title: "Acceptance Criteria"
  - id: delivery_milestones
    title: "Information Delivery Milestones"
  - id: information_standard
    title: "Information Standard (Naming, Classification)"
  - id: production_methods
    title: "Information Production Methods and Procedures"

File: STRUXIO_Logic/skills/iso_19650/national_annexes/generic_iso.yaml
Simplified version without UK-specific naming convention. Uses ISO 19650 base requirements only.

File: STRUXIO_Logic/skills/iso_19650/templates/bep_template.md
Markdown template with {placeholder} variables for each BEP section. The LLM fills these.

File: STRUXIO_Logic/skills/iso_19650/templates/eir_template.md
Same pattern for EIR.

Task 2: Build the FastAPI Service — 90 min

Create the service at STRUXIO_App/api_gateways/iso19650_engine/:

iso19650_engine/
├── Dockerfile
├── requirements.txt
├── app/
│   ├── __init__.py
│   ├── main.py              # FastAPI app, CORS, routes
│   ├── config.py             # Settings (LiteLLM URL, rules path)
│   ├── models/
│   │   ├── __init__.py
│   │   ├── wizard_input.py   # Pydantic: WizardInput schema (from MVP §4.3)
│   │   └── generation_output.py  # Pydantic: GeneratedDocuments response
│   ├── engine/
│   │   ├── __init__.py
│   │   ├── rules_loader.py   # Load national_annex YAML
│   │   ├── template_mapper.py # Map wizard input → template slots
│   │   ├── llm_assembler.py  # LiteLLM call to Gemini Flash
│   │   └── watermark.py      # Add watermark for free trial
│   └── routes/
│       ├── __init__.py
│       ├── generate.py       # POST /api/v1/generate
│       ├── preview.py        # POST /api/v1/preview (watermarked)
│       └── health.py         # GET /api/v1/health
└── tests/
    ├── __init__.py
    ├── test_wizard_input.py  # Validates Pydantic models
    ├── test_rules_loader.py  # Validates UK annex loads
    ├── test_template_mapper.py
    └── test_generate.py      # E2E test with mock LLM

Key implementation details:

wizard_input.py — Pydantic model matching MVP Blueprint §4.3:

class WizardInput(BaseModel):
    """Input from the 15-minute wizard. Matches MVP Blueprint §4.3 schema."""
    organization: Organization
    project: Project
    stakeholders: StakeholderMatrix
    information: InformationRequirements
    delivery: DeliveryTimeline
    tier: Literal["starter", "professional", "enterprise"] = "professional"

llm_assembler.py — calls LiteLLM proxy:

# The LLM receives:
# 1. System prompt: "You are an ISO 19650 BIM compliance document writer..."
# 2. Template with filled deterministic sections (from rules DB)
# 3. Wizard data for context-specific prose
# The LLM ONLY writes the prose paragraphs. It does NOT decide requirements.

import httpx

LITELLM_URL = "http://devxio-litellm:4000/v1/chat/completions"

async def assemble_bep_section(section_id: str, template: str, wizard_data: dict) -> str:
    """Call Gemini Flash via LiteLLM to assemble one BEP section."""
    response = await httpx.AsyncClient().post(LITELLM_URL, json={
        "model": "gemini-3-flash",
        "messages": [
            {"role": "system", "content": BEP_SYSTEM_PROMPT},
            {"role": "user", "content": f"Section: {section_id}\nTemplate:\n{template}\nProject data:\n{json.dumps(wizard_data)}"}
        ],
        "temperature": 0.3,
        "max_tokens": 2048
    })
    return response.json()["choices"][0]["message"]["content"]

Task 3: Docker Integration — 20 min

Add to STRUXIO_OS/02_infra/docker-compose.yml:

  # ── ISO 19650 BEP/EIR Generation Engine ──────────────────────────────────
  iso19650-engine:
    build:
      context: ../../STRUXIO_App/api_gateways/iso19650_engine
      dockerfile: Dockerfile
    container_name: iso19650-engine
    ports:
      - "${PORT_ISO_ENGINE:-8010}:8000"
    environment:
      - LITELLM_BASE_URL=http://devxio-litellm:4000
      - RULES_PATH=/app/rules
    volumes:
      # Mount the rules database (read-only)
      - ../../STRUXIO_Logic/skills/iso_19650:/app/rules:ro
    depends_on:
      - devxio-litellm
    networks:
      - devxio-network
    restart: unless-stopped

Dockerfile:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

requirements.txt:

fastapi>=0.115.0
uvicorn[standard]>=0.34.0
pydantic>=2.10.0
httpx>=0.28.0
pyyaml>=6.0
jinja2>=3.1.0

Task 4: Write Tests — 30 min

All tests must pass BEFORE the Docker build:

cd STRUXIO_App/api_gateways/iso19650_engine
pip install -r requirements.txt
pip install pytest pytest-asyncio httpx
pytest tests/ -v

Test 1: test_wizard_input.py — validates Pydantic schema accepts sample wizard data and rejects invalid data.

Test 2: test_rules_loader.py — validates UK annex YAML loads and contains all required sections.

Test 3: test_template_mapper.py — given wizard input + UK annex, produces a complete template with all placeholders filled.

Test 4: test_generate.py — E2E test with mocked LLM response. Verifies the full pipeline: input → rules → template → (mock) LLM → structured Markdown output.

Task 5: Deploy to Hetzner — 20 min

# SSH to Hetzner
ssh root@89.167.96.154

# Pull latest code
cd ~/STRUXIO_Workspace/STRUXIO_App && git pull
cd ~/STRUXIO_Workspace/STRUXIO_Logic && git pull
cd ~/STRUXIO_Workspace/STRUXIO_OS && git pull

# Build and start the new service
cd ~/STRUXIO_Workspace/STRUXIO_OS/02_infra
docker compose up -d --build iso19650-engine

# Verify
curl http://localhost:8010/api/v1/health
# Expected: {"status": "ok", "version": "0.1.0"}

Task 6: Smoke Test with Sample Data — 15 min

# Send a test generation request
curl -X POST http://89.167.96.154:8010/api/v1/generate \
  -H "Content-Type: application/json" \
  -d '{
    "organization": {
      "name": "Acme Construction Ltd",
      "abbreviation": "ACM",
      "country": "UK",
      "company_size": "51-200"
    },
    "project": {
      "name": "London Bridge Tower Renovation",
      "code": "LBT1",
      "type": "Building",
      "sub_type": "Commercial",
      "estimated_value": 25000000,
      "phases": ["Design", "Construction"]
    },
    "stakeholders": {
      "appointing_party": {"name": "City of London Corp", "role": "Appointing Party"},
      "lead_appointed_party": {"name": "Acme Construction Ltd", "role": "Lead Appointed Party"},
      "appointed_parties": [
        {"name": "Foster Architecture", "discipline": "Architecture", "abbreviation": "FOS"},
        {"name": "Arup Engineering", "discipline": "Structural", "abbreviation": "ARP"}
      ]
    },
    "information": {
      "level_of_information_need": "Standard",
      "bim_uses": ["Clash Detection", "Cost Estimation", "FM Handover"],
      "file_formats": ["IFC", "Revit", "PDF"],
      "classification_system": "Uniclass"
    },
    "delivery": {
      "project_start": "2026-06-01",
      "key_milestones": [
        {"name": "RIBA Stage 3 Complete", "date": "2026-09-01", "deliverables": "Developed design models"}
      ],
      "handover_date": "2028-03-01"
    },
    "tier": "professional"
  }'

Expected: JSON response with bep_markdown and eir_markdown fields containing structured ISO 19650 compliant documents.

Acceptance Criteria

Verification Commands

# V1: Service is running
curl -s http://89.167.96.154:8010/api/v1/health | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['status']=='ok'; print('V1 PASS:', d)"

# V2: Tests pass
cd STRUXIO_App/api_gateways/iso19650_engine && pytest tests/ -v 2>&1 | tail -5
# Expected: "X passed" with 0 failures

# V3: Rules load
python3 -c "import yaml; d=yaml.safe_load(open('STRUXIO_Logic/skills/iso_19650/national_annexes/uk_bim_framework.yaml')); print('V3 PASS: UK annex has', len(d['bep_required_sections']), 'BEP sections')"
# Expected: "V3 PASS: UK annex has 8 BEP sections"

# V4: Docker container healthy
docker ps --filter name=iso19650-engine --format '{{.Status}}'
# Expected: "Up X minutes"

# V5: Smoke test (full pipeline)
# Run the curl command from Task 6, check response contains "BIM Execution Plan"
curl -s -X POST http://89.167.96.154:8010/api/v1/generate -H "Content-Type: application/json" -d '...(sample data)...' | python3 -c "import sys,json; d=json.load(sys.stdin); assert 'bep_markdown' in d; print('V5 PASS: BEP length', len(d['bep_markdown']), 'chars')"

Rollback Plan

  1. docker compose stop iso19650-engine && docker compose rm iso19650-engine
  2. Remove service block from docker-compose.yml
  3. Existing DEVXIO services are unaffected (separate container, no shared state)

Affected Files (New)

What This Does NOT Cover (Future Tickets)


STRUXIO.ai // Confidential & Proprietary // © 2026