136 lines
7.1 KiB
Python
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())
|