Большинство AI-ассистентов — это чат: написал вопрос, получил ответ. Coreness Flow изначально был построен иначе. Это локальное Windows-приложение, где агент реагирует на события — сообщение, webhook, cron — и выполняет цепочки действий, описанные в YAML. Хочешь поменять поведение агента — меняешь конфиг, не код. Никакой облачной части: всё работает на своей машине.

Репозиторий: github.com/Vensus137/Coreness-Flow

Идея: не «вопрос–ответ», а «событие → цепочка»

Стандартный AI-чат — это синхронный цикл: пользователь что-то написал, модель ответила. Coreness Flow разрывает этот цикл.

Здесь центральная единица — событие. Пришло сообщение в чат — событие. Сработал cron в 9 утра — событие. Прилетел webhook от внешнего сервиса — тоже событие. Движок сценариев находит подходящий сценарий по триггеру и выполняет цепочку шагов: читает данные, вызывает LLM, ищет по базе знаний, отправляет ответ. Один и тот же механизм — для разных источников.

Это открывает сценарии, которые в обычном чат-боте требуют написания кода — здесь всё описывается в YAML: ежедневная сводка по расписанию, автоматический ответ на webhook, реакция на конкретную команду с разветвлённой логикой — всё описывается в YAML.

Поведение агента задаётся конфигом и сценариями, а не кодом.

Event Flow
Event Flow

Архитектура: три слоя, одна шина

Приложение разделено на три слоя, которые не знают друг о друге напрямую — только общаются через 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 }. Движок сценариев не делает различий — он просто вызывает действия по имени и читает результат.

Api Bus
Api Bus

Почему такое разделение

Самый частый архитектурный грех в подобных приложениях — 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: интерфейс из конфига

Четыре точки контрибьюта:

Точка

Что добавляет

workspace

Вкладка в основном контенте с виджетом (список, форма настроек и т.д.)

settings

Секция на общей вкладке «Настройки»

sidebar

Пункт в сайдбаре (клик вызывает action)

menus

Пункты в выпадающих меню

Тип виджета вкладки описывается дескриптором: 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.

Конвейер обработки сообщения

AI-routing
AI-routing

Когда приходит сообщение из чата, выполняется такая последовательность:

  1. Загрузка контекста — из хранилища читаются настройки: системный промпт, список инструментов и сценариев ответа, лимит шагов.

  2. Сборка истории — берётся история чата с лимитом по объёму, при необходимости подмешиваются найденные фрагменты из RAG.

  3. Роутинг — запрос к LLM с промптом и описанием «инструментов». Модель решает: вызвать один из инструментов (например, поиск по базе знаний) или сразу сформировать ответ.

  4. Выполнение — если выбран инструмент, запускается соответствующий сценарий, затем снова роутинг (цикл до лимита шагов).

  5. Финализация — итоговый запрос к модели, ответ отправляется в чат.

Новый инструмент = описание в конфиге + файл сценария; ядро не трогается.

Инструменты — это те же YAML-сценарии: добавить новый значит описать его в конфиге хранилища и добавить файл сценария. Ядро приложения не трогается.

Чат
Чат

RAG локально: никаких серверов, никакого облака

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

RAG
RAG

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, подписываются на события; нет прямых вызовов между модулями.

Ссылки

Coreness — Create. Automate. Scale.