frontend-patterns/scripts/research_bench.py
tegwick cfa249ab13 Reduce research calls with screened reuse and draft VC-02 walkthrough
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0cffe-206f-7323-89d3-b20ca4cf4bf9
2026-09-25 08:43:32 +02:00

304 lines
16 KiB
Python

"""Frontend research adapter for infospace-bench; one immutable source per run."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
from urllib import error, request
from urllib.parse import urlsplit
from pathlib import Path
import shutil
import yaml
from infospace_bench import create_infospace, load_infospace, register_artifact
from infospace_bench.errors import InfospaceError
from infospace_bench.workflow import FixtureAssistedGenerationAdapter, run_workflow
from infospace_bench.workflow import AssistedGenerationResult
from llm_connect.models import RunConfig
from research_execution import ResearchExecution
REPO = Path(__file__).resolve().parents[1]
PROFILE = REPO / "research/bench/profile"
WORKFLOW = "frontend-source-investigation"
def digest(path):
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def source_metadata(path):
text = Path(path).read_text()
parts = text.split("---", 2)
if len(parts) != 3 or parts[0].strip():
raise ValueError("Source needs YAML frontmatter and normalized Markdown text")
data = yaml.safe_load(parts[1])
required = ("title", "author", "locator", "version", "accessed", "evidence_kind")
if not isinstance(data, dict) or any(not data.get(k) for k in required):
raise ValueError(f"Required source metadata: {', '.join(required)}")
if data["evidence_kind"] not in ("primary", "secondary", "synthetic"):
raise ValueError("evidence_kind must be primary, secondary or synthetic")
if not parts[2].strip():
raise ValueError("Source body is empty")
return data
def initialize(source, workspace, slug):
source = Path(source).resolve()
metadata = source_metadata(source)
space = create_infospace(workspace, slug, name=metadata["title"], topic_domain="infotech")
shutil.copytree(PROFILE, space.root / "profile")
config_path = space.root / "infospace.yaml"
config = yaml.safe_load(config_path.read_text())
config["workflows"] = yaml.safe_load((PROFILE / "workflow.yaml").read_text())["workflows"]
config["viability"] = {} # Structural metrics are not a research acceptance gate.
config_path.write_text(yaml.safe_dump(config, sort_keys=False))
target = space.root / "artifacts/sources/source.md"
shutil.copyfile(source, target)
register_artifact(space.root, artifact_id="source/source.md", path=target,
kind="source", title=metadata["title"], provenance={
"sha256": digest(target), "bibliography": metadata,
"evidence_kind": metadata["evidence_kind"],
})
return space.root
def plan(root):
root = Path(root).resolve()
space = load_infospace(root)
sources = [a for a in space.artifacts if a.kind == "source"]
if len(sources) != 1:
raise ValueError("Exactly one source per workspace is required for stage isolation")
source = sources[0]
if digest(root / source.path) != source.provenance.get("sha256"):
raise ValueError("Source changed since intake; initialize a new workspace")
workflows = [w for w in space.config.workflows if w["id"] == WORKFLOW]
if len(workflows) != 1:
raise ValueError("Missing frontend research workflow")
stages = workflows[0]["stages"]
if any(s["input"] != "source" for s in stages):
raise ValueError("All stages must use the isolated source input")
return {"root": str(root), "workflow": WORKFLOW,
"source_sha256": source.provenance["sha256"],
"evidence_kind": source.provenance["evidence_kind"],
"stages": [s["id"] for s in stages],
"provider_calls": (3 if (root / "profile/execution.json").exists() else
sum(s["kind"] == "assisted" for s in stages)),
"provider_calls_minimum": 2 if (root / "profile/execution.json").exists() else 4,
"execution_policy": "conditional-v1" if (root / "profile/execution.json").exists() else "legacy",
"cost_usd": "unknown; call limit is not a dollar budget",
"review_status": "unreviewed"}
def diagnostic_summary(data):
"""Allowlist numeric usage/settings; never persist raw debug data."""
debug = data.get("debug") or {}
payload = (debug.get("provider_request") or {}).get("payload") or {}
body = (debug.get("provider_response") or {}).get("body") or {}
usage = body.get("usage") or {}
settings = {key: payload[key] for key in ("temperature", "max_tokens", "top_p")
if type(payload.get(key)) in (int, float)}
settings["reasoning_effort_present"] = "reasoning_effort" in payload
settings["reasoning_present"] = "reasoning" in payload
counts = {key: usage[key] for key in ("prompt_tokens", "completion_tokens", "total_tokens", "cost")
if type(usage.get(key)) in (int, float)}
details = usage.get("completion_tokens_details") or {}
counts["completion_tokens_details"] = {key: details[key] for key in
("reasoning_tokens", "accepted_prediction_tokens", "rejected_prediction_tokens")
if type(details.get(key)) in (int, float)}
return {"available": bool(debug), "effective_settings": settings, "provider_usage": counts}
class LLMConnectAdapter:
"""Client of llm-connect's /execute contract; credentials stay at the service."""
def __init__(self, endpoint, model, max_tokens=4000, reasoning_effort=None, diagnostics=False):
parsed = urlsplit(endpoint)
if (parsed.scheme not in ("http", "https") or not parsed.hostname
or parsed.username or parsed.password or parsed.query or parsed.fragment):
raise ValueError("Use an HTTP(S) llm-connect URL without credentials or query")
if max_tokens <= 0:
raise ValueError("--max-tokens must be positive")
self.endpoint, self.model = endpoint.rstrip("/"), model
self.config = RunConfig(model_name=model, temperature=0.2, max_tokens=max_tokens)
if reasoning_effort is not None:
self.config.model_params["reasoning_effort"] = reasoning_effort
self.rejected_content = None
self.diagnostics = diagnostics
self.responses = []
def generate(self, generation):
payload = {"prompt": generation.prompt, "config": self.config.to_dict()}
req = request.Request(self.endpoint + ("/execute?debug=1" if self.diagnostics else "/execute"),
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"})
try:
with request.urlopen(req, timeout=self.config.timeout_seconds + 15) as response:
data = json.load(response)
except error.HTTPError as exc:
raise InfospaceError("llm_connect_http", "llm-connect request failed",
{"error": f"HTTP Error {exc.code}"}) from None
except (error.URLError, TimeoutError):
raise InfospaceError("llm_connect_transport", "llm-connect unavailable", {}) from None
except ValueError:
raise InfospaceError("llm_connect_response", "Invalid llm-connect JSON", {}) from None
if not isinstance(data, dict):
raise ValueError("Invalid llm-connect response object")
metadata = {key: data.get(key) for key in ("model", "usage", "finish_reason")}
extra = data.get("metadata") or {}
metadata.update({key: extra[key] for key in (
"provider", "response_id", "latency_seconds", "profile", "profile_model",
"profile_provider") if key in extra})
if self.diagnostics:
metadata["diagnostics"] = diagnostic_summary(data)
content = data.get("content")
metadata["content_characters"] = len(content) if isinstance(content, str) else None
metadata["content_sha256"] = (hashlib.sha256(content.encode()).hexdigest()
if isinstance(content, str) else None)
self.responses.append({"stage": generation.stage_id, **metadata})
if (not isinstance(data.get("content"), str) or not data["content"].strip()
or data.get("finish_reason") != "stop"):
self.rejected_content = content if isinstance(content, str) else None
raise ValueError("llm-connect returned empty or incomplete generation; see receipt")
return AssistedGenerationResult(markdown=data["content"], provider="llm-connect",
metadata=metadata)
class LimitedAdapter:
def __init__(self, adapter, limit):
self.adapter, self.limit, self.calls = adapter, limit, 0
def generate(self, request):
if self.calls >= self.limit:
raise ValueError("Provider call limit reached")
self.calls += 1
return self.adapter.generate(request)
def execute(root, adapter, max_calls=4, reuse_from=None):
root = Path(root).resolve()
manifest = plan(root)
if max_calls < 0:
raise ValueError("--max-calls must be nonnegative")
if manifest["execution_policy"] == "legacy" and manifest["provider_calls"] > max_calls:
raise ValueError("Planned provider calls exceed --max-calls")
receipt = root / "reports/research-run.json"
if receipt.exists():
raise ValueError("Run already attempted; initialize a new workspace to preserve evidence")
# Record inputs before execution, including on failed or interrupted runs.
manifest["input_digests"] = {
str(p.relative_to(root)): digest(p)
for p in [root / "infospace.yaml", *sorted((root / "profile").rglob("*.md")),
root / "profile/workflow.yaml", *sorted((root / "profile").glob("*.json"))]
}
manifest["status"] = "started"
manifest["adapter"] = type(adapter).__name__
manifest["model"] = getattr(adapter, "model", "")
if isinstance(adapter, LLMConnectAdapter):
manifest["execution"] = {"endpoint": adapter.endpoint,
"config": adapter.config.to_dict(),
"diagnostics_requested": adapter.diagnostics,
"call_limit_scope": "service requests; server retries unknown"}
limited = LimitedAdapter(adapter, max_calls)
execution = None
if manifest["execution_policy"] == "conditional-v1":
policy = json.loads((root / "profile/execution.json").read_text())
if policy != {"policy": "conditional-v1"}:
raise ValueError("Unsupported execution policy")
execution = ResearchExecution(limited, {
"source": manifest["source_sha256"],
"profile": {k: v for k, v in manifest["input_digests"].items() if k.startswith("profile/")},
"runner": digest(__file__),
"execution_code": digest(Path(__file__).with_name("research_execution.py")),
"screen_code": digest(Path(__file__).with_name("research_admission.py")),
"workflows": load_infospace(root).config.workflows,
}, reuse_from)
elif reuse_from:
raise ValueError("Reuse is available only for conditional-v1 workspaces")
receipt.write_text(json.dumps(manifest, indent=2) + "\n")
try:
result = run_workflow(root, WORKFLOW, assisted_adapter=execution or limited)
manifest.update(status=result.status, run_record=result.run_record_path)
except Exception as exc:
manifest.update(status="failed", error_type=type(exc).__name__)
if isinstance(exc, InfospaceError):
manifest["error_code"] = exc.code
# Persist only the status code, never provider response bodies or headers.
status = re.search(r"HTTP Error (\d{3})", str(exc.detail.get("error", "")))
if status:
manifest["http_status"] = int(status.group(1))
raise
finally:
manifest["provider_calls_attempted"] = limited.calls
if execution:
cache = root / "reports/stage-cache.json"
cache.write_text(json.dumps(execution.cache, indent=2) + "\n")
manifest["stage_cache_sha256"] = digest(cache)
manifest["execution_events"] = execution.events
manifest["cache_hits"] = sum(e["execution"] == "cache" for e in execution.events)
manifest["deterministic_stages"] = sum(e["execution"] == "deterministic" for e in execution.events)
manifest["rejected_assertions"] = sum(e.get("rejected_assertions", 0) for e in execution.events)
manifest["editorial_corrections"] = None # Unknown until a named review exists.
if hasattr(execution, "rejected"):
(root / "reports/rejected-stage.json").write_text(json.dumps(execution.rejected, indent=2) + "\n")
if isinstance(adapter, LLMConnectAdapter):
manifest["service_requests_attempted"] = limited.calls
manifest["responses"] = adapter.responses
manifest["reported_tokens_this_run"] = sum((r.get("usage") or {}).get("total_tokens", 0)
for r in adapter.responses)
if adapter.rejected_content is not None:
rejected = root / "reports/rejected-response.md"
rejected.write_text(adapter.rejected_content)
manifest["rejected_response"] = {"path": str(rejected.relative_to(root)),
"sha256": digest(rejected),
"admission": "rejected_incomplete"}
receipt.write_text(json.dumps(manifest, indent=2) + "\n")
return manifest
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
init = commands.add_parser("init")
init.add_argument("source", type=Path)
init.add_argument("--workspace", type=Path, default=REPO / "research/bench")
init.add_argument("--slug", required=True)
preview = commands.add_parser("plan")
preview.add_argument("root", type=Path)
run = commands.add_parser("run")
run.add_argument("root", type=Path)
providers = run.add_mutually_exclusive_group(required=True)
providers.add_argument("--fixture-responses", type=Path)
providers.add_argument("--llm-connect-url", help="llm-connect service base URL")
run.add_argument("--model", help="Model or runtime profile requested from llm-connect")
run.add_argument("--max-tokens", type=int, default=4000)
run.add_argument("--diagnostics", action="store_true",
help="Retain only allowlisted effective settings and token counts")
run.add_argument("--reasoning-effort", choices=["low", "medium", "high"],
help="Explicit service model_params override; provider support may vary")
run.add_argument("--reuse-from", type=Path, help="Explicitly reuse matching completed stage results")
run.add_argument("--max-calls", type=int, default=4)
args = parser.parse_args()
try:
if args.command == "init":
result = {"root": str(initialize(args.source, args.workspace, args.slug))}
elif args.command == "plan":
result = plan(args.root)
else:
if args.fixture_responses:
if plan(args.root)["evidence_kind"] != "synthetic":
raise ValueError("Fixture responses may only run against synthetic sources")
adapter = FixtureAssistedGenerationAdapter.from_file(args.fixture_responses)
else:
if not args.model:
raise ValueError("Live generation requires --model")
adapter = LLMConnectAdapter(args.llm_connect_url, args.model, args.max_tokens,
args.reasoning_effort, args.diagnostics)
result = execute(args.root, adapter, args.max_calls, args.reuse_from)
print(json.dumps(result, indent=2, default=str))
except (ValueError, OSError, InfospaceError) as exc:
parser.exit(1, f"{exc}\n")
if __name__ == "__main__":
main()