Add single-call measurement for prepared investigation prompts

This commit is contained in:
Nico committed 2026-10-17 12:46:36 +02:00
1 parent 3989a07e05
commit 7c168182c1
4 files changed
+140 -2

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
# Einzelmessung des kompakten Investigation-Prompts
```powershell
.\.venv\Scripts\python.exe -m viano_bug_analysis.prompt_probe --run
```
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
--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.
Das vorhandene Gateway-Konfigurationsprofil bestimmt Modell, Denkstufe,
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.
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.
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.
Testgrundlage ist der erste H1-Kontext von Bug 20 vor jeder Investigation-
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.
@@ -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 case['id'].endswith('_json') and result['status']=='SUCCESS':
try: valid=Draft202012Validator(SCHEMA).is_valid(json.loads(text))
if 'responseJsonSchema' in case['payload']['generationConfig'] and result['status']=='SUCCESS':
try: valid=Draft202012Validator(case['payload']['generationConfig']['responseJsonSchema']).is_valid(json.loads(text))
except ValueError: valid=False
result['schema_valid']=valid
if not valid: result['status']='INVALID_JSON'
+69
View File
@@ -0,0 +1,69 @@
"""Measure one prepared Gemini request without executing research tools."""
import argparse
import hashlib
import json
from pathlib import Path
from jsonschema import Draft202012Validator
from .llm import GatewayConfig, LLMError
from .llm.gateway import PRIVACY
from .minimal_prompt_test import measure
from .telemetry.safety import safe_code
DEFAULT = Path('service-data/benchmarks/compact-investigation-20261017/request.json')
def prepare(provider,request):
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')
messages=request['messages']
if (not isinstance(messages,list) or [m.get('role') for m in messages if isinstance(m,dict)]!=['system','user'] or
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':request['output_schema']}
if provider.reasoning_effort is not None:generation['thinkingConfig']={'thinkingLevel':provider.reasoning_effort}
payload={'systemInstruction':{'parts':[{'text':messages[0]['content']}]},
'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),
'payload':payload}
def main(argv=None):
p=argparse.ArgumentParser(description=__doc__)
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('--output',type=Path)
p.add_argument('--run',action='store_true',help='Make exactly one potentially billable API call')
args=p.parse_args(argv)
try:
config=GatewayConfig.load(args.llm_config)
providers=[v for v in config.providers if v.enabled and v.privacy in PRIVACY[config.privacy_profile]]
provider=next((v for v in providers if v.id==args.provider),None) if args.provider else next(iter(providers),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)
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}
if not args.run:
print(json.dumps({'status':'PREPARED','network_used':False,**metadata,
'message_characters':case['prompt_characters']}));return 0
output=args.output or args.request.with_name('result.json')
# Exclusive reservation prevents accidental duplicate paid execution.
with output.open('x',encoding='utf-8') as f:json.dump({'status':'RUNNING',**metadata},f)
result={**measure(provider,case,config.max_response_bytes),**metadata}
output.write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps(result,ensure_ascii=True))
return 0 if result['status']=='SUCCESS' else 2
except KeyboardInterrupt:
print(json.dumps({'status':'INTERRUPTED'}));return 130
except Exception as error:
print(json.dumps({'status':'ERROR','code':safe_code(getattr(error,'code',None))}));return 2
if __name__=='__main__':raise SystemExit(main())
+36
View File
@@ -0,0 +1,36 @@
import copy
import pytest
from viano_bug_analysis.prompt_probe import prepare
from viano_bug_analysis.minimal_prompt_test import measure
from viano_bug_analysis.llm import LLMError
from test_minimal_prompt_test import provider
def request():return {'prompt_version':'fixture','messages':[{'role':'system','content':'Test instructions'},
{'role':'user','content':'Test context'}], 'output_schema':{'type':'object','required':['query'],
'properties':{'query':{'const':'database.schema'}},'additionalProperties':False}}
def test_native_schema_sent_once_without_wrapper_or_tools():
r=request();before=copy.deepcopy(r);case=prepare(provider(),r)
payload=case['payload']
assert set(payload)=={'systemInstruction','contents','generationConfig'}
assert payload['contents'][0]['parts'][0]['text']=='Test context'
assert payload['generationConfig']['responseJsonSchema']==r['output_schema']
assert payload['generationConfig']['thinkingConfig']=={'thinkingLevel':'low'}
assert r==before
def test_validates_the_prepared_schema_not_the_minimal_test_schema():
case=prepare(provider(),request())
def post(*args):return {'candidates':[{'finishReason':'STOP','content':{'parts':[{'text':'{"query":"database.schema"}'}]}}],
'usageMetadata':{'promptTokenCount':200,'candidatesTokenCount':8,'thoughtsTokenCount':30}}
result=measure(provider(),case,10000,post)
assert result['status']=='SUCCESS' and result['schema_valid']
assert result['usage']['input_tokens']==200
@pytest.mark.parametrize('field,value',[('messages',[]),('messages',[{'role':'user','content':'x'}])])
def test_rejects_invalid_envelopes(field,value):
r=request();r[field]=value
with pytest.raises(LLMError):prepare(provider(),r)