Files
hkuds--deeptutor/deeptutor/knowledge/progress_tracker.py
T
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

248 lines
8.9 KiB
Python

"""
Progress Tracker - Tracks knowledge base initialization progress
"""
import asyncio
from collections.abc import Callable
from datetime import datetime
from enum import Enum
import json
import logging
from pathlib import Path
# Use unified logging system
_logger = logging.getLogger(__name__)
def _logger_instance():
return _logger
class ProgressStage(Enum):
"""Initialization stage"""
INITIALIZING = "initializing" # Initializing
PROCESSING_DOCUMENTS = "processing_documents" # Processing documents
PROCESSING_FILE = "processing_file" # Processing single file
COMPLETED = "completed" # Completed
ERROR = "error" # Error
class ProgressTracker:
"""Progress tracker"""
def __init__(self, kb_name: str, base_dir: Path):
self.kb_name = kb_name
self.base_dir = base_dir
self.kb_dir = base_dir / kb_name
self.progress_file = self.kb_dir / ".progress.json"
self._callbacks: list = [] # Support multiple callbacks
self.task_id: str | None = None # Task ID (for log identification)
def set_callback(self, callback: Callable[[dict], None]):
"""Set progress callback function (can be called multiple times to add multiple callbacks)"""
if callback not in self._callbacks:
self._callbacks.append(callback)
def remove_callback(self, callback: Callable[[dict], None]):
"""Remove progress callback function"""
if callback in self._callbacks:
self._callbacks.remove(callback)
def _notify(self, progress: dict):
"""Notify progress update (call all callbacks)"""
from deeptutor.runtime.mode import is_server
if is_server():
try:
from deeptutor.api.utils.progress_broadcaster import ProgressBroadcaster
broadcaster = ProgressBroadcaster.get_instance()
try:
loop = asyncio.get_running_loop()
loop.create_task(broadcaster.broadcast(self.kb_name, progress))
except RuntimeError:
pass
except (ImportError, Exception):
pass
for callback in self._callbacks:
try:
callback(progress)
except Exception as e:
_logger_instance().debug("Progress callback error: %s", e)
def _save_progress(self, progress: dict):
"""Save progress to kb_config.json and local .progress.json file"""
# Save to kb_config.json (centralized config)
try:
from deeptutor.knowledge.manager import KnowledgeBaseManager
manager = KnowledgeBaseManager(base_dir=str(self.base_dir))
# Determine status based on stage
stage = progress.get("stage", "")
if stage == "completed":
status = "ready"
elif stage == "error":
status = "error"
elif stage in [
"initializing",
"processing_documents",
"processing_file",
]:
status = "processing"
else:
status = "initializing"
# Update kb_config.json with status and progress
manager.update_kb_status(
name=self.kb_name,
status=status,
progress={
"stage": progress.get("stage"),
"message": progress.get("message"),
"percent": progress.get("progress_percent", 0),
"current": progress.get("current", 0),
"total": progress.get("total", 0),
"file_name": progress.get("file_name"),
"error": progress.get("error"),
"timestamp": progress.get("timestamp"),
"task_id": progress.get("task_id"),
"indexed_count": progress.get("indexed_count"),
"index_changed": progress.get("index_changed"),
"index_action": progress.get("index_action"),
},
)
except Exception as e:
_logger_instance().warning("Failed to save progress to kb_config.json: %s", e)
# Persist the last seen progress snapshot so websocket subscribers and
# page reloads can recover the live state without relying on in-memory callbacks.
try:
self.kb_dir.mkdir(parents=True, exist_ok=True)
temp_progress_file = self.progress_file.parent / f"{self.progress_file.name}.tmp"
with open(temp_progress_file, "w", encoding="utf-8") as f:
json.dump(progress, f, indent=2, ensure_ascii=False)
f.flush()
temp_progress_file.replace(self.progress_file)
except Exception as e:
_logger_instance().warning(
"Failed to persist progress snapshot for '%s': %s", self.kb_name, e
)
def update(
self,
stage: ProgressStage,
message: str = "",
current: int = 0,
total: int = 0,
file_name: str = "",
error: str | None = None,
indexed_count: int | None = None,
index_changed: bool | None = None,
index_action: str | None = None,
):
"""Update progress"""
progress = {
"kb_name": self.kb_name,
"task_id": self.task_id,
"stage": stage.value,
"message": message,
"current": current,
"total": total,
"file_name": file_name,
"progress_percent": int(current / total * 100) if total > 0 else 0,
"timestamp": datetime.now().isoformat(),
}
if indexed_count is not None:
progress["indexed_count"] = indexed_count
if index_changed is not None:
progress["index_changed"] = index_changed
if index_action:
progress["index_action"] = index_action
if error:
progress["error"] = error
progress["stage"] = ProgressStage.ERROR.value
# Output to logger (terminal and log file)
try:
logger = _logger_instance()
prefix = f"[{self.task_id}]" if self.task_id else ""
if total > 0:
percent = progress["progress_percent"]
progress_msg = f"{prefix} {message} ({current}/{total}, {percent}%)"
if file_name:
progress_msg += f" - File: {file_name}"
else:
progress_msg = f"{prefix} {message}"
if file_name:
progress_msg += f" - File: {file_name}"
if error:
logger.error(f"{progress_msg} - Error: {error}")
else:
logger.info(progress_msg)
except Exception:
# If unified logging fails unexpectedly, use stdlib logger as fallback.
fallback_logger = logging.getLogger("deeptutor.ProgressTracker")
prefix = f"[{self.task_id}]" if self.task_id else ""
fallback_logger.warning(
"%s [ProgressTracker] %s (%s/%s)",
prefix,
message,
current,
total if total > 0 else "?",
)
if error:
fallback_logger.error("%s [ProgressTracker] Error: %s", prefix, error)
self._save_progress(progress)
if self.task_id:
try:
from deeptutor.api.utils.task_log_stream import get_task_stream_manager
get_task_stream_manager().emit(self.task_id, "progress", progress)
except Exception as e:
_logger_instance().debug("Failed to emit task progress event: %s", e)
self._notify(progress)
def get_progress(self) -> dict | None:
"""Get current progress"""
if self.progress_file.exists():
try:
with open(self.progress_file, encoding="utf-8") as f:
return json.load(f)
except Exception as e:
_logger_instance().debug(f"Failed to read progress file for '{self.kb_name}': {e}")
try:
from deeptutor.knowledge.manager import KnowledgeBaseManager
manager = KnowledgeBaseManager(base_dir=str(self.base_dir))
status = manager.get_kb_status(self.kb_name)
if status and status.get("progress"):
return status.get("progress")
except Exception as e:
_logger_instance().debug(
"Failed to recover progress snapshot from kb_config for '%s': %s",
self.kb_name,
e,
)
return None
def clear(self):
"""Clear progress file"""
if self.progress_file.exists():
try:
self.progress_file.unlink()
except Exception as e:
_logger_instance().debug(f"Failed to clear progress file for '{self.kb_name}': {e}")