docs(brain): turnkey-заплата подключения мозга к движку aleksandra_engine.py
This commit is contained in:
@@ -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/` при этом можно не удалять (не мешают).
|
||||
Reference in New Issue
Block a user