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 [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 [--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 {ticket_id} ....", 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 , 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 , 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_ - salva filtro competenza\n" "/_id_p_ - salva filtro prodotto\n" "/_id_area_ - salva filtro area\n" "/_id_suba_ - salva filtro sottoarea\n" "πŸ“Œ I mie filtri - mostra i filtri salvati\n" "βœ– Cancella filtro - mostra i comandi per eliminarli\n" "/Competen_id_cf_, /Prodotto_id_cf_, /Area_id_cf_, /Sottoarea_id_cf_ - elimina filtro\n" "\n" "Ricerca soluzioni\n" "/tic - cerca ticket simili usando i tuoi filtri salvati\n" "/tic area - solo stessa area del ticket\n" "/tic sottoarea - solo stessa sottoarea\n" "/tic prodotto - solo stesso prodotto\n" "/tic competenza - solo stessa competenza\n" "/tic cliente - solo stesso cliente\n" "/tic no_cliente - esclude stesso cliente\n" "/tic area - usa area specifica\n" "/tic prodotto - usa prodotto specifico\n" "/tic competenza - usa competenza specifica\n" "/tic recenti - limita ai ticket recenti\n" "/tic codice - richiede stesso codice errore\n" "/tic media oppure alta - alza la confidenza minima\n" "/cerca - cerca da testo libero usando i filtri salvati\n" "/cerca --area --prodotto --competenza - cerca con filtri specifici\n" "/cerca --filtri - forza i filtri salvati\n" "/Ticket_dettaglio_ - scarica il dettaglio ticket\n" "\n" "Feedback e preferiti\n" "/utile__ - segna soluzione utile\n" "/non_utile__ - segna soluzione non utile\n" "/preferito_ - 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 - pausa in una fascia oraria, esempio /silenzio 22 7\n" "/silenzio_off - rimuove la fascia silenzio\n" "/digest - 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()