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'))
|
||||
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'
|
||||
|
||||
@@ -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