From 4f45015b1cb3312fadba967efe2b2009ee77519f Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:59:42 +0200 Subject: [PATCH] Add safe model diagnostics, explicit self-review stages and recheck action --- README.md | 8 ++++ atelier/app.py | 12 ++++++ atelier/provider.py | 90 +++++++++++++++++++++++++++++++++++---- atelier/static/app.js | 21 +++++++-- atelier/static/index.html | 2 +- tests/test_app.py | 12 ++++++ tests/test_review.py | 43 +++++++++++++++++++ 7 files changed, 174 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 750d107..e5ec1a6 100644 --- a/README.md +++ b/README.md @@ -140,3 +140,11 @@ Der interne Python-Paketname `atelier` ist nur ein Implementierungsdetail. ## Stand und Grenzen Erste eigenständige Version. Die KI-Überarbeitung ist absichtlich ein manueller Entwurf; ein grafischer Wort-für-Wort-Diff ist noch nicht enthalten. Versionsvorschau und Wiederherstellung sind vorhanden. Die Tests nutzen einen simulierten OpenAI-kompatiblen Modellserver; ein konkretes lokales Modell muss beim Einrichten noch geprüft werden. Docker/Unraid wird über die beiliegenden Dateien vorbereitet, ein echter Container-Test muss auf einer Maschine mit Docker erfolgen. + +## KI-Selbstprüfung und Diagnose + +Die Prüfung stammt vom selben Modell und ist keine unabhängige Verifikation. Historische Befunde zum ersten Entwurf sind getrennt von Befunden zum aktuellen Vorschlag. Bei Fehlern bleibt der Entwurf erhalten; die Meldung benennt den fehlgeschlagenen Schritt und eine Diagnose-ID. „Aktuellen Vorschlag erneut prüfen“ führt genau einen Prüfanruf ohne automatische Textänderung aus. + +Zusätzlich vergleicht Code erkennbare `{{Platzhalter}}`, explizite Portangaben und Pfadmuster. Unterschiede sind Hinweise, keine sicheren Fehler: Auch gewünschte Änderungen können markiert werden; nicht jedes mögliche Pfadformat wird erkannt. Ein Bearbeiten der Vergleichstexte setzt die Anzeige auf ungeprüft zurück. + +Containerlogs enthalten für Modellaufrufe ausschließlich Diagnose-ID, Verarbeitungsschritt, Dauer, HTTP-Status und Fehlerklasse. Prompt-/Antwortinhalte, API-Keys und Endpoint-URLs werden nicht durch diese Diagnose protokolliert. Drittanbieter-HTTP-Info-Logs sind deaktiviert. Der bisherige 90-Sekunden-Timeout bleibt unverändert. diff --git a/atelier/app.py b/atelier/app.py index 7ee8109..a9a63a1 100644 --- a/atelier/app.py +++ b/atelier/app.py @@ -67,11 +67,19 @@ class Improvement(BaseModel): instruction: str = Field(default='Formuliere klarer, präziser und hilfreicher. Bewahre die Absicht.', max_length=4000) +class Recheck(Improvement): + draft: str = Field(min_length=1, max_length=60000) + + class Login(BaseModel): token: str = Field(max_length=500) def create_app(data_dir=None, provider_transport=None): + # Suppress third-party request URLs; our provider logs only allowlisted metadata. + import logging + logging.getLogger('httpx').setLevel(logging.WARNING) + logging.getLogger('httpcore').setLevel(logging.WARNING) root = Path(data_dir or os.environ.get('DATA_DIR', './data')) root.mkdir(parents=True, exist_ok=True) os.chmod(root, 0o700) @@ -313,6 +321,10 @@ def create_app(data_dir=None, provider_transport=None): async def improve(body: Improvement): return await provider().improve_checked(body.body, body.instruction) + @app.post('/api/recheck') + async def recheck(body: Recheck): + return await provider().recheck(body.body, body.instruction, body.draft) + @app.post('/api/organize') async def organize(body: Improvement): return await provider().organize(body.body, list_categories()) diff --git a/atelier/provider.py b/atelier/provider.py index f297a79..113d2fe 100644 --- a/atelier/provider.py +++ b/atelier/provider.py @@ -1,15 +1,42 @@ import hashlib import json import math +import logging +import re +import time +import uuid from urllib.parse import urlsplit import httpx +logger = logging.getLogger('uvicorn.error') + + class ProviderError(Exception): pass +def literal_checks(original, draft): + patterns = { + 'Platzhalter': r'\{\{[^{}\n]+\}\}', + 'Port': r'(?i)\bport\s*[:=]?\s*(\d{1,5})\b|:(\d{2,5})\b', + 'Pfad': r'(?{if(!organization)return;$('p-category').valu function renderReview(report) { const box=$('ai-review');box.replaceChildren(); - const titles={passed:'Prüfung: Keine Abweichungen erkannt',corrected:'Prüfung: Abweichungen korrigiert; erneut geprüft',issues:'Prüfung: Abweichungen verbleiben',unchecked:'Nicht geprüft – keine abgeschlossene Verifikation'}; + const titles={passed:'KI-Selbstprüfung: Keine Abweichungen erkannt',corrected:'Korrigierter Entwurf: KI-Selbstprüfung abgeschlossen',issues:'KI-Selbstprüfung: Abweichungen im aktuellen Vorschlag',unchecked:report?.draft_kind==='corrected'?'Korrigierter Entwurf – Abschlussprüfung fehlgeschlagen':'Aktueller Entwurf – KI-Selbstprüfung nicht abgeschlossen'}; const heading=document.createElement('strong');heading.textContent=titles[report?.status]||titles.unchecked;box.append(heading); if(report?.warning){const p=document.createElement('p');p.textContent=report.warning;box.append(p);} function issues(label, entries){if(!entries?.length)return;const details=document.createElement('details');details.open=true;const summary=document.createElement('summary');summary.textContent=label+' ('+entries.length+')';details.append(summary);const ul=document.createElement('ul');for(const issue of entries){const li=document.createElement('li');li.textContent=issue.description;for(const [key,title]of [['original_quote','Original'],['draft_quote','Vorschlag']]){if(issue[key]){const p=document.createElement('p');p.textContent=title+': „'+issue[key]+'“';li.append(p);}}ul.append(li);}details.append(ul);box.append(details);} - issues('Befunde der ersten Prüfung',report?.initial_issues); - if(report?.status==='issues')issues('Weiterhin erkannte Abweichungen',report.issues); - const hint=document.createElement('p');hint.textContent='Modellprüfung, keine Garantie. Bitte den Vorschlag vor dem Speichern selbst prüfen.';box.append(hint); + if(report?.draft_kind==='corrected')issues('Historische Befunde zum ERSTEN Entwurf (vor der Korrektur)',report?.initial_issues); + else if(report?.status!=='issues')issues('Befunde zum angezeigten Entwurf',report?.initial_issues); + issues('Code-Prüfung: Unterschiede im aktuellen Vorschlag – bitte beurteilen',report?.literal_issues); + if(report?.status==='issues')issues('Befunde zum AKTUELLEN Vorschlag',report.issues); + const hint=document.createElement('p');hint.textContent='Selbstprüfung durch dasselbe Modell, keine unabhängige Verifikation. Die Code-Prüfung sucht nur erkennbare Platzhalter, Ports und Pfade; gewünschte Änderungen können ebenfalls markiert werden. Bitte selbst vergleichen.';box.append(hint); } bind('ai-draft','input',()=>renderReview({status:'unchecked',warning:'Vorschlag manuell geändert; diese Fassung wurde nicht geprüft.'})); + +bind('recheck-ai','click',()=>busy($('recheck-ai'),async()=>{ + const generation=editorGeneration; + const original=$('p-body').value, instruction=$('ai-instruction').value, draft=$('ai-draft').value; + if(!original.trim()||!draft.trim())throw new Error('Original und Vorschlag dürfen nicht leer sein.'); + const result=await api('/recheck',{method:'POST',body:JSON.stringify({body:original,instruction,draft})}); + if(generation!==editorGeneration||!$('editor').open)return; + if(original!==$('p-body').value||instruction!==$('ai-instruction').value||draft!==$('ai-draft').value){renderReview({status:'unchecked',warning:'Text während der Prüfung geändert. Bitte erneut prüfen.'});return;} + renderReview(result.review); +})); +for(const id of ['p-body','ai-instruction'])bind(id,'input',()=>{if(!$('ai-result').classList.contains('hidden'))renderReview({status:'unchecked',warning:'Vergleichsgrundlage geändert. Bitte erneut prüfen.'});}); diff --git a/atelier/static/index.html b/atelier/static/index.html index 62b46a0..d4be1e1 100644 --- a/atelier/static/index.html +++ b/atelier/static/index.html @@ -6,7 +6,7 @@
Workspace / Bibliothek Privat & lokal

WENIGER SUCHEN. BESSER PROMPTEN.

Deine besten Worte.

Sammeln, verfeinern und genau dann wiederfinden, wenn du sie brauchst.

0 PromptsDeine persönliche Sammlung
-

PROMPT STUDIO

Neuer Prompt

0 Zeichen
+

PROMPT STUDIO

Neuer Prompt

0 Zeichen

DEIN HAUS, DEINE REGELN

Einstellungen

01 / Deine Modelle

OpenAI-kompatibel, auch im lokalen Netz. Die Basis-URL enthält üblicherweise /v1.

Separater Endpoint für Embeddings (optional)

Ein Chatmodell verbessert Texte. Ein Embedding-Modell macht sie nach Bedeutung auffindbar. Die Modellliste sagt nicht automatisch, welcher Typ ein Modell ist.

02 / Suche nach Bedeutung

Neue und bearbeitete Prompts werden beim Speichern indexiert. Nach einem Modellwechsel bitte den Index ergänzen.

Hierbei werden Prompt-Inhalte an deinen eingestellten Embedding-Endpoint gesendet.

03 / MCP für deine Assistenten

Ein Assistent wie OpenClaw kann Prompts über MCP suchen und lesen. Transport: Streamable HTTP. Keine Schreibrechte.

Der MCP-Schlüssel gewährt Lesezugriff auf dein Archiv. Nur an deine eigenen Clients weitergeben.

04 / Dein Archiv bleibt deins

JSON-Export inklusive aller Versionen. Ein Import legt neue Kopien an und überschreibt nichts. API-Keys sind nicht enthalten.

Archiv exportieren ↗

Version

diff --git a/tests/test_app.py b/tests/test_app.py index 7168a7a..558e81a 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -158,3 +158,15 @@ def test_stale_vectors_do_not_overwrite_new_version(client): assert store.vectors('test') == {} store.put_vector(p['id'], updated['version'], 'test', [1, 0]) assert p['id'] in store.vectors('test') + + +def test_recheck_endpoint_keeps_draft_and_requires_auth(client): + configure(client) + response=client.post('/api/recheck',json={'body':'Original {{name}}','draft':'Draft {{user}}'}) + assert response.status_code==200 + assert response.json()['body']=='Draft {{user}}' + assert len(response.json()['review']['literal_issues'])==2 + assert response.json()['review']['status']=='unchecked' + assert client.get('/api/prompts').json()['items']==[] + client.post('/api/logout') + assert client.post('/api/recheck',json={'body':'original','draft':'draft'}).status_code==401 diff --git a/tests/test_review.py b/tests/test_review.py index 4bcfacf..647a36b 100644 --- a/tests/test_review.py +++ b/tests/test_review.py @@ -69,3 +69,46 @@ def test_failed_final_review_does_not_claim_success(): r, calls = run([BAD, json.dumps({'issues': ISSUES}), ORIGINAL, 500]) assert len(calls) == 4 assert r['body'] == ORIGINAL and r['review']['status'] == 'unchecked' + + +def test_recheck_is_one_call_and_keeps_text(): + calls=[] + def handler(request): + calls.append(json.loads(request.content)) + return httpx.Response(200,json={'choices':[{'message':{'content':'{"issues": []}'}}]}) + p=Provider({'base_url':'http://model/v1','chat_model':'test'},httpx.MockTransport(handler)) + r=asyncio.run(p.recheck(ORIGINAL,'Clarify',BAD)) + assert len(calls)==1 and r['body']==BAD + assert r['review']['draft_kind']=='current' + assert r['review']['status']=='passed' + + +def test_literal_checks(): + from atelier.provider import literal_checks + assert literal_checks('Use {{name}}, port 8099 and /app/data.', 'Use /app/data. Listen on port 8099 with {{name}}.')==[] + issues=literal_checks('Use {{name}} on port 8099 at /app/data', 'Use {{user}} on port 8080 at /app/new') + assert len(issues)==6 + + +@pytest.mark.parametrize('kind', ['timeout','connect','json','http']) +def test_diagnostic_logs_redact_sensitive_data(caplog,kind): + def handler(request): + if kind=='timeout':raise httpx.ReadTimeout('SECRET_RESPONSE',request=request) + if kind=='connect':raise httpx.ConnectError('SECRET_RESPONSE',request=request) + if kind=='json':return httpx.Response(200,text='SECRET_RESPONSE') + return httpx.Response(500,text='SECRET_RESPONSE') + p=Provider({'base_url':'http://secret-host/v1','api_key':'SECRET_KEY','chat_model':'test'},httpx.MockTransport(handler)) + r=asyncio.run(p.recheck('SECRET_PROMPT','SECRET_INSTRUCTION','SECRET_DRAFT')) + assert r['review']['status']=='unchecked' + assert r['review']['failed_stage']=='recheck' + assert 'error_type=' in caplog.text and 'duration_ms=' in caplog.text + assert not any(v in caplog.text for v in ['SECRET','secret-host']) + assert 'SECRET' not in r['review']['warning'] + if kind=='json': assert 'HTTP 200' in r['review']['warning'] + if kind=='timeout': assert 'Zeitüberschreitung' in r['review']['warning'] + + +def test_failed_final_review_labels_corrected_draft(): + r,_=run([BAD,json.dumps({'issues':ISSUES}),ORIGINAL,503]) + assert r['review']['draft_kind']=='corrected' + assert r['review']['failed_stage']=='final_review'