-
Notifications
You must be signed in to change notification settings - Fork 5
Architecture
Техническая архитектура AI Secretary System: текущее состояние, модульная инфраструктура и план миграции.
┌──────────────────────────────────────────────────────────────┐
│ Orchestrator (port 8002) │
│ orchestrator.py + app/routers/ (28 роутеров, ~400 endpoints)│
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Vue 3 Admin Panel (23 views, PWA) │ │
│ │ admin/dist/ │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ modules/core/ (Phase 0) │ │
│ │ EventBus · TaskRegistry · HealthRegistry │ │
│ └────────────────────────────────────────────────────────┘ │
└────────────┬──────────────┬──────────────┬───────────────────┘
│ │ │
┌───────┴──┐ ┌──────┴───┐ ┌─────┴─────┐
│ LLM │ │ TTS │ │ STT │
│ vLLM / │ │ XTTS v2 /│ │ Vosk / │
│ Cloud │ │ Piper │ │ Whisper │
└──────────┘ └──────────┘ └───────────┘
Пайплайн обработки: Сообщение пользователя → FAQ-проверка (мгновенный ответ) ИЛИ LLM → TTS → Аудио-ответ
Профили развёртывания (DEPLOYMENT_MODE): full (всё), cloud (без GPU/TTS/STT/GSM), local (= full).
| Компонент | Размер | Что содержит |
|---|---|---|
orchestrator.py |
~320 строк | Чистый wiring: imports, middleware, регистрация 28 роутеров, вызовы доменных init-функций, static files |
db/models.py |
~200 строк (фасад) | Реэкспорт моделей из modules/*/models.py (54 модели) |
db/integration.py |
~100 строк (фасад) | Импорт синглтонов и классов из modules/*/service.py под старыми именами |
| Компонент | Описание |
|---|---|
app/routers/ |
28 фасадов-реэкспортов (1–3 строки), логика в modules/*/router*.py
|
db/repositories/ |
45 файлов, чистая изоляция, BaseRepository с generic CRUD |
telegram_bot/, whatsapp_bot/
|
Отдельные процессы, общаются через HTTP API |
app/services/ |
Доменные сервисы (amoCRM, Wiki RAG, backup и др.) |
app/dependencies.py |
ServiceContainer — DI через FastAPI Depends
|
Фундамент для модульной декомпозиции. Три компонента, которые используются всеми доменными модулями.
modules/core/
├── __init__.py ← реэкспорт EventBus, TaskRegistry, HealthRegistry, ConfigChanged, etc.
├── events.py ← EventBus + BaseEvent + domain events
├── tasks.py ← TaskRegistry + TaskInfo
├── health.py ← HealthRegistry + HealthStatus
├── startup.py ← seed, setup_event_subscriptions(), init_internet_monitor, graceful_shutdown
├── internet_monitor.py ← InternetMonitor
├── protocols.py ← AuthService Protocol (target contract)
└── schemas.py ← UserInfo, LoginResult, TokenInfo, RoleInfo, WorkspaceInfo, etc.
Аналогичные protocols.py и schemas.py в каждом домене:
modules/knowledge/protocols.py ← KnowledgeService Protocol
modules/knowledge/schemas.py ← SearchResult, CollectionInfo, DocumentInfo, SyncResult, FAQEntryInfo
modules/llm/protocols.py ← LLMService Protocol
modules/llm/schemas.py ← LLMConfig, ProviderInfo, StreamChunk, ToolCall, TokenUsage
modules/chat/protocols.py ← ChatService Protocol
modules/chat/schemas.py ← SessionInfo, MessageInfo, ShareInfo, StreamChunk
from modules.core import EventBus, BaseEvent, TaskRegistry, TaskInfo, HealthRegistry, HealthStatusIn-process асинхронная шина событий для развязки межмодульного взаимодействия.
Принципы:
- Обработчики — async callable, выполняются конкурентно через
asyncio.gather - Ошибки обработчиков логируются, но не пробрасываются к издателю
-
publish()ожидает завершения всех обработчиков (не fire-and-forget) - Типизированная маршрутизация — обработчик получает только события своего типа
API:
| Метод | Описание |
|---|---|
subscribe(event_type, handler) |
Подписка async обработчика на тип события |
publish(event) |
Публикация события всем подписчикам |
clear() |
Удаление всех обработчиков (для тестов) |
Пример:
from dataclasses import dataclass
from modules.core import EventBus, BaseEvent
@dataclass
class UserCreated(BaseEvent):
user_id: int
username: str
bus = EventBus()
async def on_user_created(event: UserCreated):
print(f"New user: {event.username} (id={event.user_id})")
bus.subscribe(UserCreated, on_user_created)
await bus.publish(UserCreated(user_id=42, username="alice"))Инфраструктура (Phase 5.1):
-
Singleton:
ServiceContainer.event_bus(app/dependencies.py) — доступен черезget_container().event_bus -
Подписки: регистрируются в
setup_event_subscriptions()(modules/core/startup.py), вызывается при старте до инициализации сервисов -
Каноническое место для событий:
modules/core/events.py(core events) иmodules/{domain}/events.py(domain events)
Действующие события:
| Событие | Файл | Издатель | Подписчик |
|---|---|---|---|
InternetStatusChanged |
modules/core/events.py |
InternetMonitor |
— (GSM, future) |
UserRoleChanged |
modules/core/events.py |
WorkspaceService.update_member_role() |
on_user_role_changed → cache invalidation + session revocation |
SessionRevoked |
modules/core/events.py |
UserService (password/role/deactivation), WorkspaceService.remove_member()
|
on_session_revoked → cache invalidation + session revocation |
DatasetSynced |
modules/core/events.py |
CRM/ecommerce/kanban роутеры |
setup_knowledge_event_subscriptions() → создание коллекции + документов + reload RAG |
KnowledgeUpdated |
modules/knowledge/events.py |
FAQ CRUD в router_faq.py
|
setup_llm_event_subscriptions() → reload FAQ cache |
WidgetSessionCreated |
modules/channels/widget/events.py |
widget/router_public.py (первое сообщение) |
setup_crm_event_subscriptions() → создание лида amoCRM |
WidgetMessageSent |
modules/channels/widget/events.py |
widget/router_public.py (каждый ответ) |
setup_crm_event_subscriptions() → заметка в лиде amoCRM |
WidgetContactSubmitted |
modules/channels/widget/events.py |
widget/router_public.py (форма контактов) |
setup_crm_event_subscriptions() → контакт + привязка к лиду amoCRM |
BotProcessDied |
modules/channels/events.py |
watch_bot_processes() (periodic task, 30с) |
setup_channel_event_subscriptions() → аудит + auto-restart с backoff |
ConfigChanged |
modules/core/events.py |
ConfigService.set(), set_telegram(), set_widget()
|
on_config_changed → аудит-лог изменения конфига |
Реестр именованных фоновых задач с управлением жизненным циклом.
Два типа задач:
-
Периодические (
interval=N) — выполняются в цикле: run → sleep N секунд → repeat -
Одноразовые (
interval=None) — выполняются один раз и завершаются
Статусы: pending → running → completed | failed | cancelled
API:
| Метод | Описание |
|---|---|
register(name, coro_fn, *, interval=None, initial_delay=0) |
Регистрация задачи |
start_all() |
Создание asyncio.Task для каждой зарегистрированной задачи |
cancel_all(timeout=10.0) |
Отмена всех задач с ожиданием завершения |
list_tasks() → list[TaskInfo]
|
Информация о всех задачах |
Поведение при ошибках:
- Периодическая задача: ошибка логируется, задача продолжает работать
- Одноразовая задача: ошибка логируется, статус
failed -
CancelledError: статусcancelled, исключение пробрасывается (корректное завершение)
Пример:
from modules.core import TaskRegistry
registry = TaskRegistry()
async def cleanup_sessions():
# удаление просроченных сессий
...
async def send_welcome_email():
# одноразовая задача
...
registry.register("session-cleanup", cleanup_sessions, interval=3600)
registry.register("welcome-email", send_welcome_email, initial_delay=5)
await registry.start_all() # запуск всех задач
# ...
await registry.cancel_all() # graceful shutdownРеестр модульных health check-ов с агрегацией статусов.
Логика агрегации:
- Все
ok→ общий статусok - Хотя бы один
degraded→ общий статусdegraded - Хотя бы один
error→ общий статусerror
Защита: каждый check выполняется с индивидуальным timeout (asyncio.wait_for). Таймаут или исключение → error.
API:
| Метод | Описание |
|---|---|
register(name, check) |
Регистрация async health check |
check_all(timeout=5.0) → dict
|
Параллельный запуск всех проверок |
Формат результата check_all():
{
"status": "ok | degraded | error",
"checks": {
"database": {"status": "ok", "details": {"tables": 54}},
"redis": {"status": "degraded", "details": {"reason": "high latency"}}
}
}Пример:
from modules.core import HealthRegistry, HealthStatus
registry = HealthRegistry()
async def check_database():
# проверка подключения к БД
return HealthStatus(status="ok", details={"tables": 54})
async def check_redis():
# проверка Redis
return HealthStatus(status="ok", details={"connected": True})
registry.register("database", check_database)
registry.register("redis", check_redis)
result = await registry.check_all(timeout=5.0)
print(result["status"]) # "ok"54 SQLAlchemy модели разнесены из монолитного db/models.py по доменным файлам. db/models.py стал фасадом-реэкспортом (~200 строк).
modules/
├── core/models.py ← User, UserSession, Role, Workspace, SystemConfig, ...
├── chat/models.py ← ChatSession, ChatMessage, ChatShare
├── knowledge/models.py ← FAQEntry, KnowledgeDocument, KnowledgeCollection
├── channels/telegram/models.py ← BotInstance, TelegramSession
├── channels/whatsapp/models.py ← WhatsAppInstance
├── channels/widget/models.py ← WidgetInstance
├── kanban/models.py ← KanbanTask, KanbanTaskDependency, KanbanChecklistItem, ...
├── monitoring/models.py ← AuditLog, PaymentLog
├── llm/models.py ← CloudLLMProvider, LLMPreset
├── speech/models.py ← TTSPreset
├── crm/models.py ← AmoCRMConfig, AmoCRMSyncLog
├── ecommerce/models.py ← WooCommerceConfig
├── telephony/models.py ← GSMCallLog, GSMSMSLog
├── admin/models.py ← ResourceShare
├── claude_code/models.py ← ClaudeCodeConversation, ClaudeCodeProject
└── sales/models.py ← BotAgentPrompt, BotSegment, BotUserProfile, ...
31 менеджер-класс извлечён из db/integration.py в 15 доменных файлов. Именование: AsyncXManager → XService. Additive-only — db/integration.py пока не изменён.
| Модуль | Файл | Сервисы |
|---|---|---|
modules/core/ |
service.py |
DatabaseService, UserService, UserSessionService, RoleService, WorkspaceService, ConfigService, UserIdentityService
|
modules/chat/ |
service.py |
ChatService, ChatShareService
|
modules/knowledge/ |
service.py |
FAQService, KnowledgeDocService, KnowledgeCollectionService, GitHubRepoProjectService
|
modules/channels/telegram/ |
service.py |
BotInstanceService, TelegramSessionService
|
modules/channels/whatsapp/ |
service.py |
WhatsAppInstanceService |
modules/channels/widget/ |
service.py |
WidgetInstanceService |
modules/kanban/ |
service.py |
KanbanService, KanbanProjectService
|
modules/claude_code/ |
service.py |
ClaudeCodeService, ClaudeCodeProjectService
|
modules/llm/ |
service.py |
CloudProviderService |
modules/monitoring/ |
service.py |
AuditService, PaymentService
|
modules/admin/ |
service.py |
ResourceShareService |
modules/speech/ |
service.py, streaming.py
|
PresetService, StreamingTTSManager
|
modules/crm/ |
service.py |
AmoCRMService |
modules/ecommerce/ |
service.py |
WooCommerceService |
modules/telephony/ |
service.py |
GSMService |
Импорт: from modules.chat.service import chat_service (прямой, предпочтительный) или from db.integration import async_chat_manager (backward-compatible алиас). Синглтоны создаются в доменных service.py и импортируются фасадом.
Known issue — circular import: доменные __init__.py нельзя использовать для реэкспорта сервисов. Цепочка: db/models.py → modules/X/__init__.py → service.py → db/repositories → db/models.py (цикл). Сервисы импортируются напрямую: from modules.chat.service import ChatService. Будет решено в Phase 3+ (устранение eager imports в db/models.py).
Монолитный db/integration.py (2688 строк) заменён на ~100-строчный фасад. Файл импортирует классы и синглтоны из доменных модулей и реэкспортирует под старыми именами:
from modules.chat.service import ChatService as AsyncChatManager
from modules.chat.service import chat_service as async_chat_manager
# ... 29 классов, 30 синглтонов, 3 lifecycle-функцииНоль изменений в 28 файлах-потребителях.
30 синглтонов перенесены из фасада db/integration.py в доменные modules/*/service.py. Каждый сервисный файл создаёт экземпляр с чистым именем:
# modules/kanban/service.py
kanban_service = KanbanService()
kanban_project_service = KanbanProjectService()Фасад теперь импортирует готовые синглтоны вместо создания:
from modules.kanban.service import kanban_service as async_kanban_managerОба пути импорта ведут к одному объекту (async_kanban_manager is kanban_service).
6 «листовых» роутеров (без межроутерных зависимостей) перенесены из app/routers/ в доменные модули. Все импорты из db.integration заменены на прямые импорты из доменных сервисов. Оригинальные файлы стали тонкими фасадами (1-3 строки).
| Старый файл | Новый файл | Ключевые изменения |
|---|---|---|
app/routers/woocommerce.py |
modules/ecommerce/router.py |
4 db.integration → domain imports |
app/routers/amocrm.py |
modules/crm/router.py |
4 db.integration → domain imports, dual router (router + webhook_router) |
app/routers/gsm.py |
modules/telephony/router.py |
9 inline db.integration → 2 top-level domain imports |
app/routers/tts.py |
modules/speech/router_tts.py |
1 db.integration → domain import |
app/routers/stt.py |
modules/speech/router_stt.py |
Без изменений (0 db.integration imports) |
app/routers/services.py |
modules/speech/router_services.py |
Без изменений (0 db.integration imports) |
Замены импортов:
-
async_audit_logger→audit_serviceизmodules.monitoring.service -
async_knowledge_collection_manager→knowledge_collection_serviceизmodules.knowledge.service -
async_knowledge_doc_manager→knowledge_doc_serviceизmodules.knowledge.service -
async_woocommerce_manager→woocommerce_serviceизmodules.ecommerce.service -
async_amocrm_manager→amocrm_serviceизmodules.crm.service -
async_preset_manager→preset_serviceизmodules.speech.service -
async_config_manager(inline) →config_serviceизmodules.core.service -
async_gsm_manager(inline) →gsm_serviceизmodules.telephony.service
5 роутеров со сложными зависимостями перенесены в доменные модули. Все импорты из db.integration заменены на прямые импорты из доменных сервисов. Оригинальные файлы стали 1-строчными фасадами.
| Старый файл | Новый файл | Ключевые изменения |
|---|---|---|
app/routers/faq.py |
modules/knowledge/router_faq.py |
2 db.integration → domain imports |
app/routers/wiki_rag.py |
modules/knowledge/router_wiki_rag.py |
3 db.integration → domain imports |
app/routers/github_repos.py |
modules/knowledge/router_github_repos.py |
4 db.integration → domain imports |
app/routers/kanban.py |
modules/kanban/router.py |
5 top-level + 1 inline → 4 top-level domain imports |
app/routers/claude_code.py |
modules/claude_code/router.py |
8 inline → 2 top-level (claude_code_service, claude_code_project_service) |
Замены импортов:
-
async_audit_logger→audit_serviceизmodules.monitoring.service -
async_faq_manager→faq_serviceизmodules.knowledge.service -
async_knowledge_collection_manager→knowledge_collection_serviceизmodules.knowledge.service -
async_knowledge_doc_manager→knowledge_doc_serviceизmodules.knowledge.service -
async_github_repo_project_manager→github_repo_project_serviceизmodules.knowledge.service -
async_kanban_manager→kanban_serviceизmodules.kanban.service -
async_kanban_project_manager→kanban_project_serviceизmodules.kanban.service -
async_claude_code_manager→claude_code_serviceизmodules.claude_code.service -
async_claude_code_project_manager→claude_code_project_serviceизmodules.claude_code.service
Нюансы:
-
claude_code.py — все 8 inline
from db.integration importвнутри функций заменены на 2 top-level import из доменного сервиса - kanban.py — кросс-доменные зависимости: kanban → claude_code (cc-sessions), kanban → knowledge (dataset-sync/status/clear)
-
Lazy imports из
app.services.*иapp.dependenciesоставлены lazy (не часть Phase 3)
5 роутеров каналов и продаж перенесены в доменные модули. Все импорты из db.integration заменены на прямые импорты из доменных сервисов. Оригинальные файлы стали 1-строчными фасадами.
| Старый файл | Новый файл | Ключевые изменения |
|---|---|---|
app/routers/telegram.py (1031 строк) |
modules/channels/telegram/router.py |
6 db.integration → domain imports |
app/routers/whatsapp.py (488 строк) |
modules/channels/whatsapp/router.py |
3 db.integration → domain imports |
app/routers/widget.py (434 строк) |
modules/channels/widget/router.py |
4 db.integration → domain imports |
app/routers/bot_sales.py (1003 строк) |
modules/sales/router_bot_sales.py |
2 db.integration → domain imports |
app/routers/yoomoney_webhook.py (119 строк) |
modules/sales/router_yoomoney.py |
1 db.integration → domain import |
Замены импортов:
-
async_audit_logger→audit_serviceизmodules.monitoring.service -
async_bot_instance_manager→bot_instance_serviceизmodules.channels.telegram.service -
async_config_manager→config_serviceизmodules.core.service -
async_payment_manager→payment_serviceизmodules.monitoring.service -
async_resource_share_manager→resource_share_serviceизmodules.admin.service -
async_telegram_manager→telegram_session_serviceизmodules.channels.telegram.service -
async_whatsapp_instance_manager→whatsapp_instance_serviceизmodules.channels.whatsapp.service -
async_widget_instance_manager→widget_instance_serviceизmodules.channels.widget.service
Нюансы:
-
bot_sales.py — 13 прямых импортов из
db.repositories.*оставлены как есть (неdb.integration, вне скоупа Phase 3) -
yoomoney_webhook.py — 1 прямой импорт
BotInstanceRepositoryоставлен как есть -
Lazy imports (
app.services.yoomoney_service,fastapi.responses.HTMLResponse) оставлены lazy -
Non-db imports (
multi_bot_manager,whatsapp_manager,app.cors_middleware) оставлены без изменений
6 роутеров core/admin перенесены в доменные модули. Все импорты из db.integration заменены на прямые импорты из доменных сервисов. Оригинальные файлы стали 1-строчными фасадами.
| Старый файл | Новый файл | Ключевые изменения |
|---|---|---|
app/routers/auth.py (182 строки) |
modules/core/router_auth.py |
3 db.integration → domain imports |
app/routers/roles.py (139 строк) |
modules/core/router_roles.py |
2 db.integration → domain imports |
app/routers/workspace.py (273 строки) |
modules/core/router_workspace.py |
4 db.integration → domain imports |
app/routers/backup.py (189 строк) |
modules/admin/router_backup.py |
Чистый перенос (0 db.integration imports) |
app/routers/legal.py (637 строк) |
modules/admin/router_legal.py |
1 inline db.integration → domain import |
app/routers/github_webhook.py (317 строк) |
modules/admin/router_github_webhook.py |
1 inline db.integration → domain import |
Замены импортов:
-
async_audit_logger→audit_serviceизmodules.monitoring.service -
async_session_manager→user_session_serviceизmodules.core.service -
async_user_manager→user_serviceизmodules.core.service -
async_role_manager→role_serviceизmodules.core.service -
async_workspace_manager→workspace_serviceизmodules.core.service -
async_kanban_project_manager→kanban_project_serviceизmodules.kanban.service
Нюансы:
-
backup.py — 0 импортов из
db.integration, используетapp.services.backup_service— чистый перенос без изменений -
legal.py — 1 inline
from db.integration import async_audit_loggerвнутриadmin_gdpr_delete_data, заменён наfrom modules.monitoring.service import audit_service -
github_webhook.py — 1 inline
from db.integration import async_kanban_project_managerвнутри_handle_issue_event, заменён наfrom modules.kanban.service import kanban_project_service
Последние 5 роутеров перенесены в доменные модули. Все импорты из db.integration заменены на прямые импорты из доменных сервисов. Все 28 роутеров мигрированы — Phase 3 завершена.
| Старый файл | Новый файл | Ключевые изменения |
|---|---|---|
app/routers/audit.py (126 строк) |
modules/monitoring/router_audit.py |
Чистый перенос (0 db.integration imports) |
app/routers/usage.py (301 строка) |
modules/monitoring/router_usage.py |
Чистый перенос (0 db.integration imports) |
app/routers/monitor.py (246 строк) |
modules/monitoring/router_monitor.py |
Чистый перенос (0 db.integration imports) |
app/routers/chat.py (1176 строк) |
modules/chat/router.py |
7 db.integration → domain imports |
app/routers/llm.py (1633 строки) |
modules/llm/router.py |
2 db.integration → domain imports |
Замены импортов (9):
-
async_chat_manager→chat_serviceизmodules.chat.service -
async_chat_share_manager→chat_share_serviceизmodules.chat.service -
async_bot_instance_manager→bot_instance_serviceизmodules.channels.telegram.service -
async_whatsapp_instance_manager→whatsapp_instance_serviceизmodules.channels.whatsapp.service -
async_widget_instance_manager→widget_instance_serviceизmodules.channels.widget.service -
async_cloud_provider_manager→cloud_provider_serviceизmodules.llm.service -
async_knowledge_collection_manager→knowledge_collection_serviceизmodules.knowledge.service -
async_audit_logger→audit_serviceизmodules.monitoring.service
Нюансы:
-
audit.py, usage.py, monitor.py — 0 импортов из
db.integration, чистый перенос - chat.py — SSE streaming + RAG логика, 7 кросс-доменных импортов (telegram, whatsapp, widget, knowledge, llm)
-
llm.py — крупнейший роутер (1633 строки, 37 routes), inline
db.repositories/db.databaseимпорты оставлены как есть -
monitor.py — условная регистрация в cloud mode, условие остаётся в
orchestrator.py
Класс StreamingTTSManager (220 строк) извлечён из orchestrator.py в modules/speech/streaming.py. Менеджер обеспечивает параллельный синтез TTS во время streaming LLM: накапливает текст, разбивает на предложения, синтезирует в ThreadPoolExecutor, кэширует склеенное аудио.
Ключевые решения:
-
numpy— lazy import внутри_cache_full_audio()(избегает ошибок импорта в cloud mode без GPU) - Глобальная переменная
streaming_tts_managerперенесена вServiceContainer(Phase 4.7b) -
synthesize_with_current_voice()перенесена вmodules/compat/router.pyв Phase 4.3 (переписана наget_container()) - 5 неиспользуемых импортов удалены из orchestrator (
hashlib,re,OrderedDict,ThreadPoolExecutor,numpy)
Результат: orchestrator.py: 4287 → 4062 строк (−225)
6 публичных (без auth) widget endpoints + 4 helper-функции извлечены из orchestrator.py в modules/channels/widget/router_public.py.
Endpoints:
-
GET /widget.js— динамическая генерация JS-скрипта -
GET /widget/status— проверка enabled -
POST /widget/chat/session— создание сессии -
GET /widget/chat/session/{id}— получение с историей -
POST /widget/chat/session/{id}/stream— SSE стриминг ответа -
POST /widget/chat/session/{id}/contacts— контакты → amoCRM
Ключевые решения:
- Импорты модернизированы:
async_*_manager→ domain singletons (widget_instance_service,chat_service,config_service,cloud_provider_service,user_identity_service) - amoCRM helpers оставлены lazy (try/except) для graceful degradation
- Widget path:
Path(__file__).parents[3](учитывает глубину модуля) - 4 неиспользуемых импорта удалены из orchestrator
Результат: orchestrator.py: 4062 → 3544 строк (−518)
Извлечены legacy telephony, OpenAI-compatible и core health endpoints из orchestrator.py.
modules/compat/router.py (~510 строк):
- 5 Pydantic-моделей (
ConversationRequest,TTSRequest,OpenAISpeechRequest,ChatMessage,ChatCompletionRequest) -
synthesize_with_current_voice()— хелпер выбора TTS-движка (переписан наget_container()) - 5 legacy telephony endpoints:
POST /tts,POST /stt,POST /chat,POST /process_call,POST /reset_conversation - 4 OpenAI-compatible endpoints:
GET /v1/models,GET /v1/voices,POST /v1/audio/speech,POST /v1/chat/completions
modules/core/router_health.py (~110 строк):
-
GET /— root status -
GET /health— полная диагностика (services, database, internet monitor, streaming TTS stats) -
GET /admin/deployment-mode— текущий профиль
Ключевые решения:
- Все обращения к глобальным переменным (
voice_service,llm_service,stt_serviceи т.д.) заменены наget_container()→container.* - Удалены 4 мёртвых дубликата (3 auth endpoints +
/admin/status), которые были перекрыты роутерами, зарегистрированными раньше -
DEPLOYMENT_MODEчитается изos.getenv()в каждом модуле (без зависимости от глобала в orchestrator)
Результат: orchestrator.py: 3544 → 2875 строк (−669)
Извлечены все LLM и TTS finetune endpoints из orchestrator.py.
modules/llm/router_finetune.py (~260 строк):
- 4 Pydantic-модели (
DatasetProcessRequest,GenerateProjectDatasetRequest,FinetuneConfigRequest,AdapterRequest) - 17 эндпоинтов: dataset (upload, process, config, stats, list, augment, generate-project), training (config, start, stop, status, log), adapters (list, activate, delete)
- Prefix:
/admin/finetune
modules/speech/router_finetune.py (~150 строк):
- 13 эндпоинтов: config, samples (list, upload, delete, transcript), transcribe, prepare, processing-status, train (start, stop, status, log), models
- Prefix:
/admin/tts-finetune -
tts_finetune_manager— optional import (try/except)
Ключевые решения:
- Оба роутера регистрируются только при
DEPLOYMENT_MODE != "cloud"(GPU-only), внутри существующегоif-блока - Эндпоинты не зависят от глобальных сервисов (
voice_service,llm_service) — работают через свои менеджеры - Удалены из orchestrator:
get_finetune_manager,get_tts_finetune_manager,TTS_FINETUNE_AVAILABLE,File,UploadFile
Результат: orchestrator.py: 2875 → 2471 строк (−404)
Извлечены ВСЕ оставшиеся inline-эндпоинты из orchestrator.py. После этой фазы в файле ноль @app.get/post/put/delete — остались только @app.on_event("startup") и @app.on_event("shutdown").
modules/speech/router_voices.py (~205 строк):
-
VoiceRequestPydantic-модель - 4 эндпоинта:
GET /admin/voices,GET /admin/voice,POST /admin/voice,POST /admin/voice/test - Все используют
get_container()вместо глобальных переменных
modules/llm/router_models.py (~113 строк):
- 10 эндпоинтов: list, scan (start/cancel/status), download (start/cancel/status), delete, search, details
- Prefix:
/admin/models - Использует
get_model_manager()
modules/monitoring/router_logs.py (~53 строки):
- 3 эндпоинта: list, read, stream (SSE)
- Prefix:
/admin/logs - SSE streaming использует
require_permission("system", "view")
Что удалено из orchestrator.py:
- 52 inline-эндпоинта (35 мёртвых дубликатов + 17 уникальных)
- 18 Pydantic-моделей (все продублированы в модульных роутерах)
-
get_current_tts_service()helper - 16 неиспользуемых импортов
Ключевые решения:
-
router_voices.pyиrouter_models.pyрегистрируются внутриif DEPLOYMENT_MODE != "cloud"(GPU-only) -
router_logs.pyрегистрируется безусловно (доступен во всех режимах) - 35 из 52 эндпоинтов были мёртвыми дубликатами (shadowed модульными роутерами, зарегистрированными ранее)
Результат: orchestrator.py: 2471 → 1121 строк (−1350). Содержит: импорты, middleware, регистрацию роутеров, startup/shutdown lifecycle, раздачу статических файлов.
Перенос 6 фоновых задач из asyncio.create_task() в TaskRegistry (инфраструктура Phase 0).
Новые файлы:
| Файл | Задачи | Тип |
|---|---|---|
modules/core/maintenance.py |
cleanup_expired_sessions (1ч), periodic_vacuum (7д, initial_delay=24ч) |
periodic |
modules/knowledge/tasks.py |
build_wiki_embeddings, load_collection_indexes
|
one-shot |
modules/kanban/tasks.py |
sync_kanban_issues (15мин, initial_delay=60с) |
periodic |
modules/ecommerce/tasks.py |
woocommerce_daily_sync (ежедневно 23:00 UTC) |
one-shot с внутренним cron |
Ключевые решения:
- WooCommerce sync зарегистрирован как one-shot (управляет своим расписанием внутри), т.к. TaskRegistry не поддерживает cron
- Wiki RAG задачи используют
functools.partial(fn, wiki_rag)для передачи сервиса - Core maintenance в отдельном
maintenance.py(неtasks.py), чтобы не путать с инфраструктурой TaskRegistry -
task_registry.cancel_all()добавлен вshutdown_event()для graceful shutdown
Результат: orchestrator.py: 1121 → 1030 строк (−91). Удалены asyncio, datetime, timedelta из импортов.
Извлечение 8 helper-функций из orchestrator.py в доменные startup.py модули + добавление graceful shutdown.
Новые файлы:
| Файл | Функции |
|---|---|
modules/core/startup.py |
seed_system_roles(), seed_default_workspace()
|
modules/llm/startup.py |
get_or_create_default_gemini_provider(), auto_start_bridge()
|
modules/channels/telegram/startup.py |
auto_start_bots() |
modules/channels/whatsapp/startup.py |
auto_start_bots() |
modules/knowledge/startup.py |
reload_llm_faq(container) |
modules/speech/startup.py |
reload_voice_presets(container) |
Ключевые решения:
-
reload_llm_faqиreload_voice_presetsпринимаютcontainerкак аргумент (вместо глобальных переменных), вызываются после заполнения контейнера - Graceful shutdown в
shutdown_event():multi_bot_manager.stop_all(),whatsapp_manager.stop_all(),bridge_manager.stop()— каждый в try/except - 5 неиспользуемых импортов удалены из
db.integration
Результат: orchestrator.py: 1030 → 805 строк (−225, −22%).
Вынос всей inline-инициализации сервисов из startup_event() в доменные startup-модули. Удаление 8 глобальных переменных — ServiceContainer стал единственным источником правды.
Новые/расширенные файлы:
| Файл | Функции |
|---|---|
modules/speech/startup.py |
init_tts_services(deployment_mode), init_stt_service(), init_streaming_tts_manager()
|
modules/llm/startup.py |
init_llm_service(llm_backend) → (service, backend), create_llm_switch_callback(container)
|
modules/knowledge/startup.py |
init_wiki_rag(container, deployment_mode, task_registry) |
modules/core/startup.py |
init_internet_monitor(container, deployment_mode), check_legacy_files(), graceful_shutdown()
|
modules/telephony/startup.py |
init_gsm_services(container, deployment_mode) |
Ключевые решения:
-
_switch_llmcallback переделан в фабрику замыканийcreate_llm_switch_callback(container)— записывает только вcontainer.llm_serviceиos.environ["LLM_BACKEND"], глобальные переменные не используются - Optional imports (
VLLM_AVAILABLE,PIPER_AVAILABLE,XTTS_AVAILABLE,OPENVOICE_AVAILABLE) перенесены в соответствующие domain startup модули -
init_tts_services()возвращает dict с ключами, совпадающими с атрибутамиServiceContainer -
init_llm_service()возвращает кортеж(service, updated_backend)для обновленияos.environ
Удалённые глобальные переменные: voice_service, anna_voice_service, piper_service, openvoice_service, stt_service, llm_service, streaming_tts_manager, current_voice_config.
Результат: orchestrator.py: 805 → 321 строку (−60%). Чистый wiring: импорты, middleware, регистрация роутеров, вызовы доменных init-функций, static files.
Первая реализация EventBus-паттерна в продакшене: инфраструктура (singleton в контейнере, setup_event_subscriptions) + два реальных события.
Инфраструктура:
-
event_bus: EventBusдобавлен вServiceContainer(app/dependencies.py) -
setup_event_subscriptions(event_bus)вmodules/core/startup.py— вызывается вorchestrator.pystartup, регистрирует обработчики -
InternetMonitorтеперь получаетcontainer.event_bus(раньшеNone) -
ConnectivityStatusиInternetStatusChangedперенесены изinternet_monitor.pyвmodules/core/events.py
События:
| Событие | Издатель | Обработчик |
|---|---|---|
UserRoleChanged |
WorkspaceService.update_member_role() |
_member_role_cache.invalidate_user() + revoke_all_user_sessions()
|
SessionRevoked |
UserService.update_password(), set_role(), set_active(), WorkspaceService.remove_member()
|
_member_role_cache.invalidate_user() + revoke_all_user_sessions()
|
Что убрано:
- Прямые вызовы
_member_role_cache.invalidate_user()иrevoke_all_user_sessions()изrouter_workspace.py - Прямой вызов
auth_manager.revoke_all_user_sessions()изUserService._revoke_user_sessions() - Метод
UserService._revoke_user_sessions()заменён на_publish_session_revoked()
Конвенция для следующих Phase 5.x:
- События определяются в
modules/{domain}/events.py(илиmodules/core/events.pyдля core) - Подписки регистрируются в
setup_events()функции вstartup.pyдомена - Издатель получает bus через
get_container().event_bus(lazy import)
Развязка доменов knowledge и llm: FAQ-роутер публикует KnowledgeUpdated событие, LLM-домен подписывается и перезагружает FAQ-кеш.
Новый файл: modules/knowledge/events.py
@dataclass
class KnowledgeUpdated(BaseEvent):
kind: str = "" # "faq", "wiki", "collection"
action: str = "" # "created", "updated", "deleted", "reloaded"
item_id: int | None = None| Событие | Издатель | Обработчик |
|---|---|---|
KnowledgeUpdated(kind="faq") |
FAQ CRUD в router_faq.py (add, update, delete, reload) |
setup_llm_event_subscriptions() → reload FAQ cache в LLM service |
Ключевые решения:
- Startup FAQ reload (
modules/knowledge/startup.py:reload_llm_faq) остаётся прямым вызовом — надёжность при инициализации - Обработчик живёт в
modules/llm/startup.py(рядом с LLM логикой), вызывается изsetup_event_subscriptions() -
/reloadи/testendpoints по-прежнему читаютcontainer.llm_serviceнапрямую (view-layer, не coupling) - Event generic — в будущем wiki/collection updates могут переиспользовать
KnowledgeUpdated
Новое событие DatasetSynced в modules/core/events.py развязывает CRM, ecommerce и kanban от knowledge-домена.
@dataclass
class DatasetSynced(BaseEvent):
dataset_name: str = ""
file_path: str = ""
record_count: int = 0
source_module: str = ""| Событие | Издатель | Обработчик |
|---|---|---|
DatasetSynced |
modules/crm/router.py, modules/ecommerce/router.py, modules/ecommerce/sync.py, modules/kanban/router.py
|
setup_knowledge_event_subscriptions() → создание коллекции, документов, reload RAG |
DatasetCleared (= DatasetSynced с record_count=0) |
те же роутеры (clear endpoints) |
setup_knowledge_event_subscriptions() → удаление документов/коллекции, reload RAG |
Ключевые решения:
- Издатели сохраняют файл на диск и публикуют событие — knowledge-обработчик забирает файл
-
DatasetCleared— тот жеDatasetSyncedсrecord_count=0и пустымfile_path - Knowledge handler в
modules/knowledge/startup.py— создаёт коллекцию, парсит файл, создаёт документы
Вся amoCRM-логика (~150 строк) перенесена из widget/router_public.py в CRM-домен. Виджет теперь публикует 3 события; CRM подписывается и реагирует.
Новый файл: modules/channels/widget/events.py
@dataclass
class WidgetSessionCreated(BaseEvent):
session_id: str = ""
first_message: str = ""
visitor_metadata: dict = field(default_factory=dict)
@dataclass
class WidgetMessageSent(BaseEvent):
session_id: str = ""
lead_id: int = 0
user_message: str = ""
assistant_response: str = ""
@dataclass
class WidgetContactSubmitted(BaseEvent):
session_id: str = ""
contact_name: str = ""
phone: str = ""
email: str = ""
visitor_metadata: dict = field(default_factory=dict)Обработчик: setup_crm_event_subscriptions() в modules/crm/startup.py
| Событие | Издатель | Обработчик |
|---|---|---|
WidgetSessionCreated |
widget/router_public.py (первое сообщение, fire-and-forget через asyncio.create_task) |
Создание лида amoCRM + заметка с metadata (IP, UTM, referrer) |
WidgetMessageSent |
widget/router_public.py (после каждого ответа AI, fire-and-forget) |
Добавление заметки к лиду (user + AI текст) |
WidgetContactSubmitted |
widget/router_public.py (форма контактов, await) |
Создание контакта amoCRM + привязка к лиду (или создание нового лида) |
Ключевые решения:
-
WidgetSessionCreatedиWidgetMessageSentпубликуются черезasyncio.create_task()(fire-and-forget) — не блокируют SSE-стрим -
WidgetContactSubmittedпубликуется черезawait— синхронно, но endpoint возвращает{"ok": True}без CRM-статуса - CRM-обработчики используют lazy import
from app.services import amocrm_serviceи оборачивают всё в try/except - Удалены:
_save_amocrm_lead_id(),_widget_create_amocrm_lead(),_widget_add_note_to_lead()из router_public.py
Новый функционал — периодический watcher-task обнаруживает завершённые бот-процессы и реагирует через EventBus.
Новый файл: modules/channels/events.py
@dataclass
class BotProcessDied(BaseEvent):
channel: str = "" # "telegram" | "whatsapp"
instance_id: str = ""
exit_code: int | None = None
uptime_seconds: float = 0.0| Компонент | Описание |
|---|---|
| Watcher |
watch_bot_processes() в modules/channels/startup.py, TaskRegistry interval=30s |
| Аудит |
_audit_bot_death() → AuditService.log(action="process_died")
|
| Auto-restart |
_auto_restart_bot() — backoff 10/20/30с, max 3 попытки, сброс при uptime > 60с |
| Graceful stop | exit_code=0 → только логирование, перезапуск не запускается |
При изменении глобального конфига через ConfigService публикуется ConfigChanged, подписчики применяют настройки без рестарта.
@dataclass
class ConfigChanged(BaseEvent):
key: str = ""
value: Any = None
previous_value: Any = None
namespace: str = "" # первый сегмент ключа: "widget", "telegram", "tts"| Компонент | Описание |
|---|---|
| Publisher |
ConfigService.set(), set_telegram(), set_widget() — после session.commit()
|
| Namespace | Производный из ключа (widget.colors.primary → widget) для фильтрации |
| Аудит |
on_config_changed → AuditService.log(action="config_changed") с previous/new value |
| Область | Только глобальная таблица config. Инстанс-настройки (telegram_instances, widget_instances) — через доменные API |
Protocol-классы описывают идеальные (целевые) контракты сервисов — архитектурный north star. Текущие реализации пока не соответствуют Protocol-ам; приведение к ним — задача Phase 7 (#641).
TypedDict-схемы описывают идеальные формы данных (не текущие to_dict()). Обратно совместимы с dict — существующий код session["id"] продолжает работать.
| Protocol | Файл | Ключевые методы |
|---|---|---|
| KnowledgeService | modules/knowledge/protocols.py |
search(), retrieve_context(), get_collections(), sync_documents(), find_faq_answer()
|
| LLMService | modules/llm/protocols.py |
generate(), stream(), resolve_backend(), list_providers()
|
| ChatService | modules/chat/protocols.py |
create_session(), send_message(), stream_message(), get_history(), share_session()
|
| AuthService | modules/core/protocols.py |
authenticate(), validate_token(), get_permissions(), has_permission(), get_roles()
|
| Домен | Файл | Типы |
|---|---|---|
| Knowledge | modules/knowledge/schemas.py |
SearchResult, CollectionInfo, DocumentInfo, SyncResult, FAQEntryInfo
|
| LLM | modules/llm/schemas.py |
LLMConfig, ProviderInfo, LLMParams, StreamChunk, ToolCall, TokenUsage
|
| Chat | modules/chat/schemas.py |
SessionInfo, SessionSummary, MessageInfo, StreamChunk, ShareInfo
|
| Core | modules/core/schemas.py |
UserInfo, TokenInfo, LoginResult, PermissionMap, RoleInfo, WorkspaceInfo, WorkspaceMemberInfo
|
- Все Protocol-ы помечены
@runtime_checkableдля поддержкиisinstance() - Импорты schema-типов в Protocol-файлах — через
TYPE_CHECKINGблок (требование ruff TCH) - Все методы Protocol-ов —
async -
total=Falseна LLMConfig и StreamChunk (все поля опциональны)
Unit-тесты для core-инфраструктуры и EventBus-событий:
pytest tests/unit/test_event_bus.py tests/unit/test_event_subscriptions.py tests/unit/test_knowledge_events.py tests/unit/test_dataset_synced.py tests/unit/test_widget_crm_events.py tests/unit/test_bot_process_watcher.py tests/unit/test_config_changed.py tests/unit/test_task_registry.py tests/unit/test_health_registry.py -v| Файл | Тестов | Что покрывает |
|---|---|---|
test_event_bus.py |
11 | publish/subscribe, error isolation, type filtering, clear, UserRoleChanged, SessionRevoked |
test_event_subscriptions.py |
3 | setup_event_subscriptions wiring — cache invalidation + session revocation via events |
test_knowledge_events.py |
3 | KnowledgeUpdated → FAQ reload, non-faq ignored, event fields |
test_dataset_synced.py |
6 | DatasetSynced → collection + docs, reuse collection, DatasetCleared, event fields |
test_widget_crm_events.py |
7 | WidgetSessionCreated → lead, skip disabled, skip no config, note, contact+link, contact+new lead, event fields |
test_bot_process_watcher.py |
13 | BotProcessDied detection, audit, auto-restart, backoff, max retries, graceful skip |
test_config_changed.py |
9 | ConfigChanged fields, namespace, publish from set(), audit subscriber, idempotency, error isolation |
test_task_registry.py |
8 | periodic/one-shot, cancel, errors, initial_delay, list |
test_health_registry.py |
8 | aggregation (ok/degraded/error), timeout, exceptions |
test_protocols.py |
28 | TypedDict schema construction, Protocol importability, method signature verification |
Полный план: issue #489 Стратегия: Strangler Fig — новый код рядом со старым, постепенная миграция импортов, старые файлы → фасады-реэкспорты.
| Фаза | Описание | Issue | Статус |
|---|---|---|---|
| 0 | Инфраструктура core (EventBus, TaskRegistry, HealthRegistry) | #490 | ✅ Завершена |
| 1 | Разделение db/models.py → доменные модули |
#491 | ✅ Завершена |
| 2 | Разделение db/integration.py → доменные сервисы + фасад |
#492 | ✅ Завершена (#501, #502, #503) |
| 3 | Перенос роутеров в доменные модули | #493 | ✅ Завершена (#508 ✅, #509 ✅, #510 ✅, #511 ✅, #512 ✅, #513 ✅, #514) |
| 4 | Декомпозиция orchestrator.py
|
#494 | ✅ Завершена (4.1–4.7b) |
| 5 | Внедрение EventBus-событий | #495 | ✅ Завершена (5.1–5.6) |
| 6 | Протокольные интерфейсы (Protocol + TypedDict) | #496 | ✅ Завершена |
| 7 | Service facade migration — приведение к Protocol-ам | #641 | ⏳ (7.1–7.4) |
-
Alembic-миграции —
db/models.pyостаётся как фасад-реэкспорт, чтобы не ломатьfrom db.models import ...в существующих миграциях -
SQLAlchemy Base — единый
Baseизdb/database.pyдля всех доменных моделей (create_all, relationships, autogenerate) -
Cross-domain FK —
workspace_id,owner_id → User.id— нормальная зависимость "все зависят от core", FK сохраняются - Параллельная разработка — каждую фазу делает одна машина, мелкие PR с чёткими границами
← Database | Deployment-Profiles →