---
name: project_pet_assistant
description: «Квантик-ассистент» — сквозной помощник поверх питомца; Ф0/Ф1/«Спроси» РЕАЛИЗОВАНЫ на master (правиловый движок)
metadata: 
  node_type: memory
  type: project
  originSessionId: 60467058-b40e-4bd9-9f7f-d1e362e8039a
---

Дизайн-доку: `plans/pet-assistant/PLAN.md`. Реализация: commit **3f8009c** (2026-06-04), на master.

Суть: питомец «Квантик» стал плавающим помощником (низ-слева) на всех страницах — контекстные
подсказки, проактивные напоминания, поздравления, панель «Спроси Квантика». Движок **правиловый**
(без LLM), правила инлайн в `frontend/js/assistant.js`.

Фича-гейт: **отдельный `assistant`** (commit e1cde83 — сменили с reuse 'pet'), `requireFeature('assistant')`,
дефолт ON; админ включает/выключает в Управление→фичи (adminController allowed + games.js GAME_FEATURES,
key 'assistant'). Включён **всем** (assistant_enabled DEFAULT 1 — личный тумблер в профиле);
«видел» — **серверная** таблица `assistant_seen` (cross-device); ассистент **и на учебнике** (через
DEEPLINK_INJECT в server.js); тон **консервативный** (дневной лимит 2, кулдауны, «не показывать»);
«Спроси» — **поиск по FAQ + точка расширения под локальную модель** (в `ask()` контроллера).

Файлы: миграция `062_assistant.sql` (assistant_enabled + assistant_seen); `assistantController.js`
(FAQ инлайн — `backend/src/data/` в .gitignore!) + `routes/assistant.js`; mount `/api/assistant`
под `requireFeature('pet')`; `js/api.js` (assistantContext/Seen/Dismiss/Settings/Ask); загрузчик в
`js/sidebar.js` (как flashcard-fab); тумблер в `profile.html` («Настройки» → prefAssistant).
Эндпоинты: GET /context, POST /seen, /dismiss, /ask, PATCH /settings. Лицо — `pet-sprite.js`,
данные — `/api/pet` + `/api/assistant/context` (dueCards, homework).

Сделано: Ф0 (каркас+контекстные подсказки) + Ф1 (проактив: домашка/карточки/серия/квест/
activeLesson «продолжи урок» + поздравления левелап/серия) + Ф2 (коачмарк-тур новичка по разделам,
офер на дашборде, повтор Assistant.tour()) + Ф3-lite (FAQ-«Спроси»). Тур-правило id 'onboarding'
в assistant_seen. activeLesson (commit 9baaca7) — запрос как «продолжить чтение» из courseController.
+ Контент-апгрейд (commit c33295e): контекстные подсказки на ВСЕ разделы (PAGE_HINTS),
«Совет дня» (tip-daily, дашборд), FAQ ~10→~50, «Спроси» ищет и по платформе (LS.globalSearch),
умный проактив weakSubject (слабый предмет по test_sessions, в /api/assistant/context) +
daily-plan (из квестов+карточек). LLM подключена (commit 9dbc044): ask() вызывает OpenAI-совместимую модель с грунтовкой по топ-FAQ,
source:'model', таймаут 12с, откат на FAQ при ошибке/без ключа. Конфиг ENV в backend/.env(.example):
ASSISTANT_LLM_URL (дефолт Groq chat/completions), ASSISTANT_LLM_KEY (пусто → FAQ), ASSISTANT_LLM_MODEL
(дефолт llama-3.3-70b-versatile); локальный Ollama без ключа поддержан (localhost → зовётся без Bearer).
АКТИВНО (2026-06-04): подключён **Google Gemini** через OpenAI-совместимый эндпоинт
(ASSISTANT_LLM_URL=https://generativelanguage.googleapis.com/v1beta/openai/chat/completions),
рабочая модель **gemini-2.5-flash** (ключ в backend/.env, не в гите). Гочи по моделям на этом ключе:
gemini-2.0-flash / -lite → 429 limit:0 (нет free-квоты); gemini-1.5-* → 404; gemini-2.5-flash / gemini-flash-latest → 200.
Ключ валиден (auth ок). Сменить провайдера/модель — только через backend/.env + рестарт.
Возможности-апгрейд (commit 479c621): ответы модели рендерятся markdown + KaTeX (ленивая загрузка
katex с jsdelivr; модель просим LaTeX $...$); ask принимает context; «Объяснить выделенное» (запоминаем
selection на mouseup) и «Объяснить/Конспект параграфа» на учебнике (getPageContext по .sec.active);
«Флешкарты из параграфа» → POST /api/assistant/flashcards (модель→JSON, есть починка обрезанного,
max_tokens 1400) → колода через LS.fcCreateDeck/fcAddCard; репетитор на экзамене — кнопка «Спросить
Квантика» в task-card.js (tc-ask) → Assistant.ask(условие+ответ+решение). Экспорт Assistant.ask(q,context)
и explainSelection(). Гочи: на этом Gemini-ключе free-квота есть только у gemini-2.5-flash; placeholder
в renderRich — @@M..@@ (не цифры-в-пробелах, иначе коллизии).
Админ-панель + чат (commit dc073e2): конфиг LLM теперь в **app_settings** (assistant_llm_url/key/model),
правится из Управление→игры (карточка «Помощник Квантик — модель»: пресеты Gemini/Groq/OpenRouter/Ollama,
URL/модель/ключ, Сохранить/Проверить/Очистить, статус). Эндпоинты GET/PUT/POST /api/admin/assistant(/test)
admin-only. llmConfig() читает app_settings→ENV→дефолт; нет ключа → FAQ-режим. Текущий Gemini-ключ пока
в backend/.env (DB пусто → берётся ENV; можно перенести в БД через админку). «Спроси» — многоходовой чат
(history последние 6 реплик уходят модели, лента сообщений, «Очистить»).
RAG+кэш+учитель (commit 2252bbd, миграция 063): **RAG** — индексатор backend/scripts/index-textbooks.js
→ таблица textbook_chunks; ask() подмешивает релевантные куски (LIKE-скоринг, ≥2 слова). ГОЧА: бол-во
учебников рендерят текст ЧЕРЕЗ JS-движки → в статическом HTML только заголовки §, поэтому индекс берёт
только статично-текстовые (≈132 чанка: chemistry, часть physics); JS-рендеримые покрываются контекстом
страницы (getPageContext читает отрендеренный DOM). Полное покрытие = headless-рендер — СДЕЛАНО (commit 0119ea0): scripts/index-textbooks-headless.js
(puppeteer-core + системный Chrome, служебный JWT в localStorage т.к. /textbook требует логина) рендерит
учебники через локальный сервер, кликает §, забирает рендерный текст движков. Прогон: 87/107 книг, индекс
132→**746 чанков** (physics-9 и др. JS-учебники теперь покрыты). npm: index:textbooks:full. Сервер не
требует рестарта — ragContext читает БД на каждом запросе.

Батч 4 фич (commit 4224a22, миграция 064): **Источники** — ragContext отдаёт sources (slug/section/section_ref),
под ответом ссылка «по учебнику X, §N» на /textbook/slug#sec-<ref>; section_ref заполняет headless-индексатор
(psel-card data-id); статический индексатор больше НЕ делает delAll (per-book — не затирает headless).
**Режим-наставник**: ask(mode: answer/hint/check) + промпт; в «Спроси» переключатель из 3 кнопок; на карточке
экзамена (task-card.js) кнопка «Подсказка» (mode hint) рядом со «Спросить Квантика»; Assistant.ask(q,ctx,{mode}).
**Оценка**: лайк/дизлайк под ответом (assistant_feedback, POST /assistant/feedback) + сводка в админке (up/down/recent).
**Утренний бриф**: rule 'brief' на дашборде до 12:00 (PET.weeklyXP «N из 5 дн» + план), daily-plan сдвинут на день.
НЕ сделано: цели недели (явная установка) + напоминания по расписанию (нужен планировщик/push); голосовой ввод/TTS;
генератор заданий учителю; авто-cron на index:textbooks:full.

Мета-фильтр + тумблер экзамена (commit 961504b): вопросы про модель/нейросеть/провайдера/системный промпт
отбиваются шаблоном META_RE (саморефренция ты/тебя/твой РЯДОМ с термином модель/gpt/промпт — не блокирует
«модель атома/газа») + запрет в системном промпте. Кнопки «Подсказка»/«Спросить Квантика» на карточках
экзамена по умолчанию СКРЫТЫ (assistant_exam_buttons='0'); тумблер в админке → /context examButtons →
assistant.js вешает html.asst-exam-on (CSS показывает .tc-asst-btn). Память чата: последние 6 реплик уходят
модели, живёт в памяти вкладки до «Очистить»/reload. §-ссылки источников: section_ref у 531/746 чанков (после
headless-reindex); у остальных ссылка ведёт на главу.

Лимит-UX + отдельный раздел админки (commit 7830084): «не помнит» оказалось free-лимитом Gemini (HTTP 429,
≈20 req/min). callLLM теперь возвращает {text,error}; ask отдаёт source:'limit' («много запросов, подожди,
память не потеряется») или 'error'; фронт показывает это и НЕ ломает историю (pop неудачного вопроса). Многоходовость
работала и раньше — глушил лимит. В окне «Спроси» добавлено пояснение про память (≈6 реплик = рабочая память) +
аватар Квантика в шапке, окно шире/мягче. Управление вынесено в **отдельный раздел админки «Помощник Квантик»**
(frontend/js/admin/sections/assistant.js; AdminSections.assistant; ROUTE_TO_SECTION+ADMIN_ONLY_TABS+btn-tab-assistant;
из games.js конфиг и фича-запись удалены) — там системный вкл/выкл (feature 'assistant' через /api/admin/features) +
модель/ключ/тест/RAG/кнопки экзамена/счётчик/качество. ВАЖНО: free-квота Gemini gemini-2.5-flash = **20 запросов В СУТКИ** (quotaId GenerateRequestsPerDayPerProjectPerModel-FreeTier),
остальные модели Gemini на ключе — limit:0. Это мизер → постоянный 429. Решение: сменить провайдера на **Groq**
(free-тариф щедрый, ~тысячи/день, 30 RPM) — создать ключ console.groq.com, в админ-разделе «Помощник Квантик»
выбрать пресет Groq + вставить ключ; ИЛИ включить billing Gemini; ИЛИ локальная Ollama. Провайдер меняется в админке.

Мульти-провайдер (commit e2bff24): конфиг = список провайдеров app_settings.assistant_providers (JSON
[{id,name,url,model,key}]) + assistant_active. llmConfig=активный; providersOrdered=активный первым + остальные
с ключом; callLLMFailover перебирает их при 429/timeout/network/http (askModel и flashcards идут через него) —
второй ключ авто-подхватывает при исчерпании квоты первого. Legacy assistant_llm_* мигрируются в список при
первом GET админки. Админ-раздел «Помощник Квантик»: список провайдеров (радио=активный, Тест/Изменить/Удалить
по каждому) + форма с пресетами. Эндпоинты POST/DELETE /admin/assistant/provider(/:id), POST /admin/assistant/active.
Решение лимита Gemini (20/сутки): добавить Groq вторым провайдером — failover сам переключит.
ГОЧА: **Groq гео-заблокирован в Беларуси** («Access denied» на console.groq.com) — ключ не создать без VPN.
Железо машины подходит для ЛОКАЛЬНОЙ модели: 32 ГБ ОЗУ, GTX 1080 8 ГБ VRAM (WMI врёт 4 ГБ) → Qwen2.5-7B через
Ollama идеально. Выбор «локально vs Gemini billing vs OpenRouter» пока НЕ сделан (вопрос прерван). Failover-
уведомление (commit aac1240): callLLMFailover пишет app_settings.assistant_failover {failedName,servedName,reason,at};
при успехе активного снимается; админ-раздел показывает баннер «провайдер X недоступен — работаю на Y» / «все недоступны».

ПОДКЛЮЧЁН Kilo Code (работает из Беларуси!) как АКТИВНЫЙ провайдер: URL
**https://kilocode.ai/api/openrouter/chat/completions**, ключ — JWT (в app_settings, не в репо), модель
**nvidia/nemotron-3-ultra-550b-a55b:free** (бесплатно, чистый русский + LaTeX). Gemini остался вторым (failover).
Гочи Kilo на этом ключе: публичные `:free` (deepseek/mistral) → 404 «No endpoints»; платные → 401 «need to sign in»
(нет кредитов); kilo-auto/free → пустой ответ; рабочие бесплатные: nvidia/nemotron-3-ultra-550b-a55b:free и
openrouter/owl-alpha. Наш callLLM (без HTTP-Referer) Kilo принимает. nemotron — reasoning-модель → в промпт добавлено
«не выводи рассуждения вслух» (commit d1be2c1). Список моделей Kilo: GET .../api/openrouter/models (342 шт).
Админка (games.js, карточка): тумблер RAG, «Переиндексировать», число фрагментов. **Кэш** assistant_cache
(7 дней, только вопросы без контекста/истории) + **счётчик** assistant_usage (ИИ/кэш/FAQ, в админке).
**Учитель**: role в /context, доп. системный промпт для teacher/admin + teacher-чипы в «Спроси».
**Ключ перенесён в БД** (app_settings assistant_llm_url/key/model), из backend/.env удалён — конфиг
теперь только через админку. НЕ сделано: headless-RAG для JS-учебников; голосовой ввод; редактор промпта.
Связано: [[reference_quick_lesson]] [[reference_student_materials]] [[feedback_no_emoji]].
