Adapt prompt probe schema to Gemini subset with full local validation
This commit is contained in:
1 parent
7c168182c1
commit
2eaba70083
4 files changed
+99
-18
No files matched your search
@@ -4,30 +4,53 @@
|
|||||||
.\.venv\Scripts\python.exe -m viano_bug_analysis.prompt_probe --run
|
.\.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.
|
service-data/benchmarks/compact-investigation-20261017/request.json.
|
||||||
Standardoutput ist result.json im selben Ordner. Ein vorhandenes Ergebnis
|
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.
|
--output mit einem neuen Dateinamen verwenden.
|
||||||
|
|
||||||
Genau ein nativer Gemini-Aufruf, keine Wiederholung, kein Failover, kein
|
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,
|
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;
|
Instruktionen und Kontext werden als System-/User-Nachricht übertragen;
|
||||||
das Ausgabeschema wird ausschließlich im nativen generationConfig gesetzt.
|
das Ausgabeschema wird ausschließlich im nativen generationConfig gesetzt.
|
||||||
Erfasst werden Eingabe-, Kandidaten-, Denk-, Cache- und Gesamttokens sowie
|
Erfasst werden Eingabe-, Kandidaten-, Denk-, Cache- und Gesamttokens sowie
|
||||||
Laufzeit, Modellversion, JSON-Schemavalidität und der Hash der vorbereiteten
|
Laufzeit, Modellversion, JSON-Schemavalidität und der Hash der vorbereiteten
|
||||||
Anfrage. Eine passende JSON-Struktur allein beweist keine fachliche Qualität.
|
Anfrage. Eine passende JSON-Struktur allein beweist keine fachliche Qualität.
|
||||||
|
|
||||||
Der vorbereitete Prompt umfasst 8.396 Nachrichtenzeichen plus 2.486 Zeichen
|
Der vorbereitete Prompt umfasst 8.396 Nachrichtenzeichen plus 2.486 Zeichen
|
||||||
minifiziertes Ausgabeschema, zusammen 10.882. Das sind etwa 29 mal 375 Zeichen
|
minifiziertes Ausgabeschema, zusammen 10.882. Das sind etwa 29 mal 375 Zeichen
|
||||||
(des englischen Minimaltests). Formatierung der lesbaren PROMPT.txt zählt
|
(des englischen Minimaltests). Formatierung der lesbaren PROMPT.txt zählt
|
||||||
nicht als zusätzlicher API-Input. Keine Tokenschätzung ersetzt die API-Messung.
|
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-
|
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
|
Messartefakte und Provenienz liegen im Benchmarkordner, nicht in der produktiven
|
||||||
Fallakte. Ein Einzeltest untersucht Schrittwahl und Kontextgröße, nicht die
|
Fallakte. Ein Einzeltest untersucht Schrittwahl und Kontextgröße, nicht die
|
||||||
Qualität oder Gesamtkosten einer vollständigen Untersuchung.
|
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
|
||||||
|
abgebildete komplexe Referenz-/Bedingungsschemata werden lokal abgelehnt.
|
||||||
|
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:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\.venv\Scripts\python.exe -m viano_bug_analysis.prompt_probe --run --output service-data/benchmarks/compact-investigation-20261017/result-schemafix-1.json
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -58,7 +58,7 @@ def measure(provider,case,limit,post=None):
|
|||||||
result.update(answer=text,finish_reason=candidate.get('finishReason'),model=envelope.get('modelVersion',provider.model))
|
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'
|
result['status']='SUCCESS' if candidate.get('finishReason')=='STOP' and text else 'INCOMPLETE'
|
||||||
if 'responseJsonSchema' in case['payload']['generationConfig'] and result['status']=='SUCCESS':
|
if 'responseJsonSchema' in case['payload']['generationConfig'] and result['status']=='SUCCESS':
|
||||||
try: valid=Draft202012Validator(case['payload']['generationConfig']['responseJsonSchema']).is_valid(json.loads(text))
|
try: valid=Draft202012Validator(case.get('validation_schema',case['payload']['generationConfig']['responseJsonSchema'])).is_valid(json.loads(text))
|
||||||
except ValueError: valid=False
|
except ValueError: valid=False
|
||||||
result['schema_valid']=valid
|
result['schema_valid']=valid
|
||||||
if not valid: result['status']='INVALID_JSON'
|
if not valid: result['status']='INVALID_JSON'
|
||||||
|
|||||||
@@ -12,6 +12,37 @@ from .telemetry.safety import safe_code
|
|||||||
DEFAULT = Path('service-data/benchmarks/compact-investigation-20261017/request.json')
|
DEFAULT = Path('service-data/benchmarks/compact-investigation-20261017/request.json')
|
||||||
|
|
||||||
|
|
||||||
|
def gemini_schema(schema):
|
||||||
|
"""Provider projection only; full original schema remains the local validator.
|
||||||
|
|
||||||
|
Gemini's responseJsonSchema subset omits const and several validation
|
||||||
|
keywords. In particular, a const-only kind has no usable type after const
|
||||||
|
is ignored. Translate it rather than handing the internal contract through.
|
||||||
|
"""
|
||||||
|
allowed = {'type','format','title','description','enum','items','prefixItems',
|
||||||
|
'minItems','maxItems','minimum','maximum','anyOf','oneOf',
|
||||||
|
'properties','additionalProperties','required'}
|
||||||
|
if not isinstance(schema,dict):
|
||||||
|
raise LLMError('PROMPT_PROBE_SCHEMA_UNSUPPORTED')
|
||||||
|
if any(k in schema for k in ('$ref','$defs','allOf','not','if','then','else')):
|
||||||
|
raise LLMError('PROMPT_PROBE_SCHEMA_UNSUPPORTED')
|
||||||
|
result = {}
|
||||||
|
for key,value in schema.items():
|
||||||
|
if key not in allowed: continue
|
||||||
|
if key == 'properties': result[key] = {k:gemini_schema(v) for k,v in value.items()}
|
||||||
|
elif key in {'items','additionalProperties'} and isinstance(value,dict): result[key] = gemini_schema(value)
|
||||||
|
elif key in {'anyOf','oneOf','prefixItems'}: result['anyOf' if key=='oneOf' else key] = [gemini_schema(v) for v in value]
|
||||||
|
else: result[key] = value
|
||||||
|
if 'const' in schema:
|
||||||
|
value=schema['const']
|
||||||
|
if not isinstance(value,str): raise LLMError('PROMPT_PROBE_SCHEMA_UNSUPPORTED')
|
||||||
|
result['type']='string';result['enum']=[value]
|
||||||
|
if isinstance(result.get('type'),list):
|
||||||
|
types=result.pop('type')
|
||||||
|
result['anyOf']=[{'type':value} for value in types]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def prepare(provider,request):
|
def prepare(provider,request):
|
||||||
if (set(request)!={'prompt_version','messages','output_schema'} or
|
if (set(request)!={'prompt_version','messages','output_schema'} or
|
||||||
not isinstance(request['prompt_version'],str) or not request['prompt_version'].strip()):
|
not isinstance(request['prompt_version'],str) or not request['prompt_version'].strip()):
|
||||||
@@ -22,12 +53,12 @@ def prepare(provider,request):
|
|||||||
raise LLMError('PROMPT_PROBE_REQUEST')
|
raise LLMError('PROMPT_PROBE_REQUEST')
|
||||||
Draft202012Validator.check_schema(request['output_schema'])
|
Draft202012Validator.check_schema(request['output_schema'])
|
||||||
generation={'maxOutputTokens':provider.max_output_tokens,'responseMimeType':'application/json',
|
generation={'maxOutputTokens':provider.max_output_tokens,'responseMimeType':'application/json',
|
||||||
'responseJsonSchema':request['output_schema']}
|
'responseJsonSchema':gemini_schema(request['output_schema'])}
|
||||||
if provider.reasoning_effort is not None:generation['thinkingConfig']={'thinkingLevel':provider.reasoning_effort}
|
if provider.reasoning_effort is not None:generation['thinkingConfig']={'thinkingLevel':provider.reasoning_effort}
|
||||||
payload={'systemInstruction':{'parts':[{'text':messages[0]['content']}]},
|
payload={'systemInstruction':{'parts':[{'text':messages[0]['content']}]},
|
||||||
'contents':[{'role':'user','parts':[{'text':messages[1]['content']}]}],'generationConfig':generation}
|
'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':sum(len(m['content']) for m in messages),
|
||||||
'payload':payload}
|
'payload':payload,'validation_schema':request['output_schema']}
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
@@ -49,7 +80,9 @@ def main(argv=None):
|
|||||||
if len(json.dumps(case['payload'],ensure_ascii=False).encode('utf-8'))>config.max_request_bytes:raise LLMError('LLM_REQUEST_LIMIT')
|
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,
|
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,
|
'reasoning_effort':provider.reasoning_effort,'max_output_tokens':provider.max_output_tokens,
|
||||||
'timeout_seconds':provider.timeout_seconds,'explicit_cache':False,'research_queries_executed':0}
|
'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'}
|
||||||
if not args.run:
|
if not args.run:
|
||||||
print(json.dumps({'status':'PREPARED','network_used':False,**metadata,
|
print(json.dumps({'status':'PREPARED','network_used':False,**metadata,
|
||||||
'message_characters':case['prompt_characters']}));return 0
|
'message_characters':case['prompt_characters']}));return 0
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ def test_native_schema_sent_once_without_wrapper_or_tools():
|
|||||||
payload=case['payload']
|
payload=case['payload']
|
||||||
assert set(payload)=={'systemInstruction','contents','generationConfig'}
|
assert set(payload)=={'systemInstruction','contents','generationConfig'}
|
||||||
assert payload['contents'][0]['parts'][0]['text']=='Test context'
|
assert payload['contents'][0]['parts'][0]['text']=='Test context'
|
||||||
assert payload['generationConfig']['responseJsonSchema']==r['output_schema']
|
assert payload['generationConfig']['responseJsonSchema']['properties']['query']=={'type':'string','enum':['database.schema']}
|
||||||
|
assert case['validation_schema']==r['output_schema']
|
||||||
assert payload['generationConfig']['thinkingConfig']=={'thinkingLevel':'low'}
|
assert payload['generationConfig']['thinkingConfig']=={'thinkingLevel':'low'}
|
||||||
assert r==before
|
assert r==before
|
||||||
|
|
||||||
@@ -34,3 +35,27 @@ def test_validates_the_prepared_schema_not_the_minimal_test_schema():
|
|||||||
def test_rejects_invalid_envelopes(field,value):
|
def test_rejects_invalid_envelopes(field,value):
|
||||||
r=request();r[field]=value
|
r=request();r[field]=value
|
||||||
with pytest.raises(LLMError):prepare(provider(),r)
|
with pytest.raises(LLMError):prepare(provider(),r)
|
||||||
|
|
||||||
|
|
||||||
|
def test_gemini_projection_types_const_and_keeps_strict_local_validation():
|
||||||
|
from viano_bug_analysis.prompt_probe import gemini_schema
|
||||||
|
from jsonschema import Draft202012Validator
|
||||||
|
r=request()
|
||||||
|
r['output_schema']['properties']['query']={'const':'database.schema','minLength':5,'pattern':'^database'}
|
||||||
|
original=copy.deepcopy(r)
|
||||||
|
c=prepare(provider(),r)
|
||||||
|
wire=c['payload']['generationConfig']['responseJsonSchema']
|
||||||
|
Draft202012Validator.check_schema(wire)
|
||||||
|
assert wire['properties']['query']=={'type':'string','enum':['database.schema']}
|
||||||
|
assert r==original and c['validation_schema']==r['output_schema']
|
||||||
|
assert gemini_schema({'type':['string','number','boolean','null']})=={
|
||||||
|
'anyOf':[{'type':v} for v in ['string','number','boolean','null']]}
|
||||||
|
|
||||||
|
|
||||||
|
def test_local_constraints_not_silently_dropped_from_response_check():
|
||||||
|
r=request();r['output_schema']['properties']['query']={'type':'string','pattern':'^database'}
|
||||||
|
c=prepare(provider(),r)
|
||||||
|
def post(*args):return {'candidates':[{'finishReason':'STOP','content':{'parts':[{'text':'{"query":"invented"}'}]}}],
|
||||||
|
'usageMetadata':{'promptTokenCount':200}}
|
||||||
|
result=measure(provider(),c,10000,post)
|
||||||
|
assert result['status']=='INVALID_JSON' and result['schema_valid'] is False
|
||||||
Reference in new issue
Block a user