Add single-call measurement for prepared investigation prompts
This commit is contained in:
1 parent
3989a07e05
commit
7c168182c1
4 files changed
+140
-2
No files matched your search
@@ -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'))
|
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.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 case['id'].endswith('_json') and result['status']=='SUCCESS':
|
if 'responseJsonSchema' in case['payload']['generationConfig'] and result['status']=='SUCCESS':
|
||||||
try: valid=Draft202012Validator(SCHEMA).is_valid(json.loads(text))
|
try: valid=Draft202012Validator(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'
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -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)
|
||||||
Reference in new issue
Block a user