FlyingNunchucks's picture
Deploy tested GitHub commit 0080b137632d1d201ef3a1f6f7eebcc53777d7f0
445125c verified
Raw History Blame Contribute Delete
5.43 kB
"""Closed tool schemas and deterministic plans for three supported questions."""
from typing import Annotated, Literal
import json
from pydantic import Field, StrictInt, ValidationError
from service_analysis.analysis import Period, Grouping
from service_analysis.schemas import Contract
MAX_STEPS = 6
MAX_PLAN_BYTES = 32768
class SummaryCall(Contract):
operation: Literal["summarize"]
period: Period
grouping: Grouping = "overall"
class ComparisonCall(Contract):
operation: Literal["compare"]
baseline: Period
comparison: Period
grouping: Grouping = "overall"
class DecompositionCall(Contract):
operation: Literal["decompose"]
baseline: Period
comparison: Period
class WorkloadCall(Contract):
operation: Literal["workload"]
opening_period: Period
ToolCall = Annotated[SummaryCall | ComparisonCall | DecompositionCall | WorkloadCall,
Field(discriminator="operation")]
class ProposedStep(Contract):
call: ToolCall
depends_on: tuple[StrictInt, ...] = Field(default=(), max_length=MAX_STEPS)
class AnalysisPlan(Contract):
steps: tuple[ProposedStep, ...] = Field(min_length=1, max_length=MAX_STEPS)
class CompiledStep(Contract):
step_id: str
call: ToolCall
depends_on: tuple[str, ...]
class PlanRejected(ValueError):
def __init__(self, code):
self.code = code
super().__init__(code)
def _pairs(values):
result = {}
for key, value in values:
if key in result:
raise ValueError("Duplicate JSON key")
result[key] = value
return result
def _nonfinite(value):
raise ValueError("Nonfinite JSON value")
def parse_plan(raw):
try:
if isinstance(raw, AnalysisPlan):
raw = raw.model_dump(mode="json")
if isinstance(raw, str):
if len(raw.encode()) > MAX_PLAN_BYTES:
raise PlanRejected("PLAN_TOO_LARGE")
raw = json.loads(raw, object_pairs_hook=_pairs, parse_constant=_nonfinite)
if not isinstance(raw, dict):
raise ValueError("Expected plan object")
if len(json.dumps(raw, allow_nan=False).encode()) > MAX_PLAN_BYTES:
raise PlanRejected("PLAN_TOO_LARGE")
return AnalysisPlan.model_validate(raw)
except PlanRejected:
raise
except (ValueError, TypeError, RecursionError, ValidationError):
raise PlanRejected("INVALID_PLAN_SCHEMA") from None
def call_key(call):
return json.dumps(call.model_dump(mode="json"), sort_keys=True, separators=(",", ":"))
def compile_plan(raw, coverage, prior=()):
"""Validate the entire round before assigning executable steps.
Dependencies use zero-based positions in the entire run, including prior
rounds. The application translates positions into stable step IDs.
"""
plan = parse_plan(raw)
offset = len(prior)
if offset + len(plan.steps) > MAX_STEPS:
raise PlanRejected("STEP_BUDGET_EXCEEDED")
seen = {call_key(step.call) for step in prior}
compiled = []
for index, proposed in enumerate(plan.steps, offset):
dependencies = proposed.depends_on
if len(set(dependencies)) != len(dependencies) or any(d < 0 or d >= index for d in dependencies):
raise PlanRejected("INVALID_DEPENDENCY")
call = proposed.call
key = call_key(call)
if key in seen:
raise PlanRejected("DUPLICATE_CALL")
seen.add(key)
if isinstance(call, WorkloadCall):
periods = (call.opening_period,)
lower, upper = coverage.operations_start, coverage.operations_end
else:
periods = (call.period,) if isinstance(call, SummaryCall) else (call.baseline, call.comparison)
lower, upper = coverage.resolution_start, coverage.resolution_end
if any(p.start < lower or p.end > upper for p in periods):
raise PlanRejected("PERIOD_OUTSIDE_COVERAGE")
if isinstance(call, (ComparisonCall, DecompositionCall)) and call.baseline.end >= call.comparison.start:
raise PlanRejected("INVALID_COMPARISON_PERIODS")
compiled.append(CompiledStep(step_id=f"step-{index + 1:03d}", call=call,
depends_on=tuple(f"step-{d + 1:03d}" for d in dependencies)))
return tuple(compiled)
def deterministic_plan(question, baseline: Period, comparison: Period,
opening_period: Period | None = None) -> AnalysisPlan:
compare = ComparisonCall(operation="compare", baseline=baseline, comparison=comparison)
steps = [ProposedStep(call=compare)]
if question == "resolution_change":
pass
elif question == "segments":
steps.extend(ProposedStep(call=ComparisonCall(operation="compare", baseline=baseline,
comparison=comparison, grouping=group), depends_on=(0,)) for group in ("category", "region"))
elif question == "drivers":
if opening_period is None:
raise ValueError("Drivers question requires an explicit opening period")
steps.extend([
ProposedStep(call=DecompositionCall(operation="decompose", baseline=baseline,
comparison=comparison), depends_on=(0,)),
ProposedStep(call=WorkloadCall(operation="workload", opening_period=opening_period)),
])
else:
raise ValueError("Unsupported question")
return AnalysisPlan(steps=tuple(steps))