-
Notifications
You must be signed in to change notification settings - Fork 5
Description
Контекст
Описание желаемой модульной архитектуры AI Secretary System. Не план миграции, а целевое состояние — каким должен быть проект при идеальной декомпозиции.
Основано на аудите текущего состояния: #480
Принципы
- Каждый домен = пакет с собственными models, repository, service, router
- Зависимости однонаправленные — нижние слои не знают о верхних
- Взаимодействие через интерфейсы — модули зависят от протоколов, не от реализаций
- Нет файлов-монолитов — ни один файл не отвечает за все домены сразу
- Deployment mode — переключение на уровне модулей, не if-statements
1. Каталог модулей
modules/
├── core/ ← Фундамент (DB, auth, config, events, health)
├── knowledge/ ← RAG, wiki, коллекции, эмбеддинги
├── llm/ ← LLM-провайдеры, генерация, fallback
├── chat/ ← Сессии, сообщения, стриминг, бранчинг
├── channels/
│ ├── telegram/ ← Telegram бот-менеджер + подпроцесс
│ ├── whatsapp/ ← WhatsApp бот-менеджер + подпроцесс
│ └── widget/ ← Web-виджет, lead tracking
├── crm/ ← amoCRM (OAuth, leads, contacts, inbox)
├── ecommerce/ ← WooCommerce (products, orders)
├── sales/ ← Воронки, сегменты, платежи
├── kanban/ ← Проекты, задачи, roadmap
├── claude_code/ ← WebSocket CLI, сессии, проекты
├── speech/ ← TTS/STT/Voice (local-only)
├── telephony/ ← GSM/SIM (local-only)
├── monitoring/ ← Audit, usage, system health
└── admin/ ← Backup, GDPR, settings, FAQ
2. Структура каждого модуля
Каждый модуль — Python-пакет со стандартной структурой:
modules/{domain}/
__init__.py # Публичный API модуля (экспорты)
models.py # SQLAlchemy модели ТОЛЬКО этого домена
repository.py # Репозиторий (данные)
service.py # Бизнес-логика
router.py # FastAPI роутер
events.py # Определения событий (dataclasses)
schemas.py # Pydantic модели запросов/ответов
tasks.py # Background tasks (если есть)Модуль экспортирует через __init__.py:
- Service class (основной интерфейс)
- Event dataclasses (для подписки другими модулями)
- Router (для регистрации в приложении)
Модуль НЕ экспортирует:
- Репозитории (внутренняя деталь)
- SQLAlchemy модели напрямую (только через service API)
3. Модуль core — фундамент
core/
database.py # Engine, session factory, init/shutdown
auth/
models.py # User, Role, RolePermission, UserSession, WorkspaceMember, UserIdentity
service.py # AuthService: login, JWT, RBAC checks, session management
guards.py # require_permission(), get_current_user(), workspace_context()
cache.py # SessionCache, PermissionsCache, MemberRoleCache
router.py # /admin/auth/*, /admin/roles/*, /admin/workspace/*
config.py # Единый ConfigService: .env + DB, приоритет DB > env
events.py # EventBus (in-process pub/sub)
cache.py # CacheManager: in-memory + Redis, unified invalidation
health.py # HealthRegistry: каждый модуль регистрирует свой health check
tasks.py # TaskRegistry: named background tasks, cancel_all()
base.py # BaseRepository, BaseServiceПравила:
- core не зависит ни от одного модуля
- Все модули зависят от core
- Auth — часть core (нужен всем)
EventBus — простой in-process pub/sub:
class EventBus:
async def publish(self, event: BaseEvent) -> None: ...
def subscribe(self, event_type: type[BaseEvent], handler: Callable) -> None: ...4. Граф зависимостей
┌──────────┐
│ core │
│ db, auth,│
│ config, │
│ events │
└────┬─────┘
┌───────────┼───────────────┐
│ │ │
┌─────┴─────┐ ┌──┴──┐ ┌───────┴───────┐
│ knowledge │ │ llm │ │ speech │
│ RAG, wiki │ │ │ │ TTS/STT │
│ collections│ │ │ │ (local-only) │
└─────┬──────┘ └──┬──┘ └───────────────┘
│ │
└─────┬─────┘
│
┌─────┴─────┐
│ chat │
│ sessions, │
│ streaming │
└─────┬──────┘
┌───────────┼──────────────┐
│ │ │
┌─────┴─────┐ ┌──┴──────┐ ┌─────┴──────┐
│ channels │ │ kanban │ │claude_code │
│ TG/WA/ │ │ │ │ │
│ Widget │ └─────────┘ └────────────┘
└─────┬──────┘
│
┌─────┴─────┐
│ sales │
└────────────┘
┌─────────┐ ┌──────────┐
│ crm │ │ecommerce │
│ amoCRM │ │ WooCom. │
└────┬────┘ └────┬─────┘
└──────┬─────┘
│
зависят от:
core + knowledge + chat
Таблица зависимостей
| Модуль | Зависит от | Что берёт |
|---|---|---|
| knowledge | core | DB, auth guards |
| llm | core | DB (provider configs), config |
| speech | core | DB (presets), config |
| chat | core, knowledge, llm | auth, RAG-поиск, LLM-генерация |
| channels/telegram | core, chat | auth (internal tokens), chat API |
| channels/whatsapp | core, chat | auth (internal tokens), chat API |
| channels/widget | core, chat | chat API, public endpoints |
| sales | core, channels | подписчики, доставка сообщений |
| kanban | core | DB, auth |
| claude_code | core | DB, auth, WebSocket |
| crm | core, knowledge, chat | dataset sync, session linking |
| ecommerce | core, knowledge | dataset sync |
| monitoring | core | DB, health registry |
| admin | core | DB, backup |
| telephony | core | DB, config |
Запрещённые зависимости:
- core → любой модуль
- knowledge → chat, channels, crm
- llm → chat, channels
- chat → channels, crm, kanban
- kanban → chat (связь через события, не импорт)
5. Протоколы взаимодействия
5A. Синхронные интерфейсы (вызовы внутри процесса)
Каждый модуль экспортирует service с типизированным API:
# knowledge/__init__.py
class KnowledgeService(Protocol):
async def search(self, query: str, collection_ids: list[int] | None,
top_k: int = 3, max_chars: int = 2500) -> list[SearchResult]: ...
async def search_multi(self, query: str, collection_ids: list[int],
top_k: int = 3) -> list[SearchResult]: ...
async def get_collections(self, enabled_only: bool = True) -> list[Collection]: ...
async def sync_documents(self, collection_id: int, base_dir: str) -> SyncResult: ...# llm/__init__.py
class LLMService(Protocol):
async def generate(self, messages: list[Message], config: LLMConfig) -> str: ...
async def stream(self, messages: list[Message], config: LLMConfig) -> AsyncIterator[str]: ...
async def resolve_backend(self, backend_id: str) -> LLMProvider: ...# chat/__init__.py
class ChatService(Protocol):
async def create_session(self, source: str, source_id: str | None,
system_prompt: str | None, owner_id: int | None,
workspace_id: int) -> Session: ...
async def send_message(self, session_id: str, content: str,
llm_override: LLMConfig | None = None) -> Message: ...
async def stream_message(self, session_id: str, content: str,
llm_override: LLMConfig | None = None) -> AsyncIterator[StreamChunk]: ...
async def get_session(self, session_id: str) -> Session | None: ...
async def get_history(self, session_id: str) -> list[Message]: ...Chat внутри себя вызывает Knowledge и LLM:
# chat/service.py
class ChatServiceImpl:
def __init__(self, knowledge: KnowledgeService, llm: LLMService):
self.knowledge = knowledge
self.llm = llm
async def stream_message(self, session_id, content, llm_override=None):
session = await self._get_session(session_id)
rag_context = await self.knowledge.search(content, session.collection_ids)
prompt = self._build_prompt(session.system_prompt, rag_context)
messages = await self._build_message_list(session_id, content, prompt)
backend = await self.llm.resolve_backend(llm_override or session.llm_backend)
async for chunk in self.llm.stream(messages, backend):
yield chunk5B. Асинхронные события (fire-and-forget)
Модули публикуют события через EventBus. Другие модули подписываются. Нет прямых зависимостей.
# channels/widget/events.py
@dataclass
class WidgetSessionCreated:
session_id: str
widget_instance_id: str
visitor_metadata: dict
@dataclass
class WidgetMessageSent:
session_id: str
content: str
is_first: bool
# crm/service.py — подписывается
class CRMService:
def setup_events(self, bus: EventBus):
bus.subscribe(WidgetSessionCreated, self._on_widget_session)
bus.subscribe(WidgetMessageSent, self._on_widget_message)
async def _on_widget_session(self, event: WidgetSessionCreated):
"""Создать lead в amoCRM при создании виджет-сессии."""
...Какие события нужны:
| Событие | Публикует | Подписчики |
|---|---|---|
UserRoleChanged |
core/auth | core/cache (invalidate) |
SessionRevoked |
core/auth | core/cache (invalidate) |
WidgetSessionCreated |
channels/widget | crm (create lead) |
WidgetMessageSent |
channels/widget | crm (append note) |
WidgetContactSubmitted |
channels/widget | crm (link contact) |
BotProcessDied |
channels/telegram, /whatsapp | monitoring (alert), self (auto-restart) |
KnowledgeUpdated |
knowledge | llm (reload FAQ cache) |
ConfigChanged |
core/config | affected modules (reload) |
DatasetSynced |
crm, ecommerce | knowledge (reindex) |
5C. HTTP-контракты (кросс-процессные)
Бот-подпроцессы общаются с основным процессом только через HTTP. Контракт:
# Бот → Основной процесс
POST /internal/chat/sessions # Создать сессию
POST /internal/chat/sessions/{id}/stream # Отправить сообщение (SSE)
GET /internal/chat/sessions/{id}/history # Получить историю (при рестарте)
POST /internal/bot/heartbeat # Health ping каждые 30с
GET /internal/bot/config/{instance_id} # Получить/обновить конфиг
# Авторизация: Bearer token (internal JWT, role=bot)
# Prefix /internal/ отделяет от /admin/ и /widget/
Принципы:
- Боты НЕ импортируют DB-модули основного процесса
- Боты НЕ обращаются к SQLite напрямую
- Вся персистентность — через API
- Config polling: бот проверяет конфиг через API раз в N минут
6. Композиция приложения (startup)
Вместо одного startup event на 300 строк — каждый модуль регистрирует свой lifecycle:
# app.py (замена orchestrator.py, ~50 строк)
from core import create_app, EventBus, HealthRegistry, TaskRegistry
from modules import knowledge, llm, chat, channels, crm, ...
app = create_app()
# Фаза 1: Инициализация core
db = await init_database()
bus = EventBus()
health = HealthRegistry()
tasks = TaskRegistry()
# Фаза 2: Создание сервисов (порядок = граф зависимостей)
knowledge_svc = knowledge.create_service(db)
llm_svc = llm.create_service(db, config)
chat_svc = chat.create_service(db, knowledge_svc, llm_svc)
# Фаза 3: Регистрация роутеров
app.include_router(knowledge.router)
app.include_router(llm.router)
app.include_router(chat.router)
# Фаза 4: Подписка на события
crm_svc.setup_events(bus)
monitoring_svc.setup_events(bus)
# Фаза 5: Запуск background tasks
tasks.register("session_cleanup", chat_svc.periodic_cleanup, interval=3600)
tasks.register("vacuum", db.periodic_vacuum, interval=7*24*3600, initial_delay=24*3600)
tasks.register("bot_health", channels.health_monitor, interval=30)
# Shutdown
@app.on_event("shutdown")
async def shutdown():
await tasks.cancel_all()
await channels.stop_all_bots()
await db.close()7. Deployment modes
Вместо if DEPLOYMENT_MODE != "cloud": — модули просто не загружаются:
# app.py
MODULES = {
"full": [knowledge, llm, chat, channels, crm, ecommerce, sales,
kanban, claude_code, speech, telephony, monitoring, admin],
"cloud": [knowledge, llm, chat, channels, crm, ecommerce, sales,
kanban, claude_code, monitoring, admin],
"local": [knowledge, llm, chat, channels, crm, ecommerce, sales,
kanban, claude_code, speech, telephony, monitoring, admin],
}
for module in MODULES[DEPLOYMENT_MODE]:
svc = module.create_service(db, **deps)
app.include_router(module.router)speech и telephony просто не загружаются в cloud mode. Никаких if-statements внутри других модулей.
Health check автоматически показывает только загруженные модули.
Роутер /admin/deployment-mode возвращает список загруженных модулей → фронтенд скрывает ненужные вкладки.
8. Cross-cutting concerns
Кеширование
Единый CacheManager с namespace:
cache.get("auth:session:{jti}")
cache.get("knowledge:faq:{workspace_id}")
cache.invalidate_pattern("auth:session:*") # при mass-revokeСлои: L1 in-memory (TTL) → L2 Redis (optional). Инвалидация через EventBus: при UserRoleChanged → cache.invalidate_pattern("auth:permissions:{role}").
Кросс-доменные FK
Слабые связи (не через FK, а через ID + event):
# claude_code/models.py
class ClaudeCodeSession:
chat_session_id: str | None = None # Просто строка, не FK
kanban_task_id: int | None = None # Просто int, не FK
# Целостность — через events:
# При удалении ChatSession → publish ChatSessionDeleted
# claude_code подписывается → обнуляет chat_session_idКонфигурация
Единый источник: DB (таблица config). .env — только для секретов и bootstrap (DB URL, JWT secret). Модули объявляют свои config-ключи:
# llm/config.py
class LLMConfig(ModuleConfig):
default_backend: str = "vllm"
max_tokens: int = 40969. Сравнение с текущим состоянием
| Аспект | Сейчас | Целевое |
|---|---|---|
| Точка входа | orchestrator.py (4100 строк) | app.py (~50 строк) |
| Модели | 1 файл, 55 классов | По файлу на модуль |
| Менеджеры | 1 файл, 28 классов | service.py в каждом модуле |
| DI | ServiceContainer (только hardware) + глобальные синглтоны | Explicit constructor injection |
| Кросс-модульная связь | Прямой импорт | Protocol interfaces + EventBus |
| Background tasks | fire-and-forget create_task() | TaskRegistry с cancel_all() |
| Кеширование | 5 независимых кешей | Единый CacheManager + event invalidation |
| Deploy modes | if-statements | Модули не загружаются |
| Bot ↔ Main | Прямой DB import + HTTP | Только HTTP (/internal/ API) |
| Shutdown | Только DB close | Tasks → Bots → Bridge → DB |