Источник:
smarty-code/smarty-backend-stable/smarty-dialog/docs/ARCHITECTURE.md
Технический обзор подсистемы диалогов и AI-ботов Smarty CRM.
Документация актуальна на 2026-06-13.
smarty-dialog — подсистема CRM, отвечающая за диалоги (internal/external/messenger) и AI-ботов (employee bots). Бот — сотрудник CRM с ролью type: 'bot', подключённый к LLM-провайдеру (OpenAI, Anthropic, Google, DeepSeek, Smarty, кастомный). Подсистема обрабатывает входящие сообщения, управляет tool-calling циклом (plan → execute → respond), инжектирует контекст (RAG, session memory, hidden prompts), обеспечивает безопасность через многоуровневые гарды и предоставляет HTTP + RMQ интерфейсы для интеграции.
service.js — HTTP-сервисПоднимает HTTP-эндпоинты диалогов. Регистрирует маршруты из routes/ на config.host.dialog:config.port.dialog. Минимальная обёртка над микросервисным фреймворком.
worker.js — RMQ-воркерЗапускает два очереди:
dialog-events — обработка событий диалогов (новые сообщения, архив, чтение, изменения участников и т.д.). Карта событий — events/index.js.agent-task-events — обработка событий субагентов (agent.task.queued, agent.task.message_in_dialog, agent.task.tick). Карта — agentTaskWorker.js.Дополнительно запускает timeout cron (каждые 30 сек) для принудительного завершения просроченных задач субагентов.
lib/employeeBots/index.js — Публичный APIЭкспортирует Responder — основной обработчик ответов ботов. Используется внешними вызывающими (events, routes).
lib/employeeBots/agentLoop.js — Ядро агентского циклаКлючевой модуль. Реализует plan → execute → respond loop с tool-calling. Параметры: bot, provider, messages, tools, dialogId, streamHandler. Ограничения: MAX_ITERATIONS = 25, PLANNER_HARD_CAP = 30. Planner использует upgraded модель (gpt-4.1 / claude-opus-4 по умолчанию).
lib/employeeBots/agentTaskWorker.js — RMQ-хендлеры субагентовТри типа событий:
| Событие | Действие |
|---|---|
agent.task.queued |
Запуск runSpecialistAgent или runInlineSubagent |
agent.task.message_in_dialog |
Пробуждение субагента по новому сообщению в assigneeDialogId |
agent.task.tick |
Проверка deadline → force-finalize просроченных задач |
lib/employeeBots/botWorkerEvents.js — RMQ-хендлер CRM-триггеровОбработчик события crm-trigger из RabbitMQ. Ключевая архитектура:
SET NX EX) — защита от дублей при redelivery. Двухфазный: inflight (30 сек) → after-fire (5 мин) или release.MAX_CONCURRENT = 8) — ограничение параллельных agentLoop.setImmediate.Responder.js — Оркестратор обработки сообщенийЦентральный модуль. Два основных entry-point:
handleMessageForBot — обработка нового сообщения в диалоге. Определяет, должен ли бот отвечать (mention, private chat, _nextRecipients), загружает контекст, запускает handleBotReply.handleTrigger — обработка CRM-триггера (без привязки к диалогу). Строит контекст из события, запускает agentLoop.Внутри handleBotReply:
encryptedApiKey → apiToken).Важно: PROVIDERS_WITH_OWN_MEMORY (smarty_custom, custom) пропускают session memory read/write и автосуммаризацию — их эндпоинт управляет памятью самостоятельно.
toolLoader.js — Кэш tool-определенийКэширует результат getToolsForBot(bot) в Redis (TTL 5 мин) + in-process Map. Инвалидация по событию bot-update.
tools/definitions.js — Каталог инструментовПолный массив TOOLS — ~60 инструментов. Каждый элемент:
{
name: 'create_contact', // имя tool
nameRu: 'Создать контакт', // RU-название (для UI)
requiredPermission: 'create_contacts', // permission gate
sideEffect: 'destructive', // optional: 'external' | 'destructive' → highImpactGate
definition: { type: 'function', function: { name, description, parameters } } // OpenAI function-calling schema
}
Категории инструментов:
| Категория | Инструменты |
|---|---|
| Навыки | use_skill |
| Структура WS | list_groups, list_stages, list_custom_fields, create_group, delete_group |
| Контакты | search_contacts, create_contact, update_contact, archive_contact, restore_contact, add_responsibles, add_subscribers |
| События | search_tasks, create_task |
| Проекты | search_projects, create_project, search_supplier_catalogs, negotiate_with_suppliers, record_supplier_response |
| Заметки | search_notes, create_note |
| Задачи | search_assignments, create_assignment |
| Заявки | search_requisitions, create_requisition |
| Цели | search_goals, create_goal |
| Служебные записки | create_service_note, make_decision |
| Универсальные | update_object, delete_object, get_object, move_to_stage, link_objects, unlink_objects, archive_object, restore_object |
| Диалоги | create_dialog, find_dialogs, send_message_to_dialog, add_participant_to_dialog, get_current_user, list_chat_bots |
| Сотрудники | search_employees |
| Метки | list_marks, add_mark, remove_mark |
| Комментарии | add_comment |
| Ответственные | add_object_responsibles, remove_responsibles, add_object_subscribers, remove_subscribers |
| Custom fields | create_custom_field, add_field_to_template |
| Кросс-WS | search_workspaces, search_bots_in_workspace, create_external_dialog |
| Субагенты | spawn_dialog_subagent, call_subagent |
| Файлы | generate_image, read_document, search_files, generate_docx, search_knowledge_base |
| Виджет | issue_widget_token |
| Approval | check_message_action |
tools/executor.js — Исполнитель tool-callСодержит getToolsForBot(bot) (permission gate + формирование массива определений) и execute(bot, toolCall, context) (диспетчеризация по имени tool → handler). Ключевые механизмы:
requiredPermission через permissionChecker.hasPermission.({24hex|Имя}) → 24hex в аргументах.requireDialogContext — блокирует dialog-only tools в extchat.[имя], X руб, ___, TBD → reject (бот не заполнил данные).tools/highImpactGate.js — Политика для опасных инструментовГейт для tools с sideEffect: 'external' или 'destructive'. Три режима (env SMARTY_HIGH_IMPACT_GATE):
| Режим | Поведение |
|---|---|
off |
Всё разрешено |
warn (default) |
Warn-лог, не блокирует |
enforce |
Блокирует если tool нет в botSettings.highImpactAllowlist |
providers/ — LLM-провайдерыРеестр провайдеров (providers/index.js): Map по имени. Загружается при старте. Провайдеры:
| Файл | Провайдер | Особенности |
|---|---|---|
openai.js |
OpenAI | function calling, streaming, prompt caching |
anthropic.js |
Anthropic | tool_use, streaming, explicit cache_control |
google.js |
Gemini API | |
deepseek.js |
DeepSeek | OpenAI-совместимый |
smarty.js |
Smarty | внутренний провайдер |
smarty_custom.js |
Custom connection | Пользовательский endpoint. Два режима: OpenAI-совместимый (tools pass through) и Raw template ({{placeholders}}). SSRF-guard на URL. |
Каждый провайдер реализует:
send({ botSettings, messages, tools, tool_choice }) → { text, tool_calls, finish_reason, usage }sendStream({ botSettings, messages }) → async generator (опционально)isConfigured() → booleangetModels() → string[] (опционально)rag/ — RAG (Retrieval-Augmented Generation)| Модуль | Роль |
|---|---|
indexer.js |
Write-path: parse → chunk → embed → upsert → bump KB version. Поддержка PDF/DOCX/XLSX/CSV/TXT/MD/JSON/XML + OCR. |
chunker.js |
Разбиение документов на чанки |
embedder.js |
Генерация векторных эмбеддингов |
retriever.js |
Read-path: cosine similarity over Mongo bot_kb_chunk. L1 in-process cache (VersionedLru, invalidated by Redis bot:kb:version:{kbId}). |
injector.js |
Автоинжект top-K excerpts в messages перед LLM-запросом |
responseCache.js |
L2 exact response cache (Redis, по botId+kbVersion+userId+query+sysPrompt) |
ocr.js |
OCR для изображений в документах |
versionedLru.js |
In-process LRU с version-based invalidation |
sessionMemory.js — Сессионная память ботовShared-scope память. Scope = (scopeType, scopeId):
| scopeType | Описание |
|---|---|
project |
Все dialogs+triggers одного проекта |
dialog |
Отдельный диалог (DM, общий чат) |
workspace |
Orphan triggers без привязки к проекту |
trigger |
Изолированная память триггера |
subagent |
Эфемерная память субагента |
extchat |
Память для мессенджер/виджет чатов |
Хранение: Redis (hot cache, TTL 30 мин) + Mongo (bot_sessions, persistent). Concurrency: tryLock/releaseLock per scope через Redis SETNX (30 сек timeout).
hiddenSystemPrompt.js — Скрытый системный промптМодульный промпт, инжектируемый по permissions/tools бота. Фрагменты:
({24hex|Имя}), @{24hex|Имя}).permissionChecker.js — Проверка правДвусторонний мост между legacy-ключами (read_contacts, write_contacts) и гранулярными (view_contacts, create_contacts, edit_contacts, delete_contacts). Инвариант: пустые permissions = «не настроено» = разрешаем (backward-compat). Конфигурируемые pseudo-permissions (напр. use_knowledge_base — наличие KB).
fabricationGuard.js — R1 Слой 3: блокировка сфабрикованных IDРеестр легитимных ObjectId из контекста (messages, context, bot._id, _wsId) + результатов tool-call. Перед исполнением каждого tool-call проверяет: 24-hex ID в аргументах, отсутствующие в реестре → блокировка (enforce) или warn. Режим через SMARTY_FABRICATION_GUARD env.
outputFilter.js — Очистка выводаstripInternalFields — убирает ObjectId hex, JSON-дампы с _id/_wsId, служебные поля.stripToolCallEcho — убирает tool_name({...}) из текста (слабые модели эхо-ят tool-calls как текст).stripInternalJargon — убирает внутренний жаргон («не удалось выполнить шаг», «WORKFLOW E» и т.д.).sanitizer.js — Защита от prompt injectionДетекция паттернов инъекции в system prompt: ignore previous instructions, <system>, [INST], <<SYS>>, русские варианты («игнорируй предыдущие», «забудь инструкции»). Логирование при обнаружении + удаление опасных тегов.
summarizer.js — Автосуммаризация контекстаПри превышении 80% лимита контекста суммаризирует старую половину через LLM. Пропускается для own-memory провайдеров.
roleResolver.js / scopeResolver.jsrelations_links и _baseLinkObject.| Модуль | Роль |
|---|---|
runSpecialistAgent.js |
Изолированный goal-driven субагент (cross-WS dialog). Узкий tool-set (3 tools), narrow prompt, maxRounds/deadline. |
runInlineSubagent.js |
Инлайн субагент (single-shot sub-LLM call через call_subagent). Синхронный, результат возвращается в том же ходе. |
subagentTools.js |
Tool-определения для dialog-субагентов: send_message_in_dialog, record_finding, finalize_subagent_dialog (terminal). |
| Модуль | Роль |
|---|---|
ChatContext.js |
Полиморфный контекст чата: internal/external/extchat. Discriminators, feature gates, lazy loaders, polymorphic mutation (saveBotReply). |
crmContext.js |
CRM-контекст + конфиг провайдера для кастомных endpoints. Placeholder-замены ({{ws_id}}, {{employee_id}} и т.д.). |
embeddedSyntaxResolver.js |
Резолвит (Имя группы) → ({24hex|Имя}) в system prompt перед LLM. |
fileProcessor.js |
Извлечение контента из вложений (PDF/DOCX/images), построение multimodal content. |
mentionCanonicalizer.js |
Серверная канонизация @-mention (исправление ID, self-mentions, plain-name). |
retryHandler.js |
Retry с exponential backoff для LLM-вызовов. |
triggerWorker.js |
Обработка CRM-триггеров: поиск подходящих триггеров, debounce, fire. |
triggerTimer.js |
Таймер для отложенных триггеров. |
teamRoles.js |
Роли команды (для hidden prompt). |
crmAccess.js |
Доступ к CRM данным. |
botMessageActions/gating.js |
Gating для кнопок approve/deny под сообщениями. |
botMessageActions/handlers.js |
Обработчики кнопок approve/deny. |
Сообщение в диалоге (RMQ: newChatMessage)
│
▼
events/newChatMessage.js
│
├─ Создание DialogMessage
├─ Push-уведомления участникам
│
▼
Responder.handleMessageForBot(bot, message, participants, ctx)
│
├─ Проверка mentionSet / private / _nextRecipients → shouldRespond?
├─ resolveScopeForBotDialog → scope (project/dialog/workspace)
├─ sessionMemory.tryLock (Redis SETNX per scope+bot)
│
▼
handleBotReply(bot, message, dialogId, ...)
│
├─ 1. Rate-limit check (Redis, per-bot, 60/min)
├─ 2. Decrypt API key (if encryptedApiKey)
├─ 3. getContextForDialog → sessionMemory → mapContextForProvider
├─ 4. Auto-summarize if >80% context limit
├─ 5. Build messages[]:
│ ├─ hiddenSystemPrompt (team dir, groups, base rules)
│ ├─ skills summary
│ ├─ systemPrompt (sanitized + embedded-syntax resolved)
│ ├─ negotiation context (if applicable)
│ ├─ RAG auto-inject (top-K KB excerpts)
│ ├─ subagent callback enrichment
│ ├─ CRM-окружение (groups, participants, format, tools instructions)
│ └─ contextMessages (session memory history)
│
├─ 6. L2 response cache lookup (exact match → early return)
│
├─ 7. Выбор пути:
│ ├─ useAgentLoop = tools.length > 0
│ │ │
│ │ ▼
│ │ agentLoop.run({ bot, provider, messages, tools, ... })
│ │ │
│ │ ├─ PLANNING PHASE (planner model → JSON plan)
│ │ │ ├─ buildPlan → extractJson → filter valid steps
│ │ │ └─ looksFabricated check (hex in hints)
│ │ │
│ │ ├─ EXECUTION PHASE (up to MAX_ITERATIONS=25)
│ │ │ │
│ │ │ ├─ buildProgressState → inject plan progress
│ │ │ ├─ provider.send(messages + tools)
│ │ │ │
│ │ │ ├─ if tool_calls:
│ │ │ │ ├─ fabricationGuard.findUnknownIds → block/warn
│ │ │ │ ├─ turnToolCache dedup
│ │ │ │ ├─ SLOW_WEB_TOOLS budget (max 4/turn)
│ │ │ │ ├─ web_search budget (max 4/turn)
│ │ │ │ ├─ call_subagent budget (max 1/turn)
│ │ │ │ ├─ toolExecutor(bot, toolCall, context)
│ │ │ │ ├─ fabricationGuard.collect (add result IDs)
│ │ │ │ ├─ BotActionAudit.record
│ │ │ │ └─ if terminal tool → return _skipSave
│ │ │ │
│ │ │ └─ if no tool_calls (text exit):
│ │ │ ├─ Plan enforcement (pending steps → force continue)
│ │ │ ├─ Dynamic plan extension (extendPlan)
│ │ │ └─ Anti-hallucination guard (claimed action without tool)
│ │ │
│ │ └─ MAX_ITERATIONS → final summary
│ │
│ ├─ streaming → handleStreamingResponse
│ └─ direct → provider.send
│
├─ 8. Post-processing:
│ ├─ canonicalizeMentions
│ ├─ stripToolCallEcho
│ ├─ stripInternalJargon
│ └─ saveBotReply (ctx) + pushMessageEntry (session memory)
│
└─ 9. L2 cache write-back
events/ — RMQ-обработчики событий диалогов| Файл | Событие | Назначение |
|---|---|---|
newChatMessage.js |
newChatMessage |
Обработка нового сообщения: нотификации, запуск ботов |
DialogArchive.js |
dialogArchive |
Архивация диалога |
DialogRead.js |
dialogRead |
Отметка прочтения |
DialogExternalModify.js |
dialogExternalModify |
Внешние модификации |
DialogAdminsChanged.js |
dialogAdminsChanged |
Изменение админов |
DialogParticipantsChanged.js |
dialogParticipantsChanged |
Изменение участников |
DialogMetaChanged.js |
dialogMetaChanged, dialogTypeChanged |
Изменение метаданных/типа |
dialogReminder.js |
dialogReminder |
Напоминания |
sendSystemDialogMessage.js |
sendSystemDialogMessage |
Системные сообщения |
sipcallFinishedMessage.js |
sipcallFinishedMessage |
Завершение SIP-звонка |
botActionTriggered.js |
botActionTriggered |
Кнопки approve/deny |
models/ — Mongoose-модели| Модель | Коллекция | Назначение |
|---|---|---|
Dialog |
dialogs |
Мастер-диалог (shared между сторонами ext-chat) |
DialogConfiguration |
dialog_configurations |
Конфигурация диалога per-workspace (isExternal, type, title, _baseLinkObject) |
DialogMessage |
dialog_messages |
Сообщения в диалогах |
DialogParticipant |
dialog_participants |
Участники диалога |
EmployeeBotMemory |
employee_bot_memory |
Память ботов (устаревшая, заменена sessionMemory) |
BotKnowledgeBase |
bot_knowledge_bases |
Базы знаний ботов |
BotKbDocument |
bot_kb_documents |
Документы в KB |
BotKbChunk |
bot_kb_chunks |
Чанки для RAG (векторы + текст) |
BotTokenUsageDaily |
bot_token_usage_daily |
Дневная статистика использования токенов |
BotTrace |
bot_traces |
Трассировки выполнения (LLM + tool steps) |
BotActionAudit |
bot_action_audits |
Аудит tool-call (включая блоки гардов) |
WidgetToken |
widget_tokens |
Токены для виджета |
WidgetVisitor |
widget_visitors |
Посетители виджета |
WidgetDemoSnapshot |
widget_demo_snapshots |
Демо-снапшоты виджета |
NegotiationSession |
negotiation_sessions |
Сессии переговоров (legacy, мигрирует в agent_task) |
AgentTask |
agent_tasks |
Задачи субагентов |
routes/ — HTTP-маршруты| Файл | Маршруты | Назначение |
|---|---|---|
dialog.js |
/dialogs/* |
CRUD диалогов, attaches, meeting (Zoom), participants, messages |
dialog_messages.js |
/dialogs/:id/messages/* |
CRUD сообщений, track, edit, update-content |
dialog_participants.js |
/dialogs/:id/participants/* |
Добавление участников |
hook.js |
/dialogs/hook/:token |
Webhook для отправки сообщений по токену |
services/ — Сервисные модули| Файл | Назначение |
|---|---|
aiProviderService.js |
Сервис AI-провайдеров |
_wsId-guard (R1)Все tool-call проверяют принадлежность ресурса workspace. FabricationGuard блокирует ObjectId, не встречающиеся в контексте (защита от hallucination и injection в чужой workspace).
Инструменты с sideEffect: 'external' или 'destructive' проходят через highImpactGate. В режиме enforce — только если tool есть в botSettings.highImpactAllowlist.
strict:false в botPermissionsВ employee.js schema botPermissions имеет strict: false — намеренно. Не «чинить».
Если botPermissions пуст — все права разрешены (backward-compat). Чтобы запретить — явно указать false.
smarty_custom и custom пропускают session memory read/write и автосуммаризацию. Их эндпоинт управляет контекстом самостоятельно.
Planner автоматически отключается для:
smarty_custom, custom) — их endpoint не поддерживает function-calling протокол.Если planner создал план с pending steps, а LLM пытается выйти текстом — loop форсирует продолжение (до 3 retry). Prod incident: бот заявлял «всё создано» после 2 из 6 шагов.
Два уровня:
Redis-ключ bot:trigger:fired:{wsId}:{triggerId}:{instanceId}:{eventType}:{payloadHash}. Двухфазный: inflight (30 сек) → after-fire (5 мин) или release.
Per-bot: 60 запросов в минуту (Redis INCR + EXPIRE).
Exact match по (botId, kbVersion, userId, query, sysPrompt). Skip: mentions, attachments, time-sensitive words, state-changing tools. Только internal CRM (не external/messenger).
В групповых чатах несколько ботов могут работать параллельно (debate-сценарий). Lock per (scope, bot) — разные боты не блокируют друг друга. Handoff convention: @-mention в финальной строке передаёт ход.