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:
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user