forked from hesabix/arc
846 lines
27 KiB
Python
Executable file
846 lines
27 KiB
Python
Executable file
from __future__ import annotations
|
|
|
|
from typing import Dict, Any
|
|
from datetime import datetime
|
|
from sqlalchemy.orm import Session
|
|
import logging
|
|
|
|
from app.services.telegram_ai_chat_service import TelegramAIChatService
|
|
from app.services.providers.telegram_provider import TelegramProvider
|
|
from adapters.db.repositories.user_repo import UserRepository
|
|
from adapters.db.repositories.support.ticket_repository import TicketRepository
|
|
from adapters.db.repositories.support.message_repository import MessageRepository
|
|
from app.core.auth_dependency import AuthContext
|
|
from app.services.messenger_operator.crm_callback_map import crm_callback_data_to_command
|
|
from app.services.messenger_operator.dispatch import dispatch_operator_messenger_message
|
|
from app.services.ai.prompt_service import get_prompt_by_key
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def handle_telegram_message(
|
|
message: Dict[str, Any],
|
|
db: Session,
|
|
telegram_provider: TelegramProvider
|
|
) -> bool:
|
|
"""پردازش پیام متنی از تلگرام"""
|
|
chat = message.get("chat", {})
|
|
chat_id = chat.get("id")
|
|
text: str = message.get("text", "").strip()
|
|
|
|
logger.info(f"Processing telegram message: chat_id={chat_id}, text={text[:50] if text else 'empty'}")
|
|
|
|
if not chat_id or not text:
|
|
logger.warning(f"Invalid message: chat_id={chat_id}, text={text[:50] if text else 'empty'}")
|
|
return False
|
|
|
|
# پیدا کردن کاربر از chat_id
|
|
user = get_user_by_telegram_chat_id(db, chat_id)
|
|
if not user:
|
|
logger.warning(f"User not found for chat_id: {chat_id}")
|
|
# ارسال پیام به کاربر که اتصال برقرار نشده است
|
|
telegram_provider.send_text(
|
|
chat_id=chat_id,
|
|
text="❌ اتصال تلگرام شما برقرار نشده است.\n\nلطفاً از داخل برنامه، لینک اتصال تلگرام را ایجاد کنید و دوباره امتحان کنید."
|
|
)
|
|
return False
|
|
|
|
# ایجاد service
|
|
try:
|
|
service = TelegramAIChatService(db, user.id, chat_id, telegram_provider)
|
|
user_context = service._create_auth_context()
|
|
logger.info(f"Service created for user_id={user.id}, chat_id={chat_id}")
|
|
except Exception as e:
|
|
logger.error(f"Error creating service: {e}", exc_info=True)
|
|
if chat_id:
|
|
telegram_provider.send_text(
|
|
chat_id=chat_id,
|
|
text="❌ خطا در ایجاد سرویس. لطفاً دوباره امتحان کنید."
|
|
)
|
|
return False
|
|
|
|
# بررسی دستورات
|
|
if text == "/start":
|
|
# اگر لینک شده، منوی اصلی را نمایش بده
|
|
logger.info(f"Handling /start command for chat_id={chat_id}")
|
|
return service.send_main_menu(user_context)
|
|
elif text == "/menu":
|
|
logger.info(f"Handling /menu command for chat_id={chat_id}")
|
|
return service.send_main_menu(user_context)
|
|
elif text == "/help":
|
|
logger.info(f"Handling /help command for chat_id={chat_id}")
|
|
help_text = """🤖 دستیار هوش مصنوعی Hesabix
|
|
|
|
دستورات:
|
|
/menu - نمایش منوی اصلی
|
|
/help - راهنمای استفاده
|
|
|
|
برای شروع گفتوگو با AI، از منوی اصلی استفاده کنید."""
|
|
return telegram_provider.send_text(chat_id=chat_id, text=help_text)
|
|
|
|
# پردازش پیام متنی به عنوان سوال از AI
|
|
logger.info(f"Processing AI message for chat_id={chat_id}, text={text[:50]}")
|
|
try:
|
|
result = await service.process_message(text, user_context)
|
|
logger.info(f"process_message result for chat_id={chat_id}: {result}")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error in process_message: {e}", exc_info=True)
|
|
if chat_id:
|
|
telegram_provider.send_text(
|
|
chat_id=chat_id,
|
|
text="❌ خطا در پردازش پیام شما. لطفاً دوباره امتحان کنید."
|
|
)
|
|
return False
|
|
|
|
|
|
async def handle_telegram_callback_query(
|
|
callback_query: Dict[str, Any],
|
|
db: Session,
|
|
telegram_provider: TelegramProvider
|
|
) -> bool:
|
|
"""پردازش Callback Query از دکمهها"""
|
|
msg = callback_query.get("message") or {}
|
|
chat = msg.get("chat") or {}
|
|
chat_id = chat.get("id")
|
|
# اگر پیام حذف شده باشد، در چت خصوصی معمولاً chat_id == from.id است
|
|
if chat_id is None:
|
|
from_cb = callback_query.get("from") or {}
|
|
chat_id = from_cb.get("id")
|
|
callback_data = (callback_query.get("data") or "").strip()
|
|
query_id = callback_query.get("id", "")
|
|
|
|
if chat_id is None or not callback_data:
|
|
return False
|
|
|
|
try:
|
|
chat_id = int(chat_id)
|
|
except (TypeError, ValueError):
|
|
return False
|
|
|
|
# پاسخ به callback query (برای حذف loading)
|
|
telegram_provider.answer_callback_query(query_id)
|
|
|
|
# پیدا کردن کاربر از chat_id
|
|
user = get_user_by_telegram_chat_id(db, chat_id)
|
|
if not user:
|
|
logger.warning(f"User not found for chat_id: {chat_id}")
|
|
return False
|
|
|
|
# ایجاد service
|
|
service = TelegramAIChatService(db, user.id, chat_id, telegram_provider)
|
|
user_context = service._create_auth_context()
|
|
|
|
# تجزیه callback_data
|
|
parts = callback_data.split(":")
|
|
|
|
try:
|
|
if parts[0] == "crm":
|
|
cmd = crm_callback_data_to_command(parts[1:])
|
|
if not cmd:
|
|
return bool(
|
|
service.telegram_provider.send_text(
|
|
chat_id=chat_id, text="دکمه نامعتبر یا منقضی است.", parse_mode=None
|
|
)
|
|
)
|
|
|
|
def _crm_send(msg: str, inline_keyboard: Any = None) -> Any:
|
|
rm = {"inline_keyboard": inline_keyboard} if inline_keyboard else None
|
|
return telegram_provider.send_text(chat_id, msg, parse_mode=None, reply_markup=rm)
|
|
|
|
return bool(
|
|
dispatch_operator_messenger_message(
|
|
db,
|
|
platform="telegram",
|
|
message={"chat": {"id": chat_id}, "text": cmd},
|
|
send=_crm_send,
|
|
)
|
|
)
|
|
if parts[0] == "ai":
|
|
action = parts[1] if len(parts) > 1 else ""
|
|
approval_id = parts[2] if len(parts) > 2 else ""
|
|
if action == "approve":
|
|
return await service.confirm_pending_write(
|
|
approval_id, user_context, approved=True
|
|
)
|
|
if action == "reject":
|
|
return await service.confirm_pending_write(
|
|
approval_id, user_context, approved=False
|
|
)
|
|
logger.warning(f"Unknown ai callback_data: {callback_data}")
|
|
return False
|
|
if parts[0] == "menu":
|
|
return await handle_menu_callback(service, user_context, parts[1:])
|
|
elif parts[0] == "chat":
|
|
return await handle_chat_callback(service, user_context, parts[1:])
|
|
elif parts[0] == "ticket":
|
|
return await handle_ticket_callback(service, user_context, parts[1:], db)
|
|
elif parts[0] == "admin":
|
|
return await handle_admin_callback(service, user_context, parts[1:], db)
|
|
elif parts[0] == "back":
|
|
return await handle_back_callback(service, user_context, parts[1:])
|
|
else:
|
|
logger.warning(f"Unknown callback_data: {callback_data}")
|
|
return False
|
|
except Exception as e:
|
|
logger.error(f"Error handling callback query: {e}", exc_info=True)
|
|
return telegram_provider.send_text(
|
|
chat_id=chat_id,
|
|
text=f"❌ خطا در پردازش درخواست: {str(e)}"
|
|
)
|
|
|
|
|
|
async def handle_menu_callback(
|
|
service: TelegramAIChatService,
|
|
user_context: AuthContext,
|
|
parts: list[str]
|
|
) -> bool:
|
|
"""پردازش callback منو"""
|
|
if not parts:
|
|
return False
|
|
|
|
menu_type = parts[0]
|
|
|
|
if menu_type == "main":
|
|
return service.send_main_menu(user_context)
|
|
elif menu_type == "chat":
|
|
return service.send_chat_menu(user_context)
|
|
elif menu_type == "sessions":
|
|
return service.send_sessions_menu(user_context)
|
|
elif menu_type == "tickets":
|
|
# بررسی دسترسی
|
|
if not user_context.can_access_support_operator():
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="❌ شما دسترسی به این بخش ندارید."
|
|
)
|
|
return await send_tickets_menu(service, user_context)
|
|
elif menu_type == "admin":
|
|
# بررسی دسترسی
|
|
if not user_context.is_superadmin():
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="❌ شما دسترسی به این بخش ندارید."
|
|
)
|
|
return send_admin_menu(service, user_context)
|
|
|
|
return False
|
|
|
|
|
|
async def handle_chat_callback(
|
|
service: TelegramAIChatService,
|
|
user_context: AuthContext,
|
|
parts: list[str]
|
|
) -> bool:
|
|
"""پردازش callback گفتوگو"""
|
|
if not parts:
|
|
return False
|
|
|
|
action = parts[0]
|
|
|
|
if action == "business" and len(parts) > 1:
|
|
business_id = int(parts[1])
|
|
return service.handle_business_selection(business_id, user_context)
|
|
elif action == "new":
|
|
return service.send_chat_menu(user_context)
|
|
elif action == "session" and len(parts) > 1:
|
|
session_id = int(parts[1])
|
|
return await handle_session_selection(service, user_context, session_id)
|
|
elif action == "ask":
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="لطفاً سوال خود را بنویسید:"
|
|
)
|
|
|
|
return False
|
|
|
|
|
|
async def handle_ticket_callback(
|
|
service: TelegramAIChatService,
|
|
user_context: AuthContext,
|
|
parts: list[str],
|
|
db: Session
|
|
) -> bool:
|
|
"""پردازش callback تیکت"""
|
|
# بررسی دسترسی
|
|
if not user_context.can_access_support_operator():
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="❌ شما دسترسی به این بخش ندارید."
|
|
)
|
|
|
|
if len(parts) < 2:
|
|
return False
|
|
|
|
ticket_id = int(parts[1])
|
|
|
|
if len(parts) == 2:
|
|
# مشاهده تیکت
|
|
return await send_ticket_details(service, user_context, ticket_id, db)
|
|
elif len(parts) == 3:
|
|
action = parts[2]
|
|
if action == "suggest_reply":
|
|
return await handle_suggest_reply(service, user_context, ticket_id, db)
|
|
elif action == "auto_reply":
|
|
return await handle_auto_reply(service, user_context, ticket_id, db)
|
|
|
|
return False
|
|
|
|
|
|
async def handle_admin_callback(
|
|
service: TelegramAIChatService,
|
|
user_context: AuthContext,
|
|
parts: list[str],
|
|
db: Session
|
|
) -> bool:
|
|
"""پردازش callback مدیریت"""
|
|
# بررسی دسترسی
|
|
if not user_context.is_superadmin():
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="❌ شما دسترسی به این بخش ندارید."
|
|
)
|
|
|
|
if not parts:
|
|
return False
|
|
|
|
action = parts[0]
|
|
|
|
if action == "stats":
|
|
return await send_admin_stats(service, user_context, db)
|
|
|
|
return False
|
|
|
|
|
|
async def handle_back_callback(
|
|
service: TelegramAIChatService,
|
|
user_context: AuthContext,
|
|
parts: list[str]
|
|
) -> bool:
|
|
"""پردازش callback بازگشت"""
|
|
if not parts:
|
|
return False
|
|
|
|
back_to = parts[0]
|
|
|
|
if back_to == "main":
|
|
return service.send_main_menu(user_context)
|
|
elif back_to == "chat":
|
|
return service.send_chat_menu(user_context)
|
|
elif back_to == "tickets":
|
|
return await send_tickets_menu(service, user_context)
|
|
elif back_to == "admin":
|
|
return await send_admin_menu(service, user_context)
|
|
|
|
return False
|
|
|
|
|
|
def get_user_by_telegram_chat_id(db: Session, chat_id: int | None):
|
|
"""پیدا کردن کاربر از chat_id تلگرام"""
|
|
from sqlalchemy import select
|
|
from adapters.db.models.user import User
|
|
|
|
if chat_id is None:
|
|
return None
|
|
try:
|
|
cid = int(chat_id)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
user = db.execute(
|
|
select(User).where(User.telegram_chat_id == cid)
|
|
).scalars().first()
|
|
|
|
return user
|
|
|
|
|
|
async def send_tickets_menu(
|
|
service: TelegramAIChatService,
|
|
user_context: AuthContext
|
|
) -> bool:
|
|
"""ارسال منوی تیکتها"""
|
|
from adapters.api.v1.schemas import QueryInfo
|
|
|
|
ticket_repo = TicketRepository(service.db)
|
|
query_info = QueryInfo(skip=0, take=20, filters=None, sort_by="last_message_at", sort_desc=True)
|
|
tickets, total = ticket_repo.get_operator_tickets(query_info)
|
|
|
|
if not tickets:
|
|
buttons = [
|
|
[{"text": "🔄 بهروزرسانی", "callback_data": "menu:tickets"}],
|
|
[{"text": "⬅️ بازگشت", "callback_data": "back:main"}]
|
|
]
|
|
keyboard = service._build_inline_keyboard(buttons)
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="تیکت باز وجود ندارد.",
|
|
reply_markup=keyboard
|
|
)
|
|
|
|
buttons: list[list[dict[str, str]]] = []
|
|
|
|
# دکمههای تیکتها (3 در هر ردیف)
|
|
for i in range(0, min(len(tickets), 9), 3): # حداکثر 9 تیکت
|
|
row: list[dict[str, str]] = []
|
|
for j in range(3):
|
|
if i + j < len(tickets):
|
|
ticket = tickets[i + j]
|
|
priority_emoji = _get_priority_emoji(ticket.priority.name if ticket.priority else "medium")
|
|
row.append({
|
|
"text": f"{priority_emoji} #{ticket.id}",
|
|
"callback_data": f"ticket:{ticket.id}"
|
|
})
|
|
if row:
|
|
buttons.append(row)
|
|
|
|
buttons.append([{"text": "🔄 بهروزرسانی", "callback_data": "menu:tickets"}])
|
|
buttons.append([{"text": "⬅️ بازگشت", "callback_data": "back:main"}])
|
|
|
|
keyboard = service._build_inline_keyboard(buttons)
|
|
|
|
text = f"تیکتهای باز ({total}):\n\n"
|
|
for ticket in tickets[:9]:
|
|
priority_emoji = _get_priority_emoji(ticket.priority.name if ticket.priority else "medium")
|
|
text += f"{priority_emoji} #{ticket.id} - {ticket.title}\n"
|
|
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text=text,
|
|
reply_markup=keyboard
|
|
)
|
|
|
|
|
|
async def send_ticket_details(
|
|
service: TelegramAIChatService,
|
|
user_context: AuthContext,
|
|
ticket_id: int,
|
|
db: Session
|
|
) -> bool:
|
|
"""ارسال جزئیات تیکت"""
|
|
ticket_repo = TicketRepository(db)
|
|
ticket = ticket_repo.get_operator_ticket_with_details(ticket_id)
|
|
|
|
if not ticket:
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="❌ تیکت یافت نشد."
|
|
)
|
|
|
|
priority_emoji = _get_priority_emoji(ticket.priority.name if ticket.priority else "medium")
|
|
user_name = f"{ticket.user.first_name or ''} {ticket.user.last_name or ''}".strip() if ticket.user else "نامشخص"
|
|
|
|
text = f"""تیکت #{ticket.id}:
|
|
|
|
👤 کاربر: {user_name}
|
|
📌 موضوع: {ticket.title}
|
|
{priority_emoji} اولویت: {ticket.priority.name if ticket.priority else 'نامشخص'}
|
|
📅 تاریخ: {ticket.created_at.strftime('%Y/%m/%d %H:%M') if ticket.created_at else 'نامشخص'}
|
|
|
|
📝 توضیحات:
|
|
{ticket.description[:500]}{'...' if len(ticket.description) > 500 else ''}"""
|
|
|
|
buttons = [
|
|
[{"text": "🤖 پیشنهاد پاسخ AI", "callback_data": f"ticket:{ticket_id}:suggest_reply"}],
|
|
[{"text": "✉️ پاسخ خودکار AI", "callback_data": f"ticket:{ticket_id}:auto_reply"}],
|
|
[{"text": "⬅️ بازگشت", "callback_data": "back:tickets"}]
|
|
]
|
|
keyboard = service._build_inline_keyboard(buttons)
|
|
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text=text,
|
|
reply_markup=keyboard
|
|
)
|
|
|
|
|
|
async def handle_suggest_reply(
|
|
service: TelegramAIChatService,
|
|
user_context: AuthContext,
|
|
ticket_id: int,
|
|
db: Session
|
|
) -> bool:
|
|
"""پیشنهاد پاسخ AI برای تیکت"""
|
|
from adapters.db.repositories.support.ticket_repository import TicketRepository
|
|
from adapters.db.repositories.support.message_repository import MessageRepository
|
|
from app.services.ai.ai_service import AIService
|
|
from app.core.responses import ApiError
|
|
|
|
# ارسال پیام "در حال پردازش..."
|
|
service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="⏳ در حال دریافت پیشنهاد پاسخ AI..."
|
|
)
|
|
|
|
try:
|
|
ticket_repo = TicketRepository(db)
|
|
ticket = ticket_repo.get_operator_ticket_with_details(ticket_id)
|
|
|
|
if not ticket:
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="❌ تیکت یافت نشد."
|
|
)
|
|
|
|
# دریافت تاریخچه تیکت
|
|
ticket_messages = ticket.messages if ticket.messages else []
|
|
|
|
# ساخت context برای AI
|
|
context_messages = []
|
|
for msg in ticket_messages:
|
|
sender_type_str = msg.sender_type.value if hasattr(msg.sender_type, 'value') else str(msg.sender_type)
|
|
context_messages.append({
|
|
"role": "user" if sender_type_str == "user" else "assistant",
|
|
"content": msg.content
|
|
})
|
|
|
|
# ایجاد AI Service
|
|
# توجه: برای اپراتورها نیازی به business_id نیست چون دسترسی سیستمی دارند
|
|
ai_service = AIService(db, user_context)
|
|
|
|
user_name = f"{ticket.user.first_name or ''} {ticket.user.last_name or ''}".strip()
|
|
system_prompt = get_prompt_by_key(
|
|
db,
|
|
"support.ticket_suggest.system",
|
|
{
|
|
"user_name": user_name,
|
|
"ticket_title": ticket.title,
|
|
"category": ticket.category.name if ticket.category else "نامشخص",
|
|
"priority": ticket.priority.name if ticket.priority else "نامشخص",
|
|
},
|
|
)
|
|
|
|
ai_messages = [
|
|
{"role": "system", "content": system_prompt},
|
|
*context_messages,
|
|
{
|
|
"role": "user",
|
|
"content": get_prompt_by_key(
|
|
db,
|
|
"support.ticket_suggest.user",
|
|
{"ticket_description": ticket.description},
|
|
),
|
|
},
|
|
]
|
|
|
|
response = await ai_service.chat_completion(ai_messages, use_function_calling=True)
|
|
|
|
# بررسی سهمیه و شارژ
|
|
usage = response.get("usage", {})
|
|
input_tokens = usage.get("input_tokens", 0)
|
|
output_tokens = usage.get("output_tokens", 0)
|
|
|
|
charge_result = ai_service.check_quota_and_charge(input_tokens, output_tokens)
|
|
|
|
# ثبت لاگ استفاده
|
|
ai_service.log_usage(
|
|
provider=ai_service.config.provider if ai_service.config else "openai",
|
|
model=ai_service.config.model_name if ai_service.config else "gpt-4",
|
|
input_tokens=input_tokens,
|
|
output_tokens=output_tokens,
|
|
cost=charge_result.get("cost", 0),
|
|
payment_method=charge_result.get("payment_method", "free"),
|
|
wallet_transaction_id=charge_result.get("wallet_transaction_id"),
|
|
document_id=charge_result.get("document_id"),
|
|
context={"ticket_id": ticket_id, "type": "suggest_reply"}
|
|
)
|
|
|
|
suggested_reply = response["message"]["content"]
|
|
|
|
if not suggested_reply:
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="❌ پیشنهادی دریافت نشد."
|
|
)
|
|
|
|
# محدود کردن طول
|
|
if len(suggested_reply) > 4000:
|
|
suggested_reply = suggested_reply[:4000] + "\n\n... (متن کامل در برنامه قابل مشاهده است)"
|
|
|
|
buttons = [
|
|
[{"text": "⬅️ بازگشت", "callback_data": f"ticket:{ticket_id}"}]
|
|
]
|
|
keyboard = service._build_inline_keyboard(buttons)
|
|
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text=f"✅ پیشنهاد پاسخ:\n\n{suggested_reply}",
|
|
reply_markup=keyboard
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error suggesting reply: {e}", exc_info=True)
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text=f"❌ خطا در دریافت پیشنهاد: {str(e)}"
|
|
)
|
|
|
|
|
|
async def handle_auto_reply(
|
|
service: TelegramAIChatService,
|
|
user_context: AuthContext,
|
|
ticket_id: int,
|
|
db: Session
|
|
) -> bool:
|
|
"""پاسخ خودکار AI به تیکت"""
|
|
from adapters.db.repositories.support.ticket_repository import TicketRepository
|
|
from adapters.db.repositories.support.message_repository import MessageRepository
|
|
from app.services.ai.ai_service import AIService
|
|
from app.services.notification_service import NotificationService
|
|
|
|
# ارسال پیام "در حال پردازش..."
|
|
service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="⏳ در حال ارسال پاسخ خودکار..."
|
|
)
|
|
|
|
try:
|
|
# دریافت پیشنهاد پاسخ (از handle_suggest_reply استفاده میکنیم)
|
|
# اما این بار مستقیماً ارسال میکنیم
|
|
ticket_repo = TicketRepository(db)
|
|
ticket = ticket_repo.get_operator_ticket_with_details(ticket_id)
|
|
|
|
if not ticket:
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="❌ تیکت یافت نشد."
|
|
)
|
|
|
|
# دریافت تاریخچه تیکت
|
|
ticket_messages = ticket.messages if ticket.messages else []
|
|
|
|
# ساخت context برای AI
|
|
context_messages = []
|
|
for msg in ticket_messages:
|
|
sender_type_str = msg.sender_type.value if hasattr(msg.sender_type, 'value') else str(msg.sender_type)
|
|
context_messages.append({
|
|
"role": "user" if sender_type_str == "user" else "assistant",
|
|
"content": msg.content
|
|
})
|
|
|
|
# ایجاد AI Service
|
|
# توجه: برای اپراتورها نیازی به business_id نیست چون دسترسی سیستمی دارند
|
|
ai_service = AIService(db, user_context)
|
|
|
|
user_name = f"{ticket.user.first_name or ''} {ticket.user.last_name or ''}".strip()
|
|
system_prompt = get_prompt_by_key(
|
|
db,
|
|
"support.ticket_suggest.system",
|
|
{
|
|
"user_name": user_name,
|
|
"ticket_title": ticket.title,
|
|
"category": ticket.category.name if ticket.category else "نامشخص",
|
|
"priority": ticket.priority.name if ticket.priority else "نامشخص",
|
|
},
|
|
)
|
|
|
|
ai_messages = [
|
|
{"role": "system", "content": system_prompt},
|
|
*context_messages,
|
|
{
|
|
"role": "user",
|
|
"content": get_prompt_by_key(
|
|
db,
|
|
"support.ticket_suggest.user",
|
|
{"ticket_description": ticket.description},
|
|
),
|
|
},
|
|
]
|
|
|
|
response = await ai_service.chat_completion(ai_messages, use_function_calling=True)
|
|
|
|
# بررسی سهمیه و شارژ
|
|
usage = response.get("usage", {})
|
|
input_tokens = usage.get("input_tokens", 0)
|
|
output_tokens = usage.get("output_tokens", 0)
|
|
|
|
charge_result = ai_service.check_quota_and_charge(input_tokens, output_tokens)
|
|
|
|
# ثبت لاگ استفاده
|
|
ai_service.log_usage(
|
|
provider=ai_service.config.provider if ai_service.config else "openai",
|
|
model=ai_service.config.model_name if ai_service.config else "gpt-4",
|
|
input_tokens=input_tokens,
|
|
output_tokens=output_tokens,
|
|
cost=charge_result.get("cost", 0),
|
|
payment_method=charge_result.get("payment_method", "free"),
|
|
wallet_transaction_id=charge_result.get("wallet_transaction_id"),
|
|
document_id=charge_result.get("document_id"),
|
|
context={"ticket_id": ticket_id, "type": "auto_reply"}
|
|
)
|
|
|
|
suggested_reply = response["message"]["content"]
|
|
|
|
# ارسال پیام
|
|
message_repo = MessageRepository(db)
|
|
message = message_repo.create_message(
|
|
ticket_id=ticket_id,
|
|
sender_id=user_context.get_user_id(),
|
|
sender_type="operator",
|
|
content=suggested_reply,
|
|
is_internal=False
|
|
)
|
|
|
|
# تخصیص تیکت به اپراتور (اگر هنوز تخصیص نشده)
|
|
if ticket and not ticket.assigned_operator_id:
|
|
ticket_repo.assign_ticket(ticket_id, user_context.get_user_id())
|
|
|
|
db.commit()
|
|
|
|
# ارسال ناتیفیکیشن به کاربر
|
|
if ticket and ticket.user_id:
|
|
try:
|
|
notification_service = NotificationService(db)
|
|
operator_name = f"{user_context.user.first_name or ''} {user_context.user.last_name or ''}".strip() or "اپراتور پشتیبانی"
|
|
message_preview = suggested_reply[:200] + ("..." if len(suggested_reply) > 200 else "")
|
|
|
|
from app.services.support.notification_helpers import support_notification_context
|
|
|
|
context = support_notification_context({
|
|
"subject": f"پاسخ جدید به تیکت #{ticket_id}",
|
|
"message": f"اپراتور {operator_name} به تیکت شما پاسخ داد:\n\n{message_preview}",
|
|
"ticket_title": ticket.title if hasattr(ticket, 'title') else "تیکت",
|
|
"operator_name": operator_name,
|
|
"message_preview": message_preview,
|
|
"user_id": ticket.user_id,
|
|
}, ticket_id)
|
|
|
|
notification_service.send(
|
|
user_id=ticket.user_id,
|
|
event_key="support.operator_reply",
|
|
context=context,
|
|
preferred_channels=["inapp", "email", "telegram", "sms"],
|
|
broadcast_mode=False
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"خطا در ارسال ناتیفیکیشن برای پاسخ AI به تیکت {ticket_id}: {e}")
|
|
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="✅ پاسخ AI به تیکت ارسال شد.\nکاربر مطلع شد."
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error auto replying: {e}", exc_info=True)
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text=f"❌ خطا در ارسال پاسخ: {str(e)}"
|
|
)
|
|
|
|
|
|
async def send_admin_menu(
|
|
service: TelegramAIChatService,
|
|
user_context: AuthContext
|
|
) -> bool:
|
|
"""ارسال منوی مدیریت"""
|
|
buttons = [
|
|
[{"text": "📊 آمار سیستم", "callback_data": "admin:stats"}],
|
|
[{"text": "⬅️ بازگشت", "callback_data": "back:main"}]
|
|
]
|
|
keyboard = service._build_inline_keyboard(buttons)
|
|
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="مدیریت سیستم:",
|
|
reply_markup=keyboard
|
|
)
|
|
|
|
|
|
async def send_admin_stats(
|
|
service: TelegramAIChatService,
|
|
user_context: AuthContext,
|
|
db: Session
|
|
) -> bool:
|
|
"""ارسال آمار سیستم"""
|
|
from sqlalchemy import func, select
|
|
from adapters.db.models.user import User
|
|
from adapters.db.models.support.ticket import Ticket
|
|
from adapters.db.models.ai_usage_log import AIUsageLog
|
|
|
|
# شمارش کاربران فعال
|
|
active_users = db.execute(
|
|
select(func.count(User.id)).where(User.is_active == True) # noqa: E712
|
|
).scalar() or 0
|
|
|
|
# شمارش تیکتهای باز
|
|
open_tickets = db.execute(
|
|
select(func.count(Ticket.id)).where(Ticket.status_id.is_(None))
|
|
).scalar() or 0
|
|
|
|
# استفاده AI (آخرین 30 روز)
|
|
from datetime import timedelta
|
|
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
|
|
ai_usage = db.execute(
|
|
select(func.sum(AIUsageLog.input_tokens + AIUsageLog.output_tokens))
|
|
.where(AIUsageLog.created_at >= thirty_days_ago)
|
|
).scalar() or 0
|
|
|
|
text = f"""📊 آمار سیستم:
|
|
|
|
👥 کاربران فعال: {active_users}
|
|
🎫 تیکتهای باز: {open_tickets}
|
|
🤖 استفاده AI (30 روز): {ai_usage:,} توکن"""
|
|
|
|
buttons = [
|
|
[{"text": "⬅️ بازگشت", "callback_data": "back:admin"}]
|
|
]
|
|
keyboard = service._build_inline_keyboard(buttons)
|
|
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text=text,
|
|
reply_markup=keyboard
|
|
)
|
|
|
|
|
|
async def handle_session_selection(
|
|
service: TelegramAIChatService,
|
|
user_context: AuthContext,
|
|
session_id: int
|
|
) -> bool:
|
|
"""مدیریت انتخاب جلسه"""
|
|
ai_session = service.ai_chat_repo.get_by_id(session_id)
|
|
if not ai_session:
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text="❌ جلسه یافت نشد."
|
|
)
|
|
|
|
# بهروزرسانی جلسه تلگرام
|
|
service.session_repo.create_or_update_session(
|
|
user_id=service.user_id,
|
|
chat_id=service.chat_id,
|
|
session_id=session_id,
|
|
business_id=ai_session.business_id
|
|
)
|
|
|
|
# دریافت آخرین پیامها
|
|
messages = service.ai_message_repo.get_session_messages(session_id, limit=5)
|
|
|
|
text = f"✅ جلسه '{ai_session.title}' انتخاب شد.\n\n"
|
|
if messages:
|
|
text += "آخرین پیامها:\n"
|
|
for msg in messages[-3:]:
|
|
role = "شما" if msg.role == "user" else "AI"
|
|
content = msg.content[:100] + "..." if len(msg.content) > 100 else msg.content
|
|
text += f"\n{role}: {content}\n"
|
|
|
|
buttons = [
|
|
[{"text": "💬 ادامه گفتوگو", "callback_data": "chat:ask"}],
|
|
[{"text": "⬅️ بازگشت", "callback_data": "back:chat"}]
|
|
]
|
|
keyboard = service._build_inline_keyboard(buttons)
|
|
|
|
return service.telegram_provider.send_text(
|
|
chat_id=service.chat_id,
|
|
text=text,
|
|
reply_markup=keyboard
|
|
)
|
|
|
|
|
|
def _get_priority_emoji(priority: str) -> str:
|
|
"""تبدیل اولویت به emoji"""
|
|
priority_map = {
|
|
"high": "🔴",
|
|
"medium": "🟡",
|
|
"low": "🟢"
|
|
}
|
|
return priority_map.get(priority.lower(), "⚪")
|
|
|