Files
wehub-resource-sync 3f679793e8
Linting and Formatting / lint-and-format (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:50 +08:00

378 lines
13 KiB
Python

"""
Processing callbacks and event system for RAGAnything.
Provides a lightweight publish-subscribe mechanism that lets users hook
into every stage of the document processing pipeline — parsing, text
insertion, multimodal processing, and querying.
Usage::
from raganything.callbacks import ProcessingCallback, CallbackManager
class MyCallback(ProcessingCallback):
def on_parse_start(self, file_path: str, **kw):
print(f"Parsing started: {file_path}")
def on_parse_complete(self, file_path: str, content_blocks: int, **kw):
print(f"Parsed {content_blocks} blocks from {file_path}")
rag = RAGAnything(config=config)
rag.callback_manager.register(MyCallback())
"""
from __future__ import annotations
import logging
import time
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
import threading
logger = logging.getLogger(__name__)
@dataclass
class ProcessingEvent:
"""Immutable record of a processing pipeline event."""
event_type: str
timestamp: float = field(default_factory=time.time)
file_path: Optional[str] = None
doc_id: Optional[str] = None
stage: Optional[str] = None
details: Dict[str, Any] = field(default_factory=dict)
duration_seconds: Optional[float] = None
error: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Serialise to a plain dictionary."""
return {
"event_type": self.event_type,
"timestamp": self.timestamp,
"file_path": self.file_path,
"doc_id": self.doc_id,
"stage": self.stage,
"details": self.details,
"duration_seconds": self.duration_seconds,
"error": self.error,
}
class ProcessingCallback:
"""Base class for processing pipeline callbacks.
Override any of the ``on_*`` methods to hook into the corresponding
stage. Methods that are not overridden are silently ignored.
All methods receive ``**kwargs`` so that future versions can add
parameters without breaking existing subclasses.
"""
# ── Parsing stage ─────────────────────────────────────────────
def on_parse_start(self, file_path: str, parser: str = "", **kwargs: Any) -> None:
"""Called before document parsing begins."""
def on_parse_complete(
self,
file_path: str,
content_blocks: int = 0,
doc_id: str = "",
duration_seconds: float = 0.0,
**kwargs: Any,
) -> None:
"""Called after document parsing succeeds."""
def on_parse_error(
self, file_path: str, error: BaseException | str = "", **kwargs: Any
) -> None:
"""Called when document parsing fails."""
# ── Text insertion stage ──────────────────────────────────────
def on_text_insert_start(
self, file_path: str, text_length: int = 0, **kwargs: Any
) -> None:
"""Called before text content is inserted into LightRAG."""
def on_text_insert_complete(
self, file_path: str, duration_seconds: float = 0.0, **kwargs: Any
) -> None:
"""Called after text content insertion succeeds."""
# ── Multimodal processing stage ───────────────────────────────
def on_multimodal_start(
self, file_path: str, item_count: int = 0, **kwargs: Any
) -> None:
"""Called before multimodal content processing begins."""
def on_multimodal_item_complete(
self,
file_path: str,
item_index: int = 0,
item_type: str = "",
total_items: int = 0,
**kwargs: Any,
) -> None:
"""Called after each individual multimodal item is processed."""
def on_multimodal_complete(
self,
file_path: str,
processed_count: int = 0,
duration_seconds: float = 0.0,
**kwargs: Any,
) -> None:
"""Called after all multimodal content processing completes."""
# ── Query stage ───────────────────────────────────────────────
def on_query_start(self, query: str, mode: str = "", **kwargs: Any) -> None:
"""Called before a query is executed."""
def on_query_complete(
self,
query: str,
mode: str = "",
duration_seconds: float = 0.0,
result_length: int = 0,
**kwargs: Any,
) -> None:
"""Called after a query completes."""
def on_query_error(
self,
query: str,
mode: str = "",
error: BaseException | str = "",
**kwargs: Any,
) -> None:
"""Called when a query fails."""
# ── Document complete ─────────────────────────────────────────
def on_document_complete(
self,
file_path: str,
doc_id: str = "",
duration_seconds: float = 0.0,
**kwargs: Any,
) -> None:
"""Called when the entire document processing pipeline finishes."""
def on_document_error(
self,
file_path: str,
error: BaseException | str = "",
stage: str = "",
**kwargs: Any,
) -> None:
"""Called when document processing fails at any stage."""
# ── Batch processing ──────────────────────────────────────────
def on_batch_start(self, file_count: int = 0, **kwargs: Any) -> None:
"""Called when batch processing begins."""
def on_batch_complete(
self,
total_files: int = 0,
successful: int = 0,
failed: int = 0,
duration_seconds: float = 0.0,
**kwargs: Any,
) -> None:
"""Called when batch processing completes."""
class MetricsCallback(ProcessingCallback):
"""Built-in callback that collects processing metrics.
Access aggregated metrics via the :attr:`metrics` attribute.
Example::
metrics_cb = MetricsCallback()
rag.callback_manager.register(metrics_cb)
# ... process documents ...
print(metrics_cb.summary())
"""
def __init__(self) -> None:
self.metrics: Dict[str, Any] = {
"documents_processed": 0,
"documents_failed": 0,
"total_content_blocks": 0,
"total_multimodal_items": 0,
"total_parse_time": 0.0,
"total_insert_time": 0.0,
"total_multimodal_time": 0.0,
"queries_executed": 0,
"total_query_time": 0.0,
"errors": [],
}
def on_parse_complete(
self,
file_path: str,
content_blocks: int = 0,
duration_seconds: float = 0.0,
**kw: Any,
) -> None:
self.metrics["total_content_blocks"] += content_blocks
self.metrics["total_parse_time"] += duration_seconds
def on_text_insert_complete(
self, file_path: str, duration_seconds: float = 0.0, **kw: Any
) -> None:
self.metrics["total_insert_time"] += duration_seconds
def on_multimodal_complete(
self,
file_path: str,
processed_count: int = 0,
duration_seconds: float = 0.0,
**kw: Any,
) -> None:
self.metrics["total_multimodal_items"] += processed_count
self.metrics["total_multimodal_time"] += duration_seconds
def on_document_complete(self, file_path: str, **kw: Any) -> None:
self.metrics["documents_processed"] += 1
def on_document_error(
self,
file_path: str,
error: BaseException | str = "",
stage: str = "",
**kw: Any,
) -> None:
self.metrics["documents_failed"] += 1
self.metrics["errors"].append(
{"file": file_path, "error": str(error), "stage": stage}
)
def on_query_complete(
self, query: str, duration_seconds: float = 0.0, **kw: Any
) -> None:
self.metrics["queries_executed"] += 1
self.metrics["total_query_time"] += duration_seconds
def on_query_error(
self, query: str, error: BaseException | str = "", **kw: Any
) -> None:
self.metrics["errors"].append(
{"file": None, "error": str(error), "stage": "query"}
)
def summary(self) -> str:
"""Return a human-readable summary of collected metrics."""
m = self.metrics
lines = [
"RAGAnything Processing Metrics",
"=" * 40,
f"Documents processed : {m['documents_processed']}",
f"Documents failed : {m['documents_failed']}",
f"Content blocks : {m['total_content_blocks']}",
f"Multimodal items : {m['total_multimodal_items']}",
f"Parse time : {m['total_parse_time']:.2f}s",
f"Insert time : {m['total_insert_time']:.2f}s",
f"Multimodal time : {m['total_multimodal_time']:.2f}s",
f"Queries executed : {m['queries_executed']}",
f"Query time : {m['total_query_time']:.2f}s",
]
if m["errors"]:
lines.append(f"Errors : {len(m['errors'])}")
for err in m["errors"][:5]:
lines.append(f" - [{err['stage']}] {err['file']}: {err['error']}")
return "\n".join(lines)
def reset(self) -> None:
"""Reset all collected metrics."""
self.__init__()
class CallbackManager:
"""Manages and dispatches events to registered callbacks.
Thread-safe for registration/unregistration and event logging.
Event dispatch iterates over a snapshot of currently registered
callbacks so that callbacks can safely register/unregister others.
"""
def __init__(self) -> None:
self._callbacks: List[ProcessingCallback] = []
self._event_log: List[ProcessingEvent] = []
self._log_events: bool = False
self._lock = threading.RLock()
def register(self, callback: ProcessingCallback) -> None:
"""Register a callback to receive processing events.
Args:
callback: An instance of :class:`ProcessingCallback` (or subclass).
Raises:
TypeError: If *callback* is not a :class:`ProcessingCallback`.
"""
if not isinstance(callback, ProcessingCallback):
raise TypeError(
f"Expected ProcessingCallback instance, got {type(callback).__name__}"
)
with self._lock:
self._callbacks.append(callback)
def unregister(self, callback: ProcessingCallback) -> None:
"""Remove a previously registered callback."""
with self._lock:
self._callbacks.remove(callback)
def enable_event_log(self, enabled: bool = True) -> None:
"""Enable or disable internal event logging.
When enabled, every dispatched event is recorded in
:attr:`event_log` for later inspection.
"""
with self._lock:
self._log_events = enabled
@property
def event_log(self) -> List[ProcessingEvent]:
"""Read-only access to the internal event log."""
with self._lock:
return list(self._event_log)
def clear_event_log(self) -> None:
"""Clear the internal event log."""
with self._lock:
self._event_log.clear()
def dispatch(self, event_name: str, **kwargs: Any) -> None:
"""Dispatch an event to all registered callbacks.
Args:
event_name: Name of the callback method (e.g., ``"on_parse_start"``).
**kwargs: Arguments forwarded to the callback method.
"""
with self._lock:
callbacks_snapshot = list(self._callbacks)
log_events = self._log_events
if log_events:
event = ProcessingEvent(
event_type=event_name,
file_path=kwargs.get("file_path"),
doc_id=kwargs.get("doc_id"),
stage=kwargs.get("stage"),
details=kwargs,
duration_seconds=kwargs.get("duration_seconds"),
error=str(kwargs["error"]) if "error" in kwargs else None,
)
self._event_log.append(event)
for cb in callbacks_snapshot:
handler = getattr(cb, event_name, None)
if handler is not None:
try:
handler(**kwargs)
except Exception:
logger.exception(
"Error in callback %s.%s",
type(cb).__name__,
event_name,
)