Files
portal/docs/superpowers/2026-07-17-aleksandra-brain-engine-patch.md
T

9.8 KiB
Raw Blame History

Заплата под движок: подключить мозг знаний к 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). Достаточно скопировать папку знаний.

# с 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 или дефолтный путь.

Переменные окружения для запуска движка:

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:

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() целиком на:

def load_baza():
    return substitute_prices(load_playbook(os.path.join(KNOW, "playbook")))

substitute_prices и метки цен {{вилка}}/{{тарифы}}/… — без изменений. Старую константу BAZA_ABSBAZA_FILE) можно удалить.

1.3. research_niche() — через мозг; живой Sonar становится sonar_fn

Удалить старую research_niche() и строку OTRASL_CACHE = os.path.join(HERE, "otrasl_cache"); os.makedirs(...). Вместо них:

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-модели:

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-путь движка работает, НЕ трогая звук/телефонию:

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/ при этом можно не удалять (не мешают).