Files
VianoBugAnalysis/src/viano_bug_analysis/trial.py
T

136 lines
7.1 KiB
Python

"""Bounded, resumable single-case trial; no daemon or automatic unblocking."""
import argparse
import asyncio
import json
import os
import sys
import uuid
from pathlib import Path
from .analysis import (CaseInterpreter, HypothesisBuilder, HypothesisConfig,
Investigator, InvestigationConfig, DiagnosisBuilder,
DiagnosisConfig, ChangeRequestBuilder, ChangeRequestConfig,
OutputValidator)
from .budget import BudgetConfig
from .case import CaseBuilder
from .lifecycle import Workspace, InboxProcessor, State
from .lifecycle.locking import FileLock
from .lifecycle.storage import key
from .llm import GatewayConfig, LLMGateway
from .llm.gateway import PRIVACY
from .package import PackageReader
from .profiles.vcore import VcoreImporter
from .telemetry import Telemetry, TelemetryConfig
from .telemetry.safety import safe_code
from .analysis.correction import CorrectionConfig
from .workflow import TrialError, preflight, describe, drive, phase_complete
def positive(text):
value = int(text)
if value <= 0:
raise argparse.ArgumentTypeError("Expected a positive integer")
return value
def parser():
p = argparse.ArgumentParser(description=__doc__)
commands = p.add_subparsers(dest="command", required=True)
check = commands.add_parser("check", help="Validate configuration/key presence; no network")
check.add_argument("--llm-config", default="config/llm.local.toml")
prepare = commands.add_parser("prepare", help="Import wowdiag/wowsrc into the inbox; no LLM")
prepare.add_argument("--workspace", default="service-data")
prepare.add_argument("--wowdiag", required=True, type=Path)
prepare.add_argument("--wowsrc", type=Path)
prepare.add_argument("--case-id", required=True)
prepare.add_argument("--package-id")
run = commands.add_parser("run", help="Process one case; may make billable API calls")
run.add_argument("--workspace", default="service-data")
run.add_argument("--llm-config", default="config/llm.local.toml")
run.add_argument("--package", type=Path, help="Initial ZIP directly in this workspace's inbox")
run.add_argument("--case-id", help="Resume a managed case")
run.add_argument("--package-id", help="Stable managed package identity")
run.add_argument("--planning-mode", choices=["separate", "joint"], default="separate", help="Joint interpretation/hypothesis call for new revisions")
run.add_argument("--attempt-id", default="default")
run.add_argument("--max-dispatches", type=positive, default=100)
run.add_argument("--stop-after", choices=["case", "interpretation"], help="Stop at the confirmed phase artifact, including on resume")
run.add_argument("--telemetry-config", type=Path)
run.add_argument("--project-config", type=Path, help="Read-only source/DB research providers")
for name in ("analysis", "budget", "investigation", "diagnosis", "change-request", "correction"):
run.add_argument(f"--{name}-config", type=Path)
return p
def main(argv=None):
p = parser()
args = p.parse_args(argv)
if args.command == "run" and (bool(args.package) == bool(args.case_id and args.package_id)
or bool(args.case_id) != bool(args.package_id)):
p.error("Use --package OR both --case-id and --package-id")
try:
if args.command == "check":
gateway = preflight(args.llm_config)
result = {"status": "CONFIG_READY", "network_tested": False,
"eligible_providers": sum(p.enabled and p.privacy in PRIVACY[gateway.config.privacy_profile]
for p in gateway.config.providers)}
elif args.command == "prepare":
workspace = Workspace(args.workspace)
package_id = args.package_id or str(uuid.uuid4())
path = workspace.inbox / ("import-" + key(args.case_id + ":" + package_id) + ".zip")
VcoreImporter().create(args.wowdiag, path, case_id=args.case_id,
package_id=package_id, wowsrc=args.wowsrc)
result = describe(path, "PREPARED")
else:
gateway = preflight(args.llm_config)
policies = {"planning_mode": args.planning_mode}
if args.project_config:
from .tools.project_query import ProjectResearch
policies["project_research"] = ProjectResearch.load(args.project_config)
for name, cls in (("analysis", HypothesisConfig), ("budget", BudgetConfig),
("investigation", InvestigationConfig), ("diagnosis", DiagnosisConfig),
("correction", CorrectionConfig),
("change_request", ChangeRequestConfig)):
path = getattr(args, name + "_config")
if path:
policies[name] = cls.load(path)
config = TelemetryConfig.load(args.telemetry_config) if args.telemetry_config else None
workspace = Workspace(args.workspace)
with Telemetry(workspace, config) as telemetry:
try:
queue = InboxProcessor(workspace, telemetry=telemetry)
if args.package:
admitted = queue.process(args.package)
if admitted.status in {"ERROR", "BUSY", "QUARANTINED", "DEFERRED"}:
print(json.dumps({"status": admitted.status, "code": safe_code(admitted.code)}))
return 2 if admitted.status in {"ERROR", "QUARANTINED"} else 3
with PackageReader(admitted.package) as reader:
case_id, package_id = reader.manifest["case_id"], reader.manifest["package_id"]
else:
case_id, package_id = args.case_id, args.package_id
# Lock across this one trial; ordinary phase locks remain authoritative.
with FileLock(workspace.runtime / "locks" / ("trial-" + key(case_id) + ".lock")):
result = asyncio.run(drive(queue.store, gateway, case_id, package_id,
max_dispatches=args.max_dispatches, attempt_id=args.attempt_id, policies=policies,
stop_after=args.stop_after,
progress=lambda event: print(json.dumps(event), file=sys.stderr, flush=True)))
except Exception as error:
telemetry.guard(telemetry.exception, error)
raise
print(json.dumps(result, ensure_ascii=True))
return 0 if result["status"] in {"CONFIG_READY", "PREPARED", "PHASE_COMPLETE", "OUTPUT_READY", "REVIEW", "HANDED_OFF"} else 3
except KeyboardInterrupt:
print(json.dumps({"status": "INTERRUPTED"}), file=sys.stderr)
return 130
except Exception as error:
code = getattr(error, "code", None)
if code not in {"TRIAL_DISPATCH_LIMIT", "TRIAL_PHASE"}:
code = safe_code(code)
print(json.dumps({"status": "ERROR", "code": code}), file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main())