Source code for factor_agent.preflight

"""The report of ``Campaign.check``: what a run would refuse before its first LLM call.

A ``CheckReport`` holds one ``Check`` per part of a campaign checked on its
own, in the order they ran: each data component, the scoring, each role's
backend, the reference and known factors, the run snapshot, the vocabulary
and the templates, then with a ping each distinct backend's answer to one
short request. A check holds the problems it found (any one stops the
run), the warnings it gives (the run starts, but should be started
knowingly) or, when a part it needs failed, why it was skipped.
"""

import time
from dataclasses import dataclass

from factor_agent.llm.base import LLMBackend, Request

#: What a ping asks: an answer of one word, so that the request is short.
PING_PROMPT = "Reply with the single word OK."


[docs] @dataclass(frozen=True) class Check: """One part of a campaign checked: its ``name``, its problems and warnings, or why it was ``skipped``.""" name: str problems: tuple[str, ...] = () warnings: tuple[str, ...] = () #: Why the check did not run (a part it needs failed); None if it ran. skipped: str | None = None #: What a check that ran found besides its problems, such as how long a ping took. detail: str | None = None @property def status(self) -> str: """``"problem"``, ``"skipped"``, ``"warning"`` or ``"ok"``.""" if self.problems: return "problem" if self.skipped is not None: return "skipped" return "warning" if self.warnings else "ok"
[docs] @dataclass(frozen=True) class CheckReport: """Every ``Check`` of a campaign, in order; the run can start when ``problems`` is empty.""" checks: tuple[Check, ...] @property def problems(self) -> list[str]: """Every problem, as ``"<check>: <problem>"``.""" return [f"{c.name}: {problem}" for c in self.checks for problem in c.problems] @property def warnings(self) -> list[str]: """Every warning, as ``"<check>: <warning>"``.""" return [f"{c.name}: {warning}" for c in self.checks for warning in c.warnings] @property def skipped(self) -> list[tuple[str, str]]: """``(check, reason)`` of every check that did not run.""" return [(c.name, c.skipped) for c in self.checks if c.skipped is not None] @property def ok(self) -> bool: """Whether no check found a problem.""" return not self.problems
[docs] def text(self) -> str: """The report as a reader sees it: one line per check, its problems and warnings under it, and a summary.""" lines = [] for c in self.checks: said = c.skipped if c.skipped is not None else c.detail lines.append(f"{c.status:<8} {c.name}" + (f": {said}" if said is not None else "")) lines += [f" - {problem}" for problem in c.problems] lines += [f" ! {warning}" for warning in c.warnings] summary = ( f"{len(self.problems)} problem(s), {len(self.warnings)} warning(s), {len(self.skipped)} check(s) skipped" ) return "\n".join([*lines, summary if self.ok else summary + "; the run would be refused"])
[docs] def ping(name: str, backend: LLMBackend, role: str, model: str) -> Check: """``name``'s check of ``backend``: one short request as ``role`` to ``model``, its duration or its error. The request goes to the backend itself, outside any LLM log, and is not asked again. """ started = time.monotonic() try: backend.complete(Request(role=role, model=model, prompt=PING_PROMPT)) except Exception as exc: # the error is the problem, never raised return Check(name, problems=(f"{type(exc).__name__}: {exc}",)) return Check(name, detail=f"answered in {time.monotonic() - started:.2f} s")