Beispiele und anpassungen an Mistral

This commit is contained in:
Wolf G. Beckmann
2026-06-10 14:21:59 +02:00
parent f3ae6859bf
commit d00525c15f
44 changed files with 676 additions and 55 deletions

View File

@@ -34,7 +34,15 @@
"Bash(KUBECONFIG=/mnt/c/source/Frigosped/Dokumenten-Check/env/frigo-dev.yaml kubectl logs *)", "Bash(KUBECONFIG=/mnt/c/source/Frigosped/Dokumenten-Check/env/frigo-dev.yaml kubectl logs *)",
"Bash(KUBECONFIG=/mnt/c/source/Frigosped/Dokumenten-Check/env/frigo-dev.yaml kubectl get pods -n ai-env -l app=dc-backend)", "Bash(KUBECONFIG=/mnt/c/source/Frigosped/Dokumenten-Check/env/frigo-dev.yaml kubectl get pods -n ai-env -l app=dc-backend)",
"Bash(chmod +x *)", "Bash(chmod +x *)",
"Bash(JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 mvn compile -q)" "Bash(JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 mvn compile -q)",
"Bash(KUBECONFIG=/mnt/c/source/Frigosped/Dokumenten-Check/env/frigo-dev.yaml kubectl get nodes)",
"Bash(KUBECONFIG=/mnt/c/source/Frigosped/Dokumenten-Check/env/frigo-dev.yaml kubectl config view --minify)",
"Bash(KUBECONFIG=../env/frigo-dev.yaml kubectl get pods *)",
"Bash(KUBECONFIG=/mnt/c/source/Frigosped/Dokumenten-Check/env/frigo-dev.yaml kubectl get pods *)",
"Bash(KUBECONFIG=../env/frigo-dev.yaml kubectl apply *)",
"Bash(KUBECONFIG=../env/frigo-dev.yaml kubectl rollout restart deployment/dc-backend -n ai-env)",
"Bash(KUBECONFIG=../env/frigo-dev.yaml kubectl rollout status deployment/dc-backend -n ai-env --timeout=120s)",
"Bash(KUBECONFIG=../env/frigo-dev.yaml kubectl logs *)"
] ]
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Beispiele/4586_001.pdf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

245
Beispiele/Ladeauftrag.pdf Normal file
View File

@@ -0,0 +1,245 @@
%PDF-1.3
%âãÏÓ
%RSTXPDF3 Parameters: DRSTXbdh
%Devtype SWIN Font HELVE bold Lang DE Script: 0 ->/C001
2 0 obj
<<
/Type /FontDescriptor
/Ascent 718
/CapHeight 718
/Descent -207
/Flags 32
/FontBBox [-170 -228 1003 962]
/FontName /Helvetica-Bold
/ItalicAngle 0
/StemV 105
>>
endobj
3 0 obj
/WinAnsiEncoding
endobj
4 0 obj
<<
/Type /Font
/Subtype /Type1
/BaseFont /Helvetica-Bold
/Name /C001
/Encoding 3 0 R
/Widths
[ 0279 0321 0471 0558 0558 0879 0721 0242 0329 0329 0392 0579 0279 0329 0279 0279 0558 0558 0558 0558 0558 0558 0558 0558 0558 0558 0342 0342 0579 0579 0579 0608 0979 0692 0721 0721 0721 0671 0608 0779 0721 0279 0558 0721 0608 0829 0721 0779 0671 0779
0721 0671 0608 0721 0671 0950 0671 0658 0608 0329 0279 0329 0579 0558 0329 0558 0608 0558 0608 0558 0329 0608 0608 0279 0271 0558 0279 0892 0608 0608 0608 0608 0392 0558 0329 0608 0542 0800 0558 0529 0500 0392 0279 0392 0579]
/FirstChar 32
/LastChar 126
/FontDescriptor 2 0 R
>>
endobj
%Devtype SWIN Font HELVE normal Lang DE Script: 0 ->/C002
5 0 obj
<<
/Type /FontDescriptor
/Ascent 718
/CapHeight 718
/Descent -207
/Flags 32
/FontBBox [-166 -225 1000 931]
/FontName /Helvetica
/ItalicAngle 0
/StemV 105
>>
endobj
6 0 obj
<<
/Type /Font
/Subtype /Type1
/BaseFont /Helvetica
/Name /C002
/Encoding 3 0 R
/Widths
[ 0279 0279 0358 0558 0558 0892 0671 0192 0329 0329 0392 0579 0279 0329 0279 0279 0558 0558 0558 0558 0558 0558 0558 0558 0558 0558 0279 0279 0579 0579 0579 0558 1021 0671 0671 0721 0721 0671 0608 0779 0721 0279 0500 0671 0558 0829 0721 0779 0671 0779
0721 0671 0621 0721 0671 0992 0658 0658 0608 0279 0279 0279 0429 0558 0329 0558 0558 0500 0558 0558 0292 0550 0558 0221 0221 0500 0221 0842 0558 0558 0558 0550 0329 0500 0279 0558 0492 0708 0492 0492 0492 0329 0258 0329 0579]
/FirstChar 32
/LastChar 126
/FontDescriptor 5 0 R
>>
endobj
%Devtype SWIN Font HELVE normal Lang DE Script: 1 ->/C003
7 0 obj
<<
/Type /FontDescriptor
/Ascent 718
/CapHeight 718
/Descent -207
/Flags 32
/FontBBox [-166 -225 1000 931]
/FontName /Helvetica
/ItalicAngle 0
/StemV 105
>>
endobj
8 0 obj
/WinAnsiEncoding
endobj
9 0 obj
<<
/Type /Font
/Subtype /Type1
/BaseFont /Helvetica
/Name /C003
/Encoding 8 0 R
/Widths
[ 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0308 0558 0558 0558 0558 0258 0558 0329 0742 0371 0558 0579 0500 0742 0550 0400 0550
0329 0329 0329 0579 0542 0279 0329 0329 0371 0558 0829 0829 0829 0608 0671 0671 0671 0671 0671 0671 1000 0721 0671 0671 0671 0671 0279 0279 0279 0279 0721 0721 0779 0779 0779 0779 0779 0579 0779 0721 0721 0721 0721 0658 0671 0608 0558 0558 0558 0558
0558 0558 0892 0500 0558 0558 0558 0558 0271 0271 0271 0271 0558 0558 0558 0558 0558 0558 0558 0550 0608 0558 0558 0558 0558 0492 0558 0492]
/FirstChar 128
/LastChar 255
/FontDescriptor 7 0 R
>>
endobj
%Devtype SWIN Font COURIER normal Lang DE Script: 0 ->/C004
10 0 obj
<<
/Type /FontDescriptor
/Ascent 629
/CapHeight 562
/Descent -157
/Flags 35
/FontBBox [-23 -250 715 805]
/FontName /Courier
/ItalicAngle 0
/StemV 105
>>
endobj
11 0 obj
<<
/Type /Font
/Subtype /Type1
/BaseFont /Courier
/Name /C004
/Encoding 3 0 R
/Widths
[ 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600
0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600 0600]
/FirstChar 32
/LastChar 126
/FontDescriptor 10 0 R
>>
endobj
12 0 obj
<<
/Length 13 0 R
>>
stream
0 g BT 42.50 776.05 Td 0 Tw /C001 10.00 Tf 0.00 Tw <4C41444541554654524147>Tj ET 0 g BT 42.50 747.70 Td 0 Tw 0.00 Tw <535045444954455552>Tj ET 0 g BT 184.25 747.70 Td 0 Tw /C002 10.00 Tf 0.00 Tw <4E6F657270656C205345>Tj ET 0 g BT 42.50 733.55 Td 0 Tw
/C001 10.00 Tf 0.00 Tw <41445245535345>Tj ET 0 g BT 184.25 733.55 Td 0 Tw /C002 10.00 Tf 0.00 Tw <45726E73742D416262652D53747261>Tj /C003 10.00 Tf <DF>Tj /C002 10.00 Tf 0.00 Tw <65>Tj 0.00 Tw <2C20383930373920556C6D>Tj ET 0 g BT 42.50 703.20 Td 0 Tw
/C001 10.00 Tf 0.00 Tw <415554524147534E554D4D4552>Tj ET 0 g BT 185.65 703.20 Td 0 Tw /C002 10.00 Tf 0.00 Tw <373136383633323031>Tj ET 0 g BT 42.50 635.10 Td 0 Tw /C001 10.00 Tf 0.00 Tw <454E544C414445444154554D>Tj ET 0 g BT 185.65 635.10 Td 0 Tw
/C002 10.00 Tf 0.00 Tw <30352E30352E32303236>Tj ET 0 g BT 42.50 612.40 Td 0 Tw /C001 10.00 Tf 0.00 Tw <456E746C6164657A656974>Tj ET 0 g BT 185.65 612.40 Td 0 Tw /C002 10.00 Tf 0.00 Tw <30383A30303A3030>Tj ET 0 g BT 42.50 680.50 Td 0 Tw /C001 10.00 Tf
0.00 Tw <4C414445444154554D>Tj ET 0 g BT 185.65 680.50 Td 0 Tw /C002 10.00 Tf 0.00 Tw <30342E30352E32303236>Tj ET 0 g BT 42.50 657.80 Td 0 Tw /C001 10.00 Tf 0.00 Tw <4C4144455A454954>Tj ET 0 g BT 185.65 657.80 Td 0 Tw /C002 12.00 Tf 0.00 Tw
<31303A33303A3030>Tj ET 0 g BT 42.50 567.00 Td 0 Tw /C001 10.00 Tf 0.00 Tw <4C6164657374656C6C656E>Tj ET 0 g BT 185.65 567.00 Td 0 Tw /C002 12.00 Tf 0.00 Tw <3129205275646F6C662D44696573656C2D5374722E34342D383931353020>Tj 0.00 Tw
<4C61696368696E67656E>Tj ET 0 g BT 41.65 474.70 Td 0 Tw /C001 10.00 Tf 0.00 Tw <454E544C4144455354454C4C454E3A>Tj ET 0 g BT 41.65 460.20 Td 0 Tw /C002 10.00 Tf 0.00 Tw <776F6C666673746F726520476D6248>Tj 0.00 Tw
<2C20576576656C696E67686F76656E203238612C202034313333342020>Tj 0.00 Tw <4E6574746574616C>Tj ET 0 g BT 42.50 299.80 Td 0 Tw /C004 12.00 Tf <48494E574549533A>Tj ET 0 g BT 42.50 282.80 Td 0 Tw <3333204575726F70616C657474656E2C204669782D5465726D696E2121>Tj
ET 0 g BT 43.35 228.95 Td 0 Tw /C001 10.00 Tf 0.00 Tw <2020414E5A41484C5F4C4144454D45544552>Tj ET 0 g BT 170.90 228.95 Td 0 Tw 0.00 Tw <574152454E42455A454943484E554E47>Tj ET 0 g BT 298.45 228.95 Td 0 Tw 0.00 Tw <42525554544F5F4B47>Tj ET 0 g BT
426.00 228.95 Td 0 Tw 0.00 Tw <4652414348545052454953>Tj ET 0 g BT 95.95 214.80 Td 0 Tw /C002 10.00 Tf 0.00 Tw <202020203134>Tj ET 0 g BT 73.10 190.80 Td 0 Tw 0.00 Tw <282050616C3A2063612E2020203333202029>Tj ET 0 g BT 170.90 214.80 Td 0 Tw 0.00 Tw
<464F5253542D20554E44>Tj ET 0 g BT 170.90 202.80 Td 0 Tw 0.00 Tw <47415254454E474552>Tj /C003 10.00 Tf <C4>Tj /C002 10.00 Tf 0.00 Tw <5445>Tj ET 0 g BT 298.45 214.80 Td 0 Tw 0.00 Tw <2020202020202020202020322E37373220>Tj ET 0 g BT 426.00 214.80 Td 0 Tw
0.00 Tw <20>Tj ET 0 g BT 428.80 214.80 Td 0 Tw /C002 12.00 Tf 0.00 Tw <6C742E2056657265696E626172756E67>Tj ET q 0 0 0 RG 1.00 w 43.35 240.95 m 43.35 184.25 l S Q q 0 0 0 RG 1.00 w 553.60 240.95 m 553.60 184.25 l S Q q 0 0 0 RG 1.00 w 42.85 240.95 m
554.10 240.95 l S Q q 0 0 0 RG 1.00 w 42.85 184.25 m 554.10 184.25 l S Q 0 g BT 42.50 113.05 Td 0 Tw /C002 8.00 Tf 0.00 Tw <416E736368726966743A>Tj ET 0 g BT 42.50 107.05 Td 0 Tw 0.00 Tw <487573717661726E61204C6F6769737469637320476D6248>Tj ET 0 g BT
42.50 101.05 Td 0 Tw 0.00 Tw <48616E732D4C6F72656E7365722D5374722E203430>Tj ET 0 g BT 42.50 95.05 Td 0 Tw 0.00 Tw <383930373920556C6D>Tj ET 0 g BT 42.50 89.05 Td 0 Tw 0.00 Tw <50686F6E653A202B34392028302920373331203439302D30>Tj ET 0 g BT 42.50 83.05 Td
0 Tw 0.00 Tw <4661783A202B34392028302920373331203439302D323139>Tj ET 0 g BT 212.60 113.05 Td 0 Tw 0.00 Tw <4765736368>Tj /C003 8.00 Tf <E4>Tj /C002 8.00 Tf 0.00 Tw <66747366>Tj /C003 8.00 Tf <FC>Tj /C002 8.00 Tf 0.00 Tw <687265723A>Tj ET 0 g BT
212.60 107.05 Td 0 Tw 0.00 Tw <4368726973746F7068204461746B6F>Tj ET 0 g BT 382.70 113.05 Td 0 Tw 0.00 Tw <5369747A2064657220476573656C6C7363686166743A20556C6D>Tj ET 0 g BT 382.70 107.05 Td 0 Tw 0.00 Tw <416D74736765726963687420556C6D>Tj ET 0 g BT
382.70 101.05 Td 0 Tw 0.00 Tw <48524220373232353936>Tj ET 0 g BT 382.70 89.05 Td 0 Tw 0.00 Tw <5374657565722D4E722E203838303032202F203239343032>Tj ET 0 g BT 382.70 83.05 Td 0 Tw 0.00 Tw <5573742D49443A20444520383132303736393832>Tj ET
endstream
endobj
13 0 obj
4192
endobj
14 0 obj
<<
/Type /Page
/MediaBox
[0 0 595 842]
/Parent 1 0 R
/Resources
<<
/ProcSet
[/PDF /Text]
/Font
<<
/C001 4 0 R
/C002 6 0 R
/C003 9 0 R
/C004 11 0 R
>>
/XObject
<<
>>
>>
/Contents 12 0 R
>>
endobj
15 0 obj
<<
/Author (H658581 )
/CreationDate (D:20260430081155)
/Creator (Form /SUP/TP_LAD DE)
/Producer (SAP NetWeaver 731 )
%SAPinfoStart TOA_DARA
%FUNCTION=( )
%MANDANT=( )
%DEL_DATE=(00000000)
%SAP_OBJECT=( )
%AR_OBJECT=( )
%OBJECT_ID=( )
%FORM_ID=( )
%FORMARCHIV=( )
%RESERVE=( )
%NOTIZ=( )
%-( )
%-( )
%-( )
%SAPinfoEnd TOA_DARA
>>
endobj
1 0 obj
<<
/Type /Pages
/Kids
[ 14 0 R
]
/Count 1
>>
endobj
16 0 obj
<<
/Type /Catalog
/Pages 1 0 R
/PageMode /UseNone
>>
endobj
xref
0 17
0000000000 65535 f
0000009163 00000 n
0000000114 00000 n
0000000304 00000 n
0000000339 00000 n
0000001061 00000 n
0000001246 00000 n
0000001963 00000 n
0000002148 00000 n
0000002183 00000 n
0000003068 00000 n
0000003250 00000 n
0000003900 00000 n
0000008153 00000 n
0000008177 00000 n
0000008405 00000 n
0000009232 00000 n
trailer
<<
/Size 17
/Root 16 0 R
/Info 15 0 R
>>
startxref
9308
%%EOF

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,169 @@
-- =============================================================================
-- ORDS-Endpunkte: KI-Kostenverfolgung (ai-costs)
-- Ergänzt das Modul frigosped.dc um Template 17 (POST + GET).
--
-- Voraussetzung: dc_ai_cost_log-Tabelle und DC_COSTS_PKG bereits vorhanden
-- sqlplus user/pass@db @"Scripts/KI-Kostenverfolgung.sql"
--
-- Deployment:
-- sqlplus user/pass@db @"Scripts/ORDS-ai-costs-Endpunkt.sql"
--
-- Idempotent: bestehende Handler werden zuerst gelöscht.
-- =============================================================================
SET DEFINE OFF
BEGIN
-- Idempotenz: Handler entfernen falls vorhanden
BEGIN
ORDS.DELETE_HANDLER(
p_module_name => 'frigosped.dc',
p_uri_template => 'ai-costs',
p_method => 'POST'
);
EXCEPTION WHEN OTHERS THEN NULL;
END;
BEGIN
ORDS.DELETE_HANDLER(
p_module_name => 'frigosped.dc',
p_uri_template => 'ai-costs',
p_method => 'GET'
);
EXCEPTION WHEN OTHERS THEN NULL;
END;
BEGIN
ORDS.DELETE_TEMPLATE(
p_module_name => 'frigosped.dc',
p_uri_template => 'ai-costs'
);
EXCEPTION WHEN OTHERS THEN NULL;
END;
-- Template anlegen
ORDS.DEFINE_TEMPLATE(
p_module_name => 'frigosped.dc',
p_pattern => 'ai-costs',
p_priority => 0,
p_comments => 'KI-Kostenprotokoll: POST speichert Aufruf, GET liefert Auswertung'
);
-- POST /api/dc/ai-costs
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'ai-costs',
p_method => 'POST',
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
p_comments => '201 Created mit neuer cost_log id',
p_source => '
DECLARE
v_body CLOB;
v_new_id NUMBER;
BEGIN
v_body := :body_text;
INSERT INTO dc_ai_cost_log (
provider, model_name, operation,
project_id, catalog_id,
prompt_tokens, completion_tokens, total_tokens,
cost_eur
) VALUES (
JSON_VALUE(v_body, ''$.provider''),
JSON_VALUE(v_body, ''$.model_name''),
JSON_VALUE(v_body, ''$.operation''),
TO_NUMBER(JSON_VALUE(v_body, ''$.project_id'')),
TO_NUMBER(JSON_VALUE(v_body, ''$.catalog_id'')),
NVL(TO_NUMBER(JSON_VALUE(v_body, ''$.prompt_tokens'')), 0),
NVL(TO_NUMBER(JSON_VALUE(v_body, ''$.completion_tokens'')), 0),
NVL(TO_NUMBER(JSON_VALUE(v_body, ''$.total_tokens'')), 0),
NVL(TO_NUMBER(JSON_VALUE(v_body, ''$.cost_eur'')), 0)
)
RETURNING id INTO v_new_id;
:status_code := 201;
HTP.P(''{"id":'' || v_new_id || ''}'');
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
:status_code := 500;
HTP.P(''{"error":"'' || REPLACE(SQLERRM, ''"'', '''''''') || ''"}'' );
END;
'
);
-- GET /api/dc/ai-costs
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'ai-costs',
p_method => 'GET',
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
p_comments => 'Kostenauswertung: total, current_month, monatliche Aufschluesselung via DC_COSTS_PKG',
p_source => '
DECLARE
v_total_eur NUMBER;
v_total_tokens NUMBER;
v_total_calls NUMBER;
v_month_eur NUMBER;
v_month_tokens NUMBER;
v_month_calls NUMBER;
v_monthly_json CLOB;
BEGIN
DC_COSTS_PKG.get_summary(
v_total_eur, v_total_tokens, v_total_calls,
v_month_eur, v_month_tokens, v_month_calls
);
SELECT JSON_ARRAYAGG(
JSON_OBJECT(
''month'' VALUE month,
''cost_eur'' VALUE ROUND(cost_eur, 6),
''prompt_tokens'' VALUE prompt_tokens,
''completion_tokens'' VALUE completion_tokens,
''total_tokens'' VALUE total_tokens,
''call_count'' VALUE call_count
)
ORDER BY month DESC
RETURNING CLOB
)
INTO v_monthly_json
FROM (
SELECT
TO_CHAR(called_at, ''YYYY-MM'') AS month,
SUM(cost_eur) AS cost_eur,
SUM(prompt_tokens) AS prompt_tokens,
SUM(completion_tokens) AS completion_tokens,
SUM(total_tokens) AS total_tokens,
COUNT(*) AS call_count
FROM dc_ai_cost_log
WHERE called_at >= ADD_MONTHS(TRUNC(SYSDATE, ''MM''), -11)
GROUP BY TO_CHAR(called_at, ''YYYY-MM'')
);
:status_code := 200;
HTP.P(
''{"total":{''
|| '' "cost_eur":'' || ROUND(NVL(v_total_eur, 0), 6)
|| '', "total_tokens":'' || NVL(v_total_tokens, 0)
|| '', "call_count":'' || NVL(v_total_calls, 0)
|| ''}, "current_month":{''
|| '' "month":"'' || TO_CHAR(SYSDATE, ''YYYY-MM'') || ''"''
|| '', "cost_eur":'' || ROUND(NVL(v_month_eur, 0), 6)
|| '', "total_tokens":'' || NVL(v_month_tokens, 0)
|| '', "call_count":'' || NVL(v_month_calls, 0)
|| ''}, "monthly":'' || NVL(v_monthly_json, ''[]'')
|| ''}''
);
EXCEPTION
WHEN OTHERS THEN
:status_code := 500;
HTP.P(''{"error":"'' || REPLACE(SQLERRM, ''"'', '''''''') || ''"}'' );
END;
'
);
COMMIT;
END;
/

View File

@@ -32,10 +32,20 @@ spec:
name: dc-backend-secrets name: dc-backend-secrets
key: api-key key: api-key
optional: true optional: true
# Ollama KI-Endpunkt # KI-Provider: mistral | ollama
- name: DC_AI_BASE_URL - name: DC_AI_PROVIDER
value: "mistral"
# Mistral API-Key
- name: MISTRAL_API_KEY
valueFrom:
secretKeyRef:
name: dc-backend-secrets
key: mistral-api-key
optional: true
# Ollama-Fallback (aktiv wenn DC_AI_PROVIDER=ollama)
- name: DC_AI_OLLAMA_BASE_URL
value: "https://ollama.aquantico.de" value: "https://ollama.aquantico.de"
- name: DC_AI_API_KEY - name: DC_AI_OLLAMA_API_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: dc-backend-secrets name: dc-backend-secrets
@@ -85,4 +95,5 @@ metadata:
type: Opaque type: Opaque
stringData: stringData:
api-key: "" api-key: ""
mistral-api-key: "2AkoohJC8MjfjhfLGObQESZs9jxbDnc0"
ollama-api-key: "324GF44-50AA-4B57-9386-K435DLJ764DFR" ollama-api-key: "324GF44-50AA-4B57-9386-K435DLJ764DFR"

View File

@@ -9,6 +9,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public class EvaluationResult { public class EvaluationResult {
/** Frage-ID wird im Batch-Modus von der KI zurückgegeben */
private Long questionId;
/** Antwort auf Deutsch (Zitate in Originalsprache + Übersetzung) */ /** Antwort auf Deutsch (Zitate in Originalsprache + Übersetzung) */
private String answer; private String answer;
@@ -23,15 +26,18 @@ public class EvaluationResult {
public EvaluationResult() {} public EvaluationResult() {}
public String getAnswer() { return answer; } public Long getQuestionId() { return questionId; }
public void setAnswer(String v) { this.answer = v; } public void setQuestionId(Long v) { this.questionId = v; }
public Integer getScore() { return score; } public String getAnswer() { return answer; }
public void setScore(Integer v) { this.score = v; } public void setAnswer(String v) { this.answer = v; }
public String getResultType() { return resultType; } public Integer getScore() { return score; }
public void setResultType(String v) { this.resultType = v; } public void setScore(Integer v) { this.score = v; }
public String getTheComment() { return theComment; } public String getResultType() { return resultType; }
public void setTheComment(String v) { this.theComment = v; } public void setResultType(String v) { this.resultType = v; }
public String getTheComment() { return theComment; }
public void setTheComment(String v) { this.theComment = v; }
} }

View File

@@ -13,6 +13,7 @@ import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -195,54 +196,33 @@ public class CheckOrchestrationService {
continue; continue;
} }
for (OrdsQuestion question : questions) { List<List<OrdsQuestion>> batches = partition(questions, 4);
// Vor dem Schritt: Status auf "wertet Frage X in Dokument Y aus"
for (List<OrdsQuestion> batch : batches) {
OrdsQuestion first = batch.get(0);
pushProgress(projectId, stepHolder[0], totalSteps, "QUESTIONS", pushProgress(projectId, stepHolder[0], totalSteps, "QUESTIONS",
doc.getId(), question.getQuestionId(), startedAt); doc.getId(), first.getQuestionId(), startedAt);
Map<Long, EvaluationResult> batchResults;
try { try {
LOG.debugf("Auswertung: Dok %d × Frage %d", LOG.debugf("Batch-Auswertung: Dok %d, %d Fragen ab ID %d",
doc.getId(), question.getQuestionId()); (Object) doc.getId(), (Object) batch.size(), (Object) first.getQuestionId());
batchResults = evaluationService.evaluateBatch(batch, originalText, projectId);
EvaluationResult result = evaluationService.evaluate(question, originalText, projectId);
int deviation = 0;
int warning = 0;
if (result.getScore() != null && question.getThreshold() != null
&& result.getScore() < question.getThreshold()) {
if ("ABWEICHUNG".equalsIgnoreCase(question.getResultHandling())) {
deviation = 1;
} else if ("HINWEIS".equalsIgnoreCase(question.getResultHandling())) {
warning = 1;
}
}
ResultRequest rr = new ResultRequest(
question.getQuestionId(),
doc.getId(),
result.getAnswer(),
result.getScore(),
result.getResultType(),
deviation,
warning,
result.getTheComment()
);
Response saveResp = ordsClient.saveResult(projectId, rr);
if (saveResp.getStatus() != 201) {
LOG.warnf("Ergebnis speichern: HTTP %d (Frage %d, Dok %d)",
saveResp.getStatus(), question.getQuestionId(), doc.getId());
}
} catch (Exception e) { } catch (Exception e) {
LOG.errorf(e, "Auswertungsfehler: Frage %d / Dokument %d", LOG.warnf("Batch fehlgeschlagen (Dok %d, Frage %d+) falle auf Einzelaufrufe zurück: %s",
question.getQuestionId(), doc.getId()); doc.getId(), first.getQuestionId(), e.getMessage());
batchResults = evaluateIndividually(batch, originalText, projectId);
} }
// Nach dem Schritt: Fortschritt + ETA aktualisieren for (OrdsQuestion question : batch) {
stepHolder[0]++; EvaluationResult result = batchResults.get(question.getQuestionId());
if (result == null) continue;
saveResult(projectId, doc.getId(), question, result);
}
stepHolder[0] += batch.size();
pushProgress(projectId, stepHolder[0], totalSteps, "QUESTIONS", pushProgress(projectId, stepHolder[0], totalSteps, "QUESTIONS",
doc.getId(), question.getQuestionId(), startedAt); doc.getId(), batch.get(batch.size() - 1).getQuestionId(), startedAt);
} }
} }
@@ -277,6 +257,56 @@ public class CheckOrchestrationService {
} }
} }
/** Teilt eine Liste in Untergruppen der gewünschten Größe auf. */
private static <T> List<List<T>> partition(List<T> list, int size) {
List<List<T>> parts = new ArrayList<>();
for (int i = 0; i < list.size(); i += size) {
parts.add(list.subList(i, Math.min(i + size, list.size())));
}
return parts;
}
/** Fallback: wertet jede Frage einzeln aus, gibt Map question_id → result zurück. */
private Map<Long, EvaluationResult> evaluateIndividually(List<OrdsQuestion> questions,
String documentText,
long projectId) {
Map<Long, EvaluationResult> map = new HashMap<>();
for (OrdsQuestion q : questions) {
try {
map.put(q.getQuestionId(),
evaluationService.evaluate(q, documentText, projectId));
} catch (Exception e) {
LOG.errorf(e, "Einzelauswertung fehlgeschlagen: Frage %d", q.getQuestionId());
}
}
return map;
}
private void saveResult(long projectId, long docId,
OrdsQuestion question, EvaluationResult result) {
try {
int deviation = 0, warning = 0;
if (result.getScore() != null && question.getThreshold() != null
&& result.getScore() < question.getThreshold()) {
if ("ABWEICHUNG".equalsIgnoreCase(question.getResultHandling())) deviation = 1;
else if ("HINWEIS".equalsIgnoreCase(question.getResultHandling())) warning = 1;
}
ResultRequest rr = new ResultRequest(
question.getQuestionId(), docId,
result.getAnswer(), result.getScore(), result.getResultType(),
deviation, warning, result.getTheComment()
);
Response saveResp = ordsClient.saveResult(projectId, rr);
if (saveResp.getStatus() != 201) {
LOG.warnf("Ergebnis speichern: HTTP %d (Frage %d, Dok %d)",
saveResp.getStatus(), question.getQuestionId(), docId);
}
} catch (Exception e) {
LOG.errorf(e, "Ergebnis-Speichern fehlgeschlagen: Frage %d / Dokument %d",
question.getQuestionId(), docId);
}
}
/** /**
* Berechnet die geschätzte Fertigstellung auf Basis der bisher verstrichenen Zeit. * Berechnet die geschätzte Fertigstellung auf Basis der bisher verstrichenen Zeit.
* ETA = jetzt + (verbleibende Schritte × Ø Dauer pro Schritt) * ETA = jetzt + (verbleibende Schritte × Ø Dauer pro Schritt)

View File

@@ -14,7 +14,9 @@ import jakarta.inject.Inject;
import jakarta.inject.Named; import jakarta.inject.Named;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -113,6 +115,157 @@ public class EvaluationService {
return parseEvaluationResult(rawText, question); return parseEvaluationResult(rawText, question);
} }
// =========================================================================
// Batch-Auswertung (bis zu 4 Fragen pro KI-Aufruf)
// =========================================================================
/**
* Wertet mehrere Fragen in einem einzigen KI-Aufruf aus.
* Gibt eine Map question_id → EvaluationResult zurück.
* Wirft eine Exception wenn die KI-Antwort nicht als JSON-Array parsbar ist,
* damit der Aufrufer auf Einzel-Aufrufe zurückfallen kann.
*/
public Map<Long, EvaluationResult> evaluateBatch(List<OrdsQuestion> questions,
String documentText,
long projectId) {
if (questions.isEmpty()) return Map.of();
LOG.debugf("Batch-Auswertung: %d Frage(n)", questions.size());
List<ChatMessage> messages = List.of(
SystemMessage.from(buildBatchSystem(questions.size())),
UserMessage.from(buildBatchUser(questions, documentText))
);
Response<AiMessage> response = mainModel.generate(messages);
costTracker.track("main", "EVALUATE", projectId, null, response.tokenUsage());
List<EvaluationResult> results = parseBatchResult(response.content().text(), questions);
Map<Long, EvaluationResult> map = new HashMap<>();
for (EvaluationResult r : results) {
if (r.getQuestionId() != null) {
map.put(r.getQuestionId(), r);
}
}
// Lücken füllen: für fehlende question_ids Fallback-Ergebnis eintragen
for (OrdsQuestion q : questions) {
map.computeIfAbsent(q.getQuestionId(), id -> {
LOG.warnf("Keine Batch-Antwort für Frage %d verwende UNKLAR-Fallback", id);
return buildFallback("Frage wurde im Batch nicht beantwortet.");
});
}
return map;
}
private String buildBatchSystem(int questionCount) {
return """
Du bist ein Experte für Transportrecht, AGB-Recht und Vertragsanalyse.
Du prüfst Transport- und Speditionsdokumente gegen einen Fragenkatalog.
Antworte AUSSCHLIESSLICH mit einem gültigen JSON-Array kein Text davor oder danach.
Das Array enthält für JEDE Frage genau ein Objekt:
[
{
"question_id": <ID der Frage exakt wie angegeben>,
"answer": "<Antwort auf Deutsch; Zitate aus dem Dokument in Originalsprache mit Übersetzung in Klammern>",
"score": <Erfüllungsgrad 0 bis 100>,
"result_type": "<OK wenn score >= Schwellwert, NOK wenn klar nicht erfüllt, UNKLAR wenn grenzwertig>",
"the_comment": "<Kurzer fachlicher Kommentar auf Deutsch, max. 3 Sätze>"
}
]
Pflicht: Das Array muss GENAU %d Einträge enthalten (einen pro Frage).
Die question_id jedes Eintrags muss exakt der ID der gestellten Frage entsprechen.
""".formatted(questionCount);
}
private String buildBatchUser(List<OrdsQuestion> questions, String documentText) {
StringBuilder sb = new StringBuilder();
sb.append("## Zu prüfendes Dokument\n\n");
sb.append(documentText).append("\n\n---\n\n");
sb.append("## Prüffragen (").append(questions.size()).append(" Fragen)\n\n");
for (int i = 0; i < questions.size(); i++) {
OrdsQuestion q = questions.get(i);
sb.append("### Frage ").append(i + 1)
.append(" (ID: ").append(q.getQuestionId())
.append(", Kategorie: ").append(q.getCategoryName()).append(")\n\n");
sb.append(q.getQuestionText()).append("\n\n");
if (q.getThreshold() != null) {
sb.append("**Schwellwert:** ").append(q.getThreshold())
.append("% unterhalb gilt die Anforderung als nicht erfüllt\n\n");
}
if (q.getExample0Percent() != null && !q.getExample0Percent().isBlank()) {
sb.append("**Beispiel 0%:** ").append(q.getExample0Percent()).append("\n\n");
}
if (q.getExample100Percent() != null && !q.getExample100Percent().isBlank()) {
sb.append("**Beispiel 100%:** ").append(q.getExample100Percent()).append("\n\n");
}
}
sb.append("---\n\nAntworte jetzt mit dem JSON-Array (").append(questions.size())
.append(" Einträge):");
return sb.toString();
}
private List<EvaluationResult> parseBatchResult(String rawText, List<OrdsQuestion> questions) {
try {
String json = extractJsonArray(rawText);
List<EvaluationResult> results = objectMapper.readValue(
json,
objectMapper.getTypeFactory().constructCollectionType(List.class, EvaluationResult.class)
);
// Fehlende Defaults setzen
for (int i = 0; i < results.size(); i++) {
EvaluationResult r = results.get(i);
if (r.getScore() == null) r.setScore(0);
if (r.getAnswer() == null) r.setAnswer("Keine Antwort erhalten.");
// question_id aus Position ableiten falls KI sie vergessen hat
if (r.getQuestionId() == null && i < questions.size()) {
r.setQuestionId(questions.get(i).getQuestionId());
}
if (r.getResultType() == null || !isValidResultType(r.getResultType())) {
Integer threshold = (i < questions.size()) ? questions.get(i).getThreshold() : null;
r.setResultType(deriveResultType(r.getScore(), threshold));
}
}
return results;
} catch (Exception e) {
LOG.warnf("Batch-JSON-Parsing fehlgeschlagen: %s", e.getMessage());
LOG.debugf("Rohe KI-Antwort: %s", rawText);
throw new RuntimeException("Batch-Parsing fehlgeschlagen: " + e.getMessage(), e);
}
}
private String extractJsonArray(String text) {
if (text == null || text.isBlank()) throw new IllegalArgumentException("Leere KI-Antwort");
// JSON in ```-Block
Matcher m = JSON_PATTERN.matcher(text);
if (m.find()) {
String candidate = m.group(1) != null ? m.group(1) : m.group(2);
if (candidate != null && candidate.stripLeading().startsWith("[")) return candidate.trim();
}
// Direkt als Array
int start = text.indexOf('[');
int end = text.lastIndexOf(']');
if (start >= 0 && end > start) return text.substring(start, end + 1);
throw new IllegalArgumentException("Kein JSON-Array in KI-Antwort gefunden");
}
private EvaluationResult buildFallback(String message) {
EvaluationResult r = new EvaluationResult();
r.setAnswer(message);
r.setScore(0);
r.setResultType("UNKLAR");
r.setTheComment("Automatische Auswertung konnte nicht zugeordnet werden.");
return r;
}
// ========================================================================= // =========================================================================
// Prompt-Builder // Prompt-Builder
// ========================================================================= // =========================================================================

3
env/frigo-dev.yaml vendored
View File

@@ -4,12 +4,11 @@ clusters:
- name: "local" - name: "local"
cluster: cluster:
server: "https://rancherdev.express-o.org/k8s/clusters/local" server: "https://rancherdev.express-o.org/k8s/clusters/local"
insecure-skip-tls-verify: true
users: users:
- name: "local" - name: "local"
user: user:
token: "kubeconfig-u-zugnvqkby7rgbsc:7f4n9tplsd9r2wpdl5bx2n72wwkmfvpvsfg56z8b88s2hq986rqsr4" token: "kubeconfig-u-zugnvqkby72pmqn:9v7gwpjbzqzfsw4srfjdrf6p6q2nvhq22c2nf5tls774z2gt64vmpc"
contexts: contexts: