168 lines
5.7 KiB
Python
168 lines
5.7 KiB
Python
from pprint import pformat
|
|
from typing import Any, Dict, Generic, List, Literal, Optional, TypeVar, cast
|
|
|
|
from pydantic import (
|
|
ConfigDict,
|
|
SerializationInfo,
|
|
SerializerFunctionWrapHandler,
|
|
TypeAdapter,
|
|
field_serializer,
|
|
)
|
|
from pydantic_core import PydanticSerializationError
|
|
|
|
from ..helper.numpy_to_python import numpy_to_python
|
|
from .hashable_base_model import HashableBaseModel
|
|
from .model import Model
|
|
|
|
T = TypeVar("T")
|
|
_json_value = TypeAdapter(Any)
|
|
|
|
|
|
class Trace(HashableBaseModel, Generic[T]):
|
|
"""Universal structure for storing prediction traces and training data.
|
|
|
|
Attributes:
|
|
trace_id: UUID4 identifier for uniquely referring to a trace.
|
|
created: Timestamp of its (original) construction.
|
|
original_execution_time_ms: Wall-time elapsed while its generating
|
|
TracingContext was alive.
|
|
logged_values: Values persisted through using `@parameter` or `log_metric()`.
|
|
models: Marks left by each encountered `@use_model` decorated function.
|
|
exception: Exception description if any was encountered.
|
|
output: Return value of the function wrapped by GreatAI.
|
|
feedback: Feedback obtained using the REST API of `add_ground_truth`.
|
|
tags: Tags used for filtering traces. Contains the name of the original
|
|
function, value of `ENVIRONMENT`, its split if has any, and either
|
|
`ground_truth` or `online` depending on the origin of the Trace.
|
|
"""
|
|
|
|
trace_id: str
|
|
created: str
|
|
original_execution_time_ms: float
|
|
logged_values: Dict[str, Any]
|
|
models: List[Model]
|
|
exception: Optional[str] = None
|
|
output: Optional[T] = None
|
|
feedback: Any = None
|
|
tags: List[str]
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
@field_serializer(
|
|
"output", "logged_values", "feedback", mode="wrap", when_used="json"
|
|
)
|
|
def serialize_numpy_values( # type: ignore[no-untyped-def]
|
|
self,
|
|
value: Any,
|
|
handler: SerializerFunctionWrapHandler,
|
|
info: SerializationInfo,
|
|
):
|
|
# An explicit return annotation would replace the generic output's JSON
|
|
# schema. Keep its declared type while providing a fallback for NumPy.
|
|
try:
|
|
return handler(value)
|
|
except PydanticSerializationError:
|
|
return _json_value.dump_python(
|
|
value,
|
|
mode="json",
|
|
include=cast(Any, info.include),
|
|
exclude=cast(Any, info.exclude),
|
|
by_alias=info.by_alias,
|
|
exclude_unset=info.exclude_unset,
|
|
exclude_defaults=info.exclude_defaults,
|
|
exclude_none=info.exclude_none,
|
|
round_trip=info.round_trip,
|
|
serialize_as_any=info.serialize_as_any,
|
|
context=info.context,
|
|
fallback=numpy_to_python,
|
|
)
|
|
|
|
@property
|
|
def input(self) -> Any:
|
|
"""Original input value, or a name-to-value mapping for multiple arguments.
|
|
|
|
Online traces omit metrics and derived argument metadata. Arguments marked
|
|
``disable_logging=True`` are omitted, so their values cannot be recovered.
|
|
"""
|
|
if "online" in self.tags and "ground_truth" not in self.tags:
|
|
arguments = {
|
|
key[len("arg:") : -len(":value")]: value
|
|
for key, value in self.logged_values.items()
|
|
if key.startswith("arg:") and key.endswith(":value")
|
|
}
|
|
if len(arguments) == 1:
|
|
return next(iter(arguments.values()))
|
|
return arguments
|
|
return (
|
|
self.logged_values["input"]
|
|
if list(self.logged_values.keys()) == ["input"]
|
|
else self.logged_values
|
|
)
|
|
|
|
@property
|
|
def models_flat(self) -> str:
|
|
return ", ".join(f"{m.key}:{m.version}" for m in self.models)
|
|
|
|
@property
|
|
def output_flat(self) -> str:
|
|
return pformat(self.output, indent=2, compact=True)
|
|
|
|
@property
|
|
def exception_flat(self) -> str:
|
|
return (
|
|
"null"
|
|
if self.exception is None
|
|
else pformat(self.exception, indent=2, compact=True)
|
|
)
|
|
|
|
@property
|
|
def feedback_flat(self) -> str:
|
|
return (
|
|
"null"
|
|
if self.feedback is None
|
|
else pformat(self.feedback, indent=2, compact=True)
|
|
)
|
|
|
|
@property
|
|
def tags_flat(self) -> str:
|
|
return ",\n".join(self.tags)
|
|
|
|
def to_flat_dict(
|
|
self,
|
|
include_original: bool = True,
|
|
*,
|
|
mode: Literal["python", "json"] = "python",
|
|
) -> Dict[str, Any]:
|
|
serialized = self.model_dump(mode=mode)
|
|
reserved = set(type(self).model_fields) | {"_id"}
|
|
return {
|
|
**(
|
|
serialized
|
|
if include_original
|
|
else {
|
|
"trace_id": self.trace_id,
|
|
"created": self.created,
|
|
"original_execution_time_ms": self.original_execution_time_ms,
|
|
}
|
|
),
|
|
**{
|
|
k: (
|
|
v
|
|
if (isinstance(v, float) or isinstance(v, int))
|
|
else pformat(v, indent=2, compact=True)
|
|
)
|
|
for k, v in serialized["logged_values"].items()
|
|
if k not in reserved
|
|
},
|
|
"models_flat": self.models_flat,
|
|
"exception_flat": self.exception_flat,
|
|
"output_flat": self.output_flat,
|
|
"feedback_flat": self.feedback_flat,
|
|
"tags_flat": self.tags_flat,
|
|
}
|
|
|
|
def __repr__(self) -> str:
|
|
formatted = pformat(self.model_dump(), indent=2, compact=True).replace(
|
|
"{ ", "{", 1
|
|
)
|
|
return f"Trace[{type(self.output).__name__}]({formatted})"
|