Files
furyhawk bb4b614711 feat: Implement RAG document and sync services with vector store integration
- Added `vectorstore.py` for managing vector storage operations using Milvus.
- Introduced `rag_document.py` for handling RAG document lifecycle, including ingestion and status updates.
- Created `rag_status.py` for streaming RAG ingestion status via Redis pub/sub.
- Developed `rag_sync.py` for managing synchronization operations and logs.
- Implemented `sync_source.py` for managing sync source configurations and triggering syncs.
- Established a lightweight task dispatcher in `dispatcher.py` for background task execution.
- Registered task functions for RAG ingestion and sync operations in `rag_tasks.py`.
- Configured task dispatcher settings in `arq_settings.py`.
- Added necessary database models and schemas for RAG document and sync operations.
2026-06-13 21:52:27 +08:00

190 lines
6.5 KiB
Python

"""Sync source service (PostgreSQL async).
Contains business logic for managing RAG sync source configurations
and triggering sync operations.
"""
import json
from datetime import UTC, datetime
from uuid import UUID
from sqlalchemy.ext.asyncio import AsyncSession
from backend.core.exceptions import BadRequestError, NotFoundError
from backend.db.models.sync_log import SyncLog
from backend.db.models.sync_source import SyncSource
from backend.repositories import sync_log_repo
from backend.repositories import sync_source_repo
from backend.schemas.sync_source import (
ConnectorConfigField,
ConnectorInfo,
ConnectorList,
SyncSourceCreate,
SyncSourceList,
SyncSourceRead,
SyncSourceUpdate,
)
from backend.rag.connectors import CONNECTOR_REGISTRY
class SyncSourceService:
"""Service for managing sync source configurations."""
def __init__(self, db: AsyncSession):
self.db = db
def _to_read(self, s: SyncSource) -> SyncSourceRead:
return SyncSourceRead(
id=str(s.id),
name=s.name,
connector_type=s.connector_type,
collection_name=s.collection_name,
config=s.config
if isinstance(s.config, dict)
else json.loads(s.config)
if s.config
else {},
sync_mode=s.sync_mode,
schedule_minutes=s.schedule_minutes,
is_active=s.is_active,
last_sync_at=s.last_sync_at.isoformat() if s.last_sync_at else None,
last_sync_status=s.last_sync_status,
last_error=s.last_error,
created_at=s.created_at.isoformat() if s.created_at else None,
)
async def list_sources(
self,
is_active: bool | None = None,
) -> SyncSourceList:
"""List all sync sources, optionally filtered by active status."""
sources = await sync_source_repo.get_all(self.db, is_active=is_active)
return SyncSourceList(items=[self._to_read(s) for s in sources], total=len(sources))
async def get_source(self, source_id: str) -> SyncSource:
"""Get a sync source by ID.
Raises:
NotFoundError: If sync source does not exist.
"""
source = await sync_source_repo.get_by_id(self.db, UUID(source_id))
if not source:
raise NotFoundError(
message="Sync source not found",
details={"source_id": source_id},
)
return source
async def create_source(self, data: SyncSourceCreate) -> SyncSourceRead:
"""Create a new sync source.
Validates the connector type and its configuration before creating.
Raises:
BadRequestError: If connector type is unknown or config is invalid.
"""
if data.connector_type not in CONNECTOR_REGISTRY:
raise BadRequestError(
message=f"Unknown connector type: {data.connector_type}",
details={"connector_type": data.connector_type},
)
connector_cls = CONNECTOR_REGISTRY[data.connector_type]
connector = connector_cls()
is_valid, error = await connector.validate_config(data.config)
if not is_valid:
raise BadRequestError(
message=f"Invalid connector config: {error}",
details={"connector_type": data.connector_type},
)
source = await sync_source_repo.create(
self.db,
name=data.name,
connector_type=data.connector_type,
collection_name=data.collection_name,
config=data.config,
sync_mode=data.sync_mode,
schedule_minutes=data.schedule_minutes,
)
return self._to_read(source)
async def update_source(self, source_id: str, data: SyncSourceUpdate) -> SyncSourceRead:
"""Update an existing sync source.
Raises:
NotFoundError: If sync source does not exist.
"""
await self.get_source(source_id) # verify exists
updates = data.model_dump(exclude_unset=True)
source = await sync_source_repo.update(self.db, UUID(source_id), **updates)
if source is None:
raise NotFoundError(message="Sync source not found", details={"source_id": source_id})
return self._to_read(source)
async def delete_source(self, source_id: str) -> None:
"""Delete a sync source.
Raises:
NotFoundError: If sync source does not exist.
"""
await self.get_source(source_id) # verify exists
await sync_source_repo.delete(self.db, UUID(source_id))
async def trigger_sync(self, source_id: str) -> SyncLog:
"""Trigger a manual sync — persists a SyncLog and dispatches the task.
Raises:
NotFoundError: If sync source does not exist.
"""
source = await self.get_source(source_id)
sync_log = await sync_log_repo.create(
self.db,
source=source.connector_type,
collection_name=source.collection_name,
mode=source.sync_mode,
sync_source_id=source.id,
)
from backend.worker.dispatcher import get_dispatcher
get_dispatcher().delay(
"sync_single_source_task",
source_id=source_id,
sync_log_id=str(sync_log.id),
)
return sync_log
async def update_after_sync(
self,
source_id: str,
status: str,
error: str | None = None,
) -> None:
"""Update sync source status after a sync operation completes."""
await sync_source_repo.update_sync_status(
self.db,
UUID(source_id),
last_sync_at=datetime.now(UTC),
last_sync_status=status,
last_error=error,
)
@staticmethod
def list_connectors() -> ConnectorList:
"""List available connector types with their config schemas."""
items = []
for _connector_type, connector_cls in CONNECTOR_REGISTRY.items():
schema_fields = {
field_name: ConnectorConfigField(**field_spec)
for field_name, field_spec in connector_cls.CONFIG_SCHEMA.items()
}
items.append(
ConnectorInfo(
type=connector_cls.CONNECTOR_TYPE,
name=connector_cls.DISPLAY_NAME,
config_schema=schema_fields,
enabled=True,
)
)
return ConnectorList(items=items)