Files
ticket/telepot/tele.py
2026-07-16 12:27:06 +02:00

1061 lines
43 KiB
Python

import os
from pathlib import Path
import re
import sys
import time
import traceback
import uuid
import telepot
from telepot.loop import MessageLoop
from telepot.namedtuple import InlineKeyboardMarkup, InlineKeyboardButton
import keyboards
import ticketToday
import insertCompetence
ROOT_DIR = Path(__file__).resolve().parents[1]
for path in (ROOT_DIR / "conn", ROOT_DIR / "api"):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
import user
import config
import filter as filter_ui
import filters as filter_store
import mysql5
import phrase_similarity
import ai_retriever
PAGE_SIZE = 3
MAX_TELEGRAM_TEXT = 3800
SESSION_TTL_SECONDS = 60 * 60
DELETE_FILTER_RE = re.compile(r"^/(Competen|Prodotto|Area|Sottoarea)_id_cf_(\d+)")
SEARCH_SESSIONS = {}
def _ticket_link(ticket_id):
return f"https://tsnew.sanmarcoweb.com/it/ticket/index/index/operation/view/id/{ticket_id}"
def _short(value, limit=900):
text = str(value or "").strip()
if len(text) <= limit:
return text
return text[:limit].rsplit(" ", 1)[0] + "..."
def _send_chunks(chat_id, text, reply_markup=None):
text = str(text or "")
if len(text) <= MAX_TELEGRAM_TEXT:
bot.sendMessage(chat_id, text, reply_markup=reply_markup)
return
chunks = [text[i:i + MAX_TELEGRAM_TEXT] for i in range(0, len(text), MAX_TELEGRAM_TEXT)]
for index, chunk in enumerate(chunks):
bot.sendMessage(chat_id, chunk, reply_markup=reply_markup if index == len(chunks) - 1 else None)
def _is_admin(user_id):
if int(user_id) == int(config.TELEGRAM_ADMIN_ID):
return True
try:
rows = user.getUserByTid(user_id)
return bool(rows and len(rows[0]) > 4 and rows[0][4])
except Exception:
return False
def _is_pending_user(user_id):
rows = user.getUserByTid(user_id)
return bool(rows and rows[0][3] == "P" and not _is_admin(user_id))
def _parse_int_token(value):
if value is None:
return None
value = str(value).strip()
if value.startswith("_"):
value = "-" + value[1:]
try:
return int(value)
except ValueError:
return None
def _looks_like_int(value):
return _parse_int_token(value) is not None
def _add_filter_value(search_filters, key, value):
parsed = _parse_int_token(value)
if parsed is None:
return False
values = search_filters.setdefault(key, [])
if parsed not in values:
values.append(parsed)
return True
def _add_label(labels, label):
if label and label not in labels:
labels.append(label)
def _has_metadata_filter_values(search_filters):
return any(
search_filters.get(key)
for key in ("competence_ids", "product_ids", "area_ids", "subarea_ids", "client_ids")
)
def _copy_filters(search_filters):
copied = {}
for key, value in (search_filters or {}).items():
if isinstance(value, list):
copied[key] = list(value)
else:
copied[key] = value
return copied
def _merge_saved_filters(search_filters, user_id, labels):
available = 0
filter_map = {
"C": ("competence_ids", "competenza"),
"P": ("product_ids", "prodotto"),
"A": ("area_ids", "area"),
"S": ("subarea_ids", "sottoarea"),
}
for row in filter_store.getFilters(user_id):
key_label = filter_map.get(str(row[2]))
if not key_label:
continue
key, label = key_label
if _parse_int_token(row[3]) is None:
continue
available += 1
if _add_filter_value(search_filters, key, row[3]):
_add_label(labels, f"mio filtro {label}")
else:
_add_label(labels, f"mio filtro {label}")
return available
def _apply_default_saved_filters(search_filters, user_id, labels):
if _has_metadata_filter_values(search_filters):
return 0
return _merge_saved_filters(search_filters, user_id, labels)
def _metadata_filter(search_filters, labels, query_ticket, metadata_key, filter_key, label):
value = (query_ticket or {}).get(metadata_key)
if _add_filter_value(search_filters, filter_key, value):
_add_label(labels, label)
return True
return False
def _parse_filter_tokens(tokens, user_id, query_ticket=None):
search_filters = {}
labels = []
i = 0
while i < len(tokens):
token = tokens[i].strip().lower().replace("-", "_")
next_token = tokens[i + 1] if i + 1 < len(tokens) else None
if token in ("area", "stessa_area"):
if _looks_like_int(next_token):
_add_filter_value(search_filters, "area_ids", next_token)
_add_label(labels, f"area {next_token}")
i += 2
else:
_metadata_filter(search_filters, labels, query_ticket, "area_id", "area_ids", "stessa area")
i += 1
elif token in ("sottoarea", "subarea", "sott_area", "stessa_sottoarea"):
if _looks_like_int(next_token):
_add_filter_value(search_filters, "subarea_ids", next_token)
_add_label(labels, f"sottoarea {next_token}")
i += 2
else:
_metadata_filter(search_filters, labels, query_ticket, "subarea_id", "subarea_ids", "stessa sottoarea")
i += 1
elif token in ("prodotto", "product", "stesso_prodotto"):
if _looks_like_int(next_token):
_add_filter_value(search_filters, "product_ids", next_token)
_add_label(labels, f"prodotto {next_token}")
i += 2
else:
_metadata_filter(search_filters, labels, query_ticket, "product_id", "product_ids", "stesso prodotto")
i += 1
elif token in ("competenza", "competence", "stessa_competenza"):
if _looks_like_int(next_token):
_add_filter_value(search_filters, "competence_ids", next_token)
_add_label(labels, f"competenza {next_token}")
i += 2
else:
_metadata_filter(search_filters, labels, query_ticket, "competence_id", "competence_ids", "stessa competenza")
i += 1
elif token in ("cliente", "client", "stesso_cliente"):
if _looks_like_int(next_token):
_add_filter_value(search_filters, "client_ids", next_token)
_add_label(labels, f"cliente {next_token}")
i += 2
else:
_metadata_filter(search_filters, labels, query_ticket, "client_id", "client_ids", "stesso cliente")
i += 1
elif token in ("no_cliente", "diverso_cliente", "escludi_cliente"):
if _looks_like_int(next_token):
_add_filter_value(search_filters, "exclude_client_ids", next_token)
_add_label(labels, f"escludi cliente {next_token}")
i += 2
else:
_metadata_filter(search_filters, labels, query_ticket, "client_id", "exclude_client_ids", "escludi stesso cliente")
i += 1
elif token in ("filtri", "miei_filtri"):
if _merge_saved_filters(search_filters, user_id, labels) == 0:
_add_label(labels, "miei filtri vuoti")
i += 1
elif token in ("codice", "code", "errore"):
search_filters["require_code_match"] = True
_add_label(labels, "stesso codice")
i += 1
elif token in ("recenti", "ultimi", "ultimi_mesi"):
months = _parse_int_token(next_token) or 24
search_filters["recent_months"] = max(1, months)
_add_label(labels, f"ultimi {max(1, months)} mesi")
i += 2 if _looks_like_int(next_token) else 1
elif token in ("media", "confidenza_media"):
search_filters["min_confidence"] = "media"
_add_label(labels, "confidenza media")
i += 1
elif token in ("alta", "confidenza_alta"):
search_filters["min_confidence"] = "alta"
_add_label(labels, "confidenza alta")
i += 1
else:
i += 1
return search_filters, labels
def _parse_ticket_search(text, user_id):
parts = text.split()
if len(parts) < 2 or not parts[1].isdigit():
return None, None, None, "Usa: /tic <numero_ticket> [filtri]"
ticket_id = int(parts[1])
query_ticket = mysql5.get_ticket_context(ticket_id)
if not query_ticket:
return None, None, None, f"Ticket {ticket_id} non trovato."
search_filters, labels = _parse_filter_tokens(parts[2:], user_id, query_ticket=query_ticket)
_apply_default_saved_filters(search_filters, user_id, labels)
return ticket_id, query_ticket, {"filters": search_filters, "labels": labels}, None
def _parse_text_search(text, user_id):
raw = text[len("/cerca "):].strip()
if not raw:
return None, None, "Usa: /cerca <testo> [--area 12] [--filtri]"
tokens = raw.split()
query_parts = []
search_filters = {}
labels = []
i = 0
while i < len(tokens):
token = tokens[i].lower()
next_token = tokens[i + 1] if i + 1 < len(tokens) else None
if token in ("--area", "--sottoarea", "--subarea", "--prodotto", "--competenza", "--cliente"):
key_map = {
"--area": ("area_ids", "area"),
"--sottoarea": ("subarea_ids", "sottoarea"),
"--subarea": ("subarea_ids", "sottoarea"),
"--prodotto": ("product_ids", "prodotto"),
"--competenza": ("competence_ids", "competenza"),
"--cliente": ("client_ids", "cliente"),
}
key, label = key_map[token]
if _add_filter_value(search_filters, key, next_token):
_add_label(labels, f"{label} {next_token}")
i += 2
else:
i += 1
elif token == "--filtri":
if _merge_saved_filters(search_filters, user_id, labels) == 0:
_add_label(labels, "miei filtri vuoti")
i += 1
elif token == "--codice":
search_filters["require_code_match"] = True
_add_label(labels, "stesso codice")
i += 1
elif token == "--ultimi-mesi":
months = _parse_int_token(next_token) or 24
search_filters["recent_months"] = max(1, months)
_add_label(labels, f"ultimi {max(1, months)} mesi")
i += 2 if _looks_like_int(next_token) else 1
elif token == "--media":
search_filters["min_confidence"] = "media"
_add_label(labels, "confidenza media")
i += 1
elif token == "--alta":
search_filters["min_confidence"] = "alta"
_add_label(labels, "confidenza alta")
i += 1
else:
query_parts.append(tokens[i])
i += 1
query_text = " ".join(query_parts).strip()
if not query_text:
return None, None, "Manca il testo da cercare."
_apply_default_saved_filters(search_filters, user_id, labels)
return query_text, {"filters": search_filters, "labels": labels}, None
def _cleanup_sessions():
now = time.time()
for session_id in list(SEARCH_SESSIONS):
if now - SEARCH_SESSIONS[session_id].get("created_at", now) > SESSION_TTL_SECONDS:
del SEARCH_SESSIONS[session_id]
def _store_session(session):
_cleanup_sessions()
session_id = uuid.uuid4().hex[:10]
session["created_at"] = time.time()
SEARCH_SESSIONS[session_id] = session
return session_id
def _session_filter_text(session):
labels = session.get("labels") or []
return ", ".join(labels) if labels else "nessun filtro stretto"
def _format_suggestion_message(suggestion, rank):
ticket_id = suggestion["ticket_id"]
product = suggestion.get("product") or "-"
competence = suggestion.get("competence") or "-"
area = suggestion.get("area") or "-"
subarea = suggestion.get("subarea") or "-"
client = suggestion.get("client") or "-"
reasons = ", ".join(suggestion.get("reasons") or [])
opened = suggestion.get("opened_at")
opened_text = opened.strftime("%Y-%m-%d") if hasattr(opened, "strftime") else str(opened or "-")
duplicate_note = ""
if suggestion.get("duplicates_skipped_before"):
duplicate_note = f"Alternative simili saltate: {suggestion['duplicates_skipped_before']}\n"
return (
f"{rank}. Confidenza {suggestion['confidence']}\n"
f"Ticket: {ticket_id}\n"
f"{_ticket_link(ticket_id)}\n"
f"Cliente: {client}\n"
f"Prodotto: {product} | Competenza: {competence}\n"
f"Area: {area} | Sottoarea: {subarea}\n"
f"Apertura: {opened_text}\n"
f"Perche: {reasons}\n"
f"{duplicate_note}"
f"Score: {suggestion['total_score']:.3f}\n"
f"SOLUZIONE: {_short(suggestion.get('solution_preview') or 'Soluzione non disponibile')}"
)
def _suggestion_keyboard(session, suggestion):
suggested_ticket = suggestion["ticket_id"]
if session["kind"] == "ticket":
query_ticket = session["query_ticket_id"]
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Dettaglio", callback_data=f"d:{query_ticket}:{suggested_ticket}")],
[
InlineKeyboardButton(text="Utile", callback_data=f"fb:u:{query_ticket}:{suggested_ticket}"),
InlineKeyboardButton(text="Non utile", callback_data=f"fb:n:{query_ticket}:{suggested_ticket}"),
],
[InlineKeyboardButton(text="Preferito", callback_data=f"fav:{query_ticket}:{suggested_ticket}")],
])
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Dettaglio", callback_data=f"D:{suggested_ticket}")],
[InlineKeyboardButton(text="Preferito", callback_data=f"F:{suggested_ticket}")],
])
def _search_controls_keyboard(session_id, session, offset, has_more):
rows = []
if has_more:
rows.append([InlineKeyboardButton(text="Altri risultati", callback_data=f"more:{session_id}:{offset + PAGE_SIZE}")])
if session["kind"] == "ticket":
query_ticket = session.get("query_ticket") or {}
first_row = []
if query_ticket.get("area_id"):
first_row.append(InlineKeyboardButton(text="Solo area", callback_data=f"mode:{session_id}:area"))
if query_ticket.get("subarea_id"):
first_row.append(InlineKeyboardButton(text="Solo sottoarea", callback_data=f"mode:{session_id}:subarea"))
if first_row:
rows.append(first_row)
second_row = []
if query_ticket.get("product_id"):
second_row.append(InlineKeyboardButton(text="Solo prodotto", callback_data=f"mode:{session_id}:product"))
if query_ticket.get("competence_id"):
second_row.append(InlineKeyboardButton(text="Solo competenza", callback_data=f"mode:{session_id}:competence"))
if second_row:
rows.append(second_row)
third_row = []
if query_ticket.get("client_id"):
third_row.append(InlineKeyboardButton(text="Stesso cliente", callback_data=f"mode:{session_id}:client"))
third_row.append(InlineKeyboardButton(text="Escludi cliente", callback_data=f"mode:{session_id}:exclude_client"))
if third_row:
rows.append(third_row)
rows.append([
InlineKeyboardButton(text="Recenti 24m", callback_data=f"mode:{session_id}:recent"),
InlineKeyboardButton(text="Stesso codice", callback_data=f"mode:{session_id}:code"),
])
rows.append([
InlineKeyboardButton(text="Min media", callback_data=f"mode:{session_id}:media"),
InlineKeyboardButton(text="Miei filtri", callback_data=f"mode:{session_id}:saved"),
InlineKeyboardButton(text="Reset", callback_data=f"mode:{session_id}:reset"),
])
return InlineKeyboardMarkup(inline_keyboard=rows)
def _send_ai_results(chat_id, session_id, offset=0, intro=None):
session = SEARCH_SESSIONS.get(session_id)
if not session:
bot.sendMessage(chat_id, "Ricerca scaduta. Rilancia /tic oppure /cerca.")
return
offset = max(0, int(offset or 0))
search_filters = session.get("filters") or {}
if session["kind"] == "ticket":
suggestions = ai_retriever.search_similar_tickets(
session["query_ticket_id"],
user_id=session["user_id"],
limit=PAGE_SIZE,
offset=offset,
filters=search_filters,
)
title = f"Ticket principale: {_ticket_link(session['query_ticket_id'])}"
else:
suggestions = ai_retriever.search_similar_text(
session["query_text"],
user_id=session["user_id"],
limit=PAGE_SIZE,
offset=offset,
filters=search_filters,
)
title = f"Ricerca testo: {_short(session['query_text'], 120)}"
if intro or offset == 0:
_send_chunks(chat_id, f"{intro or 'Risultati AI'}\n{title}\nFiltro: {_session_filter_text(session)}")
if not suggestions:
bot.sendMessage(
chat_id,
"Non ho trovato risultati con questi parametri.",
reply_markup=_search_controls_keyboard(session_id, session, offset, False),
)
return
for index, suggestion in enumerate(suggestions, start=1):
rank = offset + index
bot.sendMessage(
chat_id,
_format_suggestion_message(suggestion, rank),
reply_markup=_suggestion_keyboard(session, suggestion),
)
bot.sendMessage(
chat_id,
"Azioni ricerca",
reply_markup=_search_controls_keyboard(session_id, session, offset, len(suggestions) == PAGE_SIZE),
)
def _mode_session(session, mode):
new_session = dict(session)
new_session["filters"] = _copy_filters(session.get("filters"))
new_session["labels"] = list(session.get("labels") or [])
query_ticket = session.get("query_ticket") or {}
if mode == "reset":
new_session["filters"] = {}
new_session["labels"] = []
return new_session, "Filtri rimossi."
if mode == "area":
ok = _metadata_filter(new_session["filters"], new_session["labels"], query_ticket, "area_id", "area_ids", "stessa area")
elif mode == "subarea":
ok = _metadata_filter(new_session["filters"], new_session["labels"], query_ticket, "subarea_id", "subarea_ids", "stessa sottoarea")
elif mode == "product":
ok = _metadata_filter(new_session["filters"], new_session["labels"], query_ticket, "product_id", "product_ids", "stesso prodotto")
elif mode == "competence":
ok = _metadata_filter(new_session["filters"], new_session["labels"], query_ticket, "competence_id", "competence_ids", "stessa competenza")
elif mode == "client":
ok = _metadata_filter(new_session["filters"], new_session["labels"], query_ticket, "client_id", "client_ids", "stesso cliente")
elif mode == "exclude_client":
ok = _metadata_filter(new_session["filters"], new_session["labels"], query_ticket, "client_id", "exclude_client_ids", "escludi stesso cliente")
elif mode == "recent":
new_session["filters"]["recent_months"] = 24
_add_label(new_session["labels"], "ultimi 24 mesi")
ok = True
elif mode == "code":
new_session["filters"]["require_code_match"] = True
_add_label(new_session["labels"], "stesso codice")
ok = True
elif mode == "media":
new_session["filters"]["min_confidence"] = "media"
_add_label(new_session["labels"], "confidenza media")
ok = True
elif mode == "saved":
ok = _merge_saved_filters(new_session["filters"], session["user_id"], new_session["labels"]) > 0
else:
return None, "Filtro non riconosciuto."
if not ok:
return None, "Filtro non disponibile per questa ricerca."
return new_session, "Filtro applicato."
def _send_ticket_detail(chat_id, ticket_id, actor_id=None, first_name="", last_name="", query_ticket_id=None):
bot.sendMessage(chat_id, f"Attendere per favore, sto caricando i dettagli per il ticket <b>{ticket_id}</b> ....", parse_mode="HTML")
if query_ticket_id is not None:
try:
ai_retriever.record_feedback(actor_id or chat_id, query_ticket_id, ticket_id, "opened")
except Exception:
traceback.print_exc()
if actor_id is not None and not _is_admin(actor_id):
bot.sendMessage(
config.TELEGRAM_ADMIN_ID,
"Aperto Dettaglio Ticket nr: "
+ str(ticket_id)
+ " [ "
+ str(first_name)
+ " "
+ str(last_name)
+ " "
+ str(actor_id)
+ " ]",
)
ready_file = ticketToday.getDetailTicket(ticket_id)
html_file = "json/" + str(ticket_id) + "_details.html"
if ready_file and os.path.exists(html_file):
with open(html_file, "rb") as document:
bot.sendDocument(chat_id=chat_id, document=document, parse_mode="HTML")
os.remove(html_file)
else:
bot.sendMessage(chat_id, "Non è stato possibile scaricare i dettagli")
def _handle_ticket_search(chat_id, user_id, text, first_name, last_name):
ticket_id, query_ticket, parsed, error = _parse_ticket_search(text, user_id)
if error:
bot.sendMessage(chat_id, error)
return
bot.sendMessage(chat_id, "Attendere per favore sto cercando soluzioni...")
session_id = _store_session({
"kind": "ticket",
"user_id": user_id,
"query_ticket_id": ticket_id,
"query_ticket": query_ticket,
"filters": parsed["filters"],
"labels": parsed["labels"],
})
_send_ai_results(chat_id, session_id, offset=0, intro="Risultati AI")
if int(chat_id) != int(config.TELEGRAM_ADMIN_ID):
bot.sendMessage(config.TELEGRAM_ADMIN_ID, "Manuale Da " + str(ticket_id) + " " + str(first_name) + " " + str(last_name))
def _handle_text_search(chat_id, user_id, text):
query_text, parsed, error = _parse_text_search(text, user_id)
if error:
bot.sendMessage(chat_id, error)
return
bot.sendMessage(chat_id, "Attendere per favore sto cercando soluzioni...")
session_id = _store_session({
"kind": "text",
"user_id": user_id,
"query_text": query_text,
"filters": parsed["filters"],
"labels": parsed["labels"],
})
_send_ai_results(chat_id, session_id, offset=0, intro="Risultati AI")
def _handle_feedback_command(chat_id, text, feedback):
prefix = "/utile_" if feedback == "useful" else "/non_utile_"
payload = str(text)[len(prefix):]
parts = payload.split("_", 1)
if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
ai_retriever.record_feedback(chat_id, parts[0], parts[1], feedback)
bot.sendMessage(chat_id, "Feedback registrato: soluzione utile." if feedback == "useful" else "Feedback registrato: soluzione non utile.")
else:
bot.sendMessage(chat_id, "Feedback non valido.")
def _handle_favorite_command(chat_id, user_id, text):
ticket_id = str(text)[len("/preferito_"):].strip()
if not ticket_id.isdigit():
bot.sendMessage(chat_id, "Preferito non valido.")
return
try:
inserted = ai_retriever.record_favorite(user_id or chat_id, ticket_id)
except Exception:
traceback.print_exc()
bot.sendMessage(chat_id, "Non e' stato possibile salvare il preferito.")
return
bot.sendMessage(chat_id, "Ticket aggiunto ai preferiti." if inserted else "Ticket gia' nei preferiti.")
if int(chat_id) != int(config.TELEGRAM_ADMIN_ID):
bot.sendMessage(config.TELEGRAM_ADMIN_ID, "Preferito " + str(ticket_id) + " Da " + str(chat_id))
def _format_settings(settings):
enabled = "attive" if settings.get("notifications_enabled", True) else "in pausa"
quiet = "nessuno"
if settings.get("quiet_start_hour") is not None and settings.get("quiet_end_hour") is not None:
quiet = f"{settings['quiet_start_hour']:02d}:00-{settings['quiet_end_hour']:02d}:00"
digest = "immediato"
if int(settings.get("digest_minutes") or 0) > 0:
digest = f"ogni {settings['digest_minutes']} minuti"
gravity = "solo alta" if settings.get("high_gravity_only") else "tutte"
return (
"Stato notifiche\n"
f"Notifiche: {enabled}\n"
f"Silenzio: {quiet}\n"
f"Digest: {digest}\n"
f"Gravita: {gravity}"
)
def _handle_notification_command(chat_id, user_id, text):
if text == "/notifiche_on":
settings = ai_retriever.set_notifications_enabled(user_id, True)
bot.sendMessage(chat_id, _format_settings(settings))
return True
if text == "/notifiche_off":
settings = ai_retriever.set_notifications_enabled(user_id, False)
bot.sendMessage(chat_id, _format_settings(settings))
return True
if text == "/notifiche_status":
bot.sendMessage(chat_id, _format_settings(ai_retriever.get_user_bot_settings(user_id)))
return True
if text == "/silenzio_off":
bot.sendMessage(chat_id, _format_settings(ai_retriever.clear_quiet_hours(user_id)))
return True
if text.startswith("/silenzio "):
parts = text.split()
if len(parts) == 3 and _looks_like_int(parts[1]) and _looks_like_int(parts[2]):
settings = ai_retriever.set_quiet_hours(user_id, int(parts[1]), int(parts[2]))
bot.sendMessage(chat_id, _format_settings(settings))
else:
bot.sendMessage(chat_id, "Usa: /silenzio <ora_inizio> <ora_fine>, esempio /silenzio 22 7")
return True
if text == "/digest_off":
bot.sendMessage(chat_id, _format_settings(ai_retriever.set_digest_minutes(user_id, 0)))
return True
if text.startswith("/digest "):
parts = text.split()
if len(parts) == 2 and _looks_like_int(parts[1]):
settings = ai_retriever.set_digest_minutes(user_id, int(parts[1]))
bot.sendMessage(chat_id, _format_settings(settings))
else:
bot.sendMessage(chat_id, "Usa: /digest <minuti>, esempio /digest 30")
return True
if text == "/gravita_alta_on":
bot.sendMessage(chat_id, _format_settings(ai_retriever.set_high_gravity_only(user_id, True)))
return True
if text == "/gravita_alta_off":
bot.sendMessage(chat_id, _format_settings(ai_retriever.set_high_gravity_only(user_id, False)))
return True
return False
def _format_dt(value):
if not value:
return "-"
if hasattr(value, "strftime"):
return value.strftime("%Y-%m-%d %H:%M")
return str(value)
def _format_favorite(row):
ticket_id = row.get("ticket_id")
lines = [
f"🎫 /Ticket_dettaglio_{ticket_id}",
_ticket_link(ticket_id),
]
metadata = []
if row.get("product"):
metadata.append(str(row.get("product")))
if row.get("area"):
metadata.append(str(row.get("area")))
if metadata:
lines.append(" | ".join(metadata))
if row.get("subject_text"):
lines.append("Oggetto: " + _short(row.get("subject_text"), 180))
lines.append("Salvato: " + _format_dt(row.get("created_at")))
return "\n".join(lines)
def _send_user_favorites(chat_id, user_id):
try:
rows = ai_retriever.get_user_favorites(user_id or chat_id)
except Exception:
traceback.print_exc()
bot.sendMessage(chat_id, "Non e' stato possibile caricare i preferiti.")
return
if not rows:
bot.sendMessage(chat_id, "Non hai ancora ticket preferiti.")
return
lines = ["I tuoi preferiti"]
lines.extend(_format_favorite(row) for row in rows)
_send_chunks(chat_id, "\n\n".join(lines))
def _send_admin_report(chat_id, command):
if command == "/ai_stats":
stats = ai_retriever.get_ai_stats()
feedback = stats.get("feedback_counts") or {}
message = (
"AI stats\n"
f"Ticket indicizzati: {stats.get('indexed_count', 0)}\n"
f"Ultimo indice: {_format_dt(stats.get('last_indexed_at'))}\n"
f"Ricerche: {stats.get('search_count', 0)}\n"
f"Senza risultati: {stats.get('no_result_count', 0)}\n"
f"Tempo medio: {stats.get('avg_elapsed_ms', 0)} ms\n"
f"Feedback utile: {feedback.get('useful', 0)}\n"
f"Feedback non utile: {feedback.get('not_useful', 0)}\n"
f"Preferiti: {feedback.get('favorite', 0)}\n"
f"Dettagli aperti: {feedback.get('opened', 0)}"
)
bot.sendMessage(chat_id, message)
return
if command == "/ai_no_result":
rows = ai_retriever.get_no_result_searches()
if not rows:
bot.sendMessage(chat_id, "Nessuna ricerca senza risultati.")
return
lines = ["Ultime ricerche senza risultati"]
for row in rows:
ref = row.get("query_ticket_id") or row.get("query_kind")
lines.append(f"{_format_dt(row.get('created_at'))} | {ref} | {_short(row.get('query_text'), 120)}")
_send_chunks(chat_id, "\n".join(lines))
return
if command == "/ai_negative":
rows = ai_retriever.get_negative_feedback()
if not rows:
bot.sendMessage(chat_id, "Nessun feedback negativo.")
return
lines = ["Ultimi feedback negativi"]
for row in rows:
lines.append(
f"{_format_dt(row.get('created_at'))} | query {row.get('query_ticket_id')} -> {row.get('suggested_ticket_id')} | "
f"{row.get('product') or '-'} | {row.get('area') or '-'} | {_short(row.get('subject_text'), 80)}"
)
_send_chunks(chat_id, "\n".join(lines))
return
if command == "/ai_top_useful":
rows = ai_retriever.get_top_useful()
if not rows:
bot.sendMessage(chat_id, "Nessun feedback utile registrato.")
return
lines = ["Ticket piu utili"]
for row in rows:
lines.append(
f"{row.get('ticket_id')} | +{row.get('positive_count')} -{row.get('negative_count')} | "
f"{row.get('product') or '-'} | {row.get('area') or '-'} | {_short(row.get('subject_text'), 80)}"
)
_send_chunks(chat_id, "\n".join(lines))
def _send_help(chat_id):
_send_chunks(
chat_id,
"Help bot ticket\n"
"\n"
"Importante: i nuovi ticket automatici arrivano solo in base ai filtri salvati. Anche /tic e /cerca usano i filtri salvati se non indichi un filtro diverso.\n"
"Filtri diversi si sommano: Area + Competenza + Prodotto devono combaciare tutti. Piu valori dello stesso tipo sono alternative.\n"
"\n"
"Avvio\n"
"/start - registra o riapre il menu\n"
"/help - mostra questo aiuto\n"
"\n"
"Filtri\n"
"Usa i pulsanti del menu per creare filtri per Competenza, Prodotto, Area e Sott.Area.\n"
"/_id_c_<id> - salva filtro competenza\n"
"/_id_p_<id> - salva filtro prodotto\n"
"/_id_area_<id> - salva filtro area\n"
"/_id_suba_<id> - salva filtro sottoarea\n"
"📌 I mie filtri - mostra i filtri salvati\n"
"✖ Cancella filtro - mostra i comandi per eliminarli\n"
"/Competen_id_cf_<id>, /Prodotto_id_cf_<id>, /Area_id_cf_<id>, /Sottoarea_id_cf_<id> - elimina filtro\n"
"\n"
"Ricerca soluzioni\n"
"/tic <ticket> - cerca ticket simili usando i tuoi filtri salvati\n"
"/tic <ticket> area - solo stessa area del ticket\n"
"/tic <ticket> sottoarea - solo stessa sottoarea\n"
"/tic <ticket> prodotto - solo stesso prodotto\n"
"/tic <ticket> competenza - solo stessa competenza\n"
"/tic <ticket> cliente - solo stesso cliente\n"
"/tic <ticket> no_cliente - esclude stesso cliente\n"
"/tic <ticket> area <id> - usa area specifica\n"
"/tic <ticket> prodotto <id> - usa prodotto specifico\n"
"/tic <ticket> competenza <id> - usa competenza specifica\n"
"/tic <ticket> recenti <mesi> - limita ai ticket recenti\n"
"/tic <ticket> codice - richiede stesso codice errore\n"
"/tic <ticket> media oppure alta - alza la confidenza minima\n"
"/cerca <testo> - cerca da testo libero usando i filtri salvati\n"
"/cerca --area <id> --prodotto <id> --competenza <id> <testo> - cerca con filtri specifici\n"
"/cerca --filtri <testo> - forza i filtri salvati\n"
"/Ticket_dettaglio_<ticket> - scarica il dettaglio ticket\n"
"\n"
"Feedback e preferiti\n"
"/utile_<ticket_origine>_<ticket_suggerito> - segna soluzione utile\n"
"/non_utile_<ticket_origine>_<ticket_suggerito> - segna soluzione non utile\n"
"/preferito_<ticket> - salva un ticket nei preferiti\n"
"/preferiti - mostra i tuoi preferiti\n"
"\n"
"Notifiche\n"
"/notifiche_status - mostra stato notifiche\n"
"/notifiche_on - attiva notifiche\n"
"/notifiche_off - mette in pausa notifiche\n"
"/silenzio <ora_inizio> <ora_fine> - pausa in una fascia oraria, esempio /silenzio 22 7\n"
"/silenzio_off - rimuove la fascia silenzio\n"
"/digest <minuti> - raggruppa notifiche, esempio /digest 30\n"
"/digest_off - notifiche immediate\n"
"/gravita_alta_on - solo ticket con gravita alta\n"
"/gravita_alta_off - tutte le gravita",
)
def on_chat_message(msg):
try:
first_name = msg.get("from", {}).get("first_name", "")
last_name = msg.get("from", {}).get("last_name", "noname")
user_id = msg.get("from", {}).get("id")
content_type, chat_type, chat_id = telepot.glance(msg)
print("Chat Message:", last_name, first_name, chat_id)
print(content_type)
if _is_pending_user(user_id):
bot.sendMessage(user_id, "In attesa che un amministratore accetti la sua richiesta \n Per favore attendere a breve sarà approvata")
bot.sendMessage(config.TELEGRAM_ADMIN_ID, "Utente nuovo: [ " + str(user_id) + " " + first_name + " " + last_name + " ] Approva qui: \n" + "/Approval_" + str(chat_id))
return
if content_type != "text":
return
text = str(msg.get("text", "")).strip()
if text == "/start":
keyboard = keyboards.getKeyboard()
if user.insertUser(user_id, first_name, last_name):
bot.sendMessage(chat_id, "Registrazione effettuata correttamente. \n Per favore attendere che un amministratore approvi la sua richiesta")
bot.sendDocument(chat_id=chat_id, document=open("gifs/welcome.gif", "rb"))
bot.sendMessage(config.TELEGRAM_ADMIN_ID, "Utente nuovo: [ " + str(user_id) + " " + first_name + " " + last_name + " ] Approva qui: \n" + "/Approval_" + str(chat_id))
return
bot.sendMessage(chat_id, "Sei nel menu principale", reply_markup=keyboard)
elif text in ("/help", "Aiuto"):
_send_help(chat_id)
elif text.startswith("/Approval_"):
if not _is_admin(user_id):
bot.sendMessage(chat_id, "Comando riservato agli amministratori.")
return
telegram_id = text[len("/Approval_"):]
is_user_approved, already_approved = user.approveUser(telegram_id)
if already_approved and already_approved[0][3] == "A":
bot.sendMessage(chat_id, "Utente è stato approvato già precedentemente!")
return
if is_user_approved:
keyboard = keyboards.getKeyboard()
bot.sendMessage(int(telegram_id), "L'approvazione avvenuta con successo. \n Adesso può usare il menu principale", reply_markup=keyboard)
bot.sendMessage(chat_id, "Utente approvato correttamente")
elif text == "🖍 Crea filtro x Competenze":
comp_first, comp_second = filter_ui.get_competences()
bot.sendMessage(chat_id, comp_first)
bot.sendMessage(chat_id, comp_second)
elif text == "📦 Crea filtro x Prodotto":
bot.sendMessage(chat_id, filter_ui.get_products())
elif text == "👥 Crea filtro x Area":
bot.sendMessage(chat_id, filter_ui.get_areas())
elif text == "👥 Crea filtro x Sott.Area":
bot.sendMessage(chat_id, filter_ui.get_sub_areas(chat_id))
elif text in ("💜 Preferiti", "/preferiti"):
_send_user_favorites(chat_id, user_id)
elif text.startswith("/tic "):
_handle_ticket_search(chat_id, user_id, text, first_name, last_name)
elif text.startswith("/cerca "):
_handle_text_search(chat_id, user_id, text)
elif text.startswith("/utile_"):
_handle_feedback_command(chat_id, text, "useful")
elif text.startswith("/non_utile_"):
_handle_feedback_command(chat_id, text, "not_useful")
elif text.startswith("/preferito_"):
_handle_favorite_command(chat_id, user_id, text)
elif text.startswith("/_id_c_"):
bot.sendMessage(chat_id, insertCompetence.manageCompetence(chat_id, text[7:], "C"))
elif text.startswith("/_id_p_"):
bot.sendMessage(chat_id, insertCompetence.manageCompetence(chat_id, text[7:], "P"))
elif text.startswith("/_id_area_"):
bot.sendMessage(chat_id, insertCompetence.manageCompetence(chat_id, text[10:], "A"))
elif text.startswith("/_id_suba_"):
bot.sendMessage(chat_id, insertCompetence.manageCompetence(chat_id, text[10:], "S"))
elif text == "📌 I mie filtri":
filters_list = filter_ui.getMyFilters(chat_id)
bot.sendMessage(chat_id, filters_list if len(filters_list) > 0 else "Non hai creato ancora un filtro!")
elif text == "✖ Cancella filtro":
filters_to_delete = filter_ui.showFilterToBeDelete(chat_id)
bot.sendMessage(chat_id, filters_to_delete if len(filters_to_delete) > 0 else "Non esistono filtri da cancellare")
elif DELETE_FILTER_RE.match(text):
match = DELETE_FILTER_RE.match(text)
bot.sendMessage(chat_id, filter_ui.deleteFilter(chat_id, match.group(2)))
elif text.startswith("/Ticket_dettaglio_"):
ticket_id = text[len("/Ticket_dettaglio_"):]
_send_ticket_detail(chat_id, ticket_id, actor_id=user_id, first_name=first_name, last_name=last_name)
elif text in ("/ai_stats", "/ai_no_result", "/ai_negative", "/ai_top_useful"):
if not _is_admin(user_id):
bot.sendMessage(chat_id, "Comando riservato agli amministratori.")
return
_send_admin_report(chat_id, text)
elif _handle_notification_command(chat_id, user_id, text):
return
else:
bot.sendMessage(chat_id, "Commando non riconosciuto! Premere /start per iniziare.")
except Exception:
traceback.print_exc()
print("Error in on_chat_message")
def on_callback_query(msg):
try:
query_id, from_id, query_data = telepot.glance(msg, flavor="callback_query")
bot.answerCallbackQuery(query_id, text="Ricevuto")
print(query_data)
if query_data.startswith("d:"):
_, query_ticket, suggested_ticket = query_data.split(":", 2)
_send_ticket_detail(from_id, suggested_ticket, actor_id=from_id, query_ticket_id=query_ticket)
elif query_data.startswith("D:"):
_send_ticket_detail(from_id, query_data[2:], actor_id=from_id)
elif query_data.startswith("fb:"):
_, feedback_value, query_ticket, suggested_ticket = query_data.split(":", 3)
feedback = "useful" if feedback_value == "u" else "not_useful"
ai_retriever.record_feedback(from_id, query_ticket, suggested_ticket, feedback)
bot.sendMessage(from_id, "Feedback registrato: soluzione utile." if feedback == "useful" else "Feedback registrato: soluzione non utile.")
elif query_data.startswith("fav:"):
_, query_ticket, suggested_ticket = query_data.split(":", 2)
inserted = ai_retriever.record_favorite(from_id, suggested_ticket, query_ticket_id=query_ticket)
bot.sendMessage(from_id, "Ticket aggiunto ai preferiti." if inserted else "Ticket gia' nei preferiti.")
elif query_data.startswith("F:"):
inserted = ai_retriever.record_favorite(from_id, query_data[2:])
bot.sendMessage(from_id, "Ticket aggiunto ai preferiti." if inserted else "Ticket gia' nei preferiti.")
elif query_data.startswith("more:"):
_, session_id, offset = query_data.split(":", 2)
_send_ai_results(from_id, session_id, offset=int(offset), intro="Altri risultati AI")
elif query_data.startswith("mode:"):
_, session_id, mode = query_data.split(":", 2)
session = SEARCH_SESSIONS.get(session_id)
if not session:
bot.sendMessage(from_id, "Ricerca scaduta. Rilancia /tic oppure /cerca.")
return
new_session, message = _mode_session(session, mode)
if not new_session:
bot.sendMessage(from_id, message)
return
new_session_id = _store_session(new_session)
_send_ai_results(from_id, new_session_id, offset=0, intro=message)
except Exception:
traceback.print_exc()
print("Error in on_callback_query")
bot = telepot.Bot(config.TELEGRAM_BOT_TOKEN)
try:
MessageLoop(bot, {"chat": on_chat_message, "callback_query": on_callback_query}).run_as_thread()
print("Listening ...")
while 1:
if config.ENABLE_NOTIFICATIONS:
print("Passati 30 Secondi")
tids = user.getUsers()
for u in tids:
if u is None or u[0] is None:
continue
target_user_id = int(u[0])
if not ai_retriever.should_send_notifications(target_user_id):
continue
settings = ai_retriever.get_user_bot_settings(target_user_id)
new_ticket_list = ticketToday.getTicketTodayForNotification(
target_user_id,
high_gravity_only=bool(settings.get("high_gravity_only")),
)
sent_any = False
for ticket in new_ticket_list:
message = ticket
prefix = "🎫 /Ticket_dettaglio_"
ticket_value = ticket.split(prefix, 1)[-1].split("\n", 1)[0].strip()
saved_filters = {}
saved_labels = []
_merge_saved_filters(saved_filters, target_user_id, saved_labels)
best_matches_indices = phrase_similarity.start_guessing_manual(
ticket_value,
user_id=target_user_id,
filters=saved_filters,
limit=3,
)
for ticket_info in best_matches_indices:
message += str(ticket_info)
bot.sendMessage(target_user_id, str(message))
sent_any = True
if sent_any:
ai_retriever.mark_notification_sent(target_user_id)
else:
print("Notification scan disabled")
time.sleep(30)
except Exception:
traceback.print_exc()