v1.6.0: max_bot fixes — feature keys, flush→commit, test-run, categories, broadcast page, proxy error handling, deploy scripts

This commit is contained in:
2026-05-24 07:50:38 +03:00
parent bd048ea23d
commit 493e0b37a1
127 changed files with 6082 additions and 65 deletions
+100
View File
@@ -0,0 +1,100 @@
import json
import logging
from datetime import datetime
from typing import Optional
import aiohttp
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.models import BotConversation, Bot1CSyncLog, BotSetting
from app.settings_cache import SettingsCache
logger = logging.getLogger(__name__)
async def send_to_1c(db: AsyncSession, conversation: BotConversation) -> Optional[dict]:
webhook_url = SettingsCache.get("1c_webhook_url")
if not webhook_url:
return None
user = conversation.user
category = conversation.category
payload = {
"conversation_id": conversation.id,
"user": {
"max_user_id": user.max_user_id,
"first_name": user.first_name,
"last_name": user.last_name,
"phone": user.phone,
"email": user.email,
},
"inquiry": {
"text": conversation.inquiry_text,
"category": category.name if category else "",
"priority": conversation.priority,
"has_attachment": conversation.has_attachment,
"attachment_type": conversation.attachment_type,
},
"created_at": conversation.created_at.isoformat() if conversation.created_at else "",
}
sync_log = Bot1CSyncLog(
conversation_id=conversation.id,
status="pending",
request_payload=json.dumps(payload, ensure_ascii=False),
)
db.add(sync_log)
await db.flush()
try:
async with aiohttp.ClientSession() as session:
async with session.post(webhook_url, json=payload, timeout=aiohttp.ClientTimeout(total=30)) as resp:
response_text = await resp.text()
sync_log.status = "sent" if resp.status == 200 else "error"
sync_log.response_payload = response_text
sync_log.sent_at = datetime.now()
await db.flush()
if resp.status == 200:
try:
return await resp.json()
except Exception:
return {"raw": response_text}
logger.warning(f"1C webhook returned {resp.status}: {response_text[:500]}")
return None
except Exception as e:
logger.error(f"1C webhook error: {e}")
sync_log.status = "error"
sync_log.response_payload = str(e)
sync_log.sent_at = datetime.now()
await db.flush()
return None
async def handle_1c_response(db: AsyncSession, data: dict) -> bool:
conversation_id = data.get("conversation_id")
if not conversation_id:
return False
result = await db.execute(select(BotConversation).where(BotConversation.id == int(conversation_id)))
conv = result.scalar_one_or_none()
if not conv:
return False
sync_log = Bot1CSyncLog(
conversation_id=conv.id,
status="confirmed",
response_payload=json.dumps(data, ensure_ascii=False),
confirmed_at=datetime.now(),
)
db.add(sync_log)
status = data.get("status", "")
if status:
conv.status = status
assigned = data.get("assigned_to", "")
if assigned:
conv.assigned_to = assigned
await db.flush()
return True
+57
View File
@@ -0,0 +1,57 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.models import Object, SLAContract, Task, Incident, Customer
async def get_object_by_name_or_id(db: AsyncSession, query: str) -> Object:
try:
obj_id = int(query)
return await db.get(Object, obj_id)
except (ValueError, TypeError):
result = await db.execute(
select(Object).where(Object.name.ilike(f"%{query}%")).limit(1)
)
return result.scalar_one_or_none()
async def get_sla_for_object(db: AsyncSession, object_id: int) -> SLAContract:
result = await db.execute(
select(SLAContract).where(
SLAContract.object_id == object_id,
SLAContract.status == "active"
).order_by(SLAContract.created_at.desc()).limit(1)
)
return result.scalar_one_or_none()
async def get_objects_for_customer(db: AsyncSession, customer_name: str) -> list:
result = await db.execute(
select(Customer).where(Customer.name.ilike(f"%{customer_name}%")).limit(1)
)
customer = result.scalar_one_or_none()
if not customer:
return []
result = await db.execute(
select(Object).where(Object.customer_id == customer.id, Object.status == "active")
)
return result.scalars().all()
async def get_active_tasks_for_object(db: AsyncSession, object_id: int) -> list:
result = await db.execute(
select(Task).where(
Task.object_id == object_id,
Task.status.in_(["open", "in_progress"])
)
)
return result.scalars().all()
async def get_last_incident_for_object(db: AsyncSession, object_id: int):
result = await db.execute(
select(Incident).where(
Incident.object_id == object_id,
Incident.status.in_(["resolved", "closed"])
).order_by(Incident.created_at.desc()).limit(1)
)
return result.scalar_one_or_none()
+80
View File
@@ -0,0 +1,80 @@
import aiohttp
import logging
from app.settings_cache import SettingsCache
logger = logging.getLogger(__name__)
async def categorize_with_gpt(text: str) -> str:
gpt_key = SettingsCache.get("yandex_gpt_key")
folder_id = SettingsCache.get("yandex_folder_id")
if not gpt_key or not folder_id:
return ""
prompt = (
"Определи категорию обращения клиента из списка: "
"Видеонаблюдение, СКУД, Пожарная сигнализация, IT-инфраструктура, "
"Обслуживание, Консультация, Другое. "
"Ответь только названием категории, без пояснений. "
f"Текст обращения: {text}"
)
url = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion"
headers = {
"Authorization": f"Api-Key {gpt_key}",
"Content-Type": "application/json",
"x-folder-id": folder_id,
}
body = {
"modelUri": f"gpt://{folder_id}/yandexgpt-lite/latest",
"completionOptions": {"stream": False, "temperature": 0.1, "maxTokens": 50},
"messages": [{"role": "user", "text": prompt}],
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=body) as resp:
if resp.status == 200:
data = await resp.json()
return data.get("result", {}).get("alternatives", [{}])[0].get("message", {}).get("text", "").strip()
logger.warning(f"GPT categorize returned {resp.status}")
except Exception as e:
logger.error(f"GPT categorize error: {e}")
return ""
async def analyze_emotion_with_gpt(text: str) -> str:
gpt_key = SettingsCache.get("yandex_gpt_key")
folder_id = SettingsCache.get("yandex_folder_id")
if not gpt_key or not folder_id:
return "neutral"
prompt = (
"Определи эмоциональную окраску текста клиента. "
"Варианты: positive, neutral, negative. "
"Ответь только одним словом. "
f"Текст: {text}"
)
url = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion"
headers = {
"Authorization": f"Api-Key {gpt_key}",
"Content-Type": "application/json",
"x-folder-id": folder_id,
}
body = {
"modelUri": f"gpt://{folder_id}/yandexgpt-lite/latest",
"completionOptions": {"stream": False, "temperature": 0.1, "maxTokens": 10},
"messages": [{"role": "user", "text": prompt}],
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=body) as resp:
if resp.status == 200:
data = await resp.json()
return data.get("result", {}).get("alternatives", [{}])[0].get("message", {}).get("text", "").strip().lower()
logger.warning(f"GPT emotion analysis returned {resp.status}")
except Exception as e:
logger.error(f"GPT emotion analysis error: {e}")
return "neutral"