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.
Purpose & Architecture
The services/ layer contains business logic for each domain entity:
- CommonService Base Class: All services inherit from CommonService, providing shared methods: pagination, tenant filtering, error handling, logging.
- 15+ Domain Services: Each service manages one or more related entities (DocumentService manages Document + file uploads, DialogService manages Dialog definitions, etc.).
- State Transition Management: Services handle complex state machines (e.g., Task: pending β processing β done/error). Handlers never directly update entity state.
- Atomic Operations: Services coordinate multi-step operations (create document + queue task, create conversation + send welcome message) atomically.
- Soft Delete Coordination: Services enforce soft-delete semantics globally (queries filter deleted_at.is_null(), restore operations reset deleted_at).
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:
- DocumentService β FileService: Upload document calls FileService.upload() to save file.
- DocumentService β TaskService: Upload document calls TaskService.create() to queue processing task.
- ConversationService β MessageService: Start conversation calls MessageService.create() to send welcome message.
- DialogService β LLMService: Get dialog config calls LLMService.get() to fetch associated LLM model.
- TaskService β ChunkService: Document parsing task calls ChunkService.batch_create() to insert chunks.
Tenant Isolation in Services
Every service method that accesses or modifies data must enforce tenant isolation:
- Check tenant_id on read: When retrieving entity, verify tenant_id matches request's tenant_id.
- Enforce tenant_id on create: Always set entity.tenant_id = request.tenant_id; never trust user input.
- Filter by tenant_id on list: All list queries include WHERE tenant_id = ? filter.
- Validate tenant_id on mutation: Before update/delete, check tenant_id matches.
Performance Considerations
- Connection Pool: Services use shared connection pool (10β20). Keep transactions short; avoid long-running DB work in services.
- Query Optimization: List operations paginate results (default limit 50). Add indexes on tenant_id, created_by, deleted_at.
- Caching: Services can implement caching for slowly-changing data (LLM models, KnowledgeBases). Use Redis with TTL.
- Deadlock Retry: Services that perform multi-table updates can retry on deadlock with exponential backoff.
Security Implications
- No SQL Injection: Services use Peewee ORM; all queries are parameterized.
- Tenant Data Leaks: Missing tenant_id check = critical vulnerability. Code review must verify every service method.
- Privilege Escalation: Services don't enforce role-based access control (admin vs user). That's the API layer's responsibility. Services assume caller is authorized.
- Credential Handling: Services that work with secrets (LLM API keys) must encrypt at rest, decrypt only when needed, never log.
Future Enhancements
- Event-Driven Architecture: Add event publishing (service creates Document β emits DocumentCreated event β other services listen). Loosens coupling.
- Service Versioning: Support multiple service implementations (DocumentService v1 vs v2) for gradual migration.
- Service Composition: Build higher-level "use case" services that orchestrate multiple domain services.
- Audit Logging: Log all mutations (who, what, when, why) for compliance and debugging.
- Transaction Sagas: For multi-step workflows (upload doc β parse β embed β index), implement saga pattern for failure recovery.