Bug fixes
This commit is contained in:
parent
10feb2d067
commit
bd7a0586b6
5 changed files with 117 additions and 8 deletions
|
|
@ -1,11 +1,33 @@
|
||||||
import inspect
|
import inspect
|
||||||
from typing import Any, Awaitable, Callable, Type, Union, cast
|
from types import UnionType
|
||||||
|
from typing import (
|
||||||
|
Annotated,
|
||||||
|
Any,
|
||||||
|
Awaitable,
|
||||||
|
Callable,
|
||||||
|
Type,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
get_args,
|
||||||
|
get_origin,
|
||||||
|
)
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
from fastapi import APIRouter, FastAPI, HTTPException, status
|
from fastapi import APIRouter, FastAPI, HTTPException, status
|
||||||
from pydantic import BaseModel, create_model
|
from numpy.typing import NDArray
|
||||||
|
from pydantic import (
|
||||||
|
AfterValidator,
|
||||||
|
BaseModel,
|
||||||
|
PlainSerializer,
|
||||||
|
PlainValidator,
|
||||||
|
WithJsonSchema,
|
||||||
|
create_model,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ...errors import ArgumentValidationError
|
||||||
from ...helper import get_function_metadata_store
|
from ...helper import get_function_metadata_store
|
||||||
from ...helper.get_function_type_hints import get_function_type_hints
|
from ...helper.get_function_type_hints import get_function_type_hints
|
||||||
|
from ...helper.numpy_to_python import numpy_to_python
|
||||||
from ...views import Trace
|
from ...views import Trace
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -17,13 +39,17 @@ def bootstrap_prediction_endpoint(
|
||||||
)
|
)
|
||||||
|
|
||||||
schema = _get_schema(func)
|
schema = _get_schema(func)
|
||||||
|
return_type = _get_api_type(get_function_type_hints(func).get("return", Any))
|
||||||
|
response_model = Trace[return_type] # type: ignore[valid-type]
|
||||||
positional_names = [
|
positional_names = [
|
||||||
p.name
|
p.name
|
||||||
for p in inspect.signature(func).parameters.values()
|
for p in inspect.signature(func).parameters.values()
|
||||||
if p.kind == p.POSITIONAL_ONLY and p.name in schema.model_fields
|
if p.kind == p.POSITIONAL_ONLY and p.name in schema.model_fields
|
||||||
]
|
]
|
||||||
|
|
||||||
@router.post("/predict", status_code=status.HTTP_200_OK, response_model=Trace)
|
@router.post(
|
||||||
|
"/predict", status_code=status.HTTP_200_OK, response_model=response_model
|
||||||
|
)
|
||||||
async def predict(input_value: schema) -> Trace: # type: ignore
|
async def predict(input_value: schema) -> Trace: # type: ignore
|
||||||
try:
|
try:
|
||||||
# model_dump() recursively converts nested models to dictionaries,
|
# model_dump() recursively converts nested models to dictionaries,
|
||||||
|
|
@ -37,6 +63,11 @@ def bootstrap_prediction_endpoint(
|
||||||
*positional, **arguments
|
*positional, **arguments
|
||||||
)
|
)
|
||||||
return cast(Callable[..., Trace], func)(*positional, **arguments)
|
return cast(Callable[..., Trace], func)(*positional, **arguments)
|
||||||
|
except ArgumentValidationError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
detail=str(e),
|
||||||
|
) from e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
|
@ -51,7 +82,7 @@ def _get_schema(func: Callable) -> Type[BaseModel]:
|
||||||
type_hints = get_function_type_hints(func)
|
type_hints = get_function_type_hints(func)
|
||||||
parameters = {
|
parameters = {
|
||||||
p.name: (
|
p.name: (
|
||||||
type_hints.get(p.name, Any),
|
_get_api_type(type_hints.get(p.name, Any)),
|
||||||
p.default if p.default != inspect._empty else ...,
|
p.default if p.default != inspect._empty else ...,
|
||||||
)
|
)
|
||||||
for p in signature.parameters.values()
|
for p in signature.parameters.values()
|
||||||
|
|
@ -60,3 +91,53 @@ def _get_schema(func: Callable) -> Type[BaseModel]:
|
||||||
|
|
||||||
schema: Type[BaseModel] = create_model("InputModel", **parameters) # type: ignore
|
schema: Type[BaseModel] = create_model("InputModel", **parameters) # type: ignore
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_numpy_array(value: Any) -> np.ndarray:
|
||||||
|
if not isinstance(value, (list, np.ndarray)):
|
||||||
|
raise ValueError("Expected an array (a JSON list or NumPy ndarray)")
|
||||||
|
return np.asarray(value)
|
||||||
|
|
||||||
|
|
||||||
|
_NumpyArray = Annotated[
|
||||||
|
np.ndarray,
|
||||||
|
PlainValidator(_validate_numpy_array),
|
||||||
|
PlainSerializer(lambda value: value.tolist(), when_used="json"),
|
||||||
|
WithJsonSchema({"type": "array", "items": {}}),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_api_type(annotation: Any) -> Any:
|
||||||
|
"""Represent NumPy arrays as JSON arrays without losing other annotations."""
|
||||||
|
origin = get_origin(annotation)
|
||||||
|
if annotation in (np.ndarray, NDArray) or origin in (np.ndarray, NDArray):
|
||||||
|
return _NumpyArray
|
||||||
|
if isinstance(annotation, type) and issubclass(annotation, np.generic):
|
||||||
|
scalar_types = (
|
||||||
|
(np.bool_, bool),
|
||||||
|
(np.integer, int),
|
||||||
|
(np.floating, float),
|
||||||
|
(np.complexfloating, complex),
|
||||||
|
(np.str_, str),
|
||||||
|
(np.bytes_, bytes),
|
||||||
|
)
|
||||||
|
for numpy_type, python_type in scalar_types:
|
||||||
|
if issubclass(annotation, numpy_type):
|
||||||
|
return Annotated[
|
||||||
|
python_type,
|
||||||
|
AfterValidator(annotation),
|
||||||
|
PlainSerializer(
|
||||||
|
numpy_to_python, return_type=python_type, when_used="json"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
arguments = get_args(annotation)
|
||||||
|
if origin is Annotated:
|
||||||
|
return Annotated[_get_api_type(arguments[0]), *arguments[1:]]
|
||||||
|
converted = tuple(_get_api_type(argument) for argument in arguments)
|
||||||
|
if all(old is new for old, new in zip(arguments, converted)):
|
||||||
|
return annotation
|
||||||
|
if origin in (Union, UnionType):
|
||||||
|
return Union[converted]
|
||||||
|
if hasattr(annotation, "copy_with"):
|
||||||
|
return annotation.copy_with(converted)
|
||||||
|
return origin[converted]
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Any, Dict, Tuple
|
from typing import Any, Dict, Tuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
def make_hashable(value: Any) -> Any:
|
def make_hashable(value: Any) -> Any:
|
||||||
"""Build an immutable value key without retaining mutable container contents."""
|
"""Build an immutable value key without retaining mutable container contents."""
|
||||||
|
if isinstance(value, np.ndarray):
|
||||||
|
contents = (
|
||||||
|
make_hashable(value.tolist()) if value.dtype.hasobject else value.tobytes()
|
||||||
|
)
|
||||||
|
return type(value), value.dtype, value.shape, contents
|
||||||
|
if isinstance(value, np.generic):
|
||||||
|
return type(value), value.dtype, value.tobytes()
|
||||||
if isinstance(value, BaseModel):
|
if isinstance(value, BaseModel):
|
||||||
return type(value), make_hashable(value.model_dump())
|
return type(value), make_hashable(value.model_dump())
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
|
|
|
||||||
20
great_ai/helper/numpy_to_python.py
Normal file
20
great_ai/helper/numpy_to_python.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from pydantic_core import PydanticSerializationError
|
||||||
|
|
||||||
|
|
||||||
|
def numpy_to_python(value: Any) -> Any:
|
||||||
|
"""Convert NumPy values encountered by Pydantic's JSON serializer."""
|
||||||
|
if isinstance(value, np.ndarray):
|
||||||
|
return value.tolist()
|
||||||
|
if isinstance(value, np.floating):
|
||||||
|
return float(value)
|
||||||
|
if isinstance(value, np.generic):
|
||||||
|
converted = value.item()
|
||||||
|
if not isinstance(converted, type(value)):
|
||||||
|
return converted
|
||||||
|
raise PydanticSerializationError(
|
||||||
|
f"Unable to serialize {type(value).__name__}; convert it to a JSON-compatible "
|
||||||
|
"Python value before returning or logging it."
|
||||||
|
)
|
||||||
|
|
@ -69,7 +69,7 @@ class MongoDbDriver(TracingDatabaseDriver):
|
||||||
super().configure_credentials()
|
super().configure_credentials()
|
||||||
|
|
||||||
def save(self, trace: Trace) -> str:
|
def save(self, trace: Trace) -> str:
|
||||||
serialized = trace.to_flat_dict()
|
serialized = trace.to_flat_dict(mode="json")
|
||||||
serialized["_id"] = trace.trace_id
|
serialized["_id"] = trace.trace_id
|
||||||
|
|
||||||
with MongoClient[Any](self.mongo_connection_string) as client:
|
with MongoClient[Any](self.mongo_connection_string) as client:
|
||||||
|
|
@ -78,7 +78,7 @@ class MongoDbDriver(TracingDatabaseDriver):
|
||||||
def save_batch(self, documents: List[Trace]) -> List[str]:
|
def save_batch(self, documents: List[Trace]) -> List[str]:
|
||||||
if not documents:
|
if not documents:
|
||||||
return []
|
return []
|
||||||
serialized = [d.to_flat_dict() for d in documents]
|
serialized = [d.to_flat_dict(mode="json") for d in documents]
|
||||||
for s in serialized:
|
for s in serialized:
|
||||||
s["_id"] = s["trace_id"]
|
s["_id"] = s["trace_id"]
|
||||||
|
|
||||||
|
|
@ -175,7 +175,7 @@ class MongoDbDriver(TracingDatabaseDriver):
|
||||||
return documents, count
|
return documents, count
|
||||||
|
|
||||||
def update(self, id: str, new_version: Trace) -> None:
|
def update(self, id: str, new_version: Trace) -> None:
|
||||||
serialized = new_version.to_flat_dict()
|
serialized = new_version.to_flat_dict(mode="json")
|
||||||
serialized["_id"] = new_version.trace_id
|
serialized["_id"] = new_version.trace_id
|
||||||
|
|
||||||
with MongoClient[Any](self.mongo_connection_string) as client:
|
with MongoClient[Any](self.mongo_connection_string) as client:
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ def add_ground_truth(
|
||||||
trace_id=str(uuid4()),
|
trace_id=str(uuid4()),
|
||||||
created=created,
|
created=created,
|
||||||
original_execution_time_ms=0,
|
original_execution_time_ms=0,
|
||||||
logged_values=X if isinstance(X, dict) else {"input": X},
|
logged_values={"input": X},
|
||||||
models=[],
|
models=[],
|
||||||
output=y,
|
output=y,
|
||||||
feedback=y,
|
feedback=y,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue