Files
breakpilot-compliance/backend-compliance/compliance/api/generation_routes.py
Benjamin Admin 53ff0722a4
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 32s
CI / test-python-backend-compliance (push) Successful in 39s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
fix(backend): SQLAlchemy text() fuer alle raw SQL + UI-Verbesserungen
- CRITICAL: Alle db.execute() Aufrufe in company_profile_routes.py
  und generation_routes.py mit text() gewrapped (SQLAlchemy 2.x)
- Geschaeftsmodell-Kacheln: Nur Kurztext, Beschreibung bei Klick
- "Warum diese Fragen" in Hauptbereich unter Ueberschrift verschoben
- Sidebar-Box entfernt fuer mehr Platz

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 22:53:29 +01:00

188 lines
6.7 KiB
Python

"""
FastAPI routes for Document Generation from Stammdaten.
Endpoints:
GET /generation/preview/{doc_type} — Markdown preview from Stammdaten
POST /generation/apply/{doc_type} — Generate drafts → create Change-Requests
"""
import json
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Header
from sqlalchemy import text
from sqlalchemy.orm import Session
from classroom_engine.database import get_db
from .tenant_utils import get_tenant_id
from .document_templates import (
generate_dsfa_draft,
generate_vvt_drafts,
generate_loeschfristen_drafts,
generate_tom_drafts,
generate_obligation_drafts,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/generation", tags=["generation"])
VALID_DOC_TYPES = {"dsfa", "vvt", "tom", "loeschfristen", "obligation"}
def _get_template_context(db, tid: str) -> dict:
"""Fetch company profile and build template context."""
from .company_profile_routes import _BASE_COLUMNS, row_to_response
from database import SessionLocal
# Use a fresh session for company_profile (different DB import pattern)
cp_db = SessionLocal()
try:
result = cp_db.execute(
text(f"SELECT {_BASE_COLUMNS} FROM compliance_company_profiles WHERE tenant_id = :tid"),
{"tid": tid},
)
row = result.fetchone()
if not row:
return None
resp = row_to_response(row)
# Build flat context
return {
"company_name": f"{resp.company_name} {resp.legal_form}".strip() if resp.legal_form else resp.company_name,
"company_name_short": resp.company_name,
"legal_form": resp.legal_form,
"industry": resp.industry,
"business_model": resp.business_model,
"company_size": resp.company_size,
"employee_count": resp.employee_count,
"headquarters_country": resp.headquarters_country,
"headquarters_city": resp.headquarters_city,
"primary_jurisdiction": resp.primary_jurisdiction,
"is_data_controller": resp.is_data_controller,
"is_data_processor": resp.is_data_processor,
"uses_ai": resp.uses_ai,
"dpo_name": resp.dpo_name or "",
"dpo_email": resp.dpo_email or "",
"supervisory_authority": resp.supervisory_authority or "",
"review_cycle_months": resp.review_cycle_months,
"subject_to_nis2": resp.subject_to_nis2,
"subject_to_ai_act": resp.subject_to_ai_act,
"subject_to_iso27001": resp.subject_to_iso27001,
"offerings": resp.offerings,
"target_markets": resp.target_markets,
"ai_use_cases": resp.ai_use_cases,
"repos": resp.repos,
"document_sources": resp.document_sources,
"processing_systems": resp.processing_systems,
"ai_systems": resp.ai_systems,
"technical_contacts": resp.technical_contacts,
"has_ai_systems": len(resp.ai_systems) > 0,
"processing_system_count": len(resp.processing_systems),
"ai_system_count": len(resp.ai_systems),
"is_complete": resp.is_complete,
}
finally:
cp_db.close()
def _generate_for_type(doc_type: str, ctx: dict):
"""Call the appropriate template generator."""
if doc_type == "dsfa":
return [generate_dsfa_draft(ctx)]
elif doc_type == "vvt":
return generate_vvt_drafts(ctx)
elif doc_type == "tom":
return generate_tom_drafts(ctx)
elif doc_type == "loeschfristen":
return generate_loeschfristen_drafts(ctx)
elif doc_type == "obligation":
return generate_obligation_drafts(ctx)
else:
raise ValueError(f"Unknown doc_type: {doc_type}")
@router.get("/preview/{doc_type}")
async def preview_generation(
doc_type: str,
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""Preview what documents would be generated (no DB writes)."""
if doc_type not in VALID_DOC_TYPES:
raise HTTPException(status_code=400, detail=f"Invalid doc_type: {doc_type}. Valid: {VALID_DOC_TYPES}")
ctx = _get_template_context(db, tid)
if not ctx:
raise HTTPException(status_code=404, detail="Company profile not found — fill Stammdaten first")
drafts = _generate_for_type(doc_type, ctx)
return {
"doc_type": doc_type,
"count": len(drafts),
"drafts": drafts,
"company_name": ctx.get("company_name"),
"is_preview": True,
}
@router.post("/apply/{doc_type}")
async def apply_generation(
doc_type: str,
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
x_user_id: Optional[str] = Header(None, alias="X-User-ID"),
):
"""Generate drafts and create Change-Requests for each.
Does NOT create documents directly — all go through the CR inbox.
"""
if doc_type not in VALID_DOC_TYPES:
raise HTTPException(status_code=400, detail=f"Invalid doc_type: {doc_type}. Valid: {VALID_DOC_TYPES}")
ctx = _get_template_context(db, tid)
if not ctx:
raise HTTPException(status_code=404, detail="Company profile not found — fill Stammdaten first")
drafts = _generate_for_type(doc_type, ctx)
user = x_user_id or "system"
cr_ids = []
for draft in drafts:
title = draft.get("title") or draft.get("name") or draft.get("data_category") or f"Neues {doc_type}-Dokument"
try:
result = db.execute(
text("""
INSERT INTO compliance_change_requests
(tenant_id, trigger_type, target_document_type,
proposal_title, proposal_body, proposed_changes,
priority, created_by)
VALUES (:tid, 'generation', :doc_type,
:title, :body, CAST(:changes AS jsonb),
'normal', :user)
RETURNING id
"""),
{
"tid": tid,
"doc_type": doc_type,
"title": f"[Generiert] {title}",
"body": f"Automatisch aus Stammdaten generiert für {ctx.get('company_name', '')}",
"changes": json.dumps(draft),
"user": user,
},
)
row = result.fetchone()
if row:
cr_ids.append(str(row[0]))
except Exception as e:
logger.error(f"Failed to create CR for draft: {e}")
db.commit()
return {
"doc_type": doc_type,
"drafts_generated": len(drafts),
"change_requests_created": len(cr_ids),
"change_request_ids": cr_ids,
}