Веб-приложение, которое встраивает локальные языковые модели в low-code ETL-платформу
Loginom: веб-чат с подмешиванием таблиц в контекст, пул GGUF-моделей с
ленивой загрузкой, двусторонний файловый обмен с Loginom через программную
генерацию .lgp-пакета и перевод ответов на русский.
Выпускная квалификационная работа бакалавра, 2026.
О публикации. В репозиторий попадает только код. Текст работы, титульные листы, задание и база с реальными чатами — локально и в
.gitignore.
Аналитик в Loginom получил от заказчика CSV на 3 000 строк. Нужно разобрать свободный текст, нормализовать и вернуть результат обратно в пайплайн. Ручная разметка не масштабируется, облачный LLM нарушает требования по персональным данным.
Три барьера, и все три снимает этот проект:
- Формат. LLM отвечает текстом, Loginom ждёт таблицу. Нужен разбор markdown-таблиц из ответа и сборка таблицы с выбранными столбцами и строками.
- Обратный ход. Результат надо вернуть в Loginom. У Loginom нет REST-приёмника
«из веба» — сценарий собирается только как файл-пакет
.lgp. - Ресурсы. Локально на одной машине десятки гигабайт VRAM. Одновременно несколько моделей в памяти не помещаются.
Ядро проекта. Loginom исполняет сценарий только из файла .lgp (zip с
манифестом PackageIndex.xml и графом в Unit_0/Unit.xml), и внутри пакета
состояние дублируется ещё и в бинарных .bin. Публичного API для сборки
пакета нет.
Отсюда — реверс-инжиниринг формата в loginom_integration.py:
| Шаг | Что делает |
|---|---|
make_unique_filename() |
генерирует имя вида Files/ABC1234.csv ровно из 17 символов |
save_csv() |
пишет CSV в utf-8-sig (Loginom с кодовой страницей 65001) |
modify_xml() |
re.sub по FileName="..." в Unit.xml, зачистка <ColumnDefs> внутри TBGImportTextFile |
patch_bin() |
бинарный патч: замена FileName с сохранением длины, замена DisplayName узла, обнуление блока ColumnDefs после маркера Engine\x11TBGImportTextFile |
build_lgp() |
copytree шаблона в временную папку → патчи → zipfile (DEFLATE) → .lgp |
run_loginom() |
subprocess.run([loginom.exe, lgp]) |
Обратный путь — скрипт Loginom.js (36 строк) на движке
TBGJavaScriptEngine: читает InputTable из builtIn/Data, формирует
JSON.stringify({table: {headers, rows}}) и пишет в файл. Python читает файл
и отдаёт результат клиенту.
Полный цикл:
Клиент --POST /api/loginom/export {headers, rows}--> FastAPI
FastAPI: build_lgp → run_loginom (поток daemon) → {"status":"started","task_id"}
Клиент --GET /api/loginom/result/{task_id} каждые 2 с--> FastAPI
FastAPI читает loginom_output.json → {"status":"ready", headers, rows}
Несколько версий архитектуры за 10 дней. Изначально на каждую модель
поднимался отдельный процесс llama-server:
subprocess.Popen([LLAMA_SERVER_PATH, "-m", path, "--port", str(PORT), "-ngl", "0", "-c", "8192"])Дальше — один процесс-роутер с OpenAI-совместимым API и горячей перезагрузкой моделей:
POST /models/load
{
"model": "Ternary-Bonsai-8B-Q2_0",
"-ngl": 99, # всё на GPU
"-c": ctx, # контекст под конкретную модель
"-np": 1, # один параллельный слот
"--cont-batching": True
}Модели грузятся лениво — при старте не загружается ничего. Перед загрузкой
новой выгружаются все (_unload_all_models()), в роутере одновременно живёт
только одна модель. Клиент спрашивает GET /api/model/status раз в 3 секунды.
Контекст подбирается по имени файла:
bonsai → 16384 nanbeige → 32768
hy-mt2 → 8192 minicpm → 131072
иначе → 8192KV-кэш квантизован и ограничен 4 ГБ: --cache-type-k q8_0 --cache-type-v q8_0 --cache-ram 4096 --no-kv-offload.
POST /api/translate поднимает Hy-MT2-1.8B с {"jinja": True} (GGUF от
Tencent требует chat-шаблон Jinja) и temperature=0.3.
На фронтенде перевод двухуровневый: автоматический — если в ответе модели
есть не-ASCII, после завершения стрима перевод уходит в фон и результат
подставляется с флагом showTranslation; ручной — кнопка Languages на
каждом сообщении переключает оригинал ↔ перевод.
┌──────────────────────────────────────────────────────────────┐
│ React SPA (Vite + Tailwind 4) │
│ Prompt Builder · роли · конструктор таблиц · загрузка файлов│
│ SSE-ридер · экспорт CSV · отправка в Loginom │
└───────────────────────┬──────────────────────────────────────┘
│ HTTP /api/* + SSE
┌───────────────────────▼──────────────────────────────────────┐
│ FastAPI (main.py, 632 строки) │
│ 13 эндпоинтов · статика · автостарт Vite-дева · free_port │
└──┬──────────────┬────────────────────┬───────────────────────┘
│ │ │
┌──▼───────────┐ ┌▼─────────────────┐ ┌▼──────────────────────┐
│ SQLite │ │ loginom_ │ │ llama.cpp роутер :8080│
│ database.py │ │ integration.py │ │ (форк Prism) │
│ 7 таблиц │ │ сборка .lgp │ │ /v1/chat/completions │
│ 406 строк │ │ 229 строк │ │ /models/load|unload │
└──────────────┘ └──────────────────┘ └───────────────────────┘
7 таблиц SQLite (PRAGMA journal_mode=WAL, foreign_keys=ON):
| Таблица | Содержимое |
|---|---|
chats |
заголовок, модель, суммарные токены, даты |
messages |
роль, контент, reasoning, usage_json, tokens_per_second, context_window, tables_json, files_json |
branches |
ветки диалога при edit-send: parent_message_id + messages_json |
roles |
именованные системные промпты |
tables_data |
заголовки и строки пользовательских таблиц |
text_files |
абзацы загруженных текстовых файлов |
model_contexts |
n_ctx, зафиксированный за каждой моделью |
Поверх — generic-слой STORAGE_MAP, отображающий ключи фронтенда
(diplom_chats, diplom_roles, diplom_tables, diplom_text_files,
diplom_model_contexts) на CRUD, чтобы не плодить эндпоинты.
| Метод | Путь | Назначение |
|---|---|---|
POST |
/api/chat |
чат, синхронный или SSE |
GET |
/api/models |
список GGUF в MODELS_DIR + загруженные |
POST |
/api/model/load |
загрузка модели в роутер |
POST |
/api/model/unload |
выгрузка |
GET |
/api/model/status |
статус текущей модели |
GET |
/api/context-window |
размер контекста |
POST |
/api/upload |
CSV / XLSX / TXT / DOCX |
POST |
/api/translate |
перевод через Hy-MT2 |
POST |
/api/loginom/export |
сборка .lgp и запуск Loginom |
GET |
/api/loginom/result/{task_id} |
результат (polling) |
GET/POST/DELETE |
/api/data/{key} |
generic CRUD по хранилищу |
SSE проксирует POST /v1/chat/completions со stream=True и дописывает
служебный кадр с метриками после завершения. В потоковом режиме токены
оцениваются как len(text) // 4; точные значения берутся из
json.timings (prompt_n, predicted_n, predicted_per_second,
predicted_ms).
- edit-send с ветвлением — при правке сообщения «хвост» истории
сохраняется в
branches, сама история обрезается, запрос уходит с усечённым контекстом. Ветку можно восстановить. - Конструктор таблиц — добавление и удаление строк и столбцов, выбор строк и столбцов для включения в запрос.
- Загрузка файлов drag-and-drop: CSV, XLSX, TXT, DOCX (парсинг
openpyxlиpython-docxс ленивым импортом). - Скачивание CSV (с корректным экранированием кавычек) и кода из markdown-блоков.
- Markdown-таблицы в ответе модели: отрисовка, отправка в Loginom, скачивание.
- Блокировка автоскролла при ручном скролле, авторесайз textarea, поллинг списка моделей раз в 15 секунд.
- Тёмная тема,
⌘↵/Ctrl+Enterдля отправки, отмена активного запроса.
Backend (Python 3.12): FastAPI 0.136, uvicorn 0.46, Pydantic 2.13,
requests 2.32, python-multipart, openpyxl 3.1.5, python-docx 1.2.0,
sqlite3.
LLM-рантайм: внешний llama-server из форка Prism
(prism-ml/Ternary-Bonsai) — нужны нестандартные эндпоинты
/models/load и /models/unload, которых нет в апстриме. CUDA-сборка,
-ngl 99.
Frontend: React 18.3.1, Vite 6.3.5, Tailwind CSS 4.1.12 (CSS-first,
@theme / @custom-variant), TypeScript, lucide-react, next-themes.
Исходный UI сгенерирован в Figma Make, сборка — Vite (не Next.js).
Модели: Ternary-Bonsai-8B-Q2_0 (основная), Hy-MT2-1.8B-Q4_K_M (перевод), MiniCPM5-1B-F16 (быстрая).
Кода: 1 267 строк Python в 3 файлах бэкенда + 2 610 строк в одном App.tsx
фронтенда.
Требования: Windows, GPU с CUDA (RXT 3050 достаточно), собранный
llama.cpp с роутером (форк Prism), GGUF-модели в MODELS_DIR,
loginom.exe для экспорта.
# зависимости (нужен именно 3.12 — в 3.14 нет fastapi/uvicorn/pydantic)
py -3.12 -m pip install fastapi uvicorn pydantic requests python-multipart \
openpyxl python-docx
# фронтенд
npm.cmd --prefix ".\Chatbot interface design1" install
npm.cmd --prefix ".\Chatbot interface design1" run build
# сервер (отдаёт собранный dist + API)
py -3.12 main.pyОткрыть http://localhost:8000, Swagger — http://127.0.0.1:8000/docs.
Режим разработки: если папки dist/ нет, а порт 5173 свободен, lifespan()
сам запускает npm run dev — Vite поднимается на 5173 с прокси
/api → localhost:8000.
Переменные окружения:
MODELS_DIR # папка с *.gguf
LLAMA_SERVER_PATH # путь к llama-server.exe
FRONTEND_DIR # папка фронтендаДефолтный
LLAMA_SERVER_PATHуказывает наC:\llama.cpp\prism-worktree\build\bin\Release\llama-server.exe— путь из-под форка Prism, на чистой машине его не существует. Задайте переменную явно.
Бэкенд:
CORSоткрыт полностью:allow_origins=["*"],allow_credentials=True.uvicornслушает0.0.0.0— не выставляйте наружу.- Абсолютные пути (
C:\llama.cpp\...,C:\Users\<имя>\...,C:\Program Files\Loginom\...) зашиты как дефолтыos.environ.get.FRONTEND_DIRи путь кloginom_output.jsonуказывают на конкретную машину. main.pyпри старте принудительно убивает процесс на порту 8000 (netstat -ano→taskkill /F), и аналогично на 8080.POST /api/loginom/receiveприсутствует вUnit.xmlшаблона как REST-узел на127.0.0.1:8005— это наследие отброшенного REST-варианта. В коде такого эндпоинта нет, сервер слушает 8000._ensure_loadedв переводе не выгружает текущую модель (в отличие отload_model) — перевод может конфликтовать с активной моделью.- Промпт пользователя вставляется в документ без экранирования.
Фронтенд:
App.tsx— монолит на 2 610 строк: 60+useState, вся бизнес-логика и разметка в одном компоненте.tsconfig.jsonотсутствует — типы в коде есть, проверка типов в сборке не выполняется.- 49 shadcn-компонентов (~5 400 строк) не используются ни разу —
сгенерированы Figma Make, импортов из
./componentsвApp.tsxиmain.tsxнет. Весь фактический UI написан на «голом» Tailwind. package.jsonраздут: 24 Radix-пакета, MUI, emotion, react-slick, canvas-confetti, react-router — из всего этого реально используютсяlucide-reactиnext-themes.- Мок-данные (
INITIAL_MESSAGES, демо-таблицы выручки) зашиты в production-код. - Переводы не сохраняются в БД — живут только в состоянии React.
saveJsonпишет и вlocalStorage, и в БД без версионирования схемы.
Нет requirements.txt, Dockerfile, тестов, CI.
- shadcn/ui и Radix UI — MIT, атрибуция в
Chatbot interface design1/ATTRIBUTIONS.md. - Unsplash — кредиты указаны в том же файле, сами изображения не используются.
- llama.cpp — MIT. Используется форк
prism-ml/Ternary-Bonsai. - Loginom — проприетарный продукт; используется локально, код Loginom
в репозитории не распространяется. Формат
.lgpразобран для совместимости с собственным сценарием. - Исходный UI создан в Figma Make.