File size: 9,150 Bytes
e584a93 413a89a e584a93 413a89a b414ebf e584a93 b414ebf 421cf0f b4a1c1a b414ebf 8c8bded b414ebf 413a89a f9beea3 413a89a b4a1c1a 257c2f7 e6b5c2b 413a89a 8c8bded 413a89a 8c8bded 413a89a e6b5c2b ce004d9 e6b5c2b ce004d9 e6b5c2b ce004d9 e6b5c2b 62664f6 b414ebf 413a89a ce004d9 22ff749 4cf208c 5de4047 413a89a b414ebf e584a93 413a89a 62664f6 e584a93 413a89a e584a93 413a89a e584a93 22ff749 b414ebf 413a89a e584a93 413a89a e584a93 413a89a 8c8bded 413a89a e584a93 413a89a 545660d 62664f6 e584a93 62664f6 413a89a 545660d 413a89a 8c8bded 413a89a b414ebf e584a93 f010f62 e584a93 ce004d9 e584a93 5de4047 e584a93 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | """ORTOS AI Consultant — core message processing.
Groq (primary) -> local Llama 3.2 3B (fallback) -> 'not sure'.
Webhook handler for Bitrix24 Open Lines.
"""
import os, re, time, logging
from collections import deque
from openai import OpenAI
import httpx
from dotenv import load_dotenv
from log_store import add as log_add
from knowledge import search_debug
load_dotenv()
logger = logging.getLogger(__name__)
GROQ_API_KEY = os.getenv('GROQ_API_KEY')
LLAMA_MODEL_PATH = os.getenv('LLAMA_MODEL_PATH')
from knowledge import reload_knowledge
kb_items, kb_tfidf = reload_knowledge(['knowledge_base.json', 'knowledge_base_insoles.json'])
_llm = None
try:
from llama_cpp import Llama
if LLAMA_MODEL_PATH and os.path.exists(LLAMA_MODEL_PATH):
logger.info("Pre-loading local LLM...")
_llm = Llama(model_path=LLAMA_MODEL_PATH, n_ctx=2048, n_threads=2, verbose=False)
logger.info("Local LLM loaded")
elif LLAMA_MODEL_PATH:
logger.warning(f"LLAMA_MODEL_PATH set but file not found: {LLAMA_MODEL_PATH}")
except Exception as e:
logger.warning(f"Local LLM unavailable: {e}")
OPERATOR_TRIGGERS = [
# Явный запрос человека
re.compile(r'(оператор|человек|менеджер|специалист)\s', re.I),
re.compile(r'(свяжите|соедините|позовите|пригласите)', re.I),
re.compile(r'(живой|живого|живому)\s', re.I),
# Отмена/отказ
re.compile(r'отмен(ит|и|ю|я|иться|ять)', re.I),
re.compile(r'(удал|отписк|откаж|отзов|отпиш)(ит|и|ю|я|ись)', re.I),
re.compile(r'расторж(ение|ения|нуть)', re.I),
# Возврат/обмен/гарантия
re.compile(r'(возврат|обмен|замен|гаранти[йя])', re.I),
re.compile(r'(брак|дефект|не\s*подо[шй]л|не\s*работает)', re.I),
# Жалобы/проблемы
re.compile(r'(жалоб[ауы]|претензи[яю]|недовол(ен|ьна|ьны)|разочар)', re.I),
re.compile(r'(плох[аяоеи]{1,2})\s+', re.I),
# Продление/изменение
re.compile(r'(продл|продлит|продление)', re.I),
re.compile(r'(дополнител[ь]н|изменени[ея])', re.I),
# Медицина/здоровье
re.compile(r'(болит|боль|врач|диагноз|противопоказан[ия])', re.I),
re.compile(r'(травм[ауы]|перелом|шин[ау]|гипс[еа])', re.I),
# Запись в салон
re.compile(r'(записать(ся|сь)|запишите|запись\s+на)', re.I),
re.compile(r'(прийт[иё]|придт[иё]|приехат[ьы])', re.I),
# Проблемы с заказом
re.compile(r'(где\s+заказ|статус\s+заказ[ао]|отследить)', re.I),
re.compile(r'(не\s+приш[её]л|не\s+доставил[и]?|курьер)', re.I),
]
GREETINGS = {'привет', 'здравствуйте', 'здравствуй', 'добрый день', 'доброе утро',
'добрый вечер', 'хай', 'hi', 'hello', 'приветствую', 'салют'}
SYSTEM_PROMPT = (
"Ты — консультант по индивидуальным ортопедическим стелькам салона ORTOS. "
"Ты отвечаешь ТОЛЬКО на вопросы об индивидуальных стельках: их изготовлении, "
"материалах, показаниях, сроках, уходе, ценах. "
"Используй ТОЛЬКО переданную информацию. "
"Не выдумывай цены, характеристики, сроки. "
"Если вопрос не про индивидуальные стельки — просто напиши: "
"'Переведу вас на оператора.' "
"Если переданная информация не отвечает на вопрос клиента — тоже напиши: "
"'Переведу вас на оператора.' "
)
dialog_history: dict[str, deque] = {}
def need_operator(text: str) -> bool:
text_lower = text.lower()
for pattern in OPERATOR_TRIGGERS:
if pattern.search(text_lower):
return True
return False
def _build_user_message(question: str, context_items, history: deque | None = None):
if not context_items:
context = "(нет информации)"
else:
context = "\n\n".join(f"[{item.title}]\n{item.content}" for item in context_items)
hist_text = ""
if history:
lines = []
for msg in history:
role = "Клиент" if msg["role"] == "user" else "ORTOS"
lines.append(f"{role}: {msg['content']}")
hist_text = "История диалога:\n" + "\n".join(lines) + "\n\n"
return (
f"{hist_text}"
f"Вопрос клиента: {question}\n\n"
f"Доступная информация из базы знаний ORTOS:\n{context}\n\n"
"Дай точный ответ на вопрос клиента, используя ТОЛЬКО информацию выше. "
"Если в информации нет нужных данных — скажи что не знаешь."
)
def get_grok_response(question: str, context_items, history: deque | None = None) -> str | None:
if not GROQ_API_KEY:
return None
try:
client = OpenAI(
api_key=GROQ_API_KEY,
base_url="https://api.groq.com/openai/v1",
http_client=httpx.Client(proxy=None),
)
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": _build_user_message(question, context_items, history)},
],
temperature=0.1,
max_tokens=500,
)
return response.choices[0].message.content
except Exception as e:
logger.warning(f"Groq error: {e}")
return None
def get_local_response(question: str, context_items, history: deque | None = None) -> str | None:
if _llm is None:
return None
try:
response = _llm.create_chat_completion(
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": _build_user_message(question, context_items, history)},
],
temperature=0.1,
max_tokens=500,
)
return response["choices"][0]["message"]["content"]
except Exception as e:
logger.warning(f"Local LLM error: {e}")
return None
def process_message(text: str, dialog_id: str):
"""Process message, return reply text and send to Bitrix."""
if dialog_id not in dialog_history:
dialog_history[dialog_id] = deque(maxlen=10)
text_lower = text.lower()
if text_lower in GREETINGS or any(text_lower.startswith(g) for g in GREETINGS if " " in g):
reply = "Здравствуйте! Я — консультант салона ORTOS. Спросите что-нибудь о наших стельках, ценах, доставке, записи на консультацию!"
log_add(question=text, response=reply, mode="greeting", search_method="—", timing_ms=0)
dialog_history[dialog_id].append({"role": "user", "content": text})
dialog_history[dialog_id].append({"role": "assistant", "content": reply})
return reply
if need_operator(text):
reply = "Переход на оператора..."
log_add(question=text, response=reply, mode="operator", search_method="—", timing_ms=0)
dialog_history.pop(dialog_id, None)
return reply
t0 = time.time()
debug = search_debug(text, top_k=4)
results = debug["items"]
t1 = time.time()
response = get_grok_response(text, results, dialog_history.get(dialog_id))
mode = "groq"
llm_model = "llama-3.3-70b-versatile (Groq)"
if response is None:
response = get_local_response(text, results, dialog_history.get(dialog_id))
mode = "local"
llm_model = "Llama 3.2 3B (local)"
if response is None:
response = "Извините, не удалось получить ответ. Попробуйте позже или позвоните +375 (29) 145-03-03."
mode = "fallback"
llm_model = ""
t2 = time.time()
dialog_history[dialog_id].append({"role": "user", "content": text})
dialog_history[dialog_id].append({"role": "assistant", "content": response})
log_add(
question=text,
response=response,
mode=mode,
search_method=debug["method"],
search_details=debug["details"],
llm_model=llm_model,
timing_ms=round((t1 - t0) * 1000 + (t2 - t1) * 1000),
)
return response
|