mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
182 lines
6 KiB
Python
182 lines
6 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
from collections.abc import Sequence
|
|
from typing import Final
|
|
|
|
from pydantic import BaseModel, ConfigDict, ValidationError
|
|
|
|
from ...shared.reporting.models import SURFACES, CaseResult, RunStatus, SdkFunction, Surface
|
|
from ...shared.reporting.rendering import ReportSection
|
|
from ...shared.reporting.strategy import NotImplementedCaseSpec, SkippedCaseSpec
|
|
from ...shared.tracing.steps import PipelineStep, trace_depths
|
|
|
|
TRACE_ARTIFACT: Final = "trace"
|
|
|
|
_COLORS: Final[dict[str, str]] = {"red": "31", "cyan": "36"}
|
|
_RESET: Final = "\033[0m"
|
|
|
|
|
|
def _paint(text: str, color: str) -> str:
|
|
if not sys.stdout.isatty() or os.environ.get("NO_COLOR"):
|
|
return text
|
|
return f"\033[{_COLORS[color]}m{text}{_RESET}"
|
|
|
|
|
|
class TraceEventArtifact(BaseModel):
|
|
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
|
|
id: int
|
|
parent_id: int | None
|
|
span: str
|
|
raw: str
|
|
|
|
def step(self) -> PipelineStep:
|
|
return PipelineStep(self.id, self.parent_id, self.span, self.raw)
|
|
|
|
|
|
class TraceArtifact(BaseModel):
|
|
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
|
|
surface: Surface
|
|
sdk_function: SdkFunction
|
|
scenario: str
|
|
python: tuple[TraceEventArtifact, ...]
|
|
python_error: str | None = None
|
|
|
|
@classmethod
|
|
def from_traces(
|
|
cls,
|
|
*,
|
|
surface: Surface,
|
|
sdk_function: SdkFunction,
|
|
scenario: str,
|
|
python: Sequence[PipelineStep],
|
|
python_error: str | None = None,
|
|
) -> TraceArtifact:
|
|
return cls(
|
|
surface=surface,
|
|
sdk_function=sdk_function,
|
|
scenario=scenario,
|
|
python=tuple(
|
|
TraceEventArtifact(id=step.id, parent_id=step.parent_id, span=step.span, raw=step.raw)
|
|
for step in python
|
|
),
|
|
python_error=python_error,
|
|
)
|
|
|
|
def python_steps(self) -> tuple[PipelineStep, ...]:
|
|
return tuple(event.step() for event in self.python)
|
|
|
|
def has_errors(self) -> bool:
|
|
return self.python_error is not None
|
|
|
|
|
|
def _split_raw(raw: str) -> tuple[str, str]:
|
|
location, separator, name = raw.partition(" ")
|
|
if separator:
|
|
return name, location
|
|
return raw, ""
|
|
|
|
|
|
def _python_line(index: int, step: PipelineStep, depth: int) -> str:
|
|
name: Final = _split_raw(step.raw)[0]
|
|
location: Final = _split_raw(step.raw)[1]
|
|
suffix: Final = f" ({location})" if location else ""
|
|
return _paint(f"{index} {' ' * depth}{name}{suffix}", "cyan")
|
|
|
|
|
|
def _python_lines(steps: tuple[PipelineStep, ...]) -> str:
|
|
depths: Final = trace_depths(steps)
|
|
lines: Final = tuple(_python_line(index, step, depths[step.id]) for index, step in enumerate(steps, start=1))
|
|
return f"{_paint('PYTHON', 'cyan')} ({len(steps)} steps)\n" + ("\n".join(lines) if lines else "(empty)")
|
|
|
|
|
|
def _error_lines(artifact: TraceArtifact) -> tuple[str, ...]:
|
|
if artifact.python_error is None:
|
|
return ()
|
|
return (_paint(f"Python error: {artifact.python_error}", "red"),)
|
|
|
|
|
|
def _render_trace(artifact: TraceArtifact) -> str:
|
|
return "\n\n".join((_python_lines(artifact.python_steps()), *_error_lines(artifact)))
|
|
|
|
|
|
def _scenario(nodeid: str) -> str:
|
|
parts: Final = nodeid.split(":")
|
|
return parts[-1] if len(parts) >= 4 else "default"
|
|
|
|
|
|
def _unavailable(status: RunStatus) -> str:
|
|
return f"Trace: NOT AVAILABLE\nTest outcome: {status.value}"
|
|
|
|
|
|
def _render_artifact(body: str) -> str:
|
|
try:
|
|
artifact: Final = TraceArtifact.model_validate_json(body)
|
|
except ValidationError as error:
|
|
return f"Trace artifact is invalid: {error}"
|
|
return _render_trace(artifact)
|
|
|
|
|
|
def _scenario_section(result: CaseResult, nodeid: str, status: RunStatus) -> str:
|
|
artifacts: Final = tuple(
|
|
artifact for artifact in result.artifacts.get(nodeid, ()) if artifact.kind == TRACE_ARTIFACT
|
|
)
|
|
body: Final = (
|
|
"\n\n".join(_render_artifact(artifact.body) for artifact in artifacts) if artifacts else _unavailable(status)
|
|
)
|
|
label: Final = f"Scenario: {_scenario(nodeid)}"
|
|
return f"{label}\n{'-' * len(label)}\n\n{body}"
|
|
|
|
|
|
def _case_block(result: CaseResult) -> str:
|
|
header: Final = f"Case: {result.case.sdk_function}"
|
|
outcomes: Final = tuple(result.outcomes.items()) or (
|
|
(nodeid, RunStatus.NOT_RUN) for nodeid in sorted(result.collected)
|
|
)
|
|
sections: Final = tuple(_scenario_section(result, nodeid, status) for nodeid, status in outcomes)
|
|
return "\n\n".join((f"{header}\n{'=' * len(header)}", *sections))
|
|
|
|
|
|
def _unavailable_block(title: str, lines: tuple[str, ...]) -> str | None:
|
|
if not lines:
|
|
return None
|
|
return f"{title}\n{'-' * len(title)}\n" + "\n".join(lines)
|
|
|
|
|
|
def _surface_section(surface: Surface, results: Sequence[CaseResult]) -> ReportSection | None:
|
|
selected: Final = tuple(result for result in results if result.case.surface == surface)
|
|
if not selected:
|
|
return None
|
|
outcome_blocks: Final = tuple(_case_block(result) for result in selected if result.outcomes)
|
|
not_implemented: Final = _unavailable_block(
|
|
"Not implemented",
|
|
tuple(
|
|
f"- {result.case.sdk_function}: {spec.reason}"
|
|
for result in selected
|
|
if isinstance((spec := result.case.spec), NotImplementedCaseSpec)
|
|
),
|
|
)
|
|
skipped: Final = _unavailable_block(
|
|
"Skipped",
|
|
tuple(
|
|
f"- {result.case.sdk_function}: {spec.reason}"
|
|
for result in selected
|
|
if isinstance((spec := result.case.spec), SkippedCaseSpec)
|
|
),
|
|
)
|
|
blocks: Final = (
|
|
*outcome_blocks,
|
|
*((not_implemented,) if not_implemented else ()),
|
|
*((skipped,) if skipped else ()),
|
|
)
|
|
return ReportSection(f"{surface.upper()} traces", blocks or ("No runnable traces",))
|
|
|
|
|
|
def render_trace_results(results: Sequence[CaseResult]) -> tuple[ReportSection, ...]:
|
|
sections: Final = tuple(
|
|
section for surface in SURFACES if (section := _surface_section(surface, results)) is not None
|
|
)
|
|
return sections or (ReportSection("Traces", ("No traces selected",)),)
|