docs(brain): turnkey-заплата подключения мозга к движку aleksandra_engine.py

This commit is contained in:
Дмитрий
2026-07-17 09:29:02 +03:00
parent c972d6f3f3
commit 2bc85f6979
@@ -0,0 +1,147 @@
# Заплата под движок: подключить мозг знаний к `aleksandra_engine.py` (под ключ)
**Дата:** 17.07.2026 · **Статус:** готово к применению на сервере (2 минуты).
**Зачем:** движок `aleksandra_engine.py` (демо / GPU-сервер) заменяет слепой поход в Perplexity на мозг знаний из `knowledge/aleksandra/` — Sonar дёргается только на реально новую нишу.
**Предпосылки:** пакет `knowledge/aleksandra/brain/` и хранилище собраны и проверены живьём (ветка `feat/aleksandra-knowledge-base`); реальный индекс `knowledge/aleksandra/nishi_index.json` собран AITunnel-эмбеддингами; пороги 0.45/0.33 (лесенка 9/9, 0 обращений к Sonar на знакомых нишах).
⚠️ **Применять на GPU-сервере — только когда телефония Mango не в работе (иначе гонка сессий).** Мозг от телефонии не зависит; можно подключить и проверить offline-путь (без Cartesia), а звук/SIP — отдельно.
---
## 0. Что везём на сервер
Мозг НЕ требует новых pip-зависимостей: `brain/*` использует только stdlib (`json`, `urllib`, `re`, `math`, `pathlib`). Достаточно скопировать папку знаний.
```bash
# с Windows (или через git на сервере) — папку целиком:
scp -r "knowledge/aleksandra" root@<gpu>:/root/knowledge/aleksandra
# на сервере ожидается: /root/knowledge/aleksandra/{brain, playbook, nishi, nishi_index.json, _index.md}
```
Ключ AITunnel (тот же рублёвый, что у мозга движка) — уже есть на сервере как `aitunnel.key`; мозг возьмёт его через `ALEX_AITUNNEL_KEY` или дефолтный путь.
Переменные окружения для запуска движка:
```bash
export LIDERRA_KNOW=/root/knowledge/aleksandra
export ALEX_EMBED=aitunnel
export ALEX_AITUNNEL_KEY=/root/aitunnel.key # путь к файлу ключа (если не рядом)
```
---
## 1. Правки в `aleksandra_engine.py`
### 1.1. Шапка — подключить мозг и загрузить индекс ОДИН раз
Добавить после блока чтения ключей (после `AIT = open(...).read().strip()`), рядом с `import os`:
```python
import sys
KNOW = os.environ.get("LIDERRA_KNOW", r"/root/knowledge/aleksandra")
sys.path.insert(0, KNOW) # brain лежит в KNOW/brain → KNOW на путь импорта
from brain.research import get_brief
from brain.embed import get_embedder
from brain.playbook import load_playbook
from brain.index_store import load_index
_EMBED = get_embedder() # ALEX_EMBED=aitunnel по умолчанию
_NISHI = os.path.join(KNOW, "nishi")
_INDEX_PATH = os.path.join(KNOW, "nishi_index.json")
_INDEX = load_index(_INDEX_PATH) # грузим индекс ОДИН раз при старте (на звонке НЕ пересобираем)
```
### 1.2. `load_baza()` — собирать из `playbook/`
Заменить тело функции `load_baza()` целиком на:
```python
def load_baza():
return substitute_prices(load_playbook(os.path.join(KNOW, "playbook")))
```
`substitute_prices` и метки цен `{{вилка}}`/`{{тарифы}}`/… — **без изменений**. Старую константу `BAZA_ABS``BAZA_FILE`) можно удалить.
### 1.3. `research_niche()` — через мозг; живой Sonar становится `sonar_fn`
Удалить старую `research_niche()` и строку `OTRASL_CACHE = os.path.join(HERE, "otrasl_cache"); os.makedirs(...)`. Вместо них:
```python
def _sonar(niche):
"""Живой Perplexity(Sonar) — теперь вызывается ТОЛЬКО на промах (решает get_brief)."""
body = {"model": SONAR_MODEL, "max_tokens": 600,
"messages": [{"role": "user", "content": RESEARCH_PROMPT.format(niche=niche)}]}
req = urllib.request.Request("https://api.aitunnel.ru/v1/chat/completions",
data=json.dumps(body).encode(), method="POST")
req.add_header("Authorization", f"Bearer {AIT}"); req.add_header("Content-Type", "application/json")
r = json.load(urllib.request.urlopen(req, timeout=90, context=CTX))
return re.sub(r"\[\d+\]", "", r["choices"][0]["message"]["content"]).strip()
def research_niche(niche):
brief, source = get_brief(niche, nishi_dir=_NISHI, index=_INDEX,
embed_fn=_EMBED, sonar_fn=_sonar,
today=time.strftime("%Y-%m-%d"), index_path=_INDEX_PATH)
print(f" 🧠 источник справки «{niche}»: {source}", flush=True) # niche/domain/miss
return brief
```
`RESEARCH_PROMPT`, `SONAR_MODEL` — оставить как есть (их использует `_sonar`).
На `miss`/`domain+sonar` мозг сам допишет карточку в `nishi/` и добавит её в индекс (`index_path`) — живая память копится.
### 1.4. `detect_niche()` — извлечение ЛЮБОЙ ниши моделью (была розетка `🔌PROD-НИША`)
Заменить demo-регексы на быстрый вызов FAST-модели:
```python
def detect_niche(text):
q = ("Извлеки из реплики клиента его сферу бизнеса одним коротким словосочетанием "
"(напр. «стоматология», «займы под залог авто»). Если сферы нет — пустая строка. "
f"Реплика: «{text}»")
body = {"model": FAST, "max_tokens": 20, "temperature": 0,
"messages": [{"role": "user", "content": q}]}
req = urllib.request.Request("https://api.aitunnel.ru/v1/chat/completions",
data=json.dumps(body).encode(), method="POST")
req.add_header("Authorization", f"Bearer {AIT}"); req.add_header("Content-Type", "application/json")
try:
r = json.load(urllib.request.urlopen(req, timeout=15, context=CTX))
niche = re.sub(r"[\"«».]+", "", r["choices"][0]["message"]["content"]).strip()
return niche or None
except Exception:
return None
```
Поведение разворота в `main()` (`det != niche` → тормоз-разведка + свап) — **не трогать**, оно уже правильное.
### 1.5. Призма `PRISM_TMPL` — принцип «эксперт, а не консультант»
В `PRISM_TMPL`, сразу после пункта «(а) показать, что ты в его мире…», вставить:
```
(а1) звучи как профи И в его отрасли, И в маркетинге — точно назови боль, говори его языком,
покажи, что понимаешь, как в его нише ищут клиентов и что делают конкуренты (доверие ↑);
НО без бесплатного маркетинг-консалтинга вне Лидерры — любой такой штрих сразу в пользу Лидерры.
```
Запрет маркетинг-советов ниже по `PRISM_TMPL`**оставить как есть** (это опора).
---
## 2. Offline-смоук мозга в движке (без Cartesia/SIP)
Проверить, что brain-путь движка работает, НЕ трогая звук/телефонию:
```bash
cd <где движок>
LIDERRA_KNOW=/root/knowledge/aleksandra ALEX_EMBED=aitunnel ALEX_AITUNNEL_KEY=/root/aitunnel.key \
python -X utf8 -c "
import aleksandra_engine as E
print('load_baza символов:', len(E.load_baza())) # playbook собрался
print('detect:', E.detect_niche('у меня стоматология, импланты')) # модель извлекла нишу
print('источник (знакомая ниша):')
b = E.research_niche('стоматология') # ждём niche, Sonar НЕ зовётся
print(' длина справки:', len(b))
"
```
Ожидаемо: `load_baza` непустой; `detect` вернул сферу; для «стоматология» в логе `🧠 источник справки «стоматология»: niche` (не `miss`). Полный демо-прогон `python -X utf8 aleksandra_engine.py` — уже с Cartesia-ключом.
---
## 3. Проверка §9 спеки (6 ниш) на живом движке
Прогнать 6 реплик (2 в хранилище, 2 близкие, 1 под-ниша знакомого домена, 1 новая) и по логам `🧠 источник…` убедиться: Sonar реально вызвался **только на новой** нише, фаззи-матч попал, новая карточка завелась. Пороги (`brain/lookup.py` `THR_NICHE`/`THR_DOMAIN`) — подкрутить по слуху, если матч промахивается; они помечены как провизорные.
---
## 4. Откат
Мозг подключается тремя точками (`load_baza`, `research_niche`, `detect_niche`) + шапка. Откат — вернуть прежние тела функций и `otrasl_cache`. Хранилище и пакет `brain/` при этом можно не удалять (не мешают).