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."
Build and deploy a FastAPI service on Hetzner that:
POST /api/v1/generate)This is the CORE PRODUCT CAPABILITY. Everything else (wizard, landing page, Stripe, CDE provisioning) depends on this engine working.
BLUEPRINT_MVP_ISO19650_v1.1.md Section 4.4gemini-3-flash available (Gemini 3 Flash via Vertex AI)api.struxio.ai/v1/generate (DNS not configured yet — use direct IP:port for dev)STRUXIO_App/api_gateways/iso19650_engine/STRUXIO_Logic/skills/iso_19650/ (to be created)devxio-network.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 │
└─────────────────────────────────────────┘
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.
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"]
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
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.
# 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"}
# 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.
/api/v1/healthgemini-3-flash succeeds and returns prosepytest tests/ -v)devxio-network/api/v1/preview returns same content with "[STRUXIO PREVIEW — WATERMARK]" header# 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')"
docker compose stop iso19650-engine && docker compose rm iso19650-enginedocker-compose.ymlSTRUXIO_App/api_gateways/iso19650_engine/ (entire new service)STRUXIO_Logic/skills/iso_19650/ (rules database)STRUXIO_OS/02_infra/docker-compose.yml (new service block)STRUXIO.ai // Confidential & Proprietary // © 2026