From fb185c1a7156f36039fc9df97c4db985bb32099e Mon Sep 17 00:00:00 2001 From: Wolf Beckmann Date: Mon, 1 Jun 2026 10:33:30 +0200 Subject: [PATCH] Funktioniert --- .gitignore | 2 + .../frigosped/dc/resource/CheckResource.java | 70 +++++++++++------- .../target/classes/application.properties | 16 ++-- .../de/frigosped/dc/ai/AiModelProducer.class | Bin 3830 -> 4406 bytes .../de/frigosped/dc/client/OrdsClient.class | Bin 2048 -> 3204 bytes .../de/frigosped/dc/model/OrdsQuestion.class | Bin 2902 -> 3322 bytes .../frigosped/dc/model/ProgressRequest.class | Bin 786 -> 2077 bytes .../frigosped/dc/resource/CheckResource.class | Bin 3988 -> 5181 bytes .../service/CheckOrchestrationService.class | Bin 8755 -> 13195 bytes .../compile/default-compile/createdFiles.lst | 21 +++++- .../compile/default-compile/inputFiles.lst | 14 ++++ 11 files changed, 92 insertions(+), 31 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0d0fd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dc-backend/target + diff --git a/dc-backend/src/main/java/de/frigosped/dc/resource/CheckResource.java b/dc-backend/src/main/java/de/frigosped/dc/resource/CheckResource.java index d83a02f..90a4db4 100644 --- a/dc-backend/src/main/java/de/frigosped/dc/resource/CheckResource.java +++ b/dc-backend/src/main/java/de/frigosped/dc/resource/CheckResource.java @@ -7,25 +7,42 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; import java.util.Map; -import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; /** * REST-Endpunkte des Dokumenten-Check-Backend. * * POST /check/{projectId} - * → Startet die asynchrone Verarbeitung eines Projekts. - * → Gibt sofort 202 Accepted zurück; Fortschritt über ORDS-Endpunkte ablesbar. + * → Reiht das Projekt in eine sequentielle Warteschlange ein (202 sofort). + * → Projekte werden strikt nacheinander verarbeitet (kein paralleler Betrieb). * * GET /check/health - * → Einfacher Health-Check. + * → Einfacher Health-Check inkl. Warteschlangenlänge. */ @Path("/check") @Produces(MediaType.APPLICATION_JSON) @Tag(name = "Dokumenten-Check", description = "KI-gestützte Dokumentenprüfung") public class CheckResource { + private static final Logger LOG = Logger.getLogger(CheckResource.class); + + // Single-Thread-Executor: garantiert sequentielle Abarbeitung. + // Eingehende Aufrufe werden automatisch in die Warteschlange gelegt. + private static final ExecutorService QUEUE = + Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "dc-processing-thread"); + t.setDaemon(true); + return t; + }); + + // Anzahl Projekte in der Warteschlange (inkl. laufendem) + private static final AtomicInteger QUEUED = new AtomicInteger(0); + @Inject CheckOrchestrationService orchestrationService; @@ -36,35 +53,34 @@ public class CheckResource { @POST @Path("/{projectId}") @Operation( - summary = "Startet die Dokumentenprüfung für ein Projekt", + summary = "Reiht ein Projekt in die Verarbeitungswarteschlange ein", description = """ - Triggert den vollständigen Prüfablauf asynchron: - 1. OCR / Konvertierung der Dokumente (KI) - 2. Übersetzung ins Deutsche (KI) - 3. Auswertung des Fragenkatalogs (KI) - 4. Ergebnisse werden in ORDS gespeichert + Projekte werden strikt nacheinander verarbeitet. + Der Aufruf kehrt sofort mit 202 zurück; die eigentliche Verarbeitung + startet sobald alle vorher eingereihten Projekte abgeschlossen sind. Das Projekt muss im Status PENDING sein. Fortschritt: GET {ords}/api/dc/projects/{id} """ ) public Response startCheck(@PathParam("projectId") long projectId) { - // Asynchron starten – Fire & Forget. - // Der aufrufende Thread (z.B. APEX-Button) bekommt sofort 202. - // Der Verarbeitungs-Thread läuft im ForkJoin-CommonPool. - CompletableFuture - .runAsync(() -> orchestrationService.process(projectId)) - .exceptionally(ex -> { - // Der Orchestrator fängt intern; dies ist eine letzte Absicherung. - // Logging ist dort bereits vorhanden. - return null; - }); + int position = QUEUED.incrementAndGet(); + + QUEUE.submit(() -> { + try { + LOG.infof("Starte Projekt %d (Warteschlange: %d)", projectId, QUEUED.get()); + orchestrationService.process(projectId); + } finally { + QUEUED.decrementAndGet(); + } + }); return Response .accepted(Map.of( - "message", "Verarbeitung gestartet", - "project_id", projectId, - "hint", "Fortschritt: GET /api/dc/projects/" + projectId + "message", "Projekt in Warteschlange eingereiht", + "project_id", projectId, + "queue_position", position, + "hint", "Fortschritt: GET /api/dc/projects/" + projectId )) .build(); } @@ -75,8 +91,12 @@ public class CheckResource { @GET @Path("/health") - @Operation(summary = "Health-Check", description = "Gibt UP zurück wenn der Service läuft.") + @Operation(summary = "Health-Check", description = "Gibt UP zurück inkl. Warteschlangenlänge.") public Response health() { - return Response.ok(Map.of("status", "UP", "service", "dc-backend")).build(); + return Response.ok(Map.of( + "status", "UP", + "service", "dc-backend", + "queued", QUEUED.get() + )).build(); } } diff --git a/dc-backend/target/classes/application.properties b/dc-backend/target/classes/application.properties index 2ffa717..b9d65bc 100644 --- a/dc-backend/target/classes/application.properties +++ b/dc-backend/target/classes/application.properties @@ -18,13 +18,19 @@ dc.ai.base-url=https://ollama.aquantico.de dc.ai.api-key=324GF44-50AA-4B57-9386-K435DLJ764DFR # Hauptmodell: Auswertung + Übersetzung -dc.ai.main.model=gpt-oss:20b +dc.ai.main.model=qwen3.6:35b-a3b-q4_K_M dc.ai.main.timeout=300s dc.ai.main.max-retries=2 dc.ai.main.temperature=0.1 +# Katalog-Generierungsmodell: längerer Timeout für vollständige Katalog-Analyse +dc.ai.catalog.model=qwen3.6:35b-a3b-q4_K_M +dc.ai.catalog.timeout=1200s +dc.ai.catalog.max-retries=1 +dc.ai.catalog.temperature=0.1 + # OCR-Modell: Bildtext-Extraktion aus PDF-Seiten -dc.ai.ocr.model=quen3.5:9b +dc.ai.ocr.model=qwen3.6:35b-a3b-q4_K_M dc.ai.ocr.timeout=120s dc.ai.ocr.max-retries=2 @@ -32,7 +38,7 @@ dc.ai.ocr.max-retries=2 # ORDS REST Client # ============================================================================= # Basis-URL des ORDS-Servers (Schema-Pfad anpassen) -quarkus.rest-client.ords-api.url=http://localhost:8080/ords/FRIGOSPED_APP +quarkus.rest-client.ords-api.url=http://ords:8080/ords/ai_dev quarkus.rest-client.ords-api.scope=jakarta.inject.Singleton quarkus.rest-client.ords-api.connect-timeout=10000 quarkus.rest-client.ords-api.read-timeout=60000 @@ -49,8 +55,8 @@ quarkus.log.category."de.frigosped".level=DEBUG # KI-Kommunikation mitloggen (nur in Dev) %dev.dc.ai.log-requests=true %dev.dc.ai.log-responses=true -%prod.dc.ai.log-requests=false -%prod.dc.ai.log-responses=false +%prod.dc.ai.log-requests=true +%prod.dc.ai.log-responses=true # ============================================================================= # Jackson: snake_case für ORDS-Responses diff --git a/dc-backend/target/classes/de/frigosped/dc/ai/AiModelProducer.class b/dc-backend/target/classes/de/frigosped/dc/ai/AiModelProducer.class index 5d58c5d817832c365986d51bb394090578a010fe..00a57816d56688b4b75489877359cf448815b43c 100644 GIT binary patch delta 1539 zcma)+*-sNu6vn?PwnNJRS`?|qMDWEFEue`DiHW`#B+;PamiRKEqf}d~ZPBX0hzls< zItuQ)h&zhby6?OHg8zn(#_x8frCMWr=-hkfcfWJKIrsGbn2@M&efjz11%P?@cF)z} z&__0@F*O)oyD;q6f{WCU=0p*K3PLEC;I?3+7TTyq)L2U-g~JLq!Y!dFjRPSq+!7PY zh=M3;C5%dkRpU#vSR|lD1#MBVSqPPIQ^ZVIXJw2l*eZyNq;WXhb_J~>T%izTLgk}f zf)sUO2RdZzRIm%Xjqi?GMV%5L-tJYf58cKjd1*lp4#+sD;1CWQ9rDbAqcCI~Q*a!; z##?z#@d-u>xo{Mx6r4uyP;tIP(o6hJJ~iO8zQGp~wUIF9_qQ?ArUZMMv1%h#Z0~ZN=889*x$|gcx!sRHgkj_S}l_*Vh;;RDp6Hi3>ADHx-paZ1q zGNH3vUMFA&<;rdd*-cV5Q7*kD=xtJ+-I8sILx1(x|tvB3>G3 zbaUM11WitFi~6K_1_3N?%Ja$I(z+eN(P!aQ;EuqYslZ)<+;K2H zp2}rX-Q#|QPSKSVBnpkusBo9T4}|Bl;2$jd(9lPsHRGu#sC+D-woVRvV#1z^s=Jfv zv$2xVZET;6pBwx_!Ie|dm!mXUaVex~@7B#cS$DO>ve2k78})xAh`kcAPtx{FQpE%TMgJGjuhL6cY6K{Gs}#Cq^K$~nVnn(K$O)W` zML9i8beKzhIf0Hb&>@gGzt)$N;0jKP_c&8yfJ1nwB8zH0q8b)c8&{S&S|p)N<>-yS nmd}bv#+5aj0eLCvP(0nBf)(*4{xg`_nFhwRyz*y~#t@!ATsE|# diff --git a/dc-backend/target/classes/de/frigosped/dc/client/OrdsClient.class b/dc-backend/target/classes/de/frigosped/dc/client/OrdsClient.class index 7638612a0bafa4a6eb919664289c7953b35120bf..57bbf49c6fc4c9e4b49e4d44689e110725216b3d 100644 GIT binary patch literal 3204 zcmb7H+g95~6g{F4F5%*qT%a^5Bs2jMGHsglM%!F$heF*20g{(K4bq^9${JTAlV)Y{ zV}0lc^rO1EM# zgNG4kZxss_+pZrW;g!PMeIC8R>#;NLw}nN^EXl6e`wq`J`!lVdS#+zLvr}3Z`CWki z=(^(0cAV~US76_&8Py@Y)rJEf4YqJsbZ#nl%T;hiAi~(Rn<^ogZ?~MxAt~+aizcI2 zg$aHYWXnLbz*kxFfW_2#eU2RgeTG@?Wu3cw ztriWvRGNv!d0-tQ7m9F0p)jEBzDlJr!%B0roEY6CRCBg$wtQ?&$ZVwQ14-k|hFFt! zyNUht;tes4Og6;j@`9W+A6C+|Bi{zTi6a&Y>Z28> zZ3~5Qezd_hESi87j>KQO+WUizP)q8Fqk&&2A&pF?%Vugx-df+0#R0?4YfAAmfi>6m zaq`sr<}?w*CcROvIlKlMXflJt3l1@o%6Gk9bP!|91$Y-M$fsBy8|?&>w1BU)=(0H#~FxmPk;pqYE*GZ!TZ`> z72az39(G<_4cMuY!6l2}Ib1u4DxHE$CEoS&nk-HCmsRxf> z;6I}q7j}5Fw8PJ7Ib+t^_H}V_rpkiMC(#Ft4KKJ9M`knbUTy6uC|t zS5;bH?_?=@+>_-Y zD*uMQ&G^$S1t))3iu|5_$TZW)KAIVHe$EI6^A`P>;ftE*CiYXzp2r)_f6n)>@u^Rq w;Cr$L4K}|9SF!q@B47kRAT;1H*79>`R8>@?x)#f(7b$$3e(A`!laq4ie+Mrb>;M1& delta 667 zcmZ{hJ5K^Z6ot>M3$iN#vna6g@PTj8^@)Xrp)|3;L@KLQXf#T~V3ZgmCYEY5zr<)| zVPofy(SKmPyMQ|i7L%Dt&Ueo}_uiV#LH+&f_3-SZa$4usB|W#xS601pQRh$3 zev^rRuz0 zm~!&$gKp$u9(j@ZVf)Jb5ST$z24581>FKOLh!vEm>}};q+VG zRh?y(FZ=+0D9i3imh28qnK#>+>7MT2%>1YGm$ObX}?_XAbm_dVP}HdcP4hP*1!o0yhxMW?~Yh8I4uYmSF(l;*00Yvk%z z@9cv^Pc3Y#prUHJdR{_-e1k3D;zHazC*cMG^hcY*qgBCXKw0M{SmaCl?YTt>w`eXC z`2UnuP}djGwT`!fIf`ygZ8i?}R8X?_JbjXW#rDc};L^A?*%j`)bpbQQ@89jRfWE2< zqpPO-9m*GC=c8{JgX+nkY~GA1lm&<*l*dyQ?)%QR9pF`mxPfMc^eCA}X9obf^fLuE^K23R} zV{fUqnfJtG1|yFnGMMdnKq}jbDVhWoA7^&`2EM4rE~$DDmmRmCd1^N>h0i604X+Qs z*tqaliaL(A*kZKB=AkXVH*K+5Xp6l;TWkc{;v?428N5$`t zII_?RwB55GLHvh5Qg9o;+;z+t=Hrex; zGI{1%o-O`-rc55|RGt%h-e!4bOrAxS=NZp4WAfbUoM(sSxo+~@W_fmbp6e#hoz8hm ztbS%qp1Um1b6!8QCeOW7c~0o3%<|kcd6rn77d+2RlV`beo|i1og2}VW^1R}C7EGSC N&UyaCf3Xz&^$jsV?O*@^ literal 2902 zcma)+SyK~15XXDM5edtYgF_JIh>(OM9D)jnB8Q;iPUU^cj$x@Ki<=F?x6&#tt9ccy!~e>3y%`TghTF96tt(+~upB?zq=v_U(A-b?*PPg{DikUl!U z5qIn^!%W1t8?yT^>a&{jGfo5 zobH%3u1>bP7iNh;JoEp%T4T^v;BM{e=)NL}q-Ve4#xSVl4~iz_m}HItyTR)|;O>QO z9P18Vor@c&RM`4=&y{4}aSsyI#+3_?`53BKj{8Rne_)@NqZ&sf{hpK_RiML@!sj4} z+A)w$D%JcmVT{@FNd|8b6UuWM#%1gJ>GSQTrXW-~F7<@PZdd=`Vi=#AJhPEyB z1K3ym3BVkF$qmqlZouLq0Onxk)nJ1~_*@bs1{fx*wsfIkGvVQiI=QAer%qV&G)#o!N z^UOBnxu@s%N}jmPlUDM4Q1irPp84i^GD@B)nP*YSbD-v#l6jVz=Q&jBCnfVND|wF8 U`bo(=D-C(>>E{?<=>VMk1u&SjtpET3 diff --git a/dc-backend/target/classes/de/frigosped/dc/model/ProgressRequest.class b/dc-backend/target/classes/de/frigosped/dc/model/ProgressRequest.class index 94e0ad4cd093db373a807f67bb55fef7aad2b31c..add11687f4aba7031637dffcdfbb5203f05db808 100644 GIT binary patch literal 2077 zcma)+*-qO)6o&r^Ss*xN$pRtlDGtGD^lIYXvUUaj2mC=KBO zMq(IMFoto4C7l<~Eu&_(uDD**tHp+?^Lp{vGHVuZwLbFCZQde|!7InYknE$HQjkW5 zL8-Pai#P4#3$4W&#y##Qwq-PHl3`Lo7E`YGE3;bGsRSPHis|@HE0__!V~+9%!DN`t zvM#cjQ!p>$riHPg*<3#~8&`EMJPrswd`KxVj8u#!f8TDLaqE+IR+nUERjZ$BmLc99 zW7xhhS_~_ddljZc@>J(j2398ccig@(^<&M_8rAWqJJsCOqdn_`?|GYaVM1}^!XG&G22#^zcIu}q;3lA4F0CLKvl z2Ry_Qy@|ui6hs^@(=LK1#9=k}6R{2wuE+xn(8nyo6kkD3ievj=aM`p=A9;T6ys|AalrzOHO*iCaM^byy=MKO_MoG&kQ~D^f0Jcqg7PM zE$??EzLFtC1UKbfmL&f2u4BVVewTK^5mncE>e_A2iagz|XY@9YC^l&dZtJ;8#~D#< z1@I*OJgb2`kFXuYlXUCd}rn)UN+2lDI&_serXy8YVe%X6n+B{{fz{{Y@QS(g9+ delta 314 zcmZvXI}XAy5JboMxCkVWgoJ= z8Sk#=t;hTNPCEDJ*r*eo#7sT@Zc+uECWdl`1vx>fRMnjKu0ednHD(! diff --git a/dc-backend/target/classes/de/frigosped/dc/resource/CheckResource.class b/dc-backend/target/classes/de/frigosped/dc/resource/CheckResource.class index 4373540638cee5f8573f5ceed5df50e50f114bd2..90d10fc6e9c69e1dafe1a4c1d835753a1de5a465 100644 GIT binary patch literal 5181 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 KhJWB+SoL3qBJdah delta 1838 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