diff --git a/docs/superpowers/2026-07-17-aleksandra-brain-engine-patch.md b/docs/superpowers/2026-07-17-aleksandra-brain-engine-patch.md new file mode 100644 index 00000000..ae7d7f2c --- /dev/null +++ b/docs/superpowers/2026-07-17-aleksandra-brain-engine-patch.md @@ -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@:/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/` при этом можно не удалять (не мешают).