Большинство AI-ассистентов — это чат: написал вопрос, получил ответ. Coreness Flow изначально был построен иначе. Это локальное Windows-приложение, где агент реагирует на события — сообщение, webhook, cron — и выполняет цепочки действий, описанные в YAML. Хочешь поменять поведение агента — меняешь конфиг, не код. Никакой облачной части: всё работает на своей машине.
Репозиторий: github.com/Vensus137/Coreness-Flow
Идея: не «вопрос–ответ», а «событие → цепочка»
Стандартный AI-чат — это синхронный цикл: пользователь что-то написал, модель ответила. Coreness Flow разрывает этот цикл.
Здесь центральная единица — событие. Пришло сообщение в чат — событие. Сработал cron в 9 утра — событие. Прилетел webhook от внешнего сервиса — тоже событие. Движок сценариев находит подходящий сценарий по триггеру и выполняет цепочку шагов: читает данные, вызывает LLM, ищет по базе знаний, отправляет ответ. Один и тот же механизм — для разных источников.
Это открывает сценарии, которые в обычном чат-боте требуют написания кода — здесь всё описывается в YAML: ежедневная сводка по расписанию, автоматический ответ на webhook, реакция на конкретную команду с разветвлённой логикой — всё описывается в YAML.
Поведение агента задаётся конфигом и сценариями, а не кодом.

Архитектура: три слоя, одна шина
Приложение разделено на три слоя, которые не знают друг о друге напрямую — только общаются через API Bus.
UI Layer — Electron + React. Фронт — это просто отображение. Он подключается к Python-бэкенду по WebSocket, отправляет действия и получает события. Никакой бизнес-логики в React-коде нет — только рендер и вызовы шины.
Backend Layer — плагины на Python. Вся логика живёт здесь: обработка сообщений, вызовы LLM, работа с базой, RAG, планировщик. Каждый плагин изолирован и общается с остальными только через API Bus.
API Bus — единый контракт между всеми участниками. Два режима:
call— вызов с ожиданием ответа (нужен, когда следующий шаг зависит от результата)call_nowait— запуск в фоне без блокировки (длинные цепочки, внешние запросы)
Единый формат ответа у всех вызовов: { result, error, response_data }. Движок сценариев не делает различий — он просто вызывает действия по имени и читает результат.

Почему такое разделение
Самый частый архитектурный грех в подобных приложениях — UI знает про базу данных, а бизнес-логика вызывает компоненты напрямую. Стоит добавить новую фичу — начинается хирургия в трёх местах одновременно.
В Coreness Flow UI вообще не знает, какие плагины загружены. Фронт при старте запрашивает у бэкенда описание того, что плагины добавляют в интерфейс, и строит экран по этим данным. Новый плагин — новая вкладка без правок во фронтенде.
Добавил папку плагина — интерфейс подстроился сам.
Lifecycle: как запускается приложение
Electron main process ↓ spawn Python backend ↓ API Bus инициализируется ↓ Контейнер сканирует plugins/, мержит конфиги, создаёт экземпляры ↓ Тяжёлая инициализация плагинов (splash показывает прогресс) ↓ Фоновые задачи плагинов ↓ WebSocket-сервер поднимается ↓ Главное окно открывается, фронт подключается ↓ Сбор описаний от плагинов → UI строит сайдбар, вкладки, настройки
Каждый этап изолирован. Если плагин падает при инициализации — остальные продолжают загружаться. Graceful shutdown: таймауты на завершение плагинов и воркеров задаются в app.json, приложение корректно закрывается при выходе или обновлении.
Плагины: VS Code-like, но для десктопного приложения
Плагинная система — одна из самых интересных частей архитектуры. Идея взята из VS Code: плагин не просто добавляет бэкенд-логику, он декларирует свой вклад в приложение через config.json.
Каждый плагин — папка с двумя ключевыми файлами: config.json (что плагин умеет и что добавляет в UI) и Python-модуль (как это реализовано). Контейнер при старте рекурсивно сканирует plugins/ и загружает все папки, где есть config.json. Имя папки становится идентификатором плагина. Никакого реестра, никакого перечисления модулей в коде ядра.
Config.json — центр тяжести плагина
Конфиг описывает четыре вещи:
metadata — имя и описание плагина.
settings — схема настроек с дефолтами. При старте контейнер мержит дефолты из конфига с переопределениями пользователя из
user_settings.json. Плагин всегда получает уже готовый merged-конфиг.actions — список действий, которые плагин регистрирует в API Bus. Для каждого действия описан input (payload-схема) и output (что вернёт).
contributes — что плагин добавляет в UI. Это самая интересная часть.
Методы Python-класса с именами, совпадающими с ключами в actions, автоматически становятс�� обработчиками вызовов — отдельная регистрация не нужна. Описал действие в конфиге и реализовал метод с тем же именем — движок сам подвяжет их при загрузке плагина.
Конфиг + метод с тем же именем — связка без явной регистрации.
Contributes: интерфейс из конфига
Четыре точки контрибьюта:
Точка | Что добавляет |
|---|---|
| Вкладка в основном контенте с виджетом (список, форма настроек и т.д.) |
| Секция на общей вкладке «Настройки» |
| Пункт в сайдбаре (клик вызывает action) |
| Пункты в выпадающих меню |
Тип виджета вкладки описывается дескриптором: settingsForm, list — фронт рендерит контент без кастомного кода. Колонки списка, источник данных через action, поля формы — всё в JSON-конфиге плагина. Вкладка «Векторное хранилище» со списком чанков и кнопками удаления задаётся в том же плагине так:
"contributes": { "workspace": { "id": "vector_store_admin", "label": "Векторная база", "title": "Векторная база", "content": {"widget": "vectorStoreAdmin"} } }
vectorStoreAdmin — встроенный тип виджета, зарегистрированный во фронтенде; плагин только ссылается на него по имени. Ни одной строки React в плагине — только конфиг.
Такой подход даёт чёткую границу ответственности: бэкенд-разработчик пишет плагин и его конфиг, UI-слой адаптируется сам. Хочешь убрать вкладку — убери папку плагина. Хочешь п��реименовать — поменяй label в конфиге.
Hot reload настроек
Отдельная приятная деталь: когда пользователь меняет настройки в UI, бэкенд уведомляет затронутый плагин — тот пересоздаёт клиенты, сбрасывает кэши без перезапуска приложения. Сменил API-ключ или модель — плагин подхватил изменения сразу.
Сценарии: оркестрация без кода
Если плагины — это сервисы с действиями, то сценарии — это способ эти действия оркестрировать без написания кода.
Все сценарии живут в YAML-файлах в config/scenarios/. Движок подхватывает их рекурсивно из всех подпапок. Можно организовать как угодно: commands/, system/, scheduled/ - имена сценариев глобальны: из любого сценария можно вызвать другой по имени.
Базовая структура:
daily_report: schedule: "0 9 * * *" # Ежедневно в 9:00 step: - action: "get_storage" params: group_key: "report_config" _response_key: "config" - action: "completion" params: prompt: "Сформируй утреннюю сводку. Контекст: {_cache.config}" model: "{_cache.config.model}" - action: "send_chat_message" params: text: "{_cache.response}"
Каждый шаг — вызов action плагина. Результат кладётся в _cache под ключом _response_key. Следующий шаг берёт данные через плейсхолдер {_cache.ключ}.
Плейсхолдеры — маленькая шаблонизация
Плейсхолдеры работают во всех параметрах шагов и поддерживают цепочку модификаторов:
{event_text}— текст события{_cache.system.routing_model}— вложенное поле из кэша{now|format:datetime}— текущее время с форматированием{_cache.field|fallback:default}— значение с дефолтом, если поле пустое{_cache.result|exists}— булево: есть ли поле
Это позволяет строить достаточно гибкие цепочки без написания Python-кода — просто подстановка значений через шаблоны.
Триггеры: от простого к сложному
Самая простая форма триггера — тип события плюс текст:
trigger: - event_type: "message" event_text: "/help"
Сложная форма — поле condition с выражением на мини-языке (операторы: ==, ~ — «содержит», regex, is_null и др.; поля через $event_text, $event_type):
trigger: - event_type: "message" condition: "$event_text ~ '/'"
Несколько триггеров в списке работают по логике ИЛИ. Поля внутри одного триггера — И. Не нужно дублировать сценарий под каждый вариант — достаточно добавить триггер в список.
Переходы и ветвления
После шага можно задать transition — список правил: по результату действия (action_result: success, error и т.д.) выполняется переход (transition_action и при необходимости transition_value). Например, переход в другой сценарий:
- action: "search_chunks" params: query: "{event_text}" _response_key: "rag_result" transition: - action_result: "success" transition_action: "jump_to_scenario" transition_value: "step_with_rag" - action_result: "error" transition_action: "jump_to_scenario" transition_value: "step_without_rag"
Нашёл что-то в RAG — идём в один сценарий, не нашёл — в другой. Всё в конфиге.
Цепочка шагов и ветвления — в YAML, без кода.
Типичный сценарий-инструмент: по ссылке загрузить документ, разбить на чанки и положить в векторное хранилище; при ошибке — переход в сценарий обработки ошибки.
Конфигурация: мерж без магии
Схема конфигурации намеренно простая и одинаковая для всего:
Дефолты приложения —
config/app.jsonДефолты плагина — секция
settingsв егоconfig.jsonПользовательские переопределения —
%APPDATA%\CorenessFlow\user_settings.json(только изменённые ключи)
При старте контейнер мержит дефолты с пользовательскими значениями и передаёт плагину готовый конфиг. Секреты и API-токены хранятся в SQLite в %APPDATA% — в репозиторий не попадают.
Один и тот же формат config.json у приложения и у каждого плагина — код контейнера унифицирован, документация однородна.
Storage: ключ–значение для конфигурации агента
Плагин database даёт сценариям простое ключ–значение хранилище с группировкой: group_key + key → значение. Но интереснее другое: начальные данные задаются прямо в YAML-файлах в config/storage/ и при старте синхронизируются в SQLite.
Это делает поведение агента конфигурируемым без правки сценариев. Системный промпт роутера, список доступных инструментов, параметры моделей, лимиты — всё хранится в storage, сценарии читают эти данные при выполнении. Поменять модель для простых запросов — правка в storage, без изменений в YAML сценариев.
Роутинг агента: как один запрос превращается в цепочку решений
Агентский роутинг в Coreness Flow — это не встроенная в ядро функция, а набор системных сценариев поверх общего механизма. Пользователь видит: отправил сообщение → «Обрабатываю...» → ответ. За кадром — цепочка из нескольких сценариев и нескольких вызовов LLM.
Конвейер обработки сообщения

Когда приходит сообщение из чата, выполняется такая последовательность:
Загрузка контекста — из хранилища читаются настройки: системный промпт, список инструментов и сценариев ответа, лимит шагов.
Сборка истории — берётся история чата с лимитом по объёму, при необходимости подмешиваются найденные фрагменты из RAG.
Роутинг — запрос к LLM с промптом и описанием «инструментов». Модель решает: вызвать один из инструментов (например, поиск по базе знаний) или сразу сформировать ответ.
Выполнение — если выбран инструмент, запускается соответствующий сценарий, затем снова роутинг (цикл до лимита шагов).
Финализация — итоговый запрос к модели, ответ отправляется в чат.
Новый инструмент = описание в конфиге + файл сценария; ядро не трогается.
Инструменты — это те же YAML-сценарии: добавить новый значит описать его в конфиге хранилища и добавить файл сценария. Ядро приложения не трогается.

RAG локально: никаких серверов, никакого облака
Векторное хранилище в Coreness Flow — отдельный плагин с несколькими принципиальными решениями.

BGE-M3 в формате ONNX, квантизация INT8
Мультиязычная модель, которая умеет генерировать и dense, и sparse векторы одновременно. Модель запускается через ONNX Runtime — без PyTorch и CUDA. INT8-квантизация снижает потребление памяти примерно в 4 раза по сравнению с float32 при минимальной потере качества. Работает на обычном CPU.
Qdrant в embedded-режиме
Qdrant поднимается внутри процесса приложения и хранит данные на диске. Отдельный сервис, порты и docker не нужны.
Гибридный поиск с RRF
BGE-M3 даёт и dense-векторы (семантика), и sparse-векторы (ключевые слова). Поиск по обоим типам и слияние результатов через Reciprocal Rank Fusion даёт лучшее качество, чем один семантический поиск — особенно для запросов с конкретными терминами и именами.
В сценариях это два действия: add_chunks для индексации и search_chunks для поиска. Результат поиска кладётся в _cache и подставляется в промпт следующего шага. Документы не покидают машину.
Загрузка модели и splash-экран
BGE-M3 — тяжёлая модель, и её загрузка при старте требует отдельного решения. Она выполняется до показа главного окна: пользователь видит splash с прогрессом инициализации, главное окно открывается, только когда всё готово. Без замораживания интерфейса.

Асинхронность без боли
Actions в API Bus выполняются в пуле воркеров — отдельных потоках с собственным event loop. Число воркеров настраивается. Это значит, что долгий вызов LLM не блокирует обработку другого входящего события.
call_nowait — отдельно приятная штука. Запускаешь длинную цепочку сценариев, не ждёшь завершения, продолжаешь. Результат придёт через событие в UI. Так работает весь чат: пользователь отправил сообщение, бэкенд запустил цепочку через call_nowait, UI показывает индикатор загрузки и не блокируется.
Плагины подписываются на события шины. Так можно организовать реакцию на любые события системы без прямых зависимостей между плагинами.
Структура проекта
Coreness-Flow/ ├── run_backend.py # Точка входа бэкенда ├── app/ │ ├── runtime/ │ │ ├── container.py # Сканирование плагинов, мерж конфигов, создание экземпляров │ │ ├── api_bus.py # Шина: actions, events, воркеры │ │ └── ... │ ├── settings.py # Загрузка и мерж конфигов │ └── ws_server.py # WebSocket для фронта ├── frontend/ # Electron + React ├── plugins/ │ ├── core/ # Ключевые модули: chats, database, ai_service, vector_store, ... │ ├── base/ # Некритичные плагины │ └── extensions/ # Кастомные расширения └── config/ ├── app.json # Дефолты приложения ├── scenarios/ # YAML-сценарии └── storage/ # Начальные данные storage
В core/ — основа: чаты, база данных, вызовы LLM, векторное хранилище, движок сценариев. В base/ — дополнительные плагины, без них приложение тоже работает. В extensions/ — место для своих расширений.
Быстрый старт
Из исходников (Python 3.11, Node.js, Windows):
pip install -r requirements.txt cd frontend && npm install && cd .. .\scripts\run-dev.ps1
Бэкенд и окно поднимаются одной командой. В dev-режиме: hot reload при изменении конфигов и кода плагинов.
Из релизов: установщик под Windows в разделе Releases — Python и Node на целевой машине не нужны.
После первого запуска — задать AI-провайдера в настройках (любой OpenAI-совместимый API), при желании загрузить документы в векторное хранилище и настроить storage под свои нужды.
Стек
Компонент | Технология |
|---|---|
Frontend | Electron + React |
Backend | Python 3.11, async/await |
UI ↔ Backend | WebSocket + API Bus |
LLM | OpenAI-совместимый API (OpenRouter, Polza.AI и др.) |
RAG | BGE-M3 ONNX INT8 + Qdrant embedded |
Хранилище | SQLite + JSON (конфиг) + YAML (сценарии, storage) |
Сборка | Electron Builder + Python backend |
Ключевые решения
Чем по сути отличается Coreness Flow:
Плагины без реестра — папка с конфигом и модулем, контейнер подхватывает при старте; единый контракт для всех.
Интерфейс из конфига — плагины декларируют вкладки, настройки, пункты меню; фронт собирает UI по этим данным, без перечисления плагинов в коде.
Сценарии вместо кода — оркестрация действий в YAML: триггеры, шаги, переходы по результату; движок вызывает действия по имени.
Локальный RAG — эмбеддинги и векторная база на своей машине, ONNX и Qdrant в процессе приложения, офлайн.
События и шина — плагины общаются только через API Bus, подписываются на события; нет прямых вызовов между модулями.
Ссылки
Репозиторий: github.com/Vensus137/Coreness-Flow
Документация: docs/ в репозитории
Связь с автором: @vensus137
Coreness — Create. Automate. Scale.
