ai / _providers /executor.py
celik-muhammed's picture
Upload 144 files
cef9869 verified
Raw History Blame Contribute Delete
14 kB
# Authors: The scikit-plots developers
# SPDX-License-Identifier: BSD-3-Clause
"""
Credential-neutral provider resource execution lifecycle.
Run 128 defines the state machine every later provider executor must obey. It
contains no provider credentials and performs no provider I/O by itself. The
important invariant is ownership: once a provider-side resource is created it
is registered for cleanup immediately, and cleanup is attempted in reverse
creation order on success, error, timeout, or cancellation.
"""
from __future__ import annotations
import asyncio
from dataclasses import dataclass, field
from typing import Any, AsyncIterator, Protocol, Sequence
from typing_extensions import Self
from .base import ResourceRoute
try:
from .._utils._resource_transport import ResourceUpload
except ImportError: # standalone HF Space: _providers is top-level
from _utils._resource_transport import ResourceUpload
class ResourceExecutionError(RuntimeError):
"""A provider resource could not enter a safe executable state."""
@dataclass
class ProviderPrivateResource:
"""Provider-owned resource handle that must never be serialized publicly."""
resource_id: str
route: str
provider: str
source_sha256: str
source_size: int
opaque: Any = None
cleanup_required: bool = True
released: bool = False
@dataclass
class ResourceExecutionReceipt:
"""Sanitized chain-of-custody record for one resource lifecycle."""
resource_id: str
provider: str
model: str
route: str
source_sha256: str
source_size: int
state: str = "prepared"
cleanup_required: bool = True
cleanup_error: bool = False
metadata: dict[str, Any] = field(default_factory=dict)
def as_public_dict(self) -> dict[str, Any]:
"""Return a bounded receipt that contains no provider opaque identifier."""
return {
"resource_id": self.resource_id,
"provider": self.provider,
"model": self.model,
"route": self.route,
"source_sha256": self.source_sha256,
"source_size": self.source_size,
"state": self.state,
"cleanup_required": self.cleanup_required,
"cleanup_error": self.cleanup_error,
"metadata": dict(self.metadata),
}
class ProviderResourceExecutor(Protocol):
"""Provider-specific prepare/release boundary with route-level authority."""
name: str
enabled: bool
def executable_routes(self, model: str) -> dict[str, tuple[str, ...]]:
"""Return exact modality routes this executor can perform for *model*."""
...
async def prepare(
self,
*,
model: str,
route: ResourceRoute,
upload: ResourceUpload,
) -> ProviderPrivateResource: ...
async def release(self, handle: ProviderPrivateResource) -> None: ...
class ProviderChatExecutor(ProviderResourceExecutor, Protocol):
"""
Executable provider boundary for one prepared resource chat request.
Provider planning and temporary-resource ownership remain generic. Only an
executor registered in the separate execution registry may implement this
credential-bearing boundary. ``chat`` and ``upstream`` deliberately stay
provider-neutral here so the common proxy lifecycle does not depend on one
SDK's public types.
"""
async def open_chat(
self,
*,
chat: Any,
handles: Sequence[ProviderPrivateResource],
) -> Any: ...
async def buffered_chat_response(self, upstream: Any, *, maximum: int) -> dict: ...
def chat_sse(self, upstream: Any, *, maximum: int) -> AsyncIterator[bytes]: ...
@dataclass
class PendingProviderExecutor:
"""Explicit non-executor used until a credential-bearing adapter is enabled."""
name: str
enabled: bool = False
def executable_routes(self, model: str) -> dict[str, tuple[str, ...]]:
del model
return {}
async def prepare(
self,
*,
model: str,
route: ResourceRoute,
upload: ResourceUpload,
) -> ProviderPrivateResource:
del model, route, upload
raise ResourceExecutionError(f"{self.name} resource executor is not enabled")
async def release(self, handle: ProviderPrivateResource) -> None:
del handle
class ProviderExecutorRegistry:
"""Execution registry separate from provider capability/route planning."""
def __init__(self, names: Sequence[str] = ()) -> None:
names = [str(name).strip().lower() for name in names]
self._executors: dict[str, ProviderResourceExecutor] = {
name: PendingProviderExecutor(name) for name in names if str(name).strip()
}
def register(self, name: str, executor: ProviderResourceExecutor) -> None:
key = str(name or "").strip().lower()
if not key or key != str(executor.name or "").strip().lower():
raise ValueError("executor registry name mismatch")
self._executors[key] = executor
def unregister(self, name: str) -> None:
"""Return one provider to explicit plan-only state."""
key = str(name or "").strip().lower()
if key:
self._executors[key] = PendingProviderExecutor(key)
def get(self, name: str) -> ProviderResourceExecutor:
key = str(name or "custom").strip().lower() or "custom"
return self._executors.get(key, PendingProviderExecutor(key))
def execution_state(self, name: str) -> str:
return "enabled" if bool(self.get(name).enabled) else "plan-only"
class ResourceExecutionSession:
"""Sequential prepare + shielded LIFO cleanup for one provider request."""
def __init__(
self,
*,
provider: str,
model: str,
executor: ProviderResourceExecutor,
routes: Sequence[ResourceRoute],
) -> None:
self.provider = str(provider or "").strip().lower()
self.model = str(model or "")
self.executor = executor
self.routes = tuple(routes)
self._handles: list[ProviderPrivateResource] = []
self._receipts: list[ResourceExecutionReceipt] = []
self._closed = False
self._close_lock = asyncio.Lock()
@property
def receipts(self) -> tuple[ResourceExecutionReceipt, ...]:
return tuple(self._receipts)
@property
def private_handles(self) -> tuple[ProviderPrivateResource, ...]:
"""
Return request-private provider handles for the in-process executor only.
These handles are intentionally never exposed by receipts, health, logs,
persistence, or browser responses. Provider execution consumes them only
after :meth:`prepare` has validated the full upload/route identity.
"""
return tuple(self._handles)
def public_receipts(self) -> tuple[dict[str, Any], ...]:
return tuple(row.as_public_dict() for row in self._receipts)
def _validate_plan(
self, uploads: Sequence[ResourceUpload]
) -> dict[str, ResourceUpload]:
if not bool(getattr(self.executor, "enabled", False)):
raise ResourceExecutionError(
f"{self.provider} resource executor is not enabled"
)
by_id: dict[str, ResourceUpload] = {}
for upload in uploads:
rid = upload.verified.id
if rid in by_id:
raise ResourceExecutionError("duplicate resource upload id")
by_id[rid] = upload
route_ids: set[str] = set()
for route in self.routes:
if route.resource_id in route_ids:
raise ResourceExecutionError("duplicate resource route id")
route_ids.add(route.resource_id)
if route.route == "unsupported":
raise ResourceExecutionError("resource plan contains unsupported route")
if route_ids != set(by_id):
raise ResourceExecutionError("resource plan/upload ids do not match")
return by_id
def _register_handle(
self,
*,
route: ResourceRoute,
upload: ResourceUpload,
handle: ProviderPrivateResource,
) -> None:
"""
Validate and register one private handle before it can be used.
Batch-capable executors may create shared provider resources (for
example one retrieval store for many files). The common lifecycle
still validates every returned per-resource handle against the exact
verified upload and route so batch preparation cannot weaken identity
or receipt authority.
"""
if (
handle.resource_id != route.resource_id
or handle.route != route.route
or handle.provider != self.provider
or handle.source_sha256 != upload.verified.sha256
or int(handle.source_size) != int(upload.verified.actual_size)
):
# Register first so even a malformed provider response is released
# before the validation error escapes.
self._handles.append(handle)
raise ResourceExecutionError(
"provider resource handle failed identity validation"
)
self._handles.append(handle)
self._receipts.append(
ResourceExecutionReceipt(
resource_id=route.resource_id,
provider=self.provider,
model=self.model,
route=route.route,
source_sha256=upload.verified.sha256,
source_size=upload.verified.actual_size,
cleanup_required=bool(handle.cleanup_required),
metadata={
"modality": route.metadata.get("modality", ""),
"intent": route.metadata.get("intent", ""),
},
)
)
async def prepare(
self, uploads: Sequence[ResourceUpload]
) -> tuple[ResourceExecutionReceipt, ...]:
by_id = self._validate_plan(uploads)
ordered_uploads = tuple(by_id[route.resource_id] for route in self.routes)
try:
prepare_many = getattr(self.executor, "prepare_many", None)
if callable(prepare_many):
# A batch executor owns rollback for provider objects created
# before this method returns. Once returned, the common
# session immediately registers every handle and owns cleanup.
handles = tuple(
await prepare_many(
model=self.model,
routes=self.routes,
uploads=ordered_uploads,
)
)
if len(handles) != len(self.routes):
# Returned handles are the only provider cleanup authority
# available to the session. Register all of them so a bad
# batch count cannot silently leak those that are usable.
self._handles.extend(handles)
raise ResourceExecutionError(
"provider batch prepare returned the wrong handle count"
)
for route, upload, handle in zip(self.routes, ordered_uploads, handles):
self._register_handle(route=route, upload=upload, handle=handle)
else:
for route, upload in zip(self.routes, ordered_uploads):
handle = await self.executor.prepare(
model=self.model,
route=route,
upload=upload,
)
self._register_handle(route=route, upload=upload, handle=handle)
return self.receipts
except BaseException:
await self.close_shielded()
raise
async def _release_all(self) -> None:
# Receipts and handles are aligned only for handles that passed identity
# validation. A malformed last handle may therefore have no receipt.
receipt_by_id = {row.resource_id: row for row in self._receipts}
for handle in reversed(self._handles):
if handle.released or not handle.cleanup_required:
handle.released = True
receipt = receipt_by_id.get(handle.resource_id)
if receipt is not None:
receipt.state = "released"
continue
try:
await self.executor.release(handle)
except Exception: # noqa: BLE001
receipt = receipt_by_id.get(handle.resource_id)
if receipt is not None:
receipt.state = "cleanup_failed"
receipt.cleanup_error = True
else:
handle.released = True
receipt = receipt_by_id.get(handle.resource_id)
if receipt is not None:
receipt.state = "released"
async def close(self) -> tuple[ResourceExecutionReceipt, ...]:
async with self._close_lock:
if self._closed:
return self.receipts
await self._release_all()
self._closed = True
return self.receipts
async def close_shielded(self) -> tuple[ResourceExecutionReceipt, ...]:
"""Finish cleanup even when the caller task is being cancelled."""
task = asyncio.create_task(self.close())
try:
return await asyncio.shield(task)
except asyncio.CancelledError:
# Do not abandon provider cleanup. Preserve cancellation only after
# the cleanup task has reached its terminal state.
try:
await task
finally:
raise
async def __aenter__(self) -> Self:
return self
async def __aexit__(self, exc_type, exc, tb) -> bool:
del exc_type, exc, tb
await self.close_shielded()
return False