Files
VianoBugAnalysis/src/viano_bug_analysis/workflow.py
T

156 lines
8.1 KiB
Python

"""Shared resumable analysis dispatch used by trials and the service worker."""
import os
from .analysis import (CaseInterpreter, HypothesisBuilder, HypothesisConfig, Investigator,
DiagnosisBuilder, ChangeRequestBuilder, OutputValidator)
from .analysis.correction import CorrectionConfig
from .case import CaseBuilder
from .lifecycle import State
from .llm import GatewayConfig, LLMGateway
from .llm.gateway import PRIVACY
from .package import PackageReader
class TrialError(Exception):
def __init__(self, code):
self.code = code
super().__init__(code)
def preflight(path):
"""Validate transport configuration and credentials without sending requests."""
config = GatewayConfig.load(path)
gateway = LLMGateway(config)
eligible = [p for p in config.providers
if p.enabled and p.privacy in PRIVACY[config.privacy_profile]]
if not eligible:
raise TrialError("LLM_PRIVACY_NO_PROVIDER")
for provider in eligible:
if provider.api_key_env:
value = os.environ.get(provider.api_key_env)
if not value or any(ord(c) < 32 or ord(c) == 127 for c in value):
raise TrialError("LLM_CREDENTIALS")
return gateway
def describe(path, status, *, code=None, exported=None, retry_not_before=None):
with PackageReader(path) as reader:
m = reader.manifest
return {"status": status, "code": code, "package": str(path),
"case_id": m["case_id"], "package_id": m["package_id"],
"package_generation": m["package_generation"],
"analysis_revision": m["analysis_revision"], "state": m["state"],
"routing": m["routing"], "exported": str(exported) if exported else None,
"retry_not_before": retry_not_before}
async def drive(store, gateway, case_id, package_id, *, max_dispatches=100,
attempt_id="default", policies=None, progress=None, stop_after=None):
if type(max_dispatches) is not int or max_dispatches <= 0:
raise TrialError("TRIAL_DISPATCH_LIMIT")
if stop_after not in {None, "case", "interpretation"}:
raise TrialError("TRIAL_STOP_PHASE")
policies = policies or {}
identity = (case_id, package_id)
correction = policies.get("correction", CorrectionConfig())
from .tools import InvestigationToolRouter
project_research = policies.get("project_research")
interpreter = CaseInterpreter(store, gateway, correction_config=correction, project_research=project_research,
joint_config=policies.get("analysis", HypothesisConfig()) if policies.get("planning_mode") == "joint" else None)
hypotheses = HypothesisBuilder(store, gateway, policies.get("analysis"), correction_config=correction)
loop = Investigator(store, gateway, policies.get("investigation"),
budget_config=policies.get("budget"),
router=InvestigationToolRouter(project_research=project_research))
diagnosis = DiagnosisBuilder(store, gateway, policies.get("diagnosis"))
request = ChangeRequestBuilder(store, gateway, policies.get("change_request"))
output = OutputValidator(store, gateway)
for dispatch in range(max_dispatches):
latest = store.latest(*identity)
if latest is None:
raise TrialError("CASE_NOT_FOUND")
with PackageReader(latest, store.limits) as reader:
state = reader.manifest["state"]
artifacts = dict(reader.manifest["artifacts"])
routing = reader.manifest["routing"]
# A reopened case retains prior artifacts. They are history, not
# completed stages of the new revision.
revision = reader.manifest['analysis_revision']
from .lifecycle.checkpoints import checkpoint_chain
chain = checkpoint_chain(reader)
for pointer, step in (
('case', f'case-builder:v1:revision:{revision}:complete'),
('case_interpretation', f'case-interpreter:v1:revision:{revision}:complete'),
('hypotheses', f'hypothesis-builder:v1:revision:{revision}:complete'),
('diagnosis', f'diagnosis:v1:{revision}:complete'),
('change_request', f'change-request:v1:{revision}:complete')):
if artifacts.get(pointer) and not any(cp['step_id'] == step for _, cp in chain):
# Contracts identify current products even when newer phase
# implementations use a different completion step name.
import json
doc = json.loads(reader.read_bytes(artifacts[pointer]))
artifact_identity = doc.get('case', doc)
if artifact_identity.get('analysis_revision') != revision:
artifacts[pointer] = None
if state == State.BLOCKED:
return describe(latest, "BLOCKED", code=routing["reason"],
retry_not_before=routing["retry_not_before"])
if phase_complete(latest, stop_after):
return describe(latest, "PHASE_COMPLETE")
if progress:
progress({"dispatch": dispatch + 1, "state": state})
if state in {State.OUTPUT_READY, State.FAILED_TO_ANALYZE, State.HANDED_OFF}:
result = await output.publish(*identity)
elif state == State.BUILDING_CASE and artifacts.get("case"):
result = await interpreter.interpret(*identity, attempt_id=attempt_id)
elif state in {State.VALIDATED, State.REOPENED, State.BUILDING_CASE}:
result = CaseBuilder(store, project_research=project_research).build(*identity)
elif state == State.INTERPRETING_CASE:
if artifacts.get("case_interpretation"):
result = await hypotheses.build(*identity, attempt_id=attempt_id)
else:
result = await interpreter.interpret(*identity, attempt_id=attempt_id)
elif state == State.GENERATING_HYPOTHESES:
if artifacts.get("hypotheses"):
result = await loop.step(*identity)
else:
result = await hypotheses.build(*identity, attempt_id=attempt_id)
elif state == State.INVESTIGATING_HYPOTHESES:
result = await loop.step(*identity)
elif state == State.BUILDING_DIAGNOSIS:
if not artifacts.get("diagnosis"):
result = await diagnosis.build(*identity)
elif not artifacts.get("change_request"):
result = await request.build(*identity)
else:
result = await output.publish(*identity)
else:
raise TrialError("TRIAL_PHASE")
status = getattr(result, "status", "COMPLETED")
if status == "INCONCLUSIVE":
result = await output.publish(*identity)
status = result.status
if status in {"BLOCKED", "RETRY_PENDING", "DEFERRED", "ERROR", "BUSY",
"OUTPUT_READY", "REVIEW", "HANDED_OFF"}:
return describe(result.package, status, code=getattr(result, "code", None),
exported=getattr(result, "exported", None),
retry_not_before=getattr(result, "retry_not_before", None))
latest = store.latest(*identity)
return describe(latest, "PHASE_COMPLETE" if phase_complete(latest, stop_after) else "YIELDED")
def phase_complete(path, stop_after):
if stop_after is None:
return False
from .case.contracts import read_json
with PackageReader(path) as reader:
manifest = reader.manifest
if stop_after == "case":
ref = manifest["artifacts"].get("case")
if manifest["state"] != State.BUILDING_CASE or not ref:
return False
doc = read_json(reader, ref, "case", 8 * 1024 * 1024)["case"]
else:
ref = manifest["artifacts"].get("case_interpretation")
if manifest["state"] != State.INTERPRETING_CASE or not ref:
return False
doc = read_json(reader, ref, "case-interpretation", 2 * 1024 * 1024)
return all(doc[field] == manifest[field] for field in ("case_id", "package_id", "analysis_revision"))