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
- 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>
188 lines
6.7 KiB
Python
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,
|
|
}
|