Allow isolated prompt probes with JSON mode and local schema validation
This commit is contained in:
1 parent
f52af70646
commit
e8d0602ed3
4 files changed
+78
-38
No files matched your search
@@ -4,45 +4,45 @@
|
||||
.\.venv\Scripts\python.exe -m viano_bug_analysis.prompt_probe --run
|
||||
```
|
||||
|
||||
Ohne --run erfolgt nur eine lokale Prüfung. Standardinput ist
|
||||
Ohne --run erfolgt nur eine lokale Prüfung. Standardinput ist
|
||||
service-data/benchmarks/compact-investigation-20261017/request.json.
|
||||
Standardoutput ist result.json im selben Ordner. Ein vorhandenes Ergebnis
|
||||
verhindert unbeabsichtigte Wiederholungen; für eine bewusst neue Messung
|
||||
verhindert unbeabsichtigte Wiederholungen; für eine bewusst neue Messung
|
||||
--output mit einem neuen Dateinamen verwenden.
|
||||
|
||||
Genau ein nativer Gemini-Aufruf, keine Wiederholung, kein Failover, kein
|
||||
expliziter Cache und keinerlei Ausführung der vorgeschlagenen Recherche.
|
||||
expliziter Cache und keinerlei Ausführung der vorgeschlagenen Recherche.
|
||||
Das vorhandene Gateway-Konfigurationsprofil bestimmt Modell, Denkstufe,
|
||||
Ausgabelimit und Timeout. Produktionsprompts bleiben unverändert.
|
||||
Ausgabelimit und Timeout. Produktionsprompts bleiben unverändert.
|
||||
|
||||
Instruktionen und Kontext werden als System-/User-Nachricht übertragen;
|
||||
das Ausgabeschema wird ausschließlich im nativen generationConfig gesetzt.
|
||||
Instruktionen und Kontext werden als System-/User-Nachricht übertragen;
|
||||
das Ausgabeschema wird ausschließlich im nativen generationConfig gesetzt.
|
||||
Erfasst werden Eingabe-, Kandidaten-, Denk-, Cache- und Gesamttokens sowie
|
||||
Laufzeit, Modellversion, JSON-Schemavalidität und der Hash der vorbereiteten
|
||||
Anfrage. Eine passende JSON-Struktur allein beweist keine fachliche Qualität.
|
||||
Laufzeit, Modellversion, JSON-Schemavalidität und der Hash der vorbereiteten
|
||||
Anfrage. Eine passende JSON-Struktur allein beweist keine fachliche Qualität.
|
||||
|
||||
Der vorbereitete Prompt umfasst 8.396 Nachrichtenzeichen plus 2.486 Zeichen
|
||||
minifiziertes Ausgabeschema, zusammen 10.882. Das sind etwa 29 mal 375 Zeichen
|
||||
(des englischen Minimaltests). Formatierung der lesbaren PROMPT.txt zählt
|
||||
nicht als zusätzlicher API-Input. Keine Tokenschätzung ersetzt die API-Messung.
|
||||
(des englischen Minimaltests). Formatierung der lesbaren PROMPT.txt zählt
|
||||
nicht als zusätzlicher API-Input. Keine Tokenschätzung ersetzt die API-Messung.
|
||||
|
||||
Testgrundlage ist der erste H1-Kontext von Bug 20 vor jeder Investigation-
|
||||
Recherche; die Vorbereitung enthält keine später festgestellten Spawndaten.
|
||||
Recherche; die Vorbereitung enthält keine später festgestellten Spawndaten.
|
||||
Messartefakte und Provenienz liegen im Benchmarkordner, nicht in der produktiven
|
||||
Fallakte. Ein Einzeltest untersucht Schrittwahl und Kontextgröße, nicht die
|
||||
Qualität oder Gesamtkosten einer vollständigen Untersuchung.
|
||||
Fallakte. Ein Einzeltest untersucht Schrittwahl und Kontextgröße, nicht die
|
||||
Qualität oder Gesamtkosten einer vollständigen Untersuchung.
|
||||
|
||||
## Korrektur nach HTTP 400
|
||||
|
||||
Der erste kompakte Aufruf wurde mit HTTP 400 abgelehnt. Kein Tokenzähler wurde
|
||||
zurückgegeben. Ein genauer Anbieterfehlertext wurde vom Transport nicht erhalten;
|
||||
die exakte serverseitige Ursache bleibt daher unbestätigt. Im Schema fanden sich
|
||||
const-only Diskriminatoren ohne type. Diese sind für den nativen Gemini-Subset
|
||||
ungeeignet. Die Messhilfe übersetzt string-const in type:string plus enum,
|
||||
oneOf in anyOf und primitive Typvereinigungen in anyOf. Nicht unterstützte
|
||||
Längen-/Pattern-/Unique-Regeln werden ausschließlich lokal geprüft. Nicht
|
||||
Der erste kompakte Aufruf wurde mit HTTP 400 abgelehnt. Kein Tokenzähler wurde
|
||||
zurückgegeben. Ein genauer Anbieterfehlertext wurde vom Transport nicht erhalten;
|
||||
die exakte serverseitige Ursache bleibt daher unbestätigt. Im Schema fanden sich
|
||||
const-only Diskriminatoren ohne type. Diese sind für den nativen Gemini-Subset
|
||||
ungeeignet. Die Messhilfe übersetzt string-const in type:string plus enum,
|
||||
oneOf in anyOf und primitive Typvereinigungen in anyOf. Nicht unterstützte
|
||||
Längen-/Pattern-/Unique-Regeln werden ausschließlich lokal geprüft. Nicht
|
||||
abgebildete komplexe Referenz-/Bedingungsschemata werden lokal abgelehnt.
|
||||
Originalprompt und vollständiger lokaler Validierungsvertrag bleiben gleich.
|
||||
Originalprompt und vollständiger lokaler Validierungsvertrag bleiben gleich.
|
||||
Gemini-Referenz: https://ai.google.dev/api/generate-content#v1beta.GenerationConfig
|
||||
|
||||
Der bestehende fehlgeschlagene result.json bleibt erhalten. Neuer Versuch:
|
||||
@@ -52,21 +52,21 @@ Der bestehende fehlgeschlagene result.json bleibt erhalten. Neuer Versuch:
|
||||
```
|
||||
|
||||
Ausgangskostenstand laut Nutzer jetzt 8,30 EUR. Der vorherige Anstieg um 0,01 EUR
|
||||
ist als nachträglich sichtbare Kontodifferenz dokumentiert, nicht als exakter
|
||||
Einzelaufruf-Abrechnungsbeleg. Die Korrektur wurde nur offline geprüft.
|
||||
ist als nachträglich sichtbare Kontodifferenz dokumentiert, nicht als exakter
|
||||
Einzelaufruf-Abrechnungsbeleg. Die Korrektur wurde nur offline geprüft.
|
||||
|
||||
## Anbieterdiagnose nach erneutem HTTP 400
|
||||
|
||||
Auch der erste Schemafix wurde von der API mit 400 abgewiesen. Die Ursache
|
||||
ist weiterhin offen. Der Probe-Aufruf liest nun bei HTTP-Fehlern opt-in höchstens
|
||||
ist weiterhin offen. Der Probe-Aufruf liest nun bei HTTP-Fehlern opt-in höchstens
|
||||
8 KB des Anbieterfehlers und gibt nur begrenzte Status-/Nachrichten- und
|
||||
Feldhinweise aus. API-Schlüssel, URLs und zitierte Werte werden bereinigt;
|
||||
Rohkörper werden nicht gespeichert. Der normale Gemini-Produktionspfad behält
|
||||
Feldhinweise aus. API-Schlüssel, URLs und zitierte Werte werden bereinigt;
|
||||
Rohkörper werden nicht gespeichert. Der normale Gemini-Produktionspfad behält
|
||||
seine bisherigen kontrollierten Fehlercodes und verwirft weiterhin Rohfehler.
|
||||
|
||||
Eine bereits vorhandene Ergebnisdatei erzeugt PROMPT_PROBE_OUTPUT_EXISTS und
|
||||
keinen Netzaufruf. Auch ein fehlgeschlagener API-Versuch wird nicht überschrieben.
|
||||
Der nächste bewusst einzelne Diagnoseversuch lautet:
|
||||
keinen Netzaufruf. Auch ein fehlgeschlagener API-Versuch wird nicht überschrieben.
|
||||
Der nächste bewusst einzelne Diagnoseversuch lautet:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe -m viano_bug_analysis.prompt_probe --run --output service-data/benchmarks/compact-investigation-20261017/result-diagnostic-1.json
|
||||
@@ -74,3 +74,21 @@ Der nächste bewusst einzelne Diagnoseversuch lautet:
|
||||
|
||||
Dies ist keine weitere vermutete Schemakorrektur. Die konkrete Fehlermeldung
|
||||
muss erst vorliegen, bevor der eigentliche API-Vertrag weiter angepasst wird.
|
||||
|
||||
## Isolierung ohne natives Antwortschema
|
||||
|
||||
Google meldete auch mit Diagnosehilfe lediglich INVALID_ARGUMENT ohne Feldhinweis.
|
||||
Die genaue Ursache ist damit nicht belegt. Ein neuer expliziter Modus umgeht die
|
||||
native Schemaübergabe: --schema-mode prompt setzt nur responseMimeType auf
|
||||
application/json und fügt den vollständigen lokalen Vertrag genau einmal in den
|
||||
Systemprompt ein. Alle Antwortregeln werden weiterhin lokal geprüft. Fallkontext,
|
||||
Aufgabe und Modelleinstellungen bleiben erhalten; der API-Input beträgt in diesem
|
||||
Modus 10.925 Nachrichtenzeichen einschließlich Vertrag und Einleitung.
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe -m viano_bug_analysis.prompt_probe --schema-mode prompt --run --output service-data/benchmarks/compact-investigation-20261017/result-jsonmode-1.json
|
||||
```
|
||||
|
||||
Dies ist ein isolierender Versuch, keine bestätigte Behebung der HTTP-Ursache.
|
||||
Keine automatische Wiederholung und kein automatischer Format-Fallback. Die
|
||||
Metadaten kennzeichnen den Transportmodus, damit Messungen vergleichbar bleiben.
|
||||
@@ -57,8 +57,8 @@ def measure(provider,case,limit,post=None):
|
||||
text=''.join(part['text'] for part in candidate.get('content',{}).get('parts',[]) if 'text' in part and not part.get('thought'))
|
||||
result.update(answer=text,finish_reason=candidate.get('finishReason'),model=envelope.get('modelVersion',provider.model))
|
||||
result['status']='SUCCESS' if candidate.get('finishReason')=='STOP' and text else 'INCOMPLETE'
|
||||
if 'responseJsonSchema' in case['payload']['generationConfig'] and result['status']=='SUCCESS':
|
||||
try: valid=Draft202012Validator(case.get('validation_schema',case['payload']['generationConfig']['responseJsonSchema'])).is_valid(json.loads(text))
|
||||
if ('validation_schema' in case or 'responseJsonSchema' in case['payload']['generationConfig']) and result['status']=='SUCCESS':
|
||||
try: valid=Draft202012Validator(case['validation_schema'] if 'validation_schema' in case else case['payload']['generationConfig']['responseJsonSchema']).is_valid(json.loads(text))
|
||||
except ValueError: valid=False
|
||||
result['schema_valid']=valid
|
||||
if not valid: result['status']='INVALID_JSON'
|
||||
|
||||
@@ -46,7 +46,7 @@ def gemini_schema(schema):
|
||||
return result
|
||||
|
||||
|
||||
def prepare(provider,request):
|
||||
def prepare(provider,request, *, schema_mode="native"):
|
||||
if (set(request)!={'prompt_version','messages','output_schema'} or
|
||||
not isinstance(request['prompt_version'],str) or not request['prompt_version'].strip()):
|
||||
raise LLMError('PROMPT_PROBE_REQUEST')
|
||||
@@ -55,12 +55,17 @@ def prepare(provider,request):
|
||||
any(set(m)!={'role','content'} or not isinstance(m['content'],str) or not m['content'].strip() for m in messages)):
|
||||
raise LLMError('PROMPT_PROBE_REQUEST')
|
||||
Draft202012Validator.check_schema(request['output_schema'])
|
||||
generation={'maxOutputTokens':provider.max_output_tokens,'responseMimeType':'application/json',
|
||||
'responseJsonSchema':gemini_schema(request['output_schema'])}
|
||||
if schema_mode not in {'native','prompt'}:raise LLMError('PROMPT_PROBE_SCHEMA_MODE')
|
||||
generation={'maxOutputTokens':provider.max_output_tokens,'responseMimeType':'application/json'}
|
||||
system_text=messages[0]['content']
|
||||
if schema_mode=='native':
|
||||
generation['responseJsonSchema']=gemini_schema(request['output_schema'])
|
||||
else:
|
||||
system_text += '\nOutput JSON contract (validated locally):\n'+json.dumps(request['output_schema'],ensure_ascii=False,separators=(',',':'))
|
||||
if provider.reasoning_effort is not None:generation['thinkingConfig']={'thinkingLevel':provider.reasoning_effort}
|
||||
payload={'systemInstruction':{'parts':[{'text':messages[0]['content']}]},
|
||||
payload={'systemInstruction':{'parts':[{'text':system_text}]},
|
||||
'contents':[{'role':'user','parts':[{'text':messages[1]['content']}]}],'generationConfig':generation}
|
||||
return {'id':request['prompt_version'],'prompt_characters':sum(len(m['content']) for m in messages),
|
||||
return {'id':request['prompt_version'],'prompt_characters':len(system_text)+len(messages[1]['content']),
|
||||
'payload':payload,'validation_schema':request['output_schema']}
|
||||
|
||||
|
||||
@@ -109,6 +114,7 @@ def main(argv=None):
|
||||
p.add_argument('--request',type=Path,default=DEFAULT)
|
||||
p.add_argument('--llm-config',default='config/llm.working-brief.local.toml')
|
||||
p.add_argument('--provider')
|
||||
p.add_argument('--schema-mode',choices=['native','prompt'],default='native',help='Native schema or JSON mode with the full contract once in the prompt')
|
||||
p.add_argument('--output',type=Path)
|
||||
p.add_argument('--run',action='store_true',help='Make exactly one potentially billable API call')
|
||||
args=p.parse_args(argv)
|
||||
@@ -119,13 +125,13 @@ def main(argv=None):
|
||||
if provider is None or provider.adapter!='gemini':raise LLMError('PROMPT_PROBE_PROVIDER')
|
||||
raw=args.request.read_bytes()
|
||||
if len(raw)>config.max_request_bytes:raise LLMError('LLM_REQUEST_LIMIT')
|
||||
request=json.loads(raw);case=prepare(provider,request)
|
||||
request=json.loads(raw);case=prepare(provider,request,schema_mode=args.schema_mode)
|
||||
if len(json.dumps(case['payload'],ensure_ascii=False).encode('utf-8'))>config.max_request_bytes:raise LLMError('LLM_REQUEST_LIMIT')
|
||||
metadata={'request_sha256':hashlib.sha256(raw).hexdigest(),'provider':provider.id,'model':provider.model,
|
||||
'reasoning_effort':provider.reasoning_effort,'max_output_tokens':provider.max_output_tokens,
|
||||
'timeout_seconds':provider.timeout_seconds,'explicit_cache':False,'research_queries_executed':0,
|
||||
'wire_schema_sha256':hashlib.sha256(json.dumps(case['payload']['generationConfig']['responseJsonSchema'],sort_keys=True,separators=(',',':')).encode()).hexdigest(),
|
||||
'schema_transport':'gemini_subset_v1_local_full_validation'}
|
||||
'wire_schema_sha256':hashlib.sha256(json.dumps(case['payload']['generationConfig'].get('responseJsonSchema'),sort_keys=True,separators=(',',':')).encode()).hexdigest(),
|
||||
'schema_transport':'gemini_subset_v1_local_full_validation' if args.schema_mode=='native' else 'json_mode_schema_once_in_prompt_local_full_validation'}
|
||||
if not args.run:
|
||||
print(json.dumps({'status':'PREPARED','network_used':False,**metadata,
|
||||
'message_characters':case['prompt_characters']}));return 0
|
||||
|
||||
@@ -105,3 +105,19 @@ def test_existing_output_is_named_error_without_network(tmp_path,monkeypatch,cap
|
||||
result=prompt_probe.main(['--run','--request',str(path),'--output',str(output)])
|
||||
assert result==2 and 'PROMPT_PROBE_OUTPUT_EXISTS' in capsys.readouterr().out
|
||||
assert output.read_text()=='keep original'
|
||||
|
||||
|
||||
def test_prompt_schema_mode_avoids_native_union_and_keeps_contract_once():
|
||||
import json
|
||||
r=request();case=prepare(provider(),r,schema_mode='prompt')
|
||||
payload=case['payload']
|
||||
assert 'responseJsonSchema' not in payload['generationConfig']
|
||||
assert payload['generationConfig']['responseMimeType']=='application/json'
|
||||
contract=json.dumps(r['output_schema'],ensure_ascii=False,separators=(',',':'))
|
||||
assert payload['systemInstruction']['parts'][0]['text'].count(contract)==1
|
||||
assert payload['contents'][0]['parts'][0]['text']==r['messages'][1]['content']
|
||||
assert case['validation_schema']==r['output_schema']
|
||||
def post(*args):return {'candidates':[{'finishReason':'STOP','content':{'parts':[{'text':'{"query":"database.schema"}'}]}}]}
|
||||
assert measure(provider(),case,10000,post)['status']=='SUCCESS'
|
||||
def bad(*args):return {'candidates':[{'finishReason':'STOP','content':{'parts':[{'text':'{"query":"invented"}'}]}}]}
|
||||
assert measure(provider(),case,10000,bad)['status']=='INVALID_JSON'
|
||||
Reference in new issue
Block a user