πŸ“š What is this & why does it exist?

Domain service catalog implementing all business logic between the API handlers and the database. Each entity (KB, Document, Dialog, etc.) has a service exposing CRUD plus complex transactional operations. CommonService base class de-duplicates pagination, soft-delete, and tenant filtering.

πŸ’‘ Real-world analogy

Services are like the kitchen staff in a restaurant β€” they take orders from waiters (apps), pull ingredients from the pantry (db), follow recipes (business logic), and never speak directly to customers. The CommonService base class is the shared mise en place every cook starts from.

Services Layer: Domain Business Logic

Domain services encapsulate entity-specific business logic. All 15+ services inherit from CommonService and implement CRUD, state transitions, and multi-tenant isolation.

Analogy: If the db/ layer is a "filing cabinet," then services are "job roles" in a company. A DocumentService is like a "Document Manager"β€”they know how to create, retrieve, update, and delete documents correctly. They know which documents are deleted, which belong to which tenant, and what the rules are for naming and organizing. Handlers (API endpoints) ask services to do work, rather than directly accessing the filing cabinet.

Purpose & Architecture

The services/ layer contains business logic for each domain entity:

Common Service Base Class

All services inherit from CommonService in common_service.py:

class CommonService:
    '''Base class for all domain services.'''
    
    def __init__(self):
        self._logger = logging.getLogger(self.__class__.__name__)
    
    def get_by_id(self, id: str, tenant_id: str = None):
        '''Retrieve entity by ID with optional tenant check.'''
        entity = Entity.get_by_id(id)
        if tenant_id and entity.tenant_id != tenant_id:
            raise TenantMismatch(f"Entity {id} not in tenant {tenant_id}")
        return entity
    
    def list_by_tenant(self, tenant_id: str, offset=0, limit=50):
        '''Paginated list for tenant.'''
        return Entity.select().where(
            Entity.tenant_id == tenant_id,
            Entity.deleted_at.is_null()
        ).offset(offset).limit(limit)
    
    def delete(self, id: str, tenant_id: str):
        '''Soft delete with tenant check.'''
        entity = self.get_by_id(id, tenant_id)
        entity.deleted_at = datetime.now()
        entity.save()
        return entity

Domain Services Catalog

Service Module File Primary Entity Key Methods
DocumentService document_service.py Document create(name, file), get(doc_id), list_by_kb(kb_id), delete(doc_id), upload_file(file_path)
ChunkService chunk_service.py Chunk create(doc_id, content), get(chunk_id), batch_create(chunks), embed(chunk_id), search_by_vector(embedding)
DialogService dialog_service.py Dialog create(name, desc), get(dialog_id), list_by_tenant(tenant_id), update(dialog_id, name), delete(dialog_id)
ConversationService conversation_service.py Conversation create(dialog_id, user_id), get(conv_id), list_by_dialog(dialog_id), delete(conv_id)
MessageService message_service.py Message create(conv_id, role, content), get(msg_id), list_by_conversation(conv_id), delete(msg_id)
TaskService task_service.py Task create(doc_id, task_type), get(task_id), get_status(task_id), update_progress(task_id, progress_dict), mark_done(task_id)
KnowledgebaseService knowledgebase_service.py KnowledgeBase create(name, desc), get(kb_id), list_by_tenant(tenant_id), add_document(kb_id, doc_id), remove_document(kb_id, doc_id)
LLMService llm_service.py LLMModel create(model_name, provider, api_key), get(model_id), list_by_tenant(tenant_id), verify_credentials(model_id)
MemoryService memory_service.py Memory create(user_id, content), get(memory_id), list_by_user(user_id), update(memory_id, content)
FileService file_service.py File (S3/blob storage) upload(file_bytes, filename), download(file_id), delete(file_id), get_url(file_id)
UserService user_service.py User create(email, password), get(user_id), get_by_email(email), update_profile(user_id, name), change_password(user_id, old_pwd, new_pwd)
TenantService tenant_service.py Tenant get(tenant_id), get_by_name(name), list_for_user(user_id), update_settings(tenant_id, settings)
ParsingService parsing_service.py Document parsing parse_pdf(file_path), parse_docx(file_path), parse_txt(file_path), extract_text(document_bytes)
TeacherService teacher_service.py LLM fine-tuning start_training(dialog_id, training_data), get_training_status(training_id), list_trained_models(tenant_id)

Service Design Patterns

Pattern 1: CRUD with Tenant Isolation

class DialogService(CommonService):
    def create(self, tenant_id: str, name: str, description: str = ""):
        if not name:
            raise ValidationError("name required")
        
        dialog = Dialog.create(
            tenant_id=tenant_id,
            name=name,
            description=description,
            created_by=g.user_id
        )
        self._logger.info(f"Created dialog {dialog.id} in tenant {tenant_id}")
        return dialog
    
    def get(self, dialog_id: str, tenant_id: str = None) -> Dialog:
        dialog = Dialog.get_by_id(dialog_id)
        if tenant_id and dialog.tenant_id != tenant_id:
            raise TenantMismatch(f"Dialog {dialog_id} not in tenant {tenant_id}")
        return dialog
    
    def list_by_tenant(self, tenant_id: str) -> List[Dialog]:
        return Dialog.select().where(
            Dialog.tenant_id == tenant_id,
            Dialog.deleted_at.is_null()
        ).order_by(Dialog.created_at.desc())

Pattern 2: State Transition (Task Lifecycle)

class TaskService(CommonService):
    def create(self, doc_id: str, task_type: str, tenant_id: str) -> Task:
        doc = DocumentService().get(doc_id, tenant_id)
        
        task = Task.create(
            document_id=doc_id,
            tenant_id=tenant_id,
            status="pending",
            task_type=task_type,
            progress_json={"percent": 0, "stage": "queued"}
        )
        
        # Queue task in Redis for background executor
        self._enqueue_task(task)
        return task
    
    def update_progress(self, task_id: str, progress: dict, tenant_id: str):
        task = self.get(task_id, tenant_id)
        task.progress_json = progress
        task.save()
        
        # Notify WebSocket subscribers of progress update
        self._broadcast_progress(task_id, progress)
    
    def mark_done(self, task_id: str, tenant_id: str, result: dict = None):
        task = self.get(task_id, tenant_id)
        task.status = "done"
        task.progress_json = {"percent": 100, "stage": "complete", "result": result}
        task.save()
        
        # Clean up temporary files, send completion notification
        self._finalize_task(task)

Pattern 3: Complex Mutation (Document Upload)

class DocumentService(CommonService):
    def upload(self, file: FileStorage, tenant_id: str, kb_id: str = None):
        '''Upload document: create Document row, save file, create Task, return document.'''
        
        # Validate inputs
        if not file or not file.filename:
            raise ValidationError("file required")
        
        # Create Document row
        doc = Document.create(
            tenant_id=tenant_id,
            name=file.filename,
            file_type=self._get_file_type(file.filename),
            created_by=g.user_id
        )
        
        # Save file to blob storage (S3 or local)
        file_path = self._save_file(file, doc.id)
        doc.file_path = file_path
        doc.save()
        
        # Add to knowledge base if provided
        if kb_id:
            KnowledgebaseService().add_document(kb_id, doc.id, tenant_id)
        
        # Create processing task
        task = TaskService().create(doc.id, "parse_and_embed", tenant_id)
        
        return {"document": doc, "task": task}
    
    def _save_file(self, file: FileStorage, doc_id: str) -> str:
        '''Save file to blob storage; return file path.'''
        # Implementation: write to S3 or local storage
        pass

Joint Services for Cross-Domain Coordination

Some operations require coordinating multiple services atomically. Joint services in joint_services/ manage these:

Joint Service File Purpose
UserAccountService user_account_service.py Register new user: creates User + Tenant atomically, adds user as tenant owner, sends welcome email
MemoryMessageService memory_message_service.py User sends message that should be memorized: creates Message + Memory atomically, tagged with conversation context
TenantModelService tenant_model_service.py Configure tenant's default LLM model: updates Tenant settings + LLMModel associations atomically

Error Handling & Logging

Services implement consistent error handling and logging:

import logging
from common.exceptions import (
    ValidationError, TenantMismatch, EntityNotFound, InternalError
)

class DocumentService(CommonService):
    def create(self, tenant_id: str, name: str):
        try:
            if not name or len(name) > 255:
                raise ValidationError("name must be 1-255 chars")
            
            doc = Document.create(
                tenant_id=tenant_id,
                name=name,
                created_by=g.user_id
            )
            self._logger.info(f"Created document {doc.id}")
            return doc
            
        except ValidationError:
            raise  # Re-raise domain exceptions
        except IntegrityError as e:
            self._logger.warning(f"DB integrity error: {e}")
            raise ValidationError("name already exists in tenant")
        except Exception as e:
            self._logger.exception(f"Unexpected error creating document: {e}")
            raise InternalError("Failed to create document")

Service Dependencies & Cross-References

Services can call other services for cross-domain operations:

Tenant Isolation in Services

Every service method that accesses or modifies data must enforce tenant isolation:

Performance Considerations

Security Implications

Future Enhancements