feat: Add catalog generation functionality and related models
- Implemented CatalogResource for handling catalog generation requests and status checks. - Created CatalogGenerationService to manage the asynchronous generation process. - Added models for catalog generation responses, requests, and structured catalog data. - Introduced utility scripts for database interaction and Kubernetes pod retrieval. - Enhanced error handling and logging throughout the catalog generation workflow.
This commit is contained in:
@@ -122,7 +122,7 @@ CREATE OR REPLACE PACKAGE DC_BACKEND_PKG AS
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Generiert den Markdown-Prüfbericht für ein Projekt neu und speichert ihn
|
||||
-- in dc_projects.report_markdown. Ruft intern dc_generate_report auf.
|
||||
-- in dc_projects.report_markdown. Ruft intern DC_UTILS_PKG.generate_report auf.
|
||||
--
|
||||
-- Beispiel:
|
||||
-- DC_BACKEND_PKG.regenerate_report(1);
|
||||
@@ -131,6 +131,66 @@ CREATE OR REPLACE PACKAGE DC_BACKEND_PKG AS
|
||||
p_project_id IN NUMBER
|
||||
);
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Startet die asynchrone Katalog-Generierung aus einem Regelwerk-Dokument.
|
||||
-- Gibt sofort zurück (202 Accepted-Muster) – die KI läuft im Hintergrund.
|
||||
--
|
||||
-- p_file BLOB des Regelwerks (PDF, DOCX, ODT, TXT)
|
||||
-- p_mime_type MIME-Typ, z.B. 'application/pdf'
|
||||
-- p_filename Dateiname (wird im multipart-Part übergeben)
|
||||
-- p_catalog_name Name des anzulegenden Katalogs (z.B. 'ADSp 2017')
|
||||
-- p_document_type_id ID des Dokumenttyps (dc_document_types.id)
|
||||
-- p_description Optionale Beschreibung
|
||||
-- Rückgabe ID des Katalog-Placeholders (Status: GENERATING)
|
||||
--
|
||||
-- Beispiel (mit Warten auf Abschluss):
|
||||
-- DECLARE
|
||||
-- l_blob BLOB;
|
||||
-- l_id NUMBER;
|
||||
-- BEGIN
|
||||
-- SELECT file_blob INTO l_blob FROM dc_reference_documents WHERE id = 1;
|
||||
-- l_id := DC_BACKEND_PKG.generate_catalog(
|
||||
-- p_file => l_blob,
|
||||
-- p_mime_type => 'application/pdf',
|
||||
-- p_filename => 'ADSp-2017.pdf',
|
||||
-- p_catalog_name => 'ADSp 2017',
|
||||
-- p_document_type_id => 1,
|
||||
-- p_description => 'Allgemeine Deutsche Spediteurbedingungen 2017');
|
||||
-- DBMS_OUTPUT.PUT_LINE('Katalog ID: ' || l_id || ' (läuft im Hintergrund)');
|
||||
-- DC_BACKEND_PKG.wait_for_catalog(l_id); -- optional, wartet bis READY
|
||||
-- DBMS_OUTPUT.PUT_LINE('Katalog fertig!');
|
||||
-- END;
|
||||
-- -------------------------------------------------------------------------
|
||||
FUNCTION generate_catalog(
|
||||
p_file IN BLOB,
|
||||
p_mime_type IN VARCHAR2 DEFAULT 'application/pdf',
|
||||
p_filename IN VARCHAR2 DEFAULT 'dokument.pdf',
|
||||
p_catalog_name IN VARCHAR2,
|
||||
p_document_type_id IN NUMBER,
|
||||
p_description IN VARCHAR2 DEFAULT NULL
|
||||
) RETURN NUMBER;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Liest den Generierungsstatus direkt aus der DB (kein REST-Aufruf).
|
||||
-- Rückgabe: 'GENERATING', 'READY' oder 'ERROR'
|
||||
-- -------------------------------------------------------------------------
|
||||
FUNCTION get_catalog_status(
|
||||
p_catalog_id IN NUMBER
|
||||
) RETURN VARCHAR2;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Wartet (polling) bis der Katalog den Status READY erreicht hat.
|
||||
-- Wirft eine Exception bei Timeout oder wenn der Katalog ERROR meldet.
|
||||
--
|
||||
-- p_timeout_sec Max. Wartezeit in Sekunden (Default: 1800 = 30 Min.)
|
||||
-- p_interval_sec Polling-Intervall in Sekunden (Default: 10)
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE wait_for_catalog(
|
||||
p_catalog_id IN NUMBER,
|
||||
p_timeout_sec IN NUMBER DEFAULT 1800,
|
||||
p_interval_sec IN NUMBER DEFAULT 10
|
||||
);
|
||||
|
||||
END DC_BACKEND_PKG;
|
||||
/
|
||||
|
||||
@@ -376,7 +436,7 @@ CREATE OR REPLACE PACKAGE BODY DC_BACKEND_PKG AS
|
||||
p_project_id IN NUMBER
|
||||
) IS
|
||||
BEGIN
|
||||
dc_generate_report(p_project_id => p_project_id);
|
||||
DC_UTILS_PKG.generate_report(p_project_id => p_project_id);
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
RAISE_APPLICATION_ERROR(-20110,
|
||||
@@ -384,6 +444,190 @@ CREATE OR REPLACE PACKAGE BODY DC_BACKEND_PKG AS
|
||||
END regenerate_report;
|
||||
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Interne Hilfsfunktion: schreibt einen VARCHAR2-Text als UTF8-Bytes in ein BLOB.
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE append_text_to_blob(p_dest IN OUT BLOB, p_text IN VARCHAR2) IS
|
||||
l_raw RAW(32767);
|
||||
BEGIN
|
||||
l_raw := UTL_RAW.CAST_TO_RAW(p_text);
|
||||
DBMS_LOB.WRITEAPPEND(p_dest, UTL_RAW.LENGTH(l_raw), l_raw);
|
||||
END append_text_to_blob;
|
||||
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
FUNCTION get_catalog_status(
|
||||
p_catalog_id IN NUMBER
|
||||
) RETURN VARCHAR2 IS
|
||||
l_status VARCHAR2(20);
|
||||
BEGIN
|
||||
SELECT generation_status
|
||||
INTO l_status
|
||||
FROM dc_question_catalogs
|
||||
WHERE id = p_catalog_id;
|
||||
RETURN l_status;
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
RAISE_APPLICATION_ERROR(-20310,
|
||||
'Katalog ' || p_catalog_id || ' nicht gefunden.');
|
||||
END get_catalog_status;
|
||||
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE wait_for_catalog(
|
||||
p_catalog_id IN NUMBER,
|
||||
p_timeout_sec IN NUMBER DEFAULT 1800,
|
||||
p_interval_sec IN NUMBER DEFAULT 10
|
||||
) IS
|
||||
l_start TIMESTAMP := SYSTIMESTAMP;
|
||||
l_status VARCHAR2(20);
|
||||
l_elapsed NUMBER;
|
||||
BEGIN
|
||||
LOOP
|
||||
l_status := get_catalog_status(p_catalog_id);
|
||||
|
||||
IF l_status = 'READY' THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF l_status = 'ERROR' THEN
|
||||
DECLARE
|
||||
l_error VARCHAR2(4000);
|
||||
BEGIN
|
||||
SELECT generation_error
|
||||
INTO l_error
|
||||
FROM dc_question_catalogs
|
||||
WHERE id = p_catalog_id;
|
||||
RAISE_APPLICATION_ERROR(-20311,
|
||||
'Katalog ' || p_catalog_id || ' Generierung fehlgeschlagen: '
|
||||
|| NVL(l_error, 'Unbekannter Fehler'));
|
||||
END;
|
||||
END IF;
|
||||
|
||||
-- Timeout prüfen
|
||||
l_elapsed :=
|
||||
EXTRACT(SECOND FROM (SYSTIMESTAMP - l_start))
|
||||
+ EXTRACT(MINUTE FROM (SYSTIMESTAMP - l_start)) * 60
|
||||
+ EXTRACT(HOUR FROM (SYSTIMESTAMP - l_start)) * 3600;
|
||||
|
||||
IF l_elapsed > p_timeout_sec THEN
|
||||
RAISE_APPLICATION_ERROR(-20312,
|
||||
'Timeout nach ' || p_timeout_sec || 's – Katalog ' ||
|
||||
p_catalog_id || ' noch im Status ' || l_status || '.');
|
||||
END IF;
|
||||
|
||||
DBMS_SESSION.SLEEP(p_interval_sec);
|
||||
END LOOP;
|
||||
END wait_for_catalog;
|
||||
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
FUNCTION generate_catalog(
|
||||
p_file IN BLOB,
|
||||
p_mime_type IN VARCHAR2 DEFAULT 'application/pdf',
|
||||
p_filename IN VARCHAR2 DEFAULT 'dokument.pdf',
|
||||
p_catalog_name IN VARCHAR2,
|
||||
p_document_type_id IN NUMBER,
|
||||
p_description IN VARCHAR2 DEFAULT NULL
|
||||
) RETURN NUMBER IS
|
||||
|
||||
c_crlf CONSTANT VARCHAR2(2) := CHR(13) || CHR(10);
|
||||
l_boundary VARCHAR2(50) := 'dc-boundary-' || TO_CHAR(SYSDATE, 'YYYYMMDDHHMI')
|
||||
|| DBMS_RANDOM.STRING('X', 8);
|
||||
l_url VARCHAR2(500);
|
||||
l_body BLOB;
|
||||
l_response CLOB;
|
||||
l_status NUMBER;
|
||||
l_catalog_id NUMBER;
|
||||
|
||||
-- Hilfsfunktion: multipart-Textfeld anfügen
|
||||
PROCEDURE add_field(p_name VARCHAR2, p_value VARCHAR2) IS
|
||||
BEGIN
|
||||
IF p_value IS NULL THEN RETURN; END IF;
|
||||
append_text_to_blob(l_body,
|
||||
'--' || l_boundary || c_crlf
|
||||
|| 'Content-Disposition: form-data; name="' || p_name || '"' || c_crlf
|
||||
|| c_crlf
|
||||
|| p_value || c_crlf);
|
||||
END add_field;
|
||||
|
||||
BEGIN
|
||||
l_url := c_base_url || '/catalog/generate';
|
||||
|
||||
-- ---- multipart-Body aufbauen ----
|
||||
DBMS_LOB.CREATETEMPORARY(l_body, TRUE);
|
||||
|
||||
-- Textfelder
|
||||
add_field('catalog_name', p_catalog_name);
|
||||
add_field('document_type_id', TO_CHAR(p_document_type_id));
|
||||
add_field('description', p_description);
|
||||
|
||||
-- Datei-Part: Header
|
||||
append_text_to_blob(l_body,
|
||||
'--' || l_boundary || c_crlf
|
||||
|| 'Content-Disposition: form-data; name="file"; filename="'
|
||||
|| p_filename || '"' || c_crlf
|
||||
|| 'Content-Type: ' || p_mime_type || c_crlf
|
||||
|| c_crlf);
|
||||
|
||||
-- Datei-Part: BLOB-Inhalt anhängen
|
||||
DBMS_LOB.APPEND(l_body, p_file);
|
||||
|
||||
-- Datei-Part: Abschluss-CRLF
|
||||
append_text_to_blob(l_body, c_crlf);
|
||||
|
||||
-- Abschließende Boundary
|
||||
append_text_to_blob(l_body, '--' || l_boundary || '--' || c_crlf);
|
||||
|
||||
-- ---- HTTP-Request senden ----
|
||||
APEX_WEB_SERVICE.G_REQUEST_HEADERS.DELETE;
|
||||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(1).NAME := 'Content-Type';
|
||||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(1).VALUE :=
|
||||
'multipart/form-data; boundary=' || l_boundary;
|
||||
|
||||
IF g_api_key IS NOT NULL AND LENGTH(TRIM(g_api_key)) > 0 THEN
|
||||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(2).NAME := 'X-API-KEY';
|
||||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(2).VALUE := g_api_key;
|
||||
END IF;
|
||||
|
||||
l_response := APEX_WEB_SERVICE.MAKE_REST_REQUEST(
|
||||
p_url => l_url,
|
||||
p_http_method => 'POST',
|
||||
p_body_blob => l_body
|
||||
);
|
||||
DBMS_LOB.FREETEMPORARY(l_body);
|
||||
|
||||
l_status := APEX_WEB_SERVICE.G_STATUS_CODE;
|
||||
|
||||
-- 202 Accepted = asynchrone Verarbeitung gestartet
|
||||
IF l_status NOT IN (200, 201, 202) THEN
|
||||
RAISE_APPLICATION_ERROR(-20300,
|
||||
'Katalog-Generierung fehlgeschlagen: HTTP ' || l_status
|
||||
|| ' – ' || SUBSTR(l_response, 1, 500));
|
||||
END IF;
|
||||
|
||||
-- catalog_id aus JSON-Antwort lesen:
|
||||
-- {"catalog_id": 42, "catalog_name": "...", "generation_status": "GENERATING"}
|
||||
l_catalog_id := TO_NUMBER(JSON_VALUE(l_response, '$.catalog_id'));
|
||||
|
||||
IF l_catalog_id IS NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20301,
|
||||
'Antwort enthält keine catalog_id: ' || SUBSTR(l_response, 1, 500));
|
||||
END IF;
|
||||
|
||||
RETURN l_catalog_id;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF DBMS_LOB.ISTEMPORARY(l_body) = 1 THEN
|
||||
DBMS_LOB.FREETEMPORARY(l_body);
|
||||
END IF;
|
||||
IF SQLCODE IN (-20300, -20301) THEN RAISE; END IF;
|
||||
RAISE_APPLICATION_ERROR(-20302,
|
||||
'Verbindungsfehler bei Katalog-Generierung: ' || SQLERRM);
|
||||
END generate_catalog;
|
||||
|
||||
|
||||
END DC_BACKEND_PKG;
|
||||
/
|
||||
|
||||
|
||||
Reference in New Issue
Block a user