Spaces:
Configuration error
Configuration error
| """Domain exception hierarchy. | |
| Every domain error carries a stable machine-readable `error_code` and the HTTP | |
| `status_code` it maps to, so the global handlers in `app.main` can build the | |
| uniform error envelope without a translation table. Errors are never swallowed: | |
| full detail goes to the logs, and the caller sees a safe message plus the code. | |
| """ | |
| from __future__ import annotations | |
| from typing import ClassVar, Literal | |
| class TalentLensError(Exception): | |
| """Base class for every domain error raised by this application. | |
| Attributes: | |
| error_code: Stable identifier clients may branch on. Never localized, | |
| never renamed without an API version bump. | |
| status_code: HTTP status this error maps to. | |
| default_message: Safe, caller-facing description. Must not leak internals. | |
| """ | |
| error_code: ClassVar[str] = "INTERNAL_ERROR" | |
| status_code: ClassVar[int] = 500 | |
| default_message: ClassVar[str] = "An unexpected error occurred." | |
| def __init__(self, message: str | None = None) -> None: | |
| """Initialize the error. | |
| Args: | |
| message: Optional override for the class-level default message. | |
| """ | |
| self.message = message or self.default_message | |
| super().__init__(self.message) | |
| # --------------------------------------------------------------------------- # | |
| # Ingestion / documents # | |
| # --------------------------------------------------------------------------- # | |
| class DocumentParseError(TalentLensError): | |
| """Raised when a document cannot be parsed into text.""" | |
| error_code: ClassVar[str] = "DOCUMENT_PARSE_FAILED" | |
| status_code: ClassVar[int] = 422 | |
| default_message: ClassVar[str] = "The document could not be parsed." | |
| class UnsupportedMediaTypeError(TalentLensError): | |
| """Raised when an upload's real content type is not on the allowlist.""" | |
| error_code: ClassVar[str] = "UNSUPPORTED_MEDIA_TYPE" | |
| status_code: ClassVar[int] = 415 | |
| default_message: ClassVar[str] = "File must be a PDF or DOCX document." | |
| class PayloadTooLargeError(TalentLensError): | |
| """Raised when an upload exceeds the configured size ceiling.""" | |
| error_code: ClassVar[str] = "PAYLOAD_TOO_LARGE" | |
| status_code: ClassVar[int] = 413 | |
| default_message: ClassVar[str] = "File exceeds the maximum allowed size." | |
| class EmptyDocumentError(TalentLensError): | |
| """Raised when an upload contains no bytes.""" | |
| error_code: ClassVar[str] = "EMPTY_DOCUMENT" | |
| status_code: ClassVar[int] = 422 | |
| default_message: ClassVar[str] = "Uploaded file is empty." | |
| # --------------------------------------------------------------------------- # | |
| # Identity # | |
| # --------------------------------------------------------------------------- # | |
| class AuthenticationError(TalentLensError): | |
| """Raised when a caller's credentials are absent, malformed, or invalid.""" | |
| error_code: ClassVar[str] = "UNAUTHENTICATED" | |
| status_code: ClassVar[int] = 401 | |
| default_message: ClassVar[str] = "Authentication credentials are missing or invalid." | |
| class AuthorizationError(TalentLensError): | |
| """Raised when an authenticated caller lacks the required role.""" | |
| error_code: ClassVar[str] = "FORBIDDEN" | |
| status_code: ClassVar[int] = 403 | |
| default_message: ClassVar[str] = "You do not have permission to perform this action." | |
| # --------------------------------------------------------------------------- # | |
| # Resources # | |
| # --------------------------------------------------------------------------- # | |
| class ResourceNotFoundError(TalentLensError): | |
| """Raised when a resource does not exist, or is not visible to this tenant.""" | |
| error_code: ClassVar[str] = "NOT_FOUND" | |
| status_code: ClassVar[int] = 404 | |
| default_message: ClassVar[str] = "The requested resource was not found." | |
| class ResourceConflictError(TalentLensError): | |
| """Raised when a mutation conflicts with existing state.""" | |
| error_code: ClassVar[str] = "CONFLICT" | |
| status_code: ClassVar[int] = 409 | |
| default_message: ClassVar[str] = "The request conflicts with the current state." | |
| class ValidationFailedError(TalentLensError): | |
| """Raised when input fails a service-layer invariant.""" | |
| error_code: ClassVar[str] = "VALIDATION_FAILED" | |
| status_code: ClassVar[int] = 422 | |
| default_message: ClassVar[str] = "The request payload failed validation." | |
| class StorageError(TalentLensError): | |
| """Raised when the object store cannot satisfy a read or write.""" | |
| error_code: ClassVar[str] = "STORAGE_UNAVAILABLE" | |
| status_code: ClassVar[int] = 503 | |
| default_message: ClassVar[str] = "Document storage is temporarily unavailable." | |
| # --------------------------------------------------------------------------- # | |
| # Search / Embeddings (Phase 2) # | |
| # --------------------------------------------------------------------------- # | |
| class EmbeddingServiceUnavailableError(TalentLensError): | |
| """Raised when the embedding service cannot be reached.""" | |
| error_code: ClassVar[str] = "EMBEDDING_SERVICE_UNAVAILABLE" | |
| status_code: ClassVar[int] = 503 | |
| default_message: ClassVar[str] = "The embedding service is temporarily unavailable." | |
| class SearchError(TalentLensError): | |
| """Raised when a search operation fails unexpectedly.""" | |
| error_code: ClassVar[str] = "SEARCH_FAILED" | |
| status_code: ClassVar[int] = 500 | |
| default_message: ClassVar[str] = "The search operation failed." | |
| # --------------------------------------------------------------------------- # | |
| # Scoring (Phase 4) # | |
| # --------------------------------------------------------------------------- # | |
| class CoreStageFailedError(TalentLensError): | |
| """Raised when a stage that scoring depends on could not complete. | |
| Scoring has no safe fallback. A stage that fails must abort the run rather | |
| than substitute a default verdict, which would bias the score silently and | |
| leave a number nobody can account for. | |
| """ | |
| error_code: ClassVar[str] = "CORE_STAGE_FAILED" | |
| status_code: ClassVar[int] = 500 | |
| default_message: ClassVar[str] = "A required scoring stage failed." | |
| class EvidenceSpanMismatchError(CoreStageFailedError): | |
| """Raised when a cited span is not present verbatim at its claimed offset. | |
| Evidence is the audit trail behind a verdict. A quote that does not sit at | |
| the offset it claims cannot be reviewed, so the verdict citing it is not | |
| admissible. | |
| """ | |
| error_code: ClassVar[str] = "EVIDENCE_SPAN_MISMATCH" | |
| status_code: ClassVar[int] = 500 | |
| default_message: ClassVar[str] = "Cited evidence does not match the source document." | |
| # --------------------------------------------------------------------------- # | |
| # LLM providers (Phase 4) # | |
| # --------------------------------------------------------------------------- # | |
| #: Normalized failure kinds. Provider APIs disagree on how they report the same | |
| #: condition, so every adapter maps its own errors onto this vocabulary and the | |
| #: failover policy reads only from here. | |
| ProviderErrorKind = Literal[ | |
| "rate_limit", "auth", "context", "model_unavailable", "network", "unknown" | |
| ] | |
| class LLMProviderError(TalentLensError): | |
| """Raised when a provider call fails for a reason worth classifying. | |
| Carries the normalized ``kind`` so the failover chain can decide whether | |
| another provider could plausibly do better. A rate limit or a network blip | |
| is worth retrying elsewhere; a context overflow is not, because the same | |
| oversized prompt would overflow the next provider too. | |
| Attributes: | |
| kind: Normalized failure category. | |
| provider: Which adapter raised this, for logs and metrics. | |
| """ | |
| error_code: ClassVar[str] = "LLM_PROVIDER_ERROR" | |
| status_code: ClassVar[int] = 502 | |
| default_message: ClassVar[str] = "The language model provider failed." | |
| def __init__( | |
| self, | |
| message: str | None = None, | |
| *, | |
| kind: ProviderErrorKind = "unknown", | |
| provider: str = "", | |
| ) -> None: | |
| """Initialize the error. | |
| Args: | |
| message: Safe, caller-facing description. Must never contain an | |
| API key or prompt content. | |
| kind: Normalized failure category. | |
| provider: Name of the adapter that failed. | |
| """ | |
| super().__init__(message) | |
| self.kind = kind | |
| self.provider = provider | |
| class LLMRefusalError(TalentLensError): | |
| """Raised when a model declines to answer. | |
| A refusal is not a transport failure and must not be retried against | |
| another provider: the second model would most likely refuse too, and a | |
| refusal can be policy-correct. It is surfaced to the caller instead. | |
| """ | |
| error_code: ClassVar[str] = "LLM_REFUSAL" | |
| status_code: ClassVar[int] = 422 | |
| default_message: ClassVar[str] = "The language model declined to answer this request." | |
| class NoEligibleProviderError(TalentLensError): | |
| """Raised when no configured provider may serve a request. | |
| Two distinct causes, deliberately sharing one code: every provider in the | |
| chain failed, or none is permitted to see data at the request's PII tier. | |
| In both cases the run cannot proceed, and in neither may a default answer | |
| be substituted. | |
| """ | |
| error_code: ClassVar[str] = "NO_ELIGIBLE_PROVIDER" | |
| status_code: ClassVar[int] = 503 | |
| default_message: ClassVar[str] = "No language model provider is available for this request." | |
| class BudgetExceededError(TalentLensError): | |
| """Raised when a token or spend ceiling would be crossed by this call. | |
| Maps to 429 rather than 402: the caller should retry later or degrade, | |
| which is the same remedy as a rate limit. | |
| """ | |
| error_code: ClassVar[str] = "BUDGET_EXCEEDED" | |
| status_code: ClassVar[int] = 429 | |
| default_message: ClassVar[str] = "The configured token budget has been exhausted." | |