9.8 KiB
Заплата под движок: подключить мозг знаний к 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_ABS (и BAZA_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/ при этом можно не удалять (не мешают).