195 lines
7.1 KiB
Python
195 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
import httpx
|
|
from typing import Literal
|
|
|
|
|
|
class APIStatusError(Exception):
|
|
"""Raised when an API response has a status code of 4xx or 5xx."""
|
|
|
|
response: httpx.Response
|
|
status_code: int
|
|
request_id: str | None
|
|
|
|
def __init__(
|
|
self, message: str, *, response: httpx.Response, body: object | None
|
|
) -> None:
|
|
super().__init__(message)
|
|
self.request = response.request
|
|
self.body = body
|
|
self.response = response
|
|
self.status_code = response.status_code
|
|
self.request_id = response.headers.get("x-request-id")
|
|
|
|
|
|
class APIConnectionError(Exception):
|
|
def __init__(
|
|
self, *, message: str = "Connection error.", request: httpx.Request | None
|
|
) -> None:
|
|
super().__init__(message)
|
|
self.request = request
|
|
|
|
|
|
class BadRequestError(APIStatusError):
|
|
status_code: Literal[400] = 400 # pyright: ignore[reportIncompatibleVariableOverride]
|
|
|
|
|
|
class AuthenticationError(APIStatusError):
|
|
status_code: Literal[401] = 401 # pyright: ignore[reportIncompatibleVariableOverride]
|
|
|
|
|
|
class PermissionDeniedError(APIStatusError):
|
|
status_code: Literal[403] = 403 # pyright: ignore[reportIncompatibleVariableOverride]
|
|
|
|
|
|
class NotFoundError(APIStatusError):
|
|
status_code: Literal[404] = 404 # pyright: ignore[reportIncompatibleVariableOverride]
|
|
|
|
|
|
class ConflictError(APIStatusError):
|
|
status_code: Literal[409] = 409 # pyright: ignore[reportIncompatibleVariableOverride]
|
|
|
|
|
|
class UnprocessableEntityError(APIStatusError):
|
|
status_code: Literal[422] = 422 # pyright: ignore[reportIncompatibleVariableOverride]
|
|
|
|
|
|
class RateLimitError(APIStatusError):
|
|
status_code: Literal[429] = 429 # pyright: ignore[reportIncompatibleVariableOverride]
|
|
|
|
|
|
class APITimeoutError(APIConnectionError):
|
|
def __init__(self, request: httpx.Request | None) -> None:
|
|
super().__init__(message="Request timed out.", request=request)
|
|
|
|
|
|
class StorageNotInitializedError(RuntimeError):
|
|
"""Raised when storage operations are attempted before initialization."""
|
|
|
|
def __init__(self, storage_type: str = "Storage"):
|
|
super().__init__(
|
|
f"{storage_type} not initialized. Please ensure proper initialization:\n"
|
|
f"\n"
|
|
f" rag = LightRAG(...)\n"
|
|
f" await rag.initialize_storages() # Required - auto-initializes pipeline_status\n"
|
|
f"\n"
|
|
f"See: https://github.com/HKUDS/LightRAG#important-initialization-requirements"
|
|
)
|
|
|
|
|
|
class PipelineNotInitializedError(KeyError):
|
|
"""Raised when pipeline status is accessed before initialization."""
|
|
|
|
def __init__(self, namespace: str = ""):
|
|
msg = (
|
|
f"Pipeline namespace '{namespace}' not found.\n"
|
|
f"\n"
|
|
f"Pipeline status should be auto-initialized by initialize_storages().\n"
|
|
f"If you see this error, please ensure:\n"
|
|
f"\n"
|
|
f" 1. You called await rag.initialize_storages()\n"
|
|
f" 2. For multi-workspace setups, each LightRAG instance was properly initialized\n"
|
|
f"\n"
|
|
f"Standard initialization:\n"
|
|
f" rag = LightRAG(workspace='your_workspace')\n"
|
|
f" await rag.initialize_storages() # Auto-initializes pipeline_status\n"
|
|
f"\n"
|
|
f"If you need manual control (advanced):\n"
|
|
f" from lightrag.kg.shared_storage import initialize_pipeline_status\n"
|
|
f" await initialize_pipeline_status(workspace='your_workspace')"
|
|
)
|
|
super().__init__(msg)
|
|
|
|
|
|
class PipelineCancelledException(Exception):
|
|
"""Raised when pipeline processing is cancelled by user request."""
|
|
|
|
def __init__(self, message: str = "User cancelled"):
|
|
super().__init__(message)
|
|
self.message = message
|
|
|
|
|
|
class IndexFlushError(Exception):
|
|
"""Raised when a storage backend fails to flush buffered index ops.
|
|
|
|
Carries the storage driver name and namespace so the pipeline can abort
|
|
the batch with an actionable reason. The underlying error is preserved as
|
|
the exception ``__cause__`` (set via ``raise ... from cause``).
|
|
"""
|
|
|
|
def __init__(self, storage_name: str, namespace: str, cause: BaseException):
|
|
self.storage_name = storage_name
|
|
self.namespace = namespace
|
|
super().__init__(f"{storage_name}[{namespace}] index flush failed: {cause}")
|
|
|
|
|
|
class ChunkTokenLimitExceededError(ValueError):
|
|
"""Raised when a chunk exceeds the configured token limit."""
|
|
|
|
def __init__(
|
|
self,
|
|
chunk_tokens: int,
|
|
chunk_token_limit: int,
|
|
chunk_preview: str | None = None,
|
|
) -> None:
|
|
preview = chunk_preview.strip() if chunk_preview else None
|
|
truncated_preview = preview[:80] if preview else None
|
|
preview_note = f" Preview: '{truncated_preview}'" if truncated_preview else ""
|
|
message = (
|
|
f"Chunk token length {chunk_tokens} exceeds chunk_token_size {chunk_token_limit}."
|
|
f"{preview_note}"
|
|
)
|
|
super().__init__(message)
|
|
self.chunk_tokens = chunk_tokens
|
|
self.chunk_token_limit = chunk_token_limit
|
|
self.chunk_preview = truncated_preview
|
|
|
|
|
|
class ChunkBlockMatchError(ValueError):
|
|
"""Raised when a chunk cannot be located in the document's blocks.jsonl.
|
|
|
|
Sidecar backfill (``lightrag.sidecar.backfill``) maps F/R/V chunks back to
|
|
their source block(s) by matching chunk content against the parse-time
|
|
``*.blocks.jsonl`` merged text. When a sidecar-less chunk cannot be located,
|
|
this is raised so the pipeline marks the document FAILED rather than
|
|
persisting chunks with missing/incorrect provenance.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
chunk_order_index: int,
|
|
chunk_preview: str | None = None,
|
|
blocks_path: str | None = None,
|
|
) -> None:
|
|
preview = chunk_preview.strip() if chunk_preview else None
|
|
truncated_preview = preview[:80] if preview else None
|
|
preview_note = f" Preview: '{truncated_preview}'" if truncated_preview else ""
|
|
path_note = f" (blocks: {blocks_path})" if blocks_path else ""
|
|
message = (
|
|
f"Chunk #{chunk_order_index} could not be located in the document "
|
|
f"blocks during sidecar backfill.{preview_note}{path_note}"
|
|
)
|
|
super().__init__(message)
|
|
self.chunk_order_index = chunk_order_index
|
|
self.chunk_preview = truncated_preview
|
|
self.blocks_path = blocks_path
|
|
|
|
|
|
class DataMigrationError(Exception):
|
|
"""Raised when data migration from legacy collection/table fails."""
|
|
|
|
def __init__(self, message: str):
|
|
super().__init__(message)
|
|
self.message = message
|
|
|
|
|
|
class MultimodalAnalysisError(RuntimeError):
|
|
"""Raised when multimodal analysis must fail the current document.
|
|
|
|
Hard failures (missing required field, schema mismatch, model not
|
|
available, sidecar already carries ``status="failure"``) bubble this
|
|
exception so the pipeline marks the document failed instead of writing
|
|
an unusable analyze result. Callers persist a ``status="failure"``
|
|
sidecar entry alongside the raise so a re-run sees the failure.
|
|
"""
|