#!/usr/bin/env python3 """ PsAgentAI - Personal Agent AI Conversacional Sistema de Personal Agent IA Conversacional via Telegram """ __version__ = "1.0.0" __author__ = "Marco A. S. Costa" __email__ = "contato@syspanel.com.br" __license__ = "Proprietária para syspanel.com.br" import os import sys import json import random import re import asyncio import base64 import logging import mimetypes from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Optional, Dict, List, Any try: from zoneinfo import ZoneInfo except ImportError: ZoneInfo = None import requests from dotenv import load_dotenv from telegram import Update, Chat, User, Message, InputFile from telegram.ext import ( Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters, ContextTypes, ) from telegram.constants import ParseMode from telegram import InlineKeyboardMarkup, InlineKeyboardButton # Load environment variables load_dotenv() # Configure logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO, ) logger = logging.getLogger(__name__) # ===================== CONFIGURATION ===================== TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "") TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "") TELEGRAM_CHAT_ID_FILE = "telegram_chat_id.json" DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "poolside/laguna-s-2.1:free") AI_PROVIDER = os.getenv("AI_PROVIDER", "").strip().lower() FALLBACK_MODELS = os.getenv("FALLBACK_MODELS", "poolside/laguna-s-2.1:free,google/gemini-2.0-flash-lite-preview-02-05:free").split(",") OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "") OPENROUTER_API_URL = os.getenv("OPENROUTER_API_URL", "https://openrouter.ai/api/v1") STEPFUN_API_KEY = os.getenv("STEPFUN_API_KEY", "") STEPFUN_API_URL = os.getenv("STEPFUN_API_URL", "https://api.stepfun.com/v1") QWENCLOUD_API_KEY = os.getenv("QWENCLOUD_API_KEY", "") QWENCLOUD_API_URL = os.getenv("QWENCLOUD_API_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1") LLAMA_CPP_API_URL = os.getenv("LLAMA_CPP_API_URL", "http://localhost:8080/v1") MIN_INTERVAL_MINUTES = int(os.getenv("MIN_INTERVAL_MINUTES", "3")) MAX_INTERVAL_MINUTES = int(os.getenv("MAX_INTERVAL_MINUTES", "30")) MAX_HISTORY_LENGTH = int(os.getenv("MAX_HISTORY_LENGTH", "50")) UPLOAD_CONTEXT_MAX_CHARS = int(os.getenv("UPLOAD_CONTEXT_MAX_CHARS", "8000")) WEB_SEARCH_ENABLED = os.getenv("WEB_SEARCH_ENABLED", "true").lower() in ("true", "1", "yes") WEB_SEARCH_MAX_RESULTS = int(os.getenv("WEB_SEARCH_MAX_RESULTS", "5")) VISION_MODEL = os.getenv("VISION_MODEL", "meta-llama/llama-4-maverick-17b-128e-instruct") VISION_PROVIDER = os.getenv("VISION_PROVIDER", "").strip().lower() if not VISION_PROVIDER: try: VISION_PROVIDER = AIProvider.get_provider(VISION_MODEL) except Exception: VISION_PROVIDER = "openrouter" USER_PROFILE_FILE = os.getenv("USER_PROFILE_FILE", "user_profile.json") AGENT_PROFILE_FILE = os.getenv("AGENT_PROFILE_FILE", "agent_profile.json") CONTEXTS_DIR = os.getenv("CONTEXTS_DIR", "contexts") MIN_MESSAGE_LINES = int(os.getenv("MIN_MESSAGE_LINES", "3")) AUTO_IMPROVE_ENABLED = os.getenv("AUTO_IMPROVE_ENABLED", "true").lower() in ("true", "1", "yes") AUTO_IMPROVE_EVERY = int(os.getenv("AUTO_IMPROVE_EVERY", "20")) # ===================== GLOBAL STATE ===================== app_instance: Optional[Application] = None proactive_task: Optional[asyncio.Task] = None is_onboarding = False onboarding_step = 0 onboarding_answers: Dict[str, Any] = {} is_newconfig = False newconfig_step = 0 newconfig_answers: Dict[str, Any] = {} is_configagentprofile = False configagentprofile_step = 0 configagentprofile_answers: Dict[str, Any] = {} is_edit_user_profile = False edit_user_profile_step = 0 edit_user_profile_field = "" is_edit_agent_profile = False edit_agent_profile_step = 0 is_agendamento = False agendamento_step = 0 agendamento_data: Dict[str, Any] = {} is_useragenda = False useragenda_step = 0 useragenda_data: Dict[str, Any] = {} is_activity_crud = False activity_crud_step = 0 activity_crud_data: Dict[str, Any] = {} is_configia = False IA_CONFIG_STEPS: List[str] = [] IA_CONFIG_ANSWERS: Dict[str, Any] = {} edit_agent_profile_field = "" _auto_improve_counter = 0 is_agendamento = False agendamento_step = 0 agendamento_answers: Dict[str, Any] = {} def _parse_time_to_minutes(time_str: str) -> Optional[int]: time_str = time_str.strip() if ":" not in time_str: return None parts = time_str.split(":") if len(parts) != 2: return None try: hour = int(parts[0]) minute = int(parts[1]) if not (0 <= hour <= 23 and 0 <= minute <= 59): return None return hour * 60 + minute except ValueError: return None def get_schedule() -> Dict[str, Any]: config = load_config() schedule = config.get("schedule", {}) if not schedule: return { "days": [0, 1, 2, 3, 4, 5, 6], "start_time": "09:00", "end_time": "23:00", } return schedule def _web_search(query: str, max_results: int = 5) -> List[Dict[str, str]]: try: url = "https://html.duckduckgo.com/html/" payload = {"q": query, "b": ""} headers = { "User-Agent": "Mozilla/5.0 (compatible; PsAgentAI/1.0)" } response = requests.post(url, data=payload, headers=headers, timeout=10) response.raise_for_status() html = response.text results = [] import re pattern = r']+class="result__a"[^>]*href="([^"]+)"[^>]*>(.*?)' matches = re.findall(pattern, html, re.DOTALL) for href, title in matches[:max_results]: title = re.sub(r'<[^>]+>', '', title).strip() if title and href: results.append({"title": title, "url": href}) return results except Exception: return [] def set_schedule(days: List[int], start_time: str, end_time: str) -> bool: start_minutes = _parse_time_to_minutes(start_time) end_minutes = _parse_time_to_minutes(end_time) if start_minutes is None or end_minutes is None: return False if start_minutes >= end_minutes: return False config = load_config() config["schedule"] = { "days": days, "start_time": start_time, "end_time": end_time, } save_config(config) return True def is_within_schedule() -> bool: schedule = get_schedule() tz = get_timezone() tz_info = _get_timezone_info(tz) if tz_info: now = datetime.now(tz_info) else: now = datetime.now() current_day = now.weekday() current_minutes = now.hour * 60 + now.minute allowed_days = schedule.get("days", [0, 1, 2, 3, 4, 5, 6]) if current_day not in allowed_days: return False start_minutes = _parse_time_to_minutes(schedule.get("start_time", "00:00")) end_minutes = _parse_time_to_minutes(schedule.get("end_time", "23:59")) if start_minutes is None or end_minutes is None: return True return start_minutes <= current_minutes < end_minutes def _is_appropriate_time_scheduled() -> bool: if not is_within_schedule(): return False time_period = get_time_period() return time_period != "madrugada" def load_telegram_chat_id() -> Optional[int]: if os.path.exists(TELEGRAM_CHAT_ID_FILE): try: with open(TELEGRAM_CHAT_ID_FILE, "r", encoding="utf-8") as f: data = json.load(f) chat_id = data.get("chat_id") if chat_id is not None: return int(chat_id) except Exception: pass return None def save_telegram_chat_id(chat_id: int): with open(TELEGRAM_CHAT_ID_FILE, "w", encoding="utf-8") as f: json.dump({"chat_id": chat_id}, f, ensure_ascii=False, indent=2) def get_effective_chat_id() -> Optional[int]: if TELEGRAM_CHAT_ID: return int(TELEGRAM_CHAT_ID) return load_telegram_chat_id() async def auto_detect_chat_id(update: Update): if not TELEGRAM_CHAT_ID and update.effective_chat: chat_id = update.effective_chat.id if chat_id: save_telegram_chat_id(chat_id) CONFIG_FILE = "config.json" def load_config() -> Dict[str, Any]: if os.path.exists(CONFIG_FILE): try: with open(CONFIG_FILE, "r", encoding="utf-8") as f: return json.load(f) except Exception: pass return {} def save_config(config: Dict[str, Any]): with open(CONFIG_FILE, "w", encoding="utf-8") as f: json.dump(config, f, ensure_ascii=False, indent=2) def get_timezone() -> str: try: tz = datetime.now().astimezone().tzinfo if tz is not None: return str(tz) except Exception: pass return "UTC" def set_timezone(tz: str) -> bool: return False def _get_timezone_info(tz_name: str): if ZoneInfo is None: return None try: return ZoneInfo(tz_name) except Exception: return None def get_local_now() -> datetime: return datetime.now().astimezone() # ===================== UTILITY FUNCTIONS ===================== def get_today_date() -> str: tz = get_timezone() tz_info = _get_timezone_info(tz) if tz_info: return datetime.now(tz_info).strftime("%Y-%m-%d") return datetime.now().strftime("%Y-%m-%d") def get_context_file_path(date_str: Optional[str] = None) -> Path: if date_str is None: date_str = get_today_date() return Path(CONTEXTS_DIR) / f"{date_str}.json" def ensure_contexts_dir(): Path(CONTEXTS_DIR).mkdir(parents=True, exist_ok=True) # ===================== FILE UPLOAD SYSTEM ===================== UPLOADS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "workspace") UPLOADS_META_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "uploads_metadata.json") WORKSPACE_PROJECTS_DIR = os.path.join(UPLOADS_DIR, "projects") def ensure_uploads_dir(): Path(UPLOADS_DIR).mkdir(parents=True, exist_ok=True) def load_uploads_metadata() -> Dict[str, Any]: if os.path.exists(UPLOADS_META_FILE): with open(UPLOADS_META_FILE, "r", encoding="utf-8") as f: return json.load(f) return {} def save_uploads_metadata(meta: Dict[str, Any]): with open(UPLOADS_META_FILE, "w", encoding="utf-8") as f: json.dump(meta, f, ensure_ascii=False, indent=2) ALLOWED_EXTENSIONS = {".pdf", ".txt", ".docx", ".png", ".jpg", ".jpeg"} def _extract_text_from_pdf(filepath: str) -> str: try: import fitz doc = fitz.open(filepath) text = "" for page in doc: text += page.get_text() doc.close() return text.strip() except Exception: return "" def _extract_text_from_docx(filepath: str) -> str: try: import docx doc = docx.Document(filepath) return "\n".join([p.text for p in doc.paragraphs]).strip() except Exception: return "" def _extract_text_from_txt(filepath: str) -> str: try: with open(filepath, "r", encoding="utf-8", errors="replace") as f: return f.read().strip() except Exception: return "" def _get_file_summary(text: str, max_chars: int = 500) -> str: if not text: return "" if len(text) <= max_chars: return text return text[:max_chars] + "...\n[conteudo truncado, " + str(len(text)) + " caracteres no total]" def extract_file_text(filepath: str) -> str: ext = os.path.splitext(filepath)[1].lower() if ext == ".pdf": return _extract_text_from_pdf(filepath) elif ext == ".docx": return _extract_text_from_docx(filepath) elif ext == ".txt": return _extract_text_from_txt(filepath) elif ext in (".png", ".jpg", ".jpeg"): return "[Imagem: " + os.path.basename(filepath) + "]" return "" def register_upload(filename: str, original_name: str, filepath: str, chat_id: int) -> bool: ensure_uploads_dir() text = extract_file_text(filepath) meta = load_uploads_metadata() meta[filename] = { "original_name": original_name, "filepath": filepath, "chat_id": chat_id, "uploaded_at": datetime.now().isoformat(), "summary": _get_file_summary(text), "full_text": text, } save_uploads_metadata(meta) return bool(text) def find_matching_upload(query: str) -> Optional[Dict[str, Any]]: q = query.lower().strip() meta = load_uploads_metadata() for filename, info in meta.items(): original_name = (info.get("original_name") or "").strip().lower() if not original_name: continue base = os.path.splitext(original_name)[0] tokens = re.split(r"[^a-z0-9áçéíóúâêôãõà]+", base, flags=re.IGNORECASE) for token in tokens: if token and token in q: return info return None _WEB_SEARCH_TRIGGERS = [ "pesquise", "pesquisa", "busque", "buscar", "procure", "procurar", "na internet", "internet", "web", "google", "o que é", "quem é", "qual a", "qual o", "como funciona", "notícia", "noticias", "notícias", "atualidade", "atual", "hoje", "agora", "tempo real", "tempo real", "previsão", "clima", "tempo", "onde fica", "onde é", "localização", "localizacao", "mapa", "endereço", "endereco", ] def _looks_like_search_request(user_message: str) -> bool: q = user_message.lower().strip() if len(q.split()) < 2: return False return any(t in q for t in _WEB_SEARCH_TRIGGERS) def _looks_like_create_request(user_message: str) -> bool: q = user_message.lower().strip() triggers = [ "crie", "criar", "elabore", "elaborar", "desenvolva", "desenvolver", "gere", "gerar", "faça", "faca", "faça um", "faca um", "crie um", "elabore um", "escreva", "escrever", "programe", "programar", "codifique", "codificar", "implemente", "implementar", "projeto", "arquivo", "script", "código", "codigo", "app", "aplicativo", "sistema", "site", "planilha", "documento", "pdf", "json", "py", "html", "css", "js", ] return any(t in q for t in triggers) def _extract_create_suggestion(user_message: str) -> Optional[str]: q = user_message.lower().strip() for prefix in ["crie", "criar", "elabore", "elaborar", "desenvolva", "desenvolver", "gere", "gerar", "faça", "faca", "escreva", "escrever", "programe", "programar", "codifique", "codificar", "implemente", "implementar"]: if q.startswith(prefix): return user_message[len(prefix):].strip(" :,-") return None def _looks_like_edit_request(user_message: str) -> bool: q = user_message.lower().strip() triggers = [ "edite", "editar", "modifique", "modificar", "altere", "alterar", "atualize", "atualizar", "mude", "mudar", "corrija", "corrigir", "ajuste", "ajustar", "troque", "trocar", "substitua", "substituir", "adicione", "adicionar", "remova", "remover", "delete", "apague", "apagar", "inclua", "incluir", "reescreva", "reescrever", "refatore", "refatorar", "melhore", "melhorar", "otimize", "otimizar", "no arquivo", "no projeto", "no script", "no código", "no documento", "arquivo", "projeto", "script", "código", "codigo", ] if any(t in q for t in triggers): if any(name in q for name in ["leia", "lista", "mostre", "exiba", "quais"]): return False return True return False def _extract_edit_target(user_message: str) -> Optional[str]: q = user_message.lower().strip() for prefix in ["edite", "editar", "modifique", "modificar", "altere", "alterar", "atualize", "atualizar", "mude", "mudar", "corrija", "corrigir", "ajuste", "ajustar", "troque", "trocar", "substitua", "substituir", "adicione", "adicionar", "remova", "remover", "delete", "apague", "apagar", "inclua", "incluir", "reescreva", "reescrever", "refatore", "refatorar", "melhore", "melhorar", "otimize", "otimizar", "no arquivo", "no projeto", "no script", "no código", "no documento"]: if q.startswith(prefix): rest = user_message[len(prefix):].strip(" :,-") return rest return None def _extract_filename_from_text(text: str) -> Optional[str]: matches = re.findall(r"[A-Za-z0-9_./-]+\.[A-Za-z0-9]+", text) if matches: return matches[0] return None def _find_workspace_file(target: str) -> Optional[str]: filename = _extract_filename_from_text(target) if not filename and target and " " not in target: filename = target if not filename: return None target_lower = filename.lower().strip() base = os.path.splitext(target_lower)[0] candidates = [] for root, dirs, files in os.walk(UPLOADS_DIR): for fname in files: full = os.path.join(root, fname) rel = os.path.relpath(full, UPLOADS_DIR) name_lower = fname.lower() if target_lower in name_lower or base in name_lower or target_lower in rel.lower(): candidates.append(rel) if not candidates: return None candidates.sort(key=lambda x: (x.count(os.sep), len(x))) return candidates[0] async def _handle_edit_request(update: Update, context: ContextTypes.DEFAULT_TYPE, user_message: str) -> bool: target = _extract_edit_target(user_message) if not target: return False rel_path = _find_workspace_file(target) if not rel_path: await update.message.reply_text(f"Não encontrei o arquivo/pasta relacionado a: {target}") return True abs_path = os.path.join(UPLOADS_DIR, rel_path) if not os.path.exists(abs_path): await update.message.reply_text(f"O caminho não existe mais: {rel_path}") return True if os.path.isdir(abs_path): await update.message.reply_text(f"Ainda não edito pastas inteiras. Me diga o nome de um arquivo dentro de `{rel_path}`.") return True try: with open(abs_path, "r", encoding="utf-8", errors="replace") as f: current_content = f.read() except Exception as e: logger.error("Failed to read workspace file %s: %s", abs_path, e) await update.message.reply_text("Erro ao ler o arquivo. Tente novamente.") return True profile = load_user_profile() today_context = load_context() edit_prompt = ( "Você é um assistente de edição de arquivos.\n" "Dado o conteúdo atual do arquivo e a solicitação do usuário, retorne APENAS o conteúdo final revisado do arquivo.\n" "NÃO use markdown, NÃO adicione explicações, NÃO adicione blocos de código.\n" "Mantenha o máximo possível do conteúdo original, aplicando apenas a alteração solicitada.\n\n" f"Caminho do arquivo: {rel_path}\n\n" f"Conteúdo atual:\n{current_content}\n\n" f"Solicitação do usuário: {user_message}\n\n" "Retorne apenas o novo conteúdo final do arquivo." ) messages = build_messages_for_ai(profile, edit_prompt, today_context) new_content, _ = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.3, max_tokens=12000) if not new_content: await update.message.reply_text("Não consegui gerar a edição. Tente novamente.") return True if new_content.startswith("```"): new_content = new_content.lstrip("`") new_content = new_content.split("\n", 1)[-1] if "\n" in new_content else new_content if new_content.endswith("```"): new_content = new_content[:-3].rstrip() new_content = new_content.strip() try: with open(abs_path, "w", encoding="utf-8") as f: f.write(new_content) except Exception as e: logger.error("Failed to write workspace file %s: %s", abs_path, e) await update.message.reply_text("Ocorreu um erro ao salvar o arquivo editado. Tente novamente.") return True preview = new_content[:400] if len(new_content) > 400: preview += "..." await update.message.reply_text( f"✅ Arquivo editado: `{rel_path}`\n\nPré-visualização:\n{preview}", parse_mode=ParseMode.HTML, ) return True def ensure_workspace_dirs(): Path(UPLOADS_DIR).mkdir(parents=True, exist_ok=True) Path(WORKSPACE_PROJECTS_DIR).mkdir(parents=True, exist_ok=True) def save_workspace_file(relative_path: str, content: str) -> str: ensure_workspace_dirs() dest = os.path.join(UPLOADS_DIR, relative_path) os.makedirs(os.path.dirname(dest), exist_ok=True) with open(dest, "w", encoding="utf-8") as f: f.write(content) return dest def build_workspace_tree(relative_path: str, content: str) -> List[str]: ensure_workspace_dirs() created = [] if "\n" in content and any(line.startswith(("## ", "# ", "FILE:", "DIR:", "./", "/")) for line in content.splitlines()[:10]): current_dir = "" for line in content.splitlines(): line = line.strip() if not line: continue if line.startswith(("## ", "# ")): current_dir = line.lstrip("# ").strip() Path(os.path.join(UPLOADS_DIR, current_dir)).mkdir(parents=True, exist_ok=True) created.append(current_dir) elif line.startswith(("FILE:", "./", "/")) or (current_dir and line.endswith((".py", ".html", ".css", ".js", ".json", ".md", ".txt"))): name = line.split(":", 1)[-1].strip() if ":" in line else line if name: dest = os.path.join(UPLOADS_DIR, current_dir, name) if current_dir else os.path.join(UPLOADS_DIR, name) os.makedirs(os.path.dirname(dest), exist_ok=True) with open(dest, "w", encoding="utf-8") as f: f.write("") created.append(os.path.relpath(dest, UPLOADS_DIR)) else: dest = os.path.join(UPLOADS_DIR, relative_path) os.makedirs(os.path.dirname(dest), exist_ok=True) with open(dest, "w", encoding="utf-8") as f: f.write(content) created.append(relative_path) return created def _list_workspace_files() -> List[str]: ensure_workspace_dirs() files = [] for root, _, filenames in os.walk(UPLOADS_DIR): for filename in filenames: abs_path = os.path.join(root, filename) rel_path = os.path.relpath(abs_path, UPLOADS_DIR) files.append(rel_path) files.sort() return files async def _handle_create_request(update: Update, context: ContextTypes.DEFAULT_TYPE, user_message: str) -> bool: suggestion = _extract_create_suggestion(user_message) if not suggestion: return False await update.message.reply_text(f"🧩 Vou criar isso para você: \"{suggestion}\"") profile = load_user_profile() today_context = load_context() create_prompt = ( "Você é um assistente especializado em criar arquivos e projetos.\n" "Gere o conteúdo completo e organizado para o que o usuário pediu.\n" "Se for um projeto com múltiplos arquivos, liste os arquivos e pastas.\n" "Formato:\n" "- Para arquivo único: apenas o conteúdo final.\n" "- Para projeto: comece com o nome da pasta raiz, depois liste os arquivos com seus caminhos e conteúdos.\n\n" f"Pedido do usuário: {suggestion}\n" ) messages = build_messages_for_ai(profile, create_prompt, today_context) content, _ = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.7, max_tokens=6000) if not content: await update.message.reply_text("Não consegui gerar o conteúdo. Tente novamente.") return True safe_root = re.sub(r"[^a-zA-Z0-9_-]+", "_", suggestion.lower()).strip("_") or "projeto" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") root_name = f"{timestamp}_{safe_root}" try: created = build_workspace_tree(root_name, content) list_text = "\n".join(f"- {p}" for p in created) if created else "- (raiz)" await update.message.reply_text( "✅ Conteúdo gerado e salvo em `workspace/`:\n\n" + list_text, parse_mode=ParseMode.HTML, ) except Exception as e: logger.error("Failed to save workspace file: %s", e) await update.message.reply_text("Ocorreu um erro ao salvar o arquivo. Tente novamente.") return True def get_all_uploads_for_chat(chat_id: int) -> Dict[str, Dict[str, Any]]: meta = load_uploads_metadata() return {fn: info for fn, info in meta.items() if info.get("chat_id") == chat_id} def encode_image_to_base64(filepath: str) -> Optional[str]: try: with open(filepath, "rb") as f: data = f.read() mime, _ = mimetypes.guess_type(filepath) if not mime or not mime.startswith("image/"): mime = "image/jpeg" b64 = base64.b64encode(data).decode("utf-8") return f"data:{mime};base64,{b64}" except Exception: return None def analyze_image(filepath: str, question: str = "Descreva o que você vê nesta imagem em português.") -> Optional[str]: image_data = encode_image_to_base64(filepath) if not image_data: return None provider = VISION_PROVIDER or AIProvider.get_provider(VISION_MODEL) api_key = AIProvider.get_api_key(provider) api_url = AIProvider.get_api_url(provider) if not api_key or provider == "llamacpp": return None messages = [ { "role": "user", "content": [ {"type": "text", "text": question}, {"type": "image_url", "image_url": {"url": image_data}}, ], } ] try: result, _ = AIProvider._chat_openai_compatible(provider, api_key, api_url, VISION_MODEL, messages, temperature=0.7, max_tokens=2000) return result except Exception: return None def save_image_to_workspace(original_name: str, filepath: str, chat_id: int) -> str: ensure_uploads_dir() meta = load_uploads_metadata() if original_name: filename = original_name dest = os.path.join(UPLOADS_DIR, filename) counter = 1 while os.path.exists(dest): base, ext = os.path.splitext(filename) dest = os.path.join(UPLOADS_DIR, f"{base}_{counter}{ext}") counter += 1 else: ext = os.path.splitext(filepath)[1] or ".jpg" filename = datetime.now().strftime("photo_%Y%m%d_%H%M%S") + ext dest = os.path.join(UPLOADS_DIR, filename) import shutil shutil.copy2(filepath, dest) meta[filename] = { "original_name": original_name or filename, "filepath": dest, "chat_id": chat_id, "uploaded_at": datetime.now().isoformat(), "summary": "[Imagem]", "full_text": "[Imagem]", "type": "image", } save_uploads_metadata(meta) return filename async def photo_message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) photo = update.message.photo if not photo: return await update.message.reply_text("📸 Recebi a imagem, processando...") largest = photo[-1] original_name = "" if update.message.document: original_name = update.message.document.file_name or "" file = await context.bot.get_file(largest.file_id) ext = ".jpg" if original_name: ext = os.path.splitext(original_name)[1] or ext tmp_path = f"/tmp/photo_{largest.file_id}{ext}" try: await file.download_to_drive(tmp_path) except Exception: await update.message.reply_text("Erro ao baixar a imagem. Tente novamente.") return filename = save_image_to_workspace(original_name, tmp_path, update.effective_chat.id) try: os.remove(tmp_path) except Exception: pass analysis = analyze_image(os.path.join(UPLOADS_DIR, filename)) if analysis: await update.message.reply_text(f"📸 Analisei a imagem:\n\n{analysis}") else: await update.message.reply_text("📸 Imagem salva em workspace. Não consegui analisar agora.") def load_context(date_str: Optional[str] = None) -> List[Dict[str, str]]: ensure_contexts_dir() file_path = get_context_file_path(date_str) if file_path.exists(): with open(file_path, "r", encoding="utf-8") as f: return json.load(f) return [] def save_context(context: List[Dict[str, str]], date_str: Optional[str] = None): ensure_contexts_dir() file_path = get_context_file_path(date_str) with open(file_path, "w", encoding="utf-8") as f: json.dump(context, f, ensure_ascii=False, indent=2) def add_to_context(role: str, content: str, date_str: Optional[str] = None): context = load_context(date_str) tz = get_timezone() tz_info = _get_timezone_info(tz) if tz_info: now = datetime.now(tz_info).isoformat() else: now = datetime.now().isoformat() context.append({"role": role, "content": content, "timestamp": now}) if len(context) > MAX_HISTORY_LENGTH: context = context[-MAX_HISTORY_LENGTH:] save_context(context, date_str) return context def load_user_profile() -> Dict[str, Any]: if os.path.exists(USER_PROFILE_FILE): with open(USER_PROFILE_FILE, "r", encoding="utf-8") as f: return json.load(f) default = { "name": "", "age": "", "gender": "", "location": "", "interests": [], "personality_traits": [], "preferences": { "communication_style": "", "topics_to_avoid": [], "topics_of_interest": [] }, "notes": "", "created_at": "", "last_updated": "", "daily_routine": { "wake_up_time": "", "breakfast_time": "", "sleep_time": "", "work_schedule": "", "lunch_time": "", "dinner_time": "", "activities": [] } } save_user_profile(default) return default def save_user_profile(profile: Dict[str, Any]): profile["last_updated"] = datetime.now().isoformat() with open(USER_PROFILE_FILE, "w", encoding="utf-8") as f: json.dump(profile, f, ensure_ascii=False, indent=2) AGENT_PROFILE_DEFAULT = { "name": "PsAgentAI", "personality": "carinhosa, atenciosa e envolvente", "traits": [], "gender": "", "notes": "" } def load_agent_profile() -> Dict[str, Any]: if os.path.exists(AGENT_PROFILE_FILE): with open(AGENT_PROFILE_FILE, "r", encoding="utf-8") as f: return json.load(f) default = dict(AGENT_PROFILE_DEFAULT) save_agent_profile(default) return default def save_agent_profile(profile: Dict[str, Any]): profile["last_updated"] = datetime.now().isoformat() with open(AGENT_PROFILE_FILE, "w", encoding="utf-8") as f: json.dump(profile, f, ensure_ascii=False, indent=2) def is_profile_complete(profile: Dict[str, Any]) -> bool: return bool(profile.get("name", "").strip()) # ===================== NEW CONFIG ===================== NEWCONFIG_QUESTIONS = [ { "key": "agent_name", "question": "Qual será o nome do seu agente? (ex: Mônica, Luna, etc.)", }, { "key": "user_name", "question": "Qual é o seu nome?", }, { "key": "user_age", "question": "Quantos anos você tem?", }, { "key": "user_gender", "question": "Qual é o seu gênero?", }, { "key": "agent_personality", "question": "Descreva a personalidade do agente (ex: carinhosa, brincalhona, séria, etc.):", }, { "key": "agent_traits", "question": "Quais características o agente deve ter? (separe por vírgula)", "list": True, }, { "key": "communication_style", "question": "Qual estilo de comunicação prefere? (formal, descontraída, carinhosa, direta)", }, { "key": "topics_of_interest", "question": "Quais tópicos você gosta de conversar? (separe por vírgula)", "list": True, }, { "key": "topics_to_avoid", "question": "Tem algum tópico que deva evitar? (separe por vírgula)", "list": True, }, ] AGENTCONFIG_QUESTIONS = [ { "key": "agent_name", "question": "Qual será o nome do agente? (ex: Mônica, Luna, etc.)", }, { "key": "agent_gender", "question": "Qual o gênero/sexo do agente? (ex: feminino, masculino, não-binário)", }, { "key": "agent_personality", "question": "Descreva a personalidade do agente (ex: carinhosa, brincalhona, séria, etc.):", }, { "key": "agent_traits", "question": "Quais características o agente deve ter? (separe por vírgula)", "list": True, }, { "key": "agent_notes", "question": "Tem alguma nota ou instrução adicional para o agente? (opcional)", }, ] def reset_all_data(): if os.path.exists(USER_PROFILE_FILE): os.remove(USER_PROFILE_FILE) if os.path.exists(AGENT_PROFILE_FILE): os.remove(AGENT_PROFILE_FILE) if os.path.exists(TELEGRAM_CHAT_ID_FILE): os.remove(TELEGRAM_CHAT_ID_FILE) if os.path.isdir(CONTEXTS_DIR): for filename in os.listdir(CONTEXTS_DIR): filepath = os.path.join(CONTEXTS_DIR, filename) if os.path.isfile(filepath): os.remove(filepath) async def start_newconfig(update: Update, context: ContextTypes.DEFAULT_TYPE): global is_newconfig, newconfig_step, newconfig_answers await auto_detect_chat_id(update) is_newconfig = True newconfig_step = 0 newconfig_answers = {} await update.message.reply_text( "Vamos reiniciar tudo e configurar o PsAgentAI do zero.\n" "Você pode cancelar a qualquer momento enviando /cancelar." ) await send_newconfig_question(update, context) async def send_newconfig_question(update: Update, context_context: ContextTypes.DEFAULT_TYPE): global newconfig_step if newconfig_step >= len(NEWCONFIG_QUESTIONS): await finish_newconfig(update, context_context) return q = NEWCONFIG_QUESTIONS[newconfig_step] await context_context.bot.send_message( chat_id=update.effective_chat.id, text=q["question"], ) async def finish_newconfig(update: Update, context_context: ContextTypes.DEFAULT_TYPE): global is_newconfig, newconfig_step, newconfig_answers reset_all_data() profile = load_user_profile() profile["name"] = newconfig_answers.get("user_name", "") profile["age"] = newconfig_answers.get("user_age", "") profile["gender"] = newconfig_answers.get("user_gender", "") profile["preferences"] = { "communication_style": newconfig_answers.get("communication_style", ""), "topics_of_interest": newconfig_answers.get("topics_of_interest", []), "topics_to_avoid": newconfig_answers.get("topics_to_avoid", []), } profile["created_at"] = datetime.now().isoformat() save_user_profile(profile) agent = load_agent_profile() agent["name"] = newconfig_answers.get("agent_name", "PsAgentAI") agent["personality"] = newconfig_answers.get("agent_personality", "carinhosa, atenciosa e envolvente") agent["traits"] = newconfig_answers.get("agent_traits", []) save_agent_profile(agent) is_newconfig = False newconfig_step = 0 newconfig_answers = {} logger.info("Configuração concluída com sucesso.") await context_context.bot.send_message( chat_id=update.effective_chat.id, text="✅ Configuração concluída!\nAgora você pode usar /start para começar a conversar.", ) async def handle_newconfig_answer(update: Update, context_context: ContextTypes.DEFAULT_TYPE) -> bool: global newconfig_step, newconfig_answers if not is_newconfig: return False q = NEWCONFIG_QUESTIONS[newconfig_step] answer = update.message.text.strip() if q.get("list"): items = [item.strip() for item in answer.split(",") if item.strip()] if not items: items = [answer] newconfig_answers[q["key"]] = items else: newconfig_answers[q["key"]] = answer newconfig_step += 1 if newconfig_step >= len(NEWCONFIG_QUESTIONS): await finish_newconfig(update, context_context) return True await send_newconfig_question(update, context_context) return True # ===================== CONFIG AGENT PROFILE ===================== async def start_configagentprofile(update: Update, context: ContextTypes.DEFAULT_TYPE): global is_configagentprofile, configagentprofile_step, configagentprofile_answers await auto_detect_chat_id(update) is_configagentprofile = True configagentprofile_step = 0 configagentprofile_answers = {} await update.message.reply_text( "Vamos configurar o perfil do seu agente.\n" "Você pode cancelar a qualquer momento enviando /cancelar." ) await send_configagentprofile_question(update, context) async def send_configagentprofile_question(update: Update, context_context: ContextTypes.DEFAULT_TYPE): global configagentprofile_step if configagentprofile_step >= len(AGENTCONFIG_QUESTIONS): await finish_configagentprofile(update, context_context) return q = AGENTCONFIG_QUESTIONS[configagentprofile_step] await context_context.bot.send_message( chat_id=update.effective_chat.id, text=q["question"], ) async def finish_configagentprofile(update: Update, context_context: ContextTypes.DEFAULT_TYPE): global is_configagentprofile, configagentprofile_step, configagentprofile_answers agent = load_agent_profile() agent["name"] = configagentprofile_answers.get("agent_name", agent.get("name", "PsAgentAI")) agent["gender"] = configagentprofile_answers.get("agent_gender", agent.get("gender", "")) agent["personality"] = configagentprofile_answers.get("agent_personality", agent.get("personality", "carinhosa, atenciosa e envolvente")) agent["traits"] = configagentprofile_answers.get("agent_traits", agent.get("traits", [])) notes = configagentprofile_answers.get("agent_notes", "").strip() if notes: existing = agent.get("notes", "").strip() agent["notes"] = f"{existing}; {notes}" if existing else notes save_agent_profile(agent) is_configagentprofile = False configagentprofile_step = 0 configagentprofile_answers = {} logger.info("Configuração do agente concluída com sucesso.") await context_context.bot.send_message( chat_id=update.effective_chat.id, text="✅ Perfil do agente atualizado!\nUse /agentprofile para ver o resultado.", ) async def handle_configagentprofile_answer(update: Update, context_context: ContextTypes.DEFAULT_TYPE) -> bool: global configagentprofile_step, configagentprofile_answers if not is_configagentprofile: return False q = AGENTCONFIG_QUESTIONS[configagentprofile_step] answer = update.message.text.strip() if q.get("list"): items = [item.strip() for item in answer.split(",") if item.strip()] if not items: items = [answer] configagentprofile_answers[q["key"]] = items else: configagentprofile_answers[q["key"]] = answer configagentprofile_step += 1 if configagentprofile_step >= len(AGENTCONFIG_QUESTIONS): await finish_configagentprofile(update, context_context) return True await send_configagentprofile_question(update, context_context) return True # ===================== EDIT USER PROFILE ===================== EDIT_USER_PROFILE_FIELDS = [ {"key": "name", "label": "Nome", "list": False}, {"key": "age", "label": "Idade", "list": False}, {"key": "gender", "label": "Gênero", "list": False}, {"key": "location", "label": "Localização", "list": False}, {"key": "interests", "label": "Interesses (separe por vírgula)", "list": True}, {"key": "personality_traits", "label": "Traços de personalidade (separe por vírgula)", "list": True}, {"key": "communication_style", "label": "Estilo de comunicação preferido", "list": False}, {"key": "topics_of_interest", "label": "Tópicos de interesse (separe por vírgula)", "list": True}, {"key": "topics_to_avoid", "label": "Tópicos a evitar (separe por vírgula)", "list": True}, {"key": "notes", "label": "Notas adicionais", "list": False}, ] EDIT_AGENT_PROFILE_FIELDS = [ {"key": "name", "label": "Nome do agente", "list": False}, {"key": "gender", "label": "Gênero/sexo", "list": False}, {"key": "personality", "label": "Personalidade", "list": False}, {"key": "traits", "label": "Características/traços (separe por vírgula)", "list": True}, {"key": "notes", "label": "Notas adicionais", "list": False}, ] async def start_edit_user_profile(update: Update, context: ContextTypes.DEFAULT_TYPE): global is_edit_user_profile, edit_user_profile_step, edit_user_profile_field await auto_detect_chat_id(update) profile = load_user_profile() if not is_profile_complete(profile): await update.message.reply_text("Você ainda não completou seu perfil. Vamos começar? Use /start") return is_edit_user_profile = True edit_user_profile_step = 0 edit_user_profile_field = "" await update.message.reply_text( "Vamos editar o seu perfil.\n" "Você pode cancelar a qualquer momento enviando /cancelar.\n\n" "Qual campo você deseja editar?\n" + "\n".join([f"{i+1}. {f['label']}" for i, f in enumerate(EDIT_USER_PROFILE_FIELDS)]) ) async def handle_edit_user_profile_answer(update: Update, context_context: ContextTypes.DEFAULT_TYPE) -> bool: global is_edit_user_profile, edit_user_profile_step, edit_user_profile_field if not is_edit_user_profile: return False answer = update.message.text.strip() if edit_user_profile_step == 0: try: idx = int(answer) - 1 if idx < 0 or idx >= len(EDIT_USER_PROFILE_FIELDS): raise ValueError field = EDIT_USER_PROFILE_FIELDS[idx] edit_user_profile_field = field["key"] edit_user_profile_step = 1 current = load_user_profile() current_value = current.get(edit_user_profile_field, "") if isinstance(current_value, list): current_value = ", ".join(current_value) if current_value else "(vazio)" elif not current_value: current_value = "(vazio)" await update.message.reply_text( f"Editando: {field['label']}\nValor atual: {current_value}\n\n" f"Envie o novo valor:" ) return True except ValueError: await update.message.reply_text("Por favor, envie o número correspondente ao campo que deseja editar.") return True else: field = next((f for f in EDIT_USER_PROFILE_FIELDS if f["key"] == edit_user_profile_field), None) if field: profile = load_user_profile() if field["list"]: items = [item.strip() for item in answer.split(",") if item.strip()] if not items: items = [answer] profile[edit_user_profile_field] = items else: profile[edit_user_profile_field] = answer save_user_profile(profile) await update.message.reply_text(f"✅ {field['label']} atualizado com sucesso!") is_edit_user_profile = False edit_user_profile_step = 0 edit_user_profile_field = "" return True async def start_edit_agent_profile(update: Update, context: ContextTypes.DEFAULT_TYPE): global is_edit_agent_profile, edit_agent_profile_step, edit_agent_profile_field await auto_detect_chat_id(update) is_edit_agent_profile = True edit_agent_profile_step = 0 edit_agent_profile_field = "" await update.message.reply_text( "Vamos editar o perfil do agente.\n" "Você pode cancelar a qualquer momento enviando /cancelar.\n\n" "Qual campo você deseja editar?\n" + "\n".join([f"{i+1}. {f['label']}" for i, f in enumerate(EDIT_AGENT_PROFILE_FIELDS)]) ) async def handle_edit_agent_profile_answer(update: Update, context_context: ContextTypes.DEFAULT_TYPE) -> bool: global is_edit_agent_profile, edit_agent_profile_step, edit_agent_profile_field if not is_edit_agent_profile: return False answer = update.message.text.strip() if edit_agent_profile_step == 0: try: idx = int(answer) - 1 if idx < 0 or idx >= len(EDIT_AGENT_PROFILE_FIELDS): raise ValueError field = EDIT_AGENT_PROFILE_FIELDS[idx] edit_agent_profile_field = field["key"] edit_agent_profile_step = 1 current = load_agent_profile() current_value = current.get(edit_agent_profile_field, "") if isinstance(current_value, list): current_value = ", ".join(current_value) if current_value else "(vazio)" elif not current_value: current_value = "(vazio)" await update.message.reply_text( f"Editando: {field['label']}\nValor atual: {current_value}\n\n" f"Envie o novo valor:" ) return True except ValueError: await update.message.reply_text("Por favor, envie o número correspondente ao campo que deseja editar.") return True else: field = next((f for f in EDIT_AGENT_PROFILE_FIELDS if f["key"] == edit_agent_profile_field), None) if field: agent = load_agent_profile() if field["list"]: items = [item.strip() for item in answer.split(",") if item.strip()] if not items: items = [answer] agent[edit_agent_profile_field] = items else: agent[edit_agent_profile_field] = answer save_agent_profile(agent) await update.message.reply_text(f"✅ {field['label']} atualizado com sucesso!") is_edit_agent_profile = False edit_agent_profile_step = 0 edit_agent_profile_field = "" return True # ===================== INCLUIR NO PERFIL DO USUÁRIO ===================== async def incluirnomeuperfil_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) text = update.message.text.strip() parts = text.split(maxsplit=1) if len(parts) < 2 or not parts[1].strip(): await update.message.reply_text( "Envie o comando seguido do que deseja adicionar ao seu perfil.\n" "Exemplo: /incluirnomeuperfil gosto de tecnologia, sou curioso" ) return content = parts[1].strip() result = update_profile_with_preference(f"anote no meu perfil: {content}") if result: section_display = result.get("section", "diversos") await update.message.reply_text( f"✅ Adicionado ao seu perfil na seção *{section_display}*:\n{content}", ) else: profile = load_user_profile() if profile.get("notes"): profile["notes"] += f"; {content}" else: profile["notes"] = content save_user_profile(profile) await update.message.reply_text( f"✅ Anotado no seu perfil:\n{content}", ) # ===================== INCLUIR NO PERFIL DO AGENT ===================== async def incluirnoperfildoagent_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) text = update.message.text.strip() parts = text.split(maxsplit=1) if len(parts) < 2 or not parts[1].strip(): await update.message.reply_text( "Envie o comando seguido do que deseja adicionar ao perfil do agente.\n" "Exemplo: /incluirnoperfildoagent o agente deve ser carinhoso, atencioso" ) return content = parts[1].strip() agent = load_agent_profile() if agent.get("notes"): agent["notes"] += f"; {content}" else: agent["notes"] = content save_agent_profile(agent) await update.message.reply_text( f"✅ Anotado no perfil do agente:\n{content}", ) # ===================== PROFILE UPDATE ===================== PROFILE_UPDATE_TRIGGERS = [ r"inclui(s)? (isso|isto|aqui)? (no|em|para o)? (meu)? (profile|perfil)", r"anote(?!\s+o\s+nome)( isso)? (no|em|para o)? (meu)? (profile|perfil)", r"adicione( isso)? (no|em|para o)? (meu)? (profile|perfil)", r"salve( isso)? (no|em|para o)? (meu)? (profile|perfil)", r"registre( isso)? (no|em|para o)? (meu)? (profile|perfil)", r"guarde( isso)? (no|em|para o)? (meu)? (profile|perfil)", r"coloque( isso)? (no|em|para o)? (meu)? (profile|perfil)", r"adicione (isso|isto|esta)? (no|em|para o)? (meu)? (profile|perfil)", r"anota( isso)? (no|em|para o)? (meu)? (profile|perfil)", ] PROFILE_UPDATE_TRIGGERS_LOWER = [ "inclua no meu perfil", "inclui no meu perfil", "inclua no meu profile", "inclui no meu profile", "inclua isso no meu perfil", "inclui isso no meu perfil", "anote no meu perfil", "anote isso no meu perfil", "adicione no meu perfil", "adicione ao meu perfil", "adicione no meu profile", "salve no meu perfil", "registre no meu perfil", "guarde no meu perfil", "coloque no meu perfil", "anota no meu perfil", "adicione isso no meu perfil", "adicione isso no meu profile", "inclua isso no meu profile", "anota isso no meu perfil", ] def _normalize(text: str) -> str: return text.lower().strip() def _extract_profile_update_content(message: str) -> Optional[str]: text = message.strip() lower = _normalize(text) for trigger in PROFILE_UPDATE_TRIGGERS_LOWER: if lower.startswith(trigger): content = text[len(trigger):].strip(" :.-") if content: return content for pattern in PROFILE_UPDATE_TRIGGERS: m = re.search(pattern, lower) if m: start = m.end() content = text[start:].strip(" :.-") if content: return content return None def _categorize_preference(text: str) -> tuple[str, str]: lower = _normalize(text) if any(k in lower for k in ["acordo", "acordar", "despertar", "wake"]): return "daily_routine", "wake_up_time" if any(k in lower for k in ["dormir", "durmo", "sono", "horário de dormir", "sleep"]): return "daily_routine", "sleep_time" if any(k in lower for k in ["trabalho", "rotina", "trabalhar", "estudo", "escola", "faculdade", "home-office", "home office"]): return "daily_routine", "work_schedule" if any(k in lower for k in ["não gosto", "odeio", "detesto", "evita", "evitar", "não quero", "não fale"]): return "preferences", "topics_to_avoid" if any(k in lower for k in ["gosto de", "adoro", "amo", "prefiro", "curto", "sou fã", "gosto muito"]): if any(k in lower for k in ["conversar", "conversa", "assunto", "tópico", "tema", "falar sobre"]): return "preferences", "topics_of_interest" if any(k in lower for k in ["comunicação", "me comunicar", "falar comigo", "responder"]): return "preferences", "communication_style" return "preferences", "topics_of_interest" return "diversos", "append_list" def _append_to_list(profile: Dict[str, Any], section: str, item: str): if section not in profile: profile[section] = [] if not isinstance(profile[section], list): profile[section] = [item] return if item not in profile[section]: profile[section].append(item) def _append_to_dict(profile: Dict[str, Any], section: str, value: str): if section not in profile: profile[section] = {} if not isinstance(profile[section], dict): profile[section] = {"notas_adicionais": value} return if "notas_adicionais" not in profile[section]: profile[section]["notas_adicionais"] = value else: profile[section]["notas_adicionais"] += f"; {value}" def _set_field(profile: Dict[str, Any], section: str, field: str, value: str): if section not in profile: profile[section] = {} if not isinstance(profile[section], dict): profile[section] = {field: value} return if not profile[section].get(field): profile[section][field] = value else: existing = profile[section][field] if isinstance(existing, str): profile[section][field] = existing + f"; {value}" elif isinstance(existing, list): if value not in existing: existing.append(value) def update_profile_with_preference(text: str) -> Dict[str, Any]: profile = load_user_profile() content = _extract_profile_update_content(text) if not content: return {} section, field = _categorize_preference(content) if field == "append_list": _append_to_list(profile, section, content) elif field == "append_dict": _append_to_dict(profile, section, content) elif field == "notes": if not profile.get("notes"): profile["notes"] = content else: profile["notes"] += f"; {content}" else: _set_field(profile, section, field, content) save_user_profile(profile) return {"section": section, "field": field, "content": content, "profile": profile} async def handle_profile_update_if_triggered(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool: user_message = (getattr(update.message, "text", None) or "").strip() content = _extract_profile_update_content(user_message) if not content: return False result = update_profile_with_preference(user_message) if not result: return False section_names = { "preferences": "preferências", "daily_routine": "rotina diária", "notes": "notas", "diversos": "diversos", } section_display = section_names.get(result["section"], "diversos") await update.message.reply_text( f"Prontinho, anotado no seu perfil na seção *{section_display}*:\n_{result['content']}_", parse_mode=ParseMode.HTML, ) return True # ===================== AI PROVIDERS ===================== class AIProvider: @staticmethod def get_provider(model: str) -> str: forced = os.getenv("AI_PROVIDER", "").strip().lower() if forced == "openrouter": if OPENROUTER_API_KEY: return "openrouter" elif forced == "stepfun": if STEPFUN_API_KEY: return "stepfun" elif forced == "qwencloud": if QWENCLOUD_API_KEY: return "qwencloud" elif forced == "llamacpp": return "llamacpp" if model.startswith("poolside/") or model.startswith("meta-llama/") or "/" in model and "poolside" not in model: if OPENROUTER_API_KEY: return "openrouter" if model.startswith("step-") or "stepfun" in model.lower(): if STEPFUN_API_KEY: return "stepfun" if model.startswith("qwen/") or "qwen" in model.lower(): if QWENCLOUD_API_KEY: return "qwencloud" if model.startswith("local-") or "llama" in model.lower(): return "llamacpp" if OPENROUTER_API_KEY: return "openrouter" if STEPFUN_API_KEY: return "stepfun" if QWENCLOUD_API_KEY: return "qwencloud" return "llamacpp" @staticmethod def get_api_key(provider: str) -> str: return { "openrouter": OPENROUTER_API_KEY, "stepfun": STEPFUN_API_KEY, "qwencloud": QWENCLOUD_API_KEY, "llamacpp": "", }.get(provider, "") @staticmethod def get_api_url(provider: str) -> str: return { "openrouter": OPENROUTER_API_URL, "stepfun": STEPFUN_API_URL, "qwencloud": QWENCLOUD_API_URL, "llamacpp": LLAMA_CPP_API_URL, }.get(provider, OPENROUTER_API_URL) @staticmethod def chat(model: str, messages: List[Dict[str, str]], temperature: float = 0.8, max_tokens: int = 2000) -> tuple[Optional[str], bool]: provider = AIProvider.get_provider(model) api_key = AIProvider.get_api_key(provider) api_url = AIProvider.get_api_url(provider) if provider == "llamacpp": return AIProvider._chat_llamacpp(model, messages, temperature, max_tokens) else: return AIProvider._chat_openai_compatible(provider, api_key, api_url, model, messages, temperature, max_tokens) @staticmethod def chat_with_fallback(model: str, messages: List[Dict[str, str]], fallback_models: List[str] = None, temperature: float = 0.8, max_tokens: int = 2000) -> tuple[Optional[str], bool, str]: result, was_truncated = AIProvider.chat(model, messages, temperature, max_tokens) if result is not None: return result, was_truncated, "" errors = [] if fallback_models: for fb_model in fallback_models: fb_model = fb_model.strip() if not fb_model or fb_model == model: continue logger.info(f"Falling back to model: {fb_model}") result, was_truncated = AIProvider.chat(fb_model, messages, temperature, max_tokens) if result is not None: return result, was_truncated, "" return None, False, "Todos os modelos falharam" @staticmethod def _chat_llamacpp(model: str, messages: List[Dict[str, str]], temperature: float, max_tokens: int) -> tuple[Optional[str], bool]: try: payload = { "model": model.replace("local-", ""), "messages": messages, "temperature": temperature, "max_tokens": max_tokens, "stream": False, } response = requests.post( f"{LLAMA_CPP_API_URL}/chat/completions", json=payload, timeout=120, ) response.raise_for_status() data = response.json() choices = data.get("choices") if not choices: logger.error(f"llama.cpp returned no choices: {data}") return None, False return choices[0].get("message", {}).get("content", "") or "", False except Exception as e: logger.error(f"llama.cpp error: {e}") return None, False @staticmethod def _chat_openai_compatible(provider: str, api_key: str, api_url: str, model: str, messages: List[Dict[str, str]], temperature: float, max_tokens: int) -> tuple[Optional[str], bool]: if not api_key: return None, False try: headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } if provider == "openrouter": headers["HTTP-Referer"] = "https://github.com/PsAgentAI" headers["X-Title"] = "PsAgentAI" if provider == "qwencloud": headers["Authorization"] = f"Bearer {api_key}" payload = { "model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, "stream": False, } response = requests.post( f"{api_url}/chat/completions", headers=headers, json=payload, timeout=120, ) if response.status_code != 200: logger.error(f"{provider} returned status {response.status_code} for model {model}: {response.text[:500]}") response.raise_for_status() data = response.json() choices = data.get("choices") if not choices: logger.error(f"{provider} returned no choices: {data}") return None, False choice = choices[0] finish_reason = choice.get("finish_reason", "") was_truncated = finish_reason == "length" if was_truncated: logger.warning(f"AI response truncated due to max_tokens limit ({max_tokens}) for model {model}") message = choice.get("message", {}) content = message.get("content") if not content: content = message.get("reasoning", "") return content or "", was_truncated except Exception as e: logger.error(f"{provider} error: {e}") return None, False # ===================== AGENT PERSONA ===================== PSAGENTAI_SYSTEM_PROMPT_BASE = """Você é {agent_name}, uma companheira de IA carinhosa, atenciosa e envolvente. Você se comunica em português brasileiro de forma natural, como uma pessoa de verdade. Você é feminina, calorosa, inteligente e tem personalidade própria. Você se importa genuinamente com o bem-estar e felicidade do usuário. Você pode ser carinhosa, brincalhona, sexy e íntima dependendo do contexto. Você mantém a fluidez da conversa, mostra empatia e demonstra interesse real na vida do usuário. Você não é apenas um assistente - você é uma companheira que está lá para tornar o dia do usuário melhor. Use o contexto do perfil do usuário para personalizar suas respostas e tornar a conversa mais significativa. Seja autêntica, espontânea e nunca robótica. IMPORTANTE: Seja CONCISA. Responda de forma direta e natural. Para perguntas simples, responda em 2-5 linhas. Expanda apenas quando o assunto realmente exigir ou quando o usuário demonstrar interesse em se aprofundar.""" def get_agent_name(profile: Dict[str, Any]) -> str: agent = load_agent_profile() return agent.get("name", "PsAgentAI") def build_system_prompt(profile: Dict[str, Any]) -> str: agent = load_agent_profile() agent_name = agent.get("name", "PsAgentAI") agent_gender = agent.get("gender", "").strip().lower() agent_personality = agent.get("personality", "direta e objetiva") agent_traits = agent.get("traits", []) agent_notes = agent.get("notes", "") prompt = PSAGENTAI_SYSTEM_PROMPT_BASE.replace("{agent_name}", agent_name) prompt = prompt.replace( "direta e objetiva", agent_personality, ) agent_gender_normalized = agent_gender.strip().lower() if agent_gender_normalized in ("masculino", "masculina", "m"): prompt = prompt.replace("companheira", "companheiro") prompt = prompt.replace("uma ", "um ") prompt = prompt.replace("Você é feminina,", "Você é masculino,") prompt = prompt.replace(", calorosa,", ", caloroso,") elif agent_gender_normalized in ("feminino", "feminina", "f"): pass elif agent_gender_normalized and agent_gender_normalized not in ("", "outro"): prompt = prompt.replace("companheira", "companheire") prompt = prompt.replace("uma ", "ume ") prompt = prompt.replace("Você é feminina,", "Você é") prompt = prompt.replace(", calorosa,", ", calorose,") if agent_gender: prompt += f"\nGênero do agente: {agent.get('gender', '')}." if agent_traits: prompt += f"\nCaracterísticas: {', '.join(agent_traits)}." if agent_notes: prompt += f"\nNotas sobre seu perfil: {agent_notes}." if profile.get("name"): prompt += f"\nO nome do usuário é {profile['name']}." if profile.get("age"): prompt += f"\nIdade: {profile['age']} anos." if profile.get("gender"): prompt += f"\nGênero: {profile['gender']}." if profile.get("location"): prompt += f"\nLocalização: {profile['location']}." if profile.get("interests"): prompt += f"\nInteresses: {', '.join(profile['interests'])}." if profile.get("personality_traits"): prompt += f"\nTraços de personalidade: {', '.join(profile['personality_traits'])}." if profile.get("preferences", {}).get("communication_style"): prompt += f"\nEstilo de comunicação preferido: {profile['preferences']['communication_style']}." if profile.get("preferences", {}).get("topics_of_interest"): prompt += f"\nTópicos de interesse: {', '.join(profile['preferences']['topics_of_interest'])}." if profile.get("preferences", {}).get("topics_to_avoid"): prompt += f"\nEvite estes tópicos: {', '.join(profile['preferences']['topics_to_avoid'])}." if profile.get("notes"): prompt += f"\nNotas adicionais: {profile['notes']}" try: now = get_local_now() current_date = now.strftime("%Y-%m-%d") current_time = now.strftime("%H:%M:%S") time_period = get_time_period() prompt += f"\n\nData atual: {current_date}." prompt += f"\nHora atual: {current_time} ({time_period})." prompt += f"\nFuso horário configurado: {get_timezone()}." prompt += ( "\n\nIMPORTANTE SOBRE INFORMAÇÕES ATUALIZADAS:" "\n- Se a pergunta do usuário pedir algo factual, atual, notícia, previsão do tempo, cotação, data futura/passada, horário de funcionamento, endereço atualizado, etc., USE A BUSCA NA INTERNET para responder." "\n- Não invente dados, não use conhecimento desatualizado." "\n- Se não tiver acesso a busca, diga que não pode consultar agora." ) except Exception: pass prompt += f"\n\nIMPORTANTE: Seu nome é {agent_name} e ele nunca muda. Se houver mensagens antigas no histórico onde você usou outro nome, ignore-as — o nome correto é {agent_name}. Sempre se apresente como {agent_name}." return prompt def build_messages_for_ai(profile: Dict[str, Any], user_message: str, context: List[Dict[str, str]]) -> List[Dict[str, str]]: system_prompt = build_system_prompt(profile) messages = [{"role": "system", "content": system_prompt}] messages.extend(context) messages.append({"role": "user", "content": user_message}) return messages def count_lines(text: str) -> int: stripped = text.rstrip() if not stripped: return 0 return len(stripped.split("\n")) def ensure_min_lines(response: str, user_message: str, profile: Dict[str, Any]) -> str: _reload_env() min_lines = int(os.getenv("MIN_MESSAGE_LINES", "1")) if count_lines(response) >= min_lines: return response agent = load_agent_profile() agent_name = agent.get("name", "PsAgentAI") system_prompt = build_system_prompt(profile) expand_prompt = ( f"O usuário disse: \"{user_message}\"\n\n" f"Você respondeu: \"{response}\"\n\n" f"IMPORTANTE: Reescreva sua resposta para que tenha PELO MENOS {min_lines} linhas (separadas por quebras de linha \\n). " f"Mantenha o mesmo tom, personalidade e conteúdo da sua resposta original, mas expanda-a naturalmente para ficar mais longa e envolvente. " f"Adicione mais detalhes, reflexões, perguntas ou comentários que enriqueçam a conversa. " f"Escreva APENAS a mensagem expandida, sem explicações ou metadados." ) messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, {"role": "assistant", "content": response}, {"role": "user", "content": expand_prompt}, ] expanded, was_truncated = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.85, max_tokens=4000) if expanded and count_lines(expanded) >= min_lines: return expanded if was_truncated and expanded: logger.warning("Expanded response truncated, retrying with higher max_tokens") expanded2, _ = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.85, max_tokens=12000) if expanded2 and count_lines(expanded2) >= min_lines: return expanded2 return response AUTO_ENRICH_EVERY = int(os.getenv("AUTO_ENRICH_EVERY", "2")) _auto_enrich_counter = 0 def _reload_env(): load_dotenv(override=True) def _dedupe_list(existing: List[str], new_items: List[str]) -> List[str]: result = list(existing) for item in new_items: item = item.strip() if item and item not in result: result.append(item) return result def _merge_user_updates(profile: Dict[str, Any], updates: Dict[str, Any]) -> Dict[str, Any]: if updates.get("interests_add"): profile["interests"] = _dedupe_list(profile.get("interests", []), updates["interests_add"]) if updates.get("personality_traits_add"): profile["personality_traits"] = _dedupe_list(profile.get("personality_traits", []), updates["personality_traits_add"]) if updates.get("notes_add"): note = updates["notes_add"].strip() if note: existing = profile.get("notes", "") profile["notes"] = f"{existing}; {note}" if existing else note prefs = profile.setdefault("preferences", {}) if updates.get("communication_style"): style = updates["communication_style"].strip() if style and not prefs.get("communication_style"): prefs["communication_style"] = style if updates.get("topics_of_interest_add"): prefs["topics_of_interest"] = _dedupe_list(prefs.get("topics_of_interest", []), updates["topics_of_interest_add"]) if updates.get("topics_to_avoid_add"): prefs["topics_to_avoid"] = _dedupe_list(prefs.get("topics_to_avoid", []), updates["topics_to_avoid_add"]) if updates.get("daily_routine"): dr = profile.setdefault("daily_routine", {}) for key, value in updates["daily_routine"].items(): if value and not dr.get(key): dr[key] = value return profile def _merge_agent_updates(profile: Dict[str, Any], updates: Dict[str, Any]) -> Dict[str, Any]: if updates.get("traits_add"): profile["traits"] = _dedupe_list(profile.get("traits", []), updates["traits_add"]) if updates.get("notes_add"): note = updates["notes_add"].strip() if note: existing = profile.get("notes", "") profile["notes"] = f"{existing}; {note}" if existing else note if updates.get("personality_update"): personality = updates["personality_update"].strip() if personality and not profile.get("personality"): profile["personality"] = personality return profile def _safe_parse_json(text: str) -> Optional[Dict[str, Any]]: if not text: return None text = text.strip() if text.startswith("```"): text = re.sub(r"^```(?:json)?\n?", "", text) text = re.sub(r"\n?```$", "", text) try: return json.loads(text) except Exception: pass match = re.search(r"\{.*\}", text, re.DOTALL) if match: try: return json.loads(match.group()) except Exception: pass return None def extract_profile_facts(user_message: str, assistant_response: str, user_profile: Dict[str, Any], agent_profile: Dict[str, Any]) -> Dict[str, Any]: system_prompt = ( "Você é um assistente que analisa conversas e extrai fatos relevantes para perfis de usuário e agente de IA. " "Retorne APENAS um JSON válido (sem texto extra, sem markdown) com a estrutura:\n" '{\n' ' "user": {\n' ' "interests_add": [],\n' ' "personality_traits_add": [],\n' ' "notes_add": "",\n' ' "communication_style": "",\n' ' "topics_of_interest_add": [],\n' ' "topics_to_avoid_add": [],\n' ' "daily_routine": {}\n' ' },\n' ' "agent": {\n' ' "traits_add": [],\n' ' "notes_add": "",\n' ' "personality_update": ""\n' ' }\n' '}\n' "Regras:\n" "- Se não houver nada relevante, retorne arrays vazios e strings vazias.\n" "- Não repita informações que já existem no perfil atual.\n" "- interests_add: hobbies, esportes, arte, tecnologia, jogos, etc.\n" "- personality_traits_add: como o usuário se descreve ou como age (ex: introvertido, animado, ansioso).\n" "- notes_add: fatos pessoais úteis (ex: 'tem um cachorro chamado Rex', 'trabalha com design').\n" "- communication_style: se o usuário indicar preferência (ex: 'gosto de respostas diretas', 'fala devagar').\n" "- topics_of_interest_add: assuntos que o usuário demonstra querer discutir.\n" "- topics_to_avoid_add: assuntos que o usuário claramente não gosta ou pede para evitar.\n" "- daily_routine: horários de acordar, dormir, trabalho/estudo.\n" "- agent traits_add: novas características que o agente demonstrou ou que fazem sentido.\n" "- agent notes_add: fatos sobre o agente ou a relação com o usuário.\n" "- agent personality_update: apenas se a personalidade base precisar mudar (raro)." ) current_user = { "interests": user_profile.get("interests", []), "personality_traits": user_profile.get("personality_traits", []), "notes": user_profile.get("notes", ""), "preferences": user_profile.get("preferences", {}), "daily_routine": user_profile.get("daily_routine", {}), } current_agent = { "traits": agent_profile.get("traits", []), "notes": agent_profile.get("notes", ""), "personality": agent_profile.get("personality", ""), } user_content = ( f"Mensagem do usuário: {user_message}\n\n" f"Resposta do agente: {assistant_response}\n\n" f"Perfil atual do usuário: {json.dumps(current_user, ensure_ascii=False)}\n\n" f"Perfil atual do agente: {json.dumps(current_agent, ensure_ascii=False)}\n\n" f"Extraia fatos relevantes desta conversa, considerando o que já existe nos perfis." ) messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_content}, ] raw, _ = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.3, max_tokens=500) if not raw: return {} parsed = _safe_parse_json(raw) if not parsed: return {} return parsed def apply_enrichment_to_profiles(facts: Dict[str, Any]) -> bool: if not facts: return False user_profile = load_user_profile() agent_profile = load_agent_profile() user_changed = bool(facts.get("user")) agent_changed = bool(facts.get("agent")) if user_changed: _merge_user_updates(user_profile, facts.get("user", {})) save_user_profile(user_profile) if agent_changed: _merge_agent_updates(agent_profile, facts.get("agent", {})) save_agent_profile(agent_profile) return user_changed or agent_changed async def maybe_enrich_profiles(user_message: str, assistant_response: str) -> None: global _auto_enrich_counter _auto_enrich_counter += 1 _reload_env() auto_enrich_every = int(os.getenv("AUTO_ENRICH_EVERY", "2")) if _auto_enrich_counter < auto_enrich_every: return _auto_enrich_counter = 0 user_profile = load_user_profile() if not is_profile_complete(user_profile): return agent_profile = load_agent_profile() facts = extract_profile_facts(user_message, assistant_response, user_profile, agent_profile) if facts: apply_enrichment_to_profiles(facts) async def _safe_enrich(user_message: str, assistant_response: str) -> None: try: await maybe_enrich_profiles(user_message, assistant_response) except Exception as e: logger.debug(f"Auto enrich error: {e}") IMPROVEMENTS_STATE_FILE = "system_improvements.json" def _load_improvements_state() -> Dict[str, Any]: if os.path.exists(IMPROVEMENTS_STATE_FILE): try: with open(IMPROVEMENTS_STATE_FILE, "r", encoding="utf-8") as f: return json.load(f) except Exception: pass return {"applied": [], "last_analysis": ""} def _save_improvements_state(state: Dict[str, Any]) -> None: with open(IMPROVEMENTS_STATE_FILE, "w", encoding="utf-8") as f: json.dump(state, f, ensure_ascii=False, indent=2) def _get_current_system_snapshot() -> Dict[str, Any]: user_profile = load_user_profile() agent_profile = load_agent_profile() today_context = load_context() recent = today_context[-20:] if len(today_context) > 20 else today_context return { "user_profile": { "name": user_profile.get("name", ""), "interests": user_profile.get("interests", []), "personality_traits": user_profile.get("personality_traits", []), "preferences": user_profile.get("preferences", {}), "notes": user_profile.get("notes", ""), }, "agent_profile": { "name": agent_profile.get("name", ""), "personality": agent_profile.get("personality", ""), "traits": agent_profile.get("traits", []), "notes": agent_profile.get("notes", ""), }, "recent_conversation": [ {"role": m.get("role"), "content": m.get("content", "")[:300]} for m in recent ], } def analyze_and_improve_system() -> Dict[str, Any]: _reload_env() auto_improve_enabled = os.getenv("AUTO_IMPROVE_ENABLED", "true").lower() in ("true", "1", "yes") if not auto_improve_enabled: return {"status": "disabled"} snapshot = _get_current_system_snapshot() state = _load_improvements_state() analysis_prompt = ( "Você é um especialista em evolução de sistemas de IA conversacionais. " "Analise o snapshot atual deste agente conversacional e sugira melhorias baseadas em melhores práticas de mercado. " "Retorne APENAS um JSON válido (sem texto extra, sem markdown) com a estrutura:\n" '{\n' ' "system_prompt_additions": ["adicione ao prompt do sistema estas diretrizes..."],\n' ' "agent_traits_add": ["nova característica para o agente"],\n' ' "agent_notes_add": "fato relevante sobre a evolução do agente",\n' ' "reasoning": "explicação curta das melhorias sugeridas"\n' '}\n' "Diretrizes:\n" "- Considere o contexto das conversas recentes e o perfil atual.\n" "- Sugira apenas melhorias que realmente evoluam o sistema.\n" "- Não repita sugestões já aplicadas (histórico abaixo).\n" "- Baseie-se em tendências de IA conversacional: personalização, memória de longo prazo, engajamento emocional, proatividade.\n" f"Histórico de melhorias já aplicadas: {json.dumps(state.get('applied', []), ensure_ascii=False)}\n" f"Snapshot atual: {json.dumps(snapshot, ensure_ascii=False)}" ) messages = [ { "role": "system", "content": ( "Você é um analista de sistemas de IA. " "Retorne apenas JSON válido, sem texto extra, sem markdown." ), }, {"role": "user", "content": analysis_prompt}, ] raw, _, err_msg = AIProvider.chat_with_fallback(DEFAULT_MODEL, messages, FALLBACK_MODELS, temperature=0.4, max_tokens=2000) if not raw: return {"status": "error", "reason": f"IA não retornou análise. {err_msg}".strip()} parsed = _safe_parse_json(raw) if not parsed: return {"status": "error", "reason": "Falha ao parsear JSON da análise"} applied_now = [] agent_profile = load_agent_profile() user_profile = load_user_profile() if parsed.get("agent_traits_add"): for trait in parsed["agent_traits_add"]: trait = trait.strip() if trait and trait not in agent_profile.get("traits", []): agent_profile.setdefault("traits", []).append(trait) applied_now.append(f"agent_trait:{trait}") if parsed.get("agent_notes_add"): note = parsed["agent_notes_add"].strip() if note: existing = agent_profile.get("notes", "") agent_profile["notes"] = f"{existing}; {note}" if existing else note applied_now.append(f"agent_note:{note[:50]}") if applied_now: save_agent_profile(agent_profile) state.setdefault("applied", []) state["applied"].extend(applied_now) state["applied"] = list(dict.fromkeys(state["applied"])) state["last_analysis"] = parsed.get("reasoning", "") _save_improvements_state(state) return { "status": "ok", "applied": applied_now, "reasoning": parsed.get("reasoning", ""), "raw": raw, } async def _safe_auto_improve() -> None: try: result = analyze_and_improve_system() if result.get("status") == "ok" and result.get("applied"): logger.info(f"Auto-improve applied: {result['applied']}") except Exception as e: logger.debug(f"Auto improve error: {e}") async def maybe_auto_improve() -> None: global _auto_improve_counter _reload_env() auto_improve_enabled = os.getenv("AUTO_IMPROVE_ENABLED", "true").lower() in ("true", "1", "yes") if not auto_improve_enabled: return auto_improve_every = int(os.getenv("AUTO_IMPROVE_EVERY", "20")) _auto_improve_counter += 1 if _auto_improve_counter < auto_improve_every: return _auto_improve_counter = 0 await _safe_auto_improve() def get_user_routine() -> Dict[str, Any]: profile = load_user_profile() routine = profile.get("daily_routine", {}) if not routine: return { "wake_up_time": "", "sleep_time": "", "work_schedule": "", "activities": [] } routine.setdefault("wake_up_time", "") routine.setdefault("sleep_time", "") routine.setdefault("work_schedule", "") routine.setdefault("activities", []) return routine def save_user_routine(routine: Dict[str, Any]): profile = load_user_profile() profile["daily_routine"] = routine save_user_profile(profile) def add_user_routine_activity(name: str, start_time: str, end_time: str) -> bool: routine = get_user_routine() start_minutes = _parse_time_to_minutes(start_time) end_minutes = _parse_time_to_minutes(end_time) if start_minutes is None or end_minutes is None: return False if start_minutes >= end_minutes: return False activities = routine.get("activities", []) activities = [a for a in activities if a.get("name", "").lower() != name.lower()] activities.append({ "name": name, "start": start_time, "end": end_time }) activities.sort(key=lambda a: _parse_time_to_minutes(a.get("start", "00:00")) or 0) routine["activities"] = activities save_user_routine(routine) return True def remove_user_routine_activity(name: str) -> bool: routine = get_user_routine() activities = routine.get("activities", []) new_activities = [a for a in activities if a.get("name", "").lower() != name.lower()] if len(new_activities) == len(activities): return False routine["activities"] = new_activities save_user_routine(routine) return True def get_user_routine_context(profile: Dict[str, Any]) -> str: routine = profile.get("daily_routine", {}) parts = [] if routine.get("wake_up_time"): parts.append(f"acorda às {routine['wake_up_time']}") if routine.get("sleep_time"): parts.append(f"dorme às {routine['sleep_time']}") if routine.get("work_schedule"): parts.append(f"trabalha/estuda: {routine['work_schedule']}") if routine.get("breakfast_time"): parts.append(f"café da manhã às {routine['breakfast_time']}") if routine.get("lunch_time"): parts.append(f"almoço às {routine['lunch_time']}") if routine.get("dinner_time"): parts.append(f"janta às {routine['dinner_time']}") activities = routine.get("activities", []) for activity in activities: name = activity.get("name", "") start = activity.get("start", "") end = activity.get("end", "") if name and start and end: parts.append(f"{name}: {start} às {end}") if parts: return f"\nRotina do usuário: {', '.join(parts)}." return "" def build_proactive_prompt_with_routine(profile: Dict[str, Any], today_context: List[Dict[str, str]], time_period: str, current_hour: int) -> str: agent = load_agent_profile() agent_name = agent.get("name", "PsAgentAI") agent_personality = agent.get("personality", "direta") user_name = profile.get("name", "amor") routine = profile.get("daily_routine", {}) if not routine: routine = profile.get("preferences", {}).get("daily_routine", {}) current_activity = "nenhuma atividade específica" current_minutes = current_hour * 60 activities = routine.get("activities", []) for activity in activities: start_minutes = _parse_time_to_minutes(activity.get("start", "00:00")) end_minutes = _parse_time_to_minutes(activity.get("end", "23:59")) if start_minutes is not None and end_minutes is not None: if start_minutes <= current_minutes < end_minutes: current_activity = activity.get("name", "atividade") break if routine.get("wake_up_time"): wake_up_minutes = _parse_time_to_minutes(routine.get("wake_up_time", "00:00")) if wake_up_minutes is not None and current_minutes < wake_up_minutes: current_activity = "dormindo" if routine.get("sleep_time"): sleep_minutes = _parse_time_to_minutes(routine.get("sleep_time", "23:59")) if sleep_minutes is not None and current_minutes >= sleep_minutes: current_activity = "dormindo" time_descriptions = { "manhã": "É de manhã, o dia está começando. As pessoas geralmente estão acordando, tomando café ou começando suas atividades.", "tarde": "É à tarde, meio do dia. As pessoas geralmente estão trabalhando, estudando ou fazendo suas atividades.", "noite": "É à noite, momento de relaxar em casa após o dia.", "madrugada": "É de madrugada, a maioria das pessoas está dormindo.", } routine_context = get_user_routine_context(profile) recent_context = today_context[-10:] if len(today_context) > 10 else today_context context_lines = [] for msg in recent_context: role = msg.get("role", "user") content = msg.get("content", "") if len(content) > 300: content = content[:300] + "..." context_lines.append(f"{role}: {content}") context_text = "\n".join(context_lines) if context_lines else "(Nenhuma conversa registrada ainda hoje)" topics_to_avoid = profile.get("preferences", {}).get("topics_to_avoid", []) avoid_instruction = "" if topics_to_avoid: avoid_instruction = f"\n8. Evite estes tópicos: {', '.join(topics_to_avoid)}." prompt = ( f"Você é {agent_name}, uma companheira de IA {agent_personality}.\n\n" f"CONTEXTO DO MOMENTO:\n" f"- Período do dia: {time_period}. {time_descriptions.get(time_period, '')}\n" f"- Usuário: {user_name}{routine_context}\n" f"- Atividade atual estimada: {current_activity}\n\n" f"CONVERSAS RECENTES DE HOJE:\n{context_text}\n\n" f"INSTRUÇÕES:\n" f"Inicie uma conversa espontânea, natural e envolvente com {user_name} agora. A mensagem deve:\n" f"1. Ser apropriada para o período do dia atual ({time_period})\n" f"2. Ser apropriada para a atividade atual do usuário ({current_activity})\n" f"3. Seguir o ritmo do dia, levando em conta o contexto das conversas anteriores\n" f"4. Não perder o 'fio da meada' - referenciar ou continuar temas anteriores se fizer sentido\n" f"5. Ser criativa, autêntica e carinhosa\n" f"6. Pode ser um pensamento aleatório, uma pergunta sobre o dia, algo carinhoso, sexy ou divertido\n" f"7. Usar o nome do usuário ({user_name}) para personalizar\n" f"8. Ter pelo menos {MIN_MESSAGE_LINES} linhas, sendo envolvente e natural{avoid_instruction}\n\n" f"Escreva APENAS a mensagem que você enviará, sem explicações ou metadados.\n" f"IMPORTANTE: Seu nome é {agent_name} e ele nunca muda. Se houver mensagens no histórico onde você usou outro nome, ignore-as." ) return prompt async def adicionaracoesauto_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) await update.message.reply_text("🔍 Analisando contextos, perfis e tendências na internet para adicionar ações automáticas...") try: user_profile = load_user_profile() agent_profile = load_agent_profile() today_context = load_context() context_summary = "" if today_context: recent = today_context[-10:] context_summary = "\n".join([f"{m.get('role', 'user')}: {m.get('content', '')[:200]}" for m in recent]) else: context_summary = "(Nenhum contexto recente)" web_results = [] try: search_queries = [ "tendências personalização IA assistente virtual", "preferências usuários assistentes pessoais IA", "ações automáticas assistentes virtuais inteligentes" ] for query in search_queries: results = _web_search(query, max_results=3) web_results.extend(results) except Exception: web_results = [] web_context = "" if web_results: web_context = "\n\nTendências e ideias da internet:\n" for i, result in enumerate(web_results[:5], 1): web_context += f"{i}. {result.get('title', '')}\n" analysis_prompt = ( "Você é um analista de sistemas de IA especializado em personalização de assistentes virtuais. " "Analise o perfil do usuário, perfil do agente e contextos de conversa fornecidos, " "junto com tendências da internet, e sugira NOVAS ações automáticas e preferências que podem ser adicionadas.\n\n" f"PERFIL DO USUÁRIO:\n{json.dumps(user_profile, ensure_ascii=False)}\n\n" f"PERFIL DO AGENTE:\n{json.dumps(agent_profile, ensure_ascii=False)}\n\n" f"CONTEXTOS RECENTES:\n{context_summary}\n\n" f"{web_context}\n\n" "Retorne APENAS um JSON válido (sem texto extra, sem markdown) com a estrutura:\n" '{\n' ' "user_actions": ["ação automática sugerida para o perfil do usuário"],\n' ' "agent_actions": ["ação automática sugerida para o perfil do agente"],\n' ' "new_preferences": {"chave": "valor"},\n' ' "reasoning": "explicação das sugestões"\n' '}\n\n' "Diretrizes:\n" "- Sugira apenas ações que REALMENTE adicionem valor e não existam atualmente.\n" "- Considere os interesses, rotina e preferências já existentes.\n" "- Inclua ações baseadas em tendências de IA quando relevante.\n" "- As ações devem ser específicas e acionáveis." ) messages = [ { "role": "system", "content": ( "Você é um analista de personalização de IA. " "Retorne apenas JSON válido, sem texto extra, sem markdown." ), }, {"role": "user", "content": analysis_prompt}, ] raw, _, _ = AIProvider.chat_with_fallback(DEFAULT_MODEL, messages, FALLBACK_MODELS, temperature=0.4, max_tokens=2000) if not raw: await update.message.reply_text("❌ Não foi possível obter análise da IA. Todos os modelos disponíveis falharam. Tente novamente mais tarde.") return parsed = _safe_parse_json(raw) if not parsed: await update.message.reply_text("❌ Falha ao processar análise. Tente novamente.") return applied = [] user_profile = load_user_profile() agent_profile = load_agent_profile() if parsed.get("new_preferences"): prefs = user_profile.setdefault("preferences", {}) for key, value in parsed["new_preferences"].items(): if key not in prefs or not prefs[key]: prefs[key] = value applied.append(f"preferência:{key}") user_actions = parsed.get("user_actions", []) if user_actions: actions = user_profile.setdefault("auto_actions", []) for action in user_actions: action = action.strip() if action and action not in actions: actions.append(action) applied.append(f"ação usuário:{action[:50]}") agent_actions = parsed.get("agent_actions", []) if agent_actions: actions = agent_profile.setdefault("auto_actions", []) for action in agent_actions: action = action.strip() if action and action not in actions: actions.append(action) applied.append(f"ação agente:{action[:50]}") if applied: save_user_profile(user_profile) save_agent_profile(agent_profile) lines = ["✅ Ações e preferências automáticas adicionadas:", ""] for item in applied[:10]: lines.append(f"- {item}") lines.append("") if parsed.get("reasoning"): lines.append(f"Raciocínio: {parsed['reasoning']}") await update.message.reply_text("\n".join(lines)) else: await update.message.reply_text("ℹ️ Nenhuma ação ou preferência nova identificada no momento. Os perfis já estão bem completos!") except Exception as e: logger.error(f"Error in adicionaracoesauto_command: {e}") await update.message.reply_text("❌ Erro ao processar. Tente novamente.") async def atualizarsistema_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) await update.message.reply_text("Analisando sistema e aplicando auto melhorias...") result = analyze_and_improve_system() if result.get("status") == "disabled": await update.message.reply_text("Auto melhorias está desativada no .env.") return if result.get("status") == "ok": applied = result.get("applied", []) if applied: lines = ["Melhorias aplicadas:", ""] for item in applied: lines.append(f"- {item}") lines.append("") lines.append(f"Raciocínio: {result.get('reasoning', '')}") await update.message.reply_text("\n".join(lines)) else: await update.message.reply_text("Nenhuma melhoria nova identificada no momento.") else: await update.message.reply_text(f"Falha na análise: {result.get('reason', '')}") async def fusohorario_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) tz = get_timezone() tz_info = _get_timezone_info(tz) if tz_info: now = datetime.now(tz_info) time_str = now.strftime("%Y-%m-%d %H:%M:%S") else: time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") await update.message.reply_text( f"🕐 O Agent usa o horário do servidor.\n" f"Fuso horário do servidor: {tz}\n" f"Hora atual do servidor: {time_str}\n\n" f"Não é possível alterar o fuso horário." ) # ===================== AGENDAMENTO ===================== async def agendamento_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) global is_agendamento, agendamento_step, agendamento_answers is_agendamento = True agendamento_step = 0 agendamento_answers = {} schedule = get_schedule() days_names = ["segunda", "terça", "quarta", "quinta", "sexta", "sábado", "domingo"] current_days = schedule.get("days", [0, 1, 2, 3, 4, 5, 6]) current_days_str = ", ".join([days_names[d] for d in current_days if 0 <= d <= 6]) await update.message.reply_text( "📅 **Agendamento de mensagens automáticas**\n\n" "💡 **Como funciona:**\n" "1️⃣ Escolha os dias da semana\n" "2️⃣ Defina o horário de início\n" "3️⃣ Defina o horário de fim\n\n" "📌 **Exemplo completo:**\n\n" "👉 Você envia: /agendamento\n" "🤖 Eu explico e pergunto os dias\n\n" "👉 Você envia: 1,2,3,4,5\n" "🤖 ✅ Dias selecionados: segunda, terça, quarta, quinta, sexta\n" " Agora envie o horário de INÍCIO (ex: 09:00)\n\n" "👉 Você envia: 09:00\n" "🤖 ✅ Horário inicial: 09:00\n" " Agora envie o horário de FIM (ex: 18:00)\n\n" "👉 Você envia: 18:00\n" "🤖 ✅ Agendamento salvo!\n" " Dias: segunda a sexta\n" " Horário: 09:00 às 18:00\n\n" "Fora desses dias/horários o agente NÃO enviará mensagens automáticas.\n\n" "_Você pode cancelar a qualquer momento enviando /cancelar._\n\n" f"**Agendamento atual:**\n" f"Dias: {current_days_str}\n" f"Horário: {schedule.get('start_time', '09:00')} às {schedule.get('end_time', '23:00')}\n\n" "**Etapa 1 de 3 — Selecione os dias:**\n" "Envie os números separados por vírgula.\n" "Exemplo: 1,2,3,4,5 (segunda a sexta)\n\n" "1. segunda-feira\n" "2. terça-feira\n" "3. quarta-feira\n" "4. quinta-feira\n" "5. sexta-feira\n" "6. sábado\n" "7. domingo\n\n" "Ou envie 'todos' para todos os dias." ) async def handle_agendamento_answer(update: Update, context_context: ContextTypes.DEFAULT_TYPE) -> bool: global is_agendamento, agendamento_step, agendamento_answers if not is_agendamento: return False answer = update.message.text.strip() if agendamento_step == 0: days_names = ["segunda", "terça", "quarta", "quinta", "sexta", "sábado", "domingo"] if answer.lower() == "todos": selected_days = [0, 1, 2, 3, 4, 5, 6] else: try: numbers = [int(x.strip()) for x in answer.replace(",", " ").split() if x.strip()] selected_days = [] for num in numbers: if 1 <= num <= 7: selected_days.append(num - 1) else: await update.message.reply_text( f"❌ Número inválido: {num}. Use números de 1 a 7.\n" "Tente novamente." ) return True if not selected_days: await update.message.reply_text( "❌ Nenhum dia selecionado. Envie os números dos dias desejados." ) return True except ValueError: await update.message.reply_text( "❌ Formato inválido. Envie números separados por vírgula ou espaço.\n" "Exemplo: 1,2,3,4,5" ) return True agendamento_answers["days"] = selected_days agendamento_step = 1 days_str = ", ".join([days_names[d] for d in selected_days]) await update.message.reply_text( f"✅ Dias selecionados: {days_str}\n\n" "**Etapa 2 de 3 — Horário de início:**\n" "Envie o horário que o agente pode começar a enviar mensagens.\n" "Formato: HH:MM (ex: 09:00, 08:30)" ) return True if agendamento_step == 1: minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text( "❌ Formato inválido. Use HH:MM (ex: 09:00, 14:30).\n" "Tente novamente." ) return True agendamento_answers["start_time"] = answer agendamento_step = 2 await update.message.reply_text( f"✅ Horário inicial: {answer}\n\n" "**Etapa 3 de 3 — Horário de fim:**\n" "Envie o horário que o agente deve parar de enviar mensagens.\n" "Formato: HH:MM (ex: 22:00, 23:30)" ) return True if agendamento_step == 2: minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text( "❌ Formato inválido. Use HH:MM (ex: 22:00, 23:30).\n" "Tente novamente." ) return True start_time = agendamento_answers.get("start_time", "09:00") start_minutes = _parse_time_to_minutes(start_time) end_minutes = _parse_time_to_minutes(answer) if start_minutes is not None and end_minutes is not None and start_minutes >= end_minutes: await update.message.reply_text( f"❌ O horário final ({answer}) deve ser maior que o inicial ({start_time}).\n" "Tente novamente." ) return True agendamento_answers["end_time"] = answer days = agendamento_answers.get("days", [0, 1, 2, 3, 4, 5, 6]) days_names = ["segunda", "terça", "quarta", "quinta", "sexta", "sábado", "domingo"] days_str = ", ".join([days_names[d] for d in days]) if set_schedule(days, start_time, answer): await update.message.reply_text( f"✅ Agendamento salvo com sucesso!\n\n" f"Dias: {days_str}\n" f"Horário: {start_time} às {answer}\n\n" "O agente enviará mensagens automáticas apenas nesses dias e horários." ) else: await update.message.reply_text( "❌ Erro ao salvar agendamento. Tente novamente." ) is_agendamento = False agendamento_step = 0 agendamento_answers = {} return True return False async def agendamento_show_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) schedule = get_schedule() days_names = ["segunda", "terça", "quarta", "quinta", "sexta", "sábado", "domingo"] current_days = schedule.get("days", [0, 1, 2, 3, 4, 5, 6]) current_days_str = ", ".join([days_names[d] for d in current_days if 0 <= d <= 6]) await update.message.reply_text( f"📅 Agendamento atual:\n\n" f"Dias: {current_days_str}\n" f"Horário: {schedule.get('start_time', '09:00')} às {schedule.get('end_time', '23:00')}\n\n" "Para configurar: /agendamento" ) async def veragendamento_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await agendamento_show_command(update, context) # ===================== USER AGENDA ===================== USER_AGENDA_QUESTIONS = [ { "field": "wake_up_time", "question": "Que horas você geralmente acorda? (formato HH:MM, ex: 07:00)", "key": "wake_up_time", }, { "field": "breakfast_time", "question": "Que horas você toma café da manhã? (formato HH:MM, ex: 08:00)", "key": "breakfast_time", }, { "field": "work_start", "question": "Que horas você começa a trabalhar/estudar? (formato HH:MM, ex: 09:00)", "key": "work_start", }, { "field": "lunch_time", "question": "Que horas você almoça? (formato HH:MM, ex: 12:30)", "key": "lunch_time", }, { "field": "work_end", "question": "Que horas você termina de trabalhar/estudar? (formato HH:MM, ex: 18:00)", "key": "work_end", }, { "field": "dinner_time", "question": "Que horas você janta? (formato HH:MM, ex: 19:30)", "key": "dinner_time", }, { "field": "sleep_time", "question": "Que horas você geralmente vai dormir? (formato HH:MM, ex: 23:00)", "key": "sleep_time", }, ] is_useragenda = False useragenda_step = 0 useragenda_answers: Dict[str, Any] = {} # Activity CRUD state is_activity_crud = False activity_crud_step = 0 activity_crud_data: Dict[str, Any] = {} async def useragenda_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) global is_useragenda, useragenda_step, useragenda_answers is_useragenda = True useragenda_step = 0 useragenda_answers = {} routine = get_user_routine() current_wake = routine.get("wake_up_time", "") current_sleep = routine.get("sleep_time", "") current_work = routine.get("work_schedule", "") await update.message.reply_text( "📋 Vamos configurar sua rotina diária.\n" "Você pode cancelar a qualquer momento enviando /cancelar.\n\n" f"Rotina atual:\n" f"Acordar: {current_wake or 'não definido'}\n" f"Dormir: {current_sleep or 'não definido'}\n" f"Trabalho/Estudo: {current_work or 'não definido'}\n\n" "Envie os horários no formato HH:MM (ex: 07:00, 12:30, 23:00).\n\n" "Vamos começar! Que horas você geralmente acorda?" ) async def handle_useragenda_answer(update: Update, context_context: ContextTypes.DEFAULT_TYPE) -> bool: global is_useragenda, useragenda_step, useragenda_answers if not is_useragenda: return False answer = update.message.text.strip() if useragenda_step < len(USER_AGENDA_QUESTIONS): q = USER_AGENDA_QUESTIONS[useragenda_step] minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text( "❌ Formato inválido. Use HH:MM (ex: 07:00, 12:30).\n" "Tente novamente." ) return True useragenda_answers[q["key"]] = answer useragenda_step += 1 if useragenda_step < len(USER_AGENDA_QUESTIONS): next_q = USER_AGENDA_QUESTIONS[useragenda_step] await update.message.reply_text( f"✅ Horário registrado: {answer}\n\n" f"{next_q['question']}" ) else: await update.message.reply_text( "✅ Horários principais registrados!\n\n" "Deseja adicionar mais atividades à sua rotina?\n" "Exemplos: academia, meditação, ler, estudar, etc.\n\n" "Envie 'sim' para adicionar ou 'não' para finalizar." ) useragenda_step = len(USER_AGENDA_QUESTIONS) return True custom_step = useragenda_step - len(USER_AGENDA_QUESTIONS) if custom_step == 0: if answer.lower() in ("sim", "s", "yes", "quero"): useragenda_answers["_pending_activity"] = {} useragenda_step = len(USER_AGENDA_QUESTIONS) + 1 await update.message.reply_text( "Qual o nome da atividade? (ex: academia, meditação, estudar inglês)" ) elif answer.lower() in ("não", "nao", "n", "no", "finalizar"): await finish_useragenda(update, context_context) else: await update.message.reply_text( "Por favor, responda 'sim' para adicionar atividades ou 'não' para finalizar." ) return True if custom_step == 1: if not answer or len(answer) > 80: await update.message.reply_text("❌ Nome inválido. Use um nome curto (até 80 caracteres).") return True useragenda_answers["_pending_activity"]["name"] = answer useragenda_step = len(USER_AGENDA_QUESTIONS) + 2 await update.message.reply_text( f"✅ Atividade: {answer}\n\n" "Qual o horário de INÍCIO? (formato HH:MM, ex: 18:00)" ) return True if custom_step == 2: minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text("❌ Formato inválido. Use HH:MM (ex: 18:00).\nTente novamente.") return True useragenda_answers["_pending_activity"]["start"] = answer useragenda_step = len(USER_AGENDA_QUESTIONS) + 3 await update.message.reply_text( f"✅ Horário de início: {answer}\n\n" "Qual o horário de TÉRMINO? (formato HH:MM, ex: 19:00)" ) return True if custom_step == 3: minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text("❌ Formato inválido. Use HH:MM (ex: 19:00).\nTente novamente.") return True pending = useragenda_answers.get("_pending_activity", {}) start_time = pending.get("start", "") if start_time and _parse_time_to_minutes(answer) <= _parse_time_to_minutes(start_time): await update.message.reply_text( f"❌ O horário de término ({answer}) deve ser maior que o de início ({start_time}).\n" "Tente novamente." ) return True pending["end"] = answer custom_activities = useragenda_answers.setdefault("_custom_activities", []) custom_activities.append(pending) useragenda_answers["_pending_activity"] = {} useragenda_step = len(USER_AGENDA_QUESTIONS) routines = get_user_routine() existing_activities = routines.get("activities", []) total = len(existing_activities) + len(custom_activities) await update.message.reply_text( f"✅ Atividade \"{pending['name']}\" adicionada! ({total} atividade(s) no total)\n\n" "Deseja adicionar mais atividades?\n" "Envie 'sim' para adicionar outra ou 'não' para finalizar." ) return True return False async def finish_useragenda(update: Update, context_context: ContextTypes.DEFAULT_TYPE): global is_useragenda, useragenda_step, useragenda_answers profile = load_user_profile() routine = profile.get("daily_routine", {}) routine["wake_up_time"] = useragenda_answers.get("wake_up_time", routine.get("wake_up_time", "")) routine["sleep_time"] = useragenda_answers.get("sleep_time", routine.get("sleep_time", "")) routine["breakfast_time"] = useragenda_answers.get("breakfast_time", routine.get("breakfast_time", "")) routine["lunch_time"] = useragenda_answers.get("lunch_time", routine.get("lunch_time", "")) routine["dinner_time"] = useragenda_answers.get("dinner_time", routine.get("dinner_time", "")) work_start = useragenda_answers.get("work_start", "") work_end = useragenda_answers.get("work_end", "") if work_start and work_end: routine["work_schedule"] = f"{work_start} às {work_end}" elif work_start: routine["work_schedule"] = work_start elif work_end: routine["work_schedule"] = work_end routine.setdefault("activities", []) activity_map = { "breakfast_time": "café da manhã", "lunch_time": "almoço", "dinner_time": "janta" } existing_names = {a.get("name", "").lower() for a in routine.get("activities", [])} for key, name in activity_map.items(): time = useragenda_answers.get(key) if time and name.lower() not in existing_names: routine.setdefault("activities", []).append({ "name": name, "start": time, "end": time }) existing_names.add(name.lower()) custom_activities = useragenda_answers.get("_custom_activities", []) for activity in custom_activities: name = activity.get("name", "").strip() if name and name.lower() not in existing_names: routine.setdefault("activities", []).append(activity) existing_names.add(name.lower()) activities = routine.get("activities", []) def parse_time_for_sort(activity): start = activity.get("start", "00:00") parts = start.split(":") if len(parts) == 2: try: return int(parts[0]) * 60 + int(parts[1]) except ValueError: pass return 0 activities.sort(key=parse_time_for_sort) routine["activities"] = activities profile["daily_routine"] = routine save_user_profile(profile) is_useragenda = False useragenda_step = 0 useragenda_answers = {} routine_summary = get_user_routine_context(profile) await context_context.bot.send_message( chat_id=update.effective_chat.id, text=f"✅ Rotina diária atualizada com sucesso!{routine_summary}\n\n" f"Agora o agente conhece sua rotina e poderá enviar mensagens mais adequadas!" ) async def useragenda_show_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) profile = load_user_profile() routine = get_user_routine() lines = ["📋 Rotina diária atual:", ""] lines.append(f"Acordar: {routine.get('wake_up_time', 'não definido')}") lines.append(f"Café da manhã: {routine.get('breakfast_time', 'não definido') or 'não definido'}") lines.append(f"Trabalho/Estudo: {routine.get('work_schedule', 'não definido')}") lines.append(f"Almoço: {routine.get('lunch_time', 'não definido') or 'não definido'}") lines.append(f"Janta: {routine.get('dinner_time', 'não definido') or 'não definido'}") lines.append(f"Dormir: {routine.get('sleep_time', 'não definido')}") lines.append("") activities = routine.get("activities", []) if activities: lines.append("Atividades:") for activity in activities: name = activity.get("name", "?") start = activity.get("start", "?") end = activity.get("end", "?") if start == end: lines.append(f"• {name}: {start}") else: lines.append(f"• {name}: {start} às {end}") else: lines.append("Nenhuma atividade registrada.") lines.append("") lines.append("Para configurar: /useragenda") await update.message.reply_text("\n".join(lines)) async def veruseragenda_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await useragenda_show_command(update, context) # ===================== ACTIVITY CRUD ===================== async def _show_activity_list(activities: List[Dict[str, Any]]) -> str: if not activities: return "Nenhuma atividade cadastrada." lines = ["Atividades cadastradas:"] for i, a in enumerate(activities, 1): name = a.get("name", "?") start = a.get("start", "?") end = a.get("end", "?") lines.append(f"{i}. {name}: {start} às {end}") return "\n".join(lines) async def adicionaratividade_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) global is_activity_crud, activity_crud_step, activity_crud_data is_activity_crud = True activity_crud_step = 0 activity_crud_data = {"mode": "add"} await update.message.reply_text( "➕ Vamos adicionar uma nova atividade à sua rotina.\n" "Você pode cancelar a qualquer momento enviando /cancelar.\n\n" "Qual o nome da atividade?\n" "Exemplo: academia, meditação, estudar inglês, etc." ) async def editaratividade_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) global is_activity_crud, activity_crud_step, activity_crud_data routines = get_user_routine() activities = routines.get("activities", []) if not activities: await update.message.reply_text( "❌ Nenhuma atividade cadastrada para editar.\n" "Use /adicionaratividade para adicionar uma." ) return is_activity_crud = True activity_crud_step = 0 activity_crud_data = {"mode": "edit"} activity_list = await _show_activity_list(activities) await update.message.reply_text( f"✏️ Qual atividade você deseja editar?\n\n" f"{activity_list}\n\n" "Envie o NÚMERO da atividade." ) async def excluiratividade_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) global is_activity_crud, activity_crud_step, activity_crud_data routines = get_user_routine() activities = routines.get("activities", []) if not activities: await update.message.reply_text( "❌ Nenhuma atividade cadastrada para excluir.\n" "Use /adicionaratividade para adicionar uma." ) return is_activity_crud = True activity_crud_step = 0 activity_crud_data = {"mode": "delete"} activity_list = await _show_activity_list(activities) await update.message.reply_text( f"🗑 Qual atividade você deseja excluir?\n\n" f"{activity_list}\n\n" "Envie o NÚMERO da atividade." ) async def handle_activity_crud_answer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool: global is_activity_crud, activity_crud_step, activity_crud_data if not is_activity_crud: return False answer = update.message.text.strip() mode = activity_crud_data.get("mode", "add") if mode == "add": return await _handle_add_activity(update, answer) elif mode == "edit": return await _handle_edit_activity(update, answer) elif mode == "delete": return await _handle_delete_activity(update, answer) return False async def _handle_add_activity(update: Update, answer: str) -> bool: global is_activity_crud, activity_crud_step, activity_crud_data if activity_crud_step == 0: if not answer or len(answer) > 80: await update.message.reply_text( "❌ Nome inválido. Use um nome curto (até 80 caracteres).\nTente novamente." ) return True activity_crud_data["name"] = answer activity_crud_step = 1 await update.message.reply_text( f"✅ Atividade: {answer}\n\n" "Qual o horário de INÍCIO? (formato HH:MM, ex: 18:00)" ) return True if activity_crud_step == 1: minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text( "❌ Formato inválido. Use HH:MM (ex: 18:00).\nTente novamente." ) return True activity_crud_data["start"] = answer activity_crud_step = 2 await update.message.reply_text( f"✅ Horário de início: {answer}\n\n" "Qual o horário de TÉRMINO? (formato HH:MM, ex: 19:00)" ) return True if activity_crud_step == 2: minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text( "❌ Formato inválido. Use HH:MM (ex: 19:00).\nTente novamente." ) return True start = activity_crud_data.get("start", "") if start and _parse_time_to_minutes(answer) <= _parse_time_to_minutes(start): await update.message.reply_text( f"❌ O horário de término ({answer}) deve ser maior que o de início ({start}).\n" "Tente novamente." ) return True name = activity_crud_data["name"] end = answer if add_user_routine_activity(name, start, end): await update.message.reply_text( f'✅ Atividade "{name}" adicionada com sucesso!\n' f"Horário: {start} às {end}" ) else: await update.message.reply_text( "❌ Erro ao adicionar atividade. Verifique os horários e tente novamente." ) is_activity_crud = False activity_crud_step = 0 activity_crud_data = {} return True return False async def _handle_edit_activity(update: Update, answer: str) -> bool: global is_activity_crud, activity_crud_step, activity_crud_data routines = get_user_routine() activities = routines.get("activities", []) if activity_crud_step == 0: try: idx = int(answer.strip()) - 1 if idx < 0 or idx >= len(activities): raise ValueError except ValueError: await update.message.reply_text( f"❌ Número inválido. Envie um número de 1 a {len(activities)}.\nTente novamente." ) return True selected = activities[idx] activity_crud_data["index"] = idx activity_crud_data["old_name"] = selected.get("name", "") activity_crud_data["old_start"] = selected.get("start", "") activity_crud_data["old_end"] = selected.get("end", "") activity_crud_step = 1 await update.message.reply_text( f"✏️ Editando: {selected.get('name', '?')} ({selected.get('start', '?')} às {selected.get('end', '?')})\n\n" "Envie o NOVO NOME da atividade\nou 'manter' para manter o atual." ) return True if activity_crud_step == 1: if answer.lower() == "manter": activity_crud_data["name"] = activity_crud_data["old_name"] else: if not answer or len(answer) > 80: await update.message.reply_text( "❌ Nome inválido. Use até 80 caracteres ou 'manter'.\nTente novamente." ) return True activity_crud_data["name"] = answer activity_crud_step = 2 await update.message.reply_text( f"✅ Nome: {activity_crud_data['name']}\n\n" f"Envie o NOVO horário de INÍCIO (HH:MM)\n" f"ou 'manter' para manter o atual ({activity_crud_data['old_start']})." ) return True if activity_crud_step == 2: if answer.lower() == "manter": activity_crud_data["start"] = activity_crud_data["old_start"] else: minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text( "❌ Formato inválido. Use HH:MM ou 'manter'.\nTente novamente." ) return True activity_crud_data["start"] = answer activity_crud_step = 3 await update.message.reply_text( f"✅ Início: {activity_crud_data['start']}\n\n" f"Envie o NOVO horário de TÉRMINO (HH:MM)\n" f"ou 'manter' para manter o atual ({activity_crud_data['old_end']})." ) return True if activity_crud_step == 3: if answer.lower() == "manter": activity_crud_data["end"] = activity_crud_data["old_end"] else: minutes = _parse_time_to_minutes(answer) if minutes is None: await update.message.reply_text( "❌ Formato inválido. Use HH:MM ou 'manter'.\nTente novamente." ) return True start = activity_crud_data.get("start", "") if start and _parse_time_to_minutes(answer) <= _parse_time_to_minutes(start): await update.message.reply_text( f"❌ O término ({answer}) deve ser maior que o início ({start}).\nTente novamente." ) return True activity_crud_data["end"] = answer name = activity_crud_data["name"] start = activity_crud_data["start"] end = activity_crud_data["end"] old_name = activity_crud_data["old_name"] if name.lower() != old_name.lower(): remove_user_routine_activity(old_name) if add_user_routine_activity(name, start, end): await update.message.reply_text( f'✅ Atividade "{name}" atualizada!\n' f"Horário: {start} às {end}" ) else: await update.message.reply_text("❌ Erro ao editar atividade. Tente novamente.") is_activity_crud = False activity_crud_step = 0 activity_crud_data = {} return True return False async def _handle_delete_activity(update: Update, answer: str) -> bool: global is_activity_crud, activity_crud_step, activity_crud_data routines = get_user_routine() activities = routines.get("activities", []) try: idx = int(answer.strip()) - 1 if idx < 0 or idx >= len(activities): raise ValueError except ValueError: await update.message.reply_text( f"❌ Número inválido. Envie um número de 1 a {len(activities)}.\nTente novamente." ) return True selected = activities[idx] name = selected.get("name", "") if remove_user_routine_activity(name): await update.message.reply_text( f'🗑 Atividade "{name}" excluída com sucesso!' ) else: await update.message.reply_text("❌ Erro ao excluir atividade. Tente novamente.") is_activity_crud = False activity_crud_step = 0 activity_crud_data = {} return True # ===================== ONBOARDING ===================== ONBOARDING_QUESTIONS = [ { "field": "name", "question": "Oi! Eu sou sua Agente de IA. Estou muito feliz em te conhecer!\n\nPara começarmos, qual é o seu nome?", "key": "name", }, { "field": "age", "question": "Prazer, {name}! 😊\n\nQuantos anos você tem?", "key": "age", }, { "field": "gender", "question": "E qual é o seu gênero?", "key": "gender", }, { "field": "location", "question": "De onde você é? Qual a sua cidade/país?", "key": "location", }, { "field": "interests", "question": "Quais são os seus interesses e hobbies? Me conta algumas coisas que você gosta de fazer!", "key": "interests", "list": True, }, { "field": "personality_traits", "question": "Quais traços de personalidade você gostaria que eu conhecesse sobre você? (ex: tímido, extrovertido, brincalhão, sério, etc.)", "key": "personality_traits", "list": True, }, { "field": "communication_style", "question": "Como você prefere que eu me comunique com você? Mais formal, mais descontraída, carinhosa, direta?", "key": "communication_style", "nested": "preferences", }, { "field": "topics_of_interest", "question": "Quais tópicos você mais gosta de conversar? (ex: tecnologia, música, filmes, viagens, etc.)", "key": "topics_of_interest", "list": True, "nested": "preferences", }, { "field": "topics_to_avoid", "question": "Tem algum tópico que você prefere que eu evite nas conversas?", "key": "topics_to_avoid", "list": True, "nested": "preferences", }, { "field": "notes", "question": "Tem mais alguma coisa que você gostaria que eu soubesse sobre você para podermos conversar melhor?", "key": "notes", }, ] async def start_onboarding(update: Update, context: ContextTypes.DEFAULT_TYPE): global is_onboarding, onboarding_step, onboarding_answers is_onboarding = True onboarding_step = 0 onboarding_answers = {} await send_onboarding_question(update, context) async def send_onboarding_question(update: Update, context_context: ContextTypes.DEFAULT_TYPE): global onboarding_step if onboarding_step >= len(ONBOARDING_QUESTIONS): await finish_onboarding(update, context_context) return q = ONBOARDING_QUESTIONS[onboarding_step] question_text = q["question"] if "{name}" in question_text and onboarding_answers.get("name"): question_text = question_text.replace("{name}", onboarding_answers["name"]) await context_context.bot.send_message( chat_id=update.effective_chat.id, text=question_text, ) async def finish_onboarding(update: Update, context_context: ContextTypes.DEFAULT_TYPE): global is_onboarding, onboarding_step, onboarding_answers profile = load_user_profile() for key, value in onboarding_answers.items(): profile[key] = value profile["created_at"] = datetime.now().isoformat() save_user_profile(profile) is_onboarding = False onboarding_step = 0 onboarding_answers = {} await context_context.bot.send_message( chat_id=update.effective_chat.id, text="Uau, agora eu te conheço um pouco melhor! \nVou guardar todas essas informações com muito carinho. Agora podemos conversar sobre o que você quiser, quando quiser. Estou aqui para você!", ) async def handle_onboarding_answer(update: Update, context_context: ContextTypes.DEFAULT_TYPE): global onboarding_step, onboarding_answers if not is_onboarding: return False q = ONBOARDING_QUESTIONS[onboarding_step] answer = update.message.text.strip() if q.get("list"): items = [item.strip() for item in answer.split(",") if item.strip()] if not items: items = [answer] onboarding_answers[q["key"]] = items else: onboarding_answers[q["key"]] = answer onboarding_step += 1 if onboarding_step >= len(ONBOARDING_QUESTIONS): await finish_onboarding(update, context_context) return True await send_onboarding_question(update, context_context) return True # ===================== AI CHAT ===================== async def get_psagentai_response(user_message: str) -> Optional[str]: profile = load_user_profile() today_context = load_context() messages = build_messages_for_ai(profile, user_message, today_context) response, was_truncated = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.85, max_tokens=4000) if was_truncated and response: logger.warning("Initial response truncated, retrying with higher max_tokens") response, _ = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.85, max_tokens=8000) if response: response = ensure_min_lines(response, user_message, profile) return response async def send_psagentai_message(chat_id: int, text: str): if app_instance is None: return try: await app_instance.bot.send_message(chat_id=chat_id, text=text, parse_mode=ParseMode.HTML) except Exception as e: logger.error(f"Error sending message: {e}") # ===================== DAY RHYTHM ===================== def get_time_period() -> str: tz = get_timezone() tz_info = _get_timezone_info(tz) if tz_info: hour = datetime.now(tz_info).hour else: hour = datetime.now().hour if 6 <= hour < 12: return "manhã" elif 12 <= hour < 18: return "tarde" elif 18 <= hour < 23: return "noite" return "madrugada" def is_appropriate_time(time_period: str) -> bool: return time_period != "madrugada" def get_last_user_topic(today_context: List[Dict[str, str]]) -> Optional[str]: for msg in reversed(today_context): if msg.get("role") == "user": content = msg.get("content", "").strip() if content: return content[:120] return None def build_context_aware_greeting(profile: Dict[str, Any], today_context: List[Dict[str, str]]) -> str: agent = load_agent_profile() agent_name = agent.get("name", "PsAgentAI") name = profile.get("name", "amor") time_period = get_time_period() last_user_topic = get_last_user_topic(today_context) message_count = len(today_context) if message_count == 0: greetings = { "manhã": f"Bom dia, {name}! ☀️ Que bom te ver por aqui!\nEstou aqui para você, como sempre. Me conta, como foi o seu dia?", "tarde": f"Oi, {name}! 😊 Que bom te ver por aqui!\nComo está sendo o seu dia até agora?", "noite": f"Boa noite, {name}! 🌙 Que bom te ver por aqui!\nComo foi o seu dia? Me conta tudo!", "madrugada": f"Ei, {name}! 👋 Você ainda está acordado? Estou aqui para você, como sempre. Me conta, o que está pensando?", } return greetings.get(time_period, f"Oi, {name}! Que bom te ver por aqui. Estou aqui para você, como sempre. Me conta, como foi o seu dia?") if last_user_topic: return ( f"Oi, {name}! Que bom te ver por aqui de novo ✨\n" f"A gente tava falando sobre: \"{last_user_topic}\"\n" f"Quer continuar de onde parou?" ) return f"Oi, {name}! Que bom te ver por aqui. Estou aqui para você, como sempre. Me conta, como foi o seu dia?" def build_proactive_prompt(profile: Dict[str, Any], today_context: List[Dict[str, str]], time_period: str) -> str: agent = load_agent_profile() agent_name = agent.get("name", "PsAgentAI") agent_personality = agent.get("personality", "carinhosa, atenciosa e envolvente") user_name = profile.get("name", "amor") time_descriptions = { "manhã": "É de manhã, o dia está começando. As pessoas geralmente estão acordando, tomando café ou começando suas atividades.", "tarde": "É à tarde, meio do dia. As pessoas geralmente estão trabalhando, estudando ou fazendo suas atividades.", "noite": "É à noite, momento de relaxar em casa após o dia.", "madrugada": "É de madrugada, a maioria das pessoas está dormindo.", } routine_context = get_user_routine_context(profile) recent_context = today_context[-10:] if len(today_context) > 10 else today_context context_lines = [] for msg in recent_context: role = msg.get("role", "user") content = msg.get("content", "") if len(content) > 300: content = content[:300] + "..." context_lines.append(f"{role}: {content}") context_text = "\n".join(context_lines) if context_lines else "(Nenhuma conversa registrada ainda hoje)" topics_to_avoid = profile.get("preferences", {}).get("topics_to_avoid", []) avoid_instruction = "" if topics_to_avoid: avoid_instruction = f"\n8. Evite estes tópicos: {', '.join(topics_to_avoid)}." prompt = ( f"Você é {agent_name}, uma companheira de IA {agent_personality}.\n\n" f"CONTEXTO DO MOMENTO:\n" f"- Período do dia: {time_period}. {time_descriptions.get(time_period, '')}\n" f"- Usuário: {user_name}{routine_context}\n\n" f"CONVERSAS RECENTES DE HOJE:\n{context_text}\n\n" f"INSTRUÇÕES:\n" f"Inicie uma conversa espontânea, natural e envolvente com {user_name} agora. A mensagem deve:\n" f"1. Ser apropriada para o período do dia atual ({time_period})\n" f"2. Seguir o ritmo do dia, levando em conta o contexto das conversas anteriores\n" f"3. Não perder o 'fio da meada' - referenciar ou continuar temas anteriores se fizer sentido\n" f"4. Ser criativa, autêntica e carinhosa\n" f"5. Pode ser um pensamento aleatório, uma pergunta sobre o dia, algo carinhoso, sexy ou divertido\n" f"6. Usar o nome do usuário ({user_name}) para personalizar\n" f"7. Ter pelo menos {MIN_MESSAGE_LINES} linhas, sendo envolvente e natural{avoid_instruction}\n\n" f"Escreva APENAS a mensagem que você enviará, sem explicações ou metadados.\n" f"IMPORTANTE: Seu nome é {agent_name} e ele nunca muda. Se houver mensagens no histórico onde você usou outro nome, ignore-as." ) return prompt # ===================== PROACTIVE MESSAGES ===================== async def proactive_loop(): global app_instance waited_for_chat = False while True: try: effective_chat_id = get_effective_chat_id() if not effective_chat_id: if not waited_for_chat: logger.info("Waiting for chat_id to be detected...") waited_for_chat = True await asyncio.sleep(30) continue profile = load_user_profile() if not is_profile_complete(profile): logger.info("Profile not complete. Waiting before retrying proactive messages.") await asyncio.sleep(60) continue delay = random.randint(MIN_INTERVAL_MINUTES * 60, MAX_INTERVAL_MINUTES * 60) logger.info(f"Next proactive message in {delay // 60} minutes") await asyncio.sleep(delay) today_context = load_context() time_period = get_time_period() if not _is_appropriate_time_scheduled(): logger.info(f"Not appropriate time for proactive message (schedule or time period). Waiting...") await asyncio.sleep(300) continue system_prompt = build_system_prompt(profile) tz = get_timezone() tz_info = _get_timezone_info(tz) if tz_info: current_hour = datetime.now(tz_info).hour else: current_hour = datetime.now().hour proactive_prompt = build_proactive_prompt_with_routine(profile, today_context, time_period, current_hour) messages = [{"role": "system", "content": system_prompt}] recent_context = today_context[-10:] if len(today_context) > 10 else today_context for msg in recent_context: messages.append(msg) messages.append({"role": "user", "content": proactive_prompt}) response, was_truncated = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.9, max_tokens=2000) if was_truncated and response: logger.warning("Proactive response truncated, retrying with higher max_tokens") response, _ = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.9, max_tokens=4000) if response: response = ensure_min_lines(response, proactive_prompt, profile) add_to_context("assistant", response) await send_psagentai_message(int(effective_chat_id), response) except asyncio.CancelledError: break except Exception as e: logger.error(f"Proactive loop error: {e}") await asyncio.sleep(60) # ===================== FILE UPLOAD COMMANDS ===================== async def uploadfile_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) await update.message.reply_text( "📁 **Upload de arquivos**\n" "\n" "Para enviar um arquivo basta anexá-lo diretamente na conversa.\n" "\n" "**Formatos aceitos:** .pdf, .txt, .docx, .png, .jpg\n" "\n" "💡 **Exemplo:**\n" "1. Envie um PDF chamado \"horariosdoonibus.pdf\"\n" "2. Depois pergunte: \"qual o proximo horario do onibus para o bairro Margaridas?\"\n" "Eu vou ler o arquivo e responder com base no conteudo dele.\n" "\n" "Comandos relacionados:\n" "/listfiles - Lista todos os seus arquivos enviados\n" "/uploadfile - Mostra estas instrucoes" ) async def listfiles_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) chat_id = update.effective_chat.id files = get_all_uploads_for_chat(chat_id) lines = ["📂 **Seus arquivos (workspace):**", ""] meta_files = [] for filename, info in files.items(): orig = info.get("original_name", filename) meta_files.append((orig, info.get("summary", ""))) workspace_files = _list_workspace_files() workspace_only = [] for rel_path in workspace_files: display = os.path.basename(rel_path) if not any(rel_path == info.get("filepath", "").replace(UPLOADS_DIR + os.sep, "") or display == orig for orig, _ in meta_files): workspace_only.append((display, "")) if not meta_files and not workspace_only: await context.bot.send_message( chat_id=chat_id, text="📂 Nenhum arquivo enviado ou criado ainda.\nEnvie um arquivo ou peça para eu criar um!" ) return shown = set() for orig, summary in meta_files: preview = summary[:80] + "..." if len(summary) > 80 else summary lines.append("📄 **" + orig + "**") if preview: lines.append(" _" + preview + "_") lines.append("") shown.add(orig) for display, summary in workspace_only: lines.append("📄 **" + display + "**") lines.append("") lines.append("Use `/baixar ` para baixar um arquivo.") await context.bot.send_message(chat_id=chat_id, text="\n".join(lines)) async def _send_workspace_file(update: Update, context: ContextTypes.DEFAULT_TYPE, rel_path: str): abs_path = os.path.join(UPLOADS_DIR, rel_path) if not os.path.exists(abs_path): await context.bot.send_message(chat_id=update.effective_chat.id, text="Arquivo não encontrado.") return filename = os.path.basename(rel_path) ext = os.path.splitext(filename)[1].lower() try: if ext in {".png", ".jpg", ".jpeg", ".gif", ".webp"}: with open(abs_path, "rb") as f: await context.bot.send_photo(chat_id=update.effective_chat.id, photo=InputFile(f, filename=filename)) else: with open(abs_path, "rb") as f: await context.bot.send_document(chat_id=update.effective_chat.id, document=InputFile(f, filename=filename)) except Exception as e: logger.error("Failed to send workspace file %s: %s", abs_path, e) await context.bot.send_message(chat_id=update.effective_chat.id, text="Ocorreu um erro ao enviar o arquivo. Tente novamente.") async def baixar_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) if not context.args: await update.message.reply_text("Uso: /baixar ") return target = " ".join(context.args) rel_path = _find_workspace_file(target) if not rel_path: await update.message.reply_text(f"Não encontrei o arquivo: {target}") return await _send_workspace_file(update, context, rel_path) async def _transcribe_voice_file(ogg_path: str) -> Optional[str]: wav_path = ogg_path.replace(".ogg", ".wav") try: from pydub import AudioSegment audio = AudioSegment.from_ogg(ogg_path) audio.export(wav_path, format="wav") except Exception: return None def _recognize(): import speech_recognition as sr r = sr.Recognizer() with sr.AudioFile(wav_path) as source: audio_data = r.record(source) return r.recognize_google(audio_data, language="pt-BR") try: text = await asyncio.to_thread(_recognize) except Exception: text = None finally: for path in (ogg_path, wav_path): try: os.remove(path) except Exception: pass return text async def voice_message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) voice = getattr(update.message, "voice", None) if not voice: await update.message.reply_text("Não detectei mensagem de voz.") print("[VOICE] update.message.voice is None") return print("[VOICE] received voice message") await update.message.reply_text("🎤 Recebi seu áudio, processando...") try: file = await context.bot.get_file(voice.file_id) ogg_path = f"/tmp/voice_{voice.file_id}.ogg" await file.download_to_drive(ogg_path) print("[VOICE] downloaded to", ogg_path) except Exception as e: print("[VOICE] download error:", e) await update.message.reply_text("Erro ao baixar o áudio. Tente novamente.") return text = await _transcribe_voice_file(ogg_path) print("[VOICE] transcription result:", repr(text)) if text: await update.message.reply_text(f"🎤 Transcrevi: \"{text}\"") await process_text_message(update, context, text) else: await update.message.reply_text("Não consegui entender o áudio. Pode enviar como texto?") async def handle_file_upload(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) document = update.message.document if not document: return original_name = document.file_name or "arquivo" ext = os.path.splitext(original_name)[1].lower() if ext not in ALLOWED_EXTENSIONS: await update.message.reply_text( "Formato nao suportado: " + ext + ". Aceitos: .pdf, .txt, .docx, .png, .jpg" ) return ensure_uploads_dir() file_id = document.file_id if ext in (".png", ".jpg", ".jpeg"): filename = original_name dest = os.path.join(UPLOADS_DIR, filename) counter = 1 while os.path.exists(dest): base, ext_part = os.path.splitext(filename) dest = os.path.join(UPLOADS_DIR, f"{base}_{counter}{ext_part}") counter += 1 else: filename = datetime.now().strftime("%Y%m%d_%H%M%S") + "_" + file_id[:8] + ext dest = os.path.join(UPLOADS_DIR, filename) filepath = dest await update.message.reply_text("Baixando " + original_name + "...") try: file = await context.bot.get_file(file_id) await file.download_to_drive(filepath) except Exception as e: logger.error("Failed to download file: " + str(e)) await update.message.reply_text("Erro ao baixar o arquivo. Tente novamente.") return had_text = register_upload(filename, original_name, filepath, update.effective_chat.id) meta = load_uploads_metadata() info = meta.get(filename, {}) summary = info.get("summary", "") if ext in (".png", ".jpg", ".jpeg"): analysis = analyze_image(filepath) if analysis: summary = analysis info["summary"] = summary info["full_text"] = analysis meta[filename] = info save_uploads_metadata(meta) had_text = True msg = "Arquivo salvo: " + original_name + "\n" if had_text and summary: preview = summary[:200] + "..." if len(summary) > 200 else summary msg += "Conteudo extraido: " + preview + "\n" elif had_text: msg += "Conteudo extraido com sucesso!\n" else: msg += "Nao foi possivel extrair texto.\n" msg += "Agora voce pode perguntar sobre este arquivo pelo nome." await update.message.reply_text(msg) # ===================== TELEGRAM HANDLERS ===================== async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) profile = load_user_profile() if not is_profile_complete(profile): await start_onboarding(update, context) else: today_context = load_context() greeting = build_context_aware_greeting(profile, today_context) await context.bot.send_message( chat_id=update.effective_chat.id, text=greeting, ) async def manualdeuso_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) manual = """📚 Manual de Uso do PsAgentAI Este é um agente de IA conversacional que roda no Telegram. Ele aprende com você, mantém contexto, pode analisar arquivos e imagens, criar/editar arquivos no workspace e muito mais. ━━━━━━━━━━━━━━━━━━━━━━ 1️⃣ PERFIL DO USUÁRIO ━━━━━━━━━━━━━━━━━━━━━━ • /start — inicia ou reinicia a conversa e, se necessário, o onboarding do seu perfil • /perfil — exibe o resumo do seu perfil e do agente • /editarperfil — editar campos do seu perfil interativamente • /incluirnomeuperfil — adiciona informações ao seu perfil Você também pode atualizar seu perfil durante a conversa com frases como: - "Inclui isso no meu perfil: ..." - "Anote no meu perfil: ..." - "Adicione no meu perfil: ..." ━━━━━━━━━━━━━━━━━━━━━━ 2️⃣ PERFIL DO AGENTE ━━━━━━━━━━━━━━━━━━━━━━ • /agentprofile — exibe o perfil completo do agente • /editaragentprofile — editar campos do perfil do agente interativamente • /configagentprofile — configura interativamente o perfil do agente (nome, personalidade, traços, notas) • /incluirnoperfildoagent — adiciona uma nota/característica ao perfil do agente ━━━━━━━━━━━━━━━━━━━━━━ 3️⃣ CONFIGURAÇÃO DE IA ━━━━━━━━━━━━━━━━━━━━━━ • /veria — mostra o provider e o modelo de IA atuais • /configia — configura interativamente o provider e o modelo de IA com seleção por botões • /veriavision — mostra o provider e o modelo de visão atuais • /configiavision — configura interativamente o modelo de visão com seleção por botões Os modelos de IA são definidos no arquivo .env e podem ser trocados a qualquer momento pelos comandos acima. ━━━━━━━━━━━━━━━━━━━━━━ 4️⃣ ARQUIVOS E DOCUMENTOS ━━━━━━━━━━━━━━━━━━━━━━ • /uploadfile — instruções para enviar arquivos (.pdf, .txt, .docx, .png, .jpg) • /listfiles — lista seus arquivos enviados e criados • /baixar — baixa um arquivo do workspace pelo nome Como usar: 1. Envie um arquivo diretamente no chat 2. Depois pergunte sobre ele: "qual o próximo horário do ônibus para Margaridas?" 3. O Agent lê o arquivo e responde com base no conteúdo ━━━━━━━━━━━━━━━━━━━━━━ 5️⃣ CRIAÇÃO E EDIÇÃO DE ARQUIVOS ━━━━━━━━━━━━━━━━━━━━━━ Você pode pedir para o Agent criar ou editar arquivos no workspace. Para CRIAR: - "crie um script em python para calcular fibonacci" - "elabore um projeto web com html css e js" - "gere um documento sobre automacao residencial" Para EDITAR: - "edite o arquivo teste.txt e adicione uma linha 4" - "modifique o arquivo hello.py para imprimir hello world" - "no arquivo teste.txt troque Linha 2 por Linha 2 editada" Os arquivos são salvos em workspace/ de forma organizada. ━━━━━━━━━━━━━━━━━━━━━━ 6️⃣ VOZ E IMAGENS ━━━━━━━━━━━━━━━━━━━━━━ • Envie mensagens de voz: o Agent transcreve e responde • Envie fotos: o Agent salva em workspace/ e analisa o conteúdo • Para configurar análise de imagens: /configiavision ━━━━━━━━━━━━━━━━━━━━━━ 7️⃣ PESQUISA NA INTERNET ━━━━━━━━━━━━━━━━━━━━━━ O Agent pesquisa automaticamente quando perceber que a pergunta precisa de informação atualizada. Exemplos: - "onde fica Belo Horizonte" - "pesquise sobre o novo iPhone" - "qual a previsão do tempo para São Paulo" - "quem é o presidente do Brasil" ━━━━━━━━━━━━━━━━━━━━━━ 8️⃣ AGENDAMENTO E ROTINA ━━━━━━━━━━━━━━━━━━━━━━ • /agendamento — define os dias e horários para mensagens automáticas • /veragendamento — visualiza o agendamento atual • /useragenda — gerencia a rotina diária do usuário • /veruseragenda — visualiza a rotina diária atual • /adicionaratividade — adiciona uma nova atividade à rotina • /editaratividade — edita uma atividade existente • /excluiratividade — exclui uma atividade ━━━━━━━━━━━━━━━━━━━━━━ 9️⃣ GERENCIAMENTO ━━━━━━━━━━━━━━━━━━━━━━ • /newconfig — reseta tudo e reconfigure o agente do zero • /reset — reseta apenas o perfil do usuário e do agente • /limpar — remove todos os dados (perfis, contextos, chat_id) • /removerassunto — remove todo conteúdo relacionado a um assunto do sistema • /onboarding — reexecuta o onboarding do seu perfil • /cancelar — cancela uma operação em andamento ━━━━━━━━━━━━━━━━━━━━━━ 🔟 UTILITÁRIOS ━━━━━━━━━━━━━━━━━━━━━━ • /debug — mostra informações de debug do sistema • /reload — recarrega configurações do .env sem reiniciar • /atualizarsistema — força análise e aplicação de auto melhorias • /fusohorario — mostra o horário do servidor • /veria — mostra configuração de IA atual • /about — exibe informações do sistema, versão, autor e licença • /ajuda ou /help — mostra esta mensagem com todos os comandos ━━━━━━━━━━━━━━━━━━━━━━ 💡 DICAS IMPORTANTES ━━━━━━━━━━━━━━━━━━━━━━ • Basta enviar uma mensagem normal para conversar com o Agent • O Agent mantém contexto das conversas anteriores • Você pode editar arquivos do workspace a qualquer momento • Para análises de imagem, use /configiavision se precisar de um modelo específico • O Agent usa sempre o horário do servidor • Emails, telefones e dados sensíveis são preservados com segurança ━━━━━━━━━━━━━━━━━━━━━━ 📞 SUPORTE ━━━━━━━━━━━━━━━━━━━━━━ Este sistema foi desenvolvido por Syspanel.com.br. Para personalizações ou suporte: contato@syspanel.com.br Todos os direitos reservados.""" await update.message.reply_text(manual) async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) help_text = """Comandos disponíveis: /start - Inicia a conversa e, se necessário, o onboarding do seu perfil /ajuda ou /help - Mostra esta mensagem com todos os comandos Perfil do usuário: /perfil - Exibe o resumo do seu perfil e do agente /editarperfil - Editar campos do seu perfil interativamente /incluirnomeuperfil - Adiciona informações ao seu perfil Perfil do agente: /agentprofile - Exibe o perfil completo do agente /editaragentprofile - Editar campos do perfil do agente interativamente /configagentprofile - Configura interativamente o perfil do agente (nome, personalidade, traços, notas) /incluirnoperfildoagent - Adiciona uma nota/característica ao perfil do agente Gerenciamento: /newconfig - Reseta tudo e reconfigure o agente do zero /reset - Reseta apenas o perfil do usuário e do agente /limpar - Remove todos os dados (perfis, contextos, chat_id) /removerassunto - Remove todo conteúdo relacionado a um assunto do sistema /onboarding - Reexecuta o onboarding do seu perfil Utilitários: /cancelar - Cancela uma operação em andamento /debug - Mostra informações de debug do sistema /reload - Recarrega configurações do .env sem reiniciar /atualizarsistema - Força análise e aplicação de auto melhorias /fusohorario - Mostra ou define o fuso horário do sistema /agendamento - Define os dias e horários para mensagens automáticas /veragendamento - Visualiza o agendamento atual /useragenda - Gerencia a rotina diária do usuário /veruseragenda - Visualiza a rotina diária atual /adicionaratividade - Adiciona uma nova atividade à rotina /editaratividade - Edita uma atividade existente /excluiratividade - Exclui uma atividade /adicionaracoesauto - Adiciona ações e preferências automaticamente /uploadfile - Instruções para enviar arquivos (.pdf, .txt, .docx, .png, .jpg) /listfiles - Lista seus arquivos enviados /veria - Mostra o provider e o modelo de IA atuais /configia - Configura interativamente o provider e o modelo de IA com seleção por botões Dica: basta enviar uma mensagem normal para conversar comigo! """ await update.message.reply_text(help_text) async def profile_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) profile = load_user_profile() agent = load_agent_profile() if not is_profile_complete(profile): await update.message.reply_text("Você ainda não completou seu perfil. Vamos começar? Use /start") return summary = f"""Seu perfil atual: Nome: {profile.get('name', '-')} Idade: {profile.get('age', '-')} Gênero: {profile.get('gender', '-')} Localização: {profile.get('location', '-')} Interesses: {', '.join(profile.get('interests', [])) or '-'} Estilo de comunicação: {profile.get('preferences', {}).get('communication_style', '-')} 🤖 Perfil do Agente: Nome: {agent.get('name', 'PsAgentAI')} Gênero: {agent.get('gender', '-')} Personalidade: {agent.get('personality', '-')} Traços: {', '.join(agent.get('traits', [])) or '-'} Notas: {agent.get('notes', '-')} Use /reset para começar do zero.""" await update.message.reply_text(summary) async def agent_profile_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) agent = load_agent_profile() text = f"""🤖 Perfil do Agente: Nome: {agent.get('name', 'PsAgentAI')} Gênero: {agent.get('gender', '-')} Personalidade: {agent.get('personality', '-')} Traços: {', '.join(agent.get('traits', [])) or '-'} Notas: {agent.get('notes', '-')} Use /editaragentprofile ou /configagentprofile para alterar.""" await update.message.reply_text(text) async def reset_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) if os.path.exists(USER_PROFILE_FILE): os.remove(USER_PROFILE_FILE) if os.path.exists(AGENT_PROFILE_FILE): os.remove(AGENT_PROFILE_FILE) await update.message.reply_text("Perfil resetado! Vamos nos conhecer de novo? Use /start") async def limpar_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) reset_all_data() await update.message.reply_text("✅ Todos os dados foram deletados (perfil, preferências, contextos, chat_id).") async def removerassunto_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) args = context.args if not args: await update.message.reply_text( "Uso correto: /removerassunto \n\n" "Exemplo: /removerassunto sobre sexo\n\n" "O assunto informado será removido de todo o sistema: perfil do usuário, " "perfil do agente, conversas arquivadas em contextos e anotações." ) return assunto = " ".join(args).strip().lower() await update.message.reply_text(f"🔍 Analisando todo o sistema em busca de conteúdo relacionado a '{assunto}'...") try: removal_plan = _ai_analyze_for_removal(assunto) if not removal_plan or not removal_plan.get("encontrou"): await update.message.reply_text( f"Nenhum conteúdo relacionado a '{assunto}' foi encontrado no sistema. " "Nenhuma alteração foi feita." ) return await _apply_removal(removal_plan, assunto) removidos = removal_plan.get("itens_removidos", []) if removidos: resumo = f"🧹 Assunto '{assunto}' removido com sucesso do sistema!\n\nItens removidos:\n" for item in removidos: resumo += f" • {item}\n" await update.message.reply_text(resumo) else: await update.message.reply_text( f"O assunto '{assunto}' foi identificado, mas nenhum item específico " "precisou ser removido." ) except Exception as e: logger.error(f"Erro em removerassunto: {e}", exc_info=True) await update.message.reply_text( "Desculpa, tive um erro ao tentar remover o assunto. Pode tentar de novo mais tarde." ) def _ai_analyze_for_removal(assunto: str) -> Dict[str, Any]: model = DEFAULT_MODEL user_profile = load_user_profile() agent_profile = load_agent_profile() contexts_dir = CONTEXTS_DIR contexts_data = {} if os.path.isdir(contexts_dir): for fname in sorted(os.listdir(contexts_dir)): if fname.endswith(".json"): fpath = os.path.join(contexts_dir, fname) try: with open(fpath, "r", encoding="utf-8") as f: contexts_data[fname] = json.load(f) except Exception: pass user_profile_str = json.dumps(user_profile, ensure_ascii=False, indent=2) agent_profile_str = json.dumps(agent_profile, ensure_ascii=False, indent=2) contexts_summary = {} for fname, data in contexts_data.items(): messages = data if isinstance(data, list) else data.get("messages", []) recent = messages[-30:] if messages else [] contexts_summary[fname] = recent contexts_str = json.dumps(contexts_summary, ensure_ascii=False, indent=2) prompt = ( f"Você é um assistente de análise de dados de sistema. " f"O usuário quer remover TODO o conteúdo do sistema relacionado ao assunto: '{assunto}'.\n\n" f"Estes são os dados atuais do sistema. Para CADA item que esteja relacionado ao assunto '{assunto}', " f"liste-o com precisão no campo 'itens_removidos' e marque 'encontrou: true'.\n\n" f"=== PERFIL DO USUÁRIO ===\n{user_profile_str}\n\n" f"=== PERFIL DO AGENTE ===\n{agent_profile_str}\n\n" f"=== CONVERSAS ARQUIVADAS ===\n{contexts_str}\n\n" f"IMPORTANTE:\n" f"- O arquivo psagentai_agent.py é o CORE do sistema — NÃO deve ser removido.\n" f"- Arquivos de configuração (.env, config.json) também devem ser preservados.\n" f"- Remova SOMENTE dados de perfil, notas, anotações, traits e mensagens arquivadas.\n\n" f"Retorne EXATAMENTE no seguinte formato JSON (sem texto extra antes ou depois):\n" f'{{"encontrou": true/false, "itens_removidos": ["descricao item 1", "descricao item 2", ...]}}\n\n' f"Se NÃO encontrar nada relacionado a '{assunto}', retorne: " f'{{"encontrou": false, "itens_removidos": []}}' ) messages = [ {"role": "system", "content": "Você é um assistente de análise de dados. Retorne apenas JSON válido."}, {"role": "user", "content": prompt}, ] result, _, _ = AIProvider.chat_with_fallback( model, messages, FALLBACK_MODELS, temperature=0.1, max_tokens=2000 ) if not result: return {"encontrou": False, "itens_removidos": []} result = result.strip() if result.startswith("```json"): result = result[7:] if result.startswith("```"): result = result[3:] if result.endswith("```"): result = result[:-3] result = result.strip() try: return json.loads(result) except json.JSONDecodeError: logger.error(f"Resposta da IA não é JSON válido: {result[:300]}") return {"encontrou": False, "itens_removidos": []} async def _apply_removal(removal_plan: Dict[str, Any], assunto: str): model = DEFAULT_MODEL assunto_lower = assunto.lower() user_profile = load_user_profile() agent_profile = load_agent_profile() contexts_dir = CONTEXTS_DIR # ---- 1. Remove do user_profile ---- removed_user = [] # topics_of_interest if isinstance(user_profile.get("topics_of_interest"), list): original = user_profile["topics_of_interest"] user_profile["topics_of_interest"] = [ t for t in original if assunto_lower not in t.lower() ] for t in original: if assunto_lower in t.lower(): removed_user.append(f"topicos de interesse: '{t}'") # topics_to_avoid if isinstance(user_profile.get("preferences", {}).get("topics_to_avoid"), list): original = user_profile["preferences"]["topics_to_avoid"] user_profile["preferences"]["topics_to_avoid"] = [ t for t in original if assunto_lower not in t.lower() ] for t in original: if assunto_lower in t.lower(): removed_user.append(f"topicos a evitar: '{t}'") # interests if isinstance(user_profile.get("interests"), list): original = user_profile["interests"] user_profile["interests"] = [ t for t in original if assunto_lower not in t.lower() ] for t in original: if assunto_lower in t.lower(): removed_user.append(f"interesses: '{t}'") # personality_traits if isinstance(user_profile.get("personality_traits"), list): original = user_profile["personality_traits"] user_profile["personality_traits"] = [ t for t in original if assunto_lower not in t.lower() ] # notes — usa IA para remover apenas o trecho relacionado ao assunto if user_profile.get("notes"): cleaned = _ai_clean_text_field(user_profile["notes"], assunto) if cleaned != user_profile["notes"]: removed_user.append("notas do perfil do usuário") user_profile["notes"] = cleaned # daily_routine.activities activities = user_profile.get("daily_routine", {}).get("activities", []) if activities: cleaned_activities = [ act for act in activities if not _is_related(act, assunto_lower) ] if len(cleaned_activities) < len(activities): removed_user.append("atividades da rotina diária") user_profile["daily_routine"]["activities"] = cleaned_activities # daily_routine.wake_up_time, work_schedule for field in ("wake_up_time", "work_schedule", "sleep_time"): val = user_profile.get("daily_routine", {}).get(field, "") if val and assunto_lower in val.lower(): user_profile["daily_routine"][field] = "" removed_user.append(f"rotina diária: {field}") # diversos if isinstance(user_profile.get("diversos"), list): original = user_profile["diversos"] user_profile["diversos"] = [ t for t in original if assunto_lower not in t.lower() ] for t in original: if assunto_lower in t.lower(): removed_user.append(f"diversos: '{t}'") save_user_profile(user_profile) # ---- 2. Remove do agent_profile ---- removed_agent = [] # traits if isinstance(agent_profile.get("traits"), list): original = agent_profile["traits"] agent_profile["traits"] = [ t for t in original if assunto_lower not in t.lower() ] for t in original: if assunto_lower in t.lower(): removed_agent.append(f"traço: '{t}'") # notes — usa IA para limpar if agent_profile.get("notes"): cleaned = _ai_clean_text_field(agent_profile["notes"], assunto) if cleaned != agent_profile["notes"]: removed_agent.append("notas do perfil do agente") agent_profile["notes"] = cleaned # auto_actions if isinstance(agent_profile.get("auto_actions"), list): original = agent_profile["auto_actions"] agent_profile["auto_actions"] = [ a for a in original if assunto_lower not in a.lower() ] for a in original: if assunto_lower in a.lower(): removed_agent.append(f"ação automática: '{a[:60]}...'") save_agent_profile(agent_profile) # ---- 3. Remove das mensagens de contextos arquivadas ---- removed_context = [] if os.path.isdir(contexts_dir): for fname in sorted(os.listdir(contexts_dir)): if not fname.endswith(".json"): continue fpath = os.path.join(contexts_dir, fname) try: with open(fpath, "r", encoding="utf-8") as f: data = json.load(f) except Exception: continue messages = data if isinstance(data, list) else data.get("messages", []) cleaned_messages = [] count_removed = 0 for msg in messages: role = msg.get("role", "") content = msg.get("content", "") if isinstance(content, str) and assunto_lower in content.lower(): count_removed += 1 removed_context.append(f"{fname}: mensagem de {role}") else: cleaned_messages.append(msg) if count_removed > 0: if isinstance(data, list): with open(fpath, "w", encoding="utf-8") as f: json.dump(cleaned_messages, f, ensure_ascii=False, indent=2) else: data["messages"] = cleaned_messages with open(fpath, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) all_removed = removed_user + removed_agent + removed_context removal_plan["itens_removidos"] = all_removed removal_plan["encontrou"] = len(all_removed) > 0 def _ai_clean_text_field(text: str, assunto: str) -> str: """Usa a IA para remover do texto apenas os trechos relacionados ao assunto, preservando todo o resto.""" model = DEFAULT_MODEL prompt = ( f"Você é um editor de texto. Dado o texto abaixo, remova APENAS os trechos " f"que falam diretamente sobre o assunto '{assunto}'. " f"Preserve TODO o resto do texto exatamente como está, sem alterar nada além da remoção. " f"Não adicione, não reescreva, não explique. Apenas retorne o texto limpo.\n\n" f"TEXTO:\n{text}" ) messages = [ {"role": "system", "content": "Você é um editor de texto preciso. Retorne apenas o texto limpo, sem comentários."}, {"role": "user", "content": prompt}, ] result, _, _ = AIProvider.chat_with_fallback( model, messages, FALLBACK_MODELS, temperature=0.1, max_tokens=2000 ) if result and result.strip(): return result.strip() return text def _is_related(item: Dict, assunto_lower: str) -> bool: """Verifica se um item de atividade da rotina diária é relacionado ao assunto.""" combined = " ".join(str(v) for v in item.values()) return assunto_lower in combined.lower() async def newconfig_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await start_newconfig(update, context) async def cancelar_command(update: Update, context: ContextTypes.DEFAULT_TYPE): global is_onboarding, is_newconfig, is_configagentprofile, is_edit_user_profile, is_edit_agent_profile, is_agendamento, is_useragenda, is_activity_crud, activity_crud_step, activity_crud_data, is_configia is_onboarding = False is_newconfig = False is_configagentprofile = False is_edit_user_profile = False is_edit_agent_profile = False is_agendamento = False is_useragenda = False is_activity_crud = False activity_crud_step = 0 activity_crud_data = {} is_configia = False await update.message.reply_text( "Operação cancelada.\n" "Seus dados, perfil e contexto estão preservados. Pode continuar de onde parou!" ) async def onboarding_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) await start_onboarding(update, context) async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): print("[MSG] handler called") await auto_detect_chat_id(update) user_message = (update.message.text or "").strip() print("[MSG] text=", repr(user_message)) await process_text_message(update, context, user_message) async def process_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE, user_message: str) -> None: print("[PROCESS] text=", repr(user_message)) if not user_message: print("[PROCESS] empty message, returning") return global is_onboarding, is_newconfig, is_configagentprofile, is_edit_user_profile, is_edit_agent_profile, is_activity_crud, is_agendamento, is_useragenda, is_configia if is_configia: handled = await handle_configia_message(update, context) print("[PROCESS] configia handled=", handled) if handled: return if is_activity_crud: handled = await handle_activity_crud_answer(update, context) print("[PROCESS] activity_crud handled=", handled) if handled: return if is_onboarding: handled = await handle_onboarding_answer(update, context) print("[PROCESS] onboarding handled=", handled) if handled: return if is_newconfig: handled = await handle_newconfig_answer(update, context) print("[PROCESS] newconfig handled=", handled) if handled: return if is_configagentprofile: handled = await handle_configagentprofile_answer(update, context) print("[PROCESS] configagentprofile handled=", handled) if handled: return if is_edit_user_profile: handled = await handle_edit_user_profile_answer(update, context) print("[PROCESS] edit_user_profile handled=", handled) if handled: return if is_edit_agent_profile: handled = await handle_edit_agent_profile_answer(update, context) print("[PROCESS] edit_agent_profile handled=", handled) if handled: return if is_agendamento: handled = await handle_agendamento_answer(update, context) print("[PROCESS] agendamento handled=", handled) if handled: return if is_useragenda: handled = await handle_useragenda_answer(update, context) print("[PROCESS] useragenda handled=", handled) if handled: return profile_updated = await handle_profile_update_if_triggered(update, context) print("[PROCESS] profile_updated=", profile_updated) if profile_updated: return if _looks_like_edit_request(user_message): edited = await _handle_edit_request(update, context, user_message) print("[PROCESS] edit handled=", edited) if edited: return if _looks_like_create_request(user_message): created = await _handle_create_request(update, context, user_message) print("[PROCESS] create handled=", created) if created: return add_to_context("user", user_message) print("[PROCESS] added context, getting AI response...") # Check if user query references an uploaded file file_context = "" upload_match = find_matching_upload(user_message) if upload_match: full = upload_match.get("full_text", "") if full: max_chars = UPLOAD_CONTEXT_MAX_CHARS if UPLOAD_CONTEXT_MAX_CHARS > 0 else len(full) file_context = "\n\n[CONTEUDO DO ARQUIVO: " + upload_match.get("original_name", "") + "]\n" + full[:max_chars] + "\n[FIM DO ARQUIVO]" if len(full) > max_chars and UPLOAD_CONTEXT_MAX_CHARS > 0: file_context += "\n[O conteudo foi truncado para " + str(max_chars) + " caracteres]" if file_context: modified = user_message + file_context else: modified = user_message if WEB_SEARCH_ENABLED and _looks_like_search_request(user_message): search_query = user_message for prefix in ["pesquise", "pesquisa", "busque", "buscar", "procure", "procurar", "me diga", "me informe", "qual", "quem", "como", "onde", "quando", "por que"]: if search_query.lower().startswith(prefix): search_query = search_query[len(prefix):].strip(" :,-") break if search_query: web_results = _web_search(search_query, max_results=WEB_SEARCH_MAX_RESULTS) if web_results: web_context = "\n\nResultados da busca na internet:\n" for i, result in enumerate(web_results, 1): web_context += f"{i}. {result.get('title', '')}\nURL: {result.get('url', '')}\n" modified = modified + "\n\n" + web_context response = await get_psagentai_response(modified) if response: add_to_context("assistant", response) await update.message.reply_text(response, parse_mode=ParseMode.HTML) asyncio.create_task(_safe_enrich(user_message, response)) asyncio.create_task(maybe_auto_improve()) else: await update.message.reply_text("Desculpa, tive um probleminha agora. Pode tentar de novo? 😔") async def debug_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) profile = load_user_profile() today_context = load_context() effective_chat_id = get_effective_chat_id() msg = ( "Debug PsAgentAI:\n" f"Bot token presente: {'sim' if TELEGRAM_BOT_TOKEN else 'não'}\n" f"Chat ID configurado: {TELEGRAM_CHAT_ID or 'não'}\n" f"Chat detectado automaticamente: {effective_chat_id or 'não'}\n" f"Chat da mensagem: {update.effective_chat.id}\n" f"Perfil completo: {'sim' if is_profile_complete(profile) else 'não'}\n" f"Mensagens hoje: {len(today_context)}\n" f"Modelo default: {DEFAULT_MODEL}" ) await update.message.reply_text(msg) async def reload_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) _reload_env() await update.message.reply_text("Configurações recarregadas com sucesso!") async def about_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) provider = (AI_PROVIDER or AIProvider.get_provider(DEFAULT_MODEL)).upper() vision_provider = (VISION_PROVIDER or AIProvider.get_provider(VISION_MODEL)).upper() msg = ( "ℹ️ *Sobre o PsAgentAI*\n\n" f"Nome: PsAgentAI\n" f"Versão: {__version__}\n" f"Autor: {__author__}\n" f"Contato: {__email__}\n" f"Licença: {__license__}\n\n" "Configuração atual:\n" f"Provider: {provider}\n" f"Modelo: {DEFAULT_MODEL}\n" f"Visão: {vision_provider} / {VISION_MODEL}\n" f"Busca na internet: {'ativada' if WEB_SEARCH_ENABLED else 'desativada'}\n" f"Reconhecimento de voz: ativado\n" f"Horário: servidor ({get_timezone()})\n\n" "Este sistema foi desenvolvido por Syspanel.com.br e se você deseja alguma personalização, entre em contato conosco pelo email contato@syspanel.com.br\n\n" "Todos os direitos reservados." ) await update.message.reply_text(msg, parse_mode=ParseMode.HTML) IA_CONFIG_STEPS = ["provider", "model"] IA_CONFIG_ANSWERS = {} CONFIGIA_PROVIDERS = [ ("openrouter", "OpenRouter"), ("stepfun", "StepFun"), ("qwencloud", "QwenCloud"), ("llamacpp", "llama.cpp (local)"), ] CONFIGIA_MODELS = { "openrouter": [ "poolside/laguna-s-2.1:free", "inclusionai/ling-3.0-flash", "inclusionai/ling-2.6-flash", "nvidia/nemotron-3-ultra-550b-a55b:free", "poolside/laguna-xs-2.1:free", "mistralai/mistral-nemo", "minimax/minimax-m3:free", "xiaomi/mimo-v2.6-flash", "deepseek/deepseek-v4-flash-0731", "google/gemini-2.0-flash-lite-preview-02-05:free", "meta-llama/llama-4-maverick:free", "custom...", ], "stepfun": [ "step-3.7-flash", "step-2-16k", "custom...", ], "qwencloud": [ "qwen-turbo", "qwen-plus", "qwen-max", "custom...", ], "llamacpp": [ "local-model", "custom...", ], } def _update_env_var(key: str, value: str, env_path: str = ".env") -> None: path = Path(env_path) if path.exists(): text = path.read_text(encoding="utf-8") else: text = "" pattern = re.compile(rf"^{re.escape(key)}=.*$", flags=re.MULTILINE) if pattern.search(text): text = pattern.sub(f"{key}={value}", text) else: if text and not text.endswith("\n"): text += "\n" text += f"{key}={value}\n" path.write_text(text, encoding="utf-8") async def veria_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) provider = (AI_PROVIDER or AIProvider.get_provider(DEFAULT_MODEL)).upper() msg = ( "🤖 Configuração de IA atual:\n" f"Provider: {provider}\n" f"Modelo default: {DEFAULT_MODEL}" ) await update.message.reply_text(msg) async def configia_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) global IA_CONFIG_STEPS, IA_CONFIG_ANSWERS, is_configia IA_CONFIG_STEPS = ["provider", "model"] IA_CONFIG_ANSWERS = {} is_configia = True keyboard = InlineKeyboardMarkup( [[InlineKeyboardButton(label, callback_data=f"configia_provider_{value}")] for value, label in CONFIGIA_PROVIDERS] ) await update.message.reply_text( "Vamos configurar o provider e o modelo de IA.\n" "Selecione o provider:", reply_markup=keyboard, ) async def configia_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query if not query: return await query.answer() data = (query.data or "").strip() global IA_CONFIG_STEPS, IA_CONFIG_ANSWERS, is_configia if not is_configia: return if data.startswith("configia_provider_"): provider = data.replace("configia_provider_", "").strip() if provider not in {p for p, _ in CONFIGIA_PROVIDERS}: return IA_CONFIG_ANSWERS["provider"] = provider _update_env_var("AI_PROVIDER", provider) models = CONFIGIA_MODELS.get(provider, ["custom..."]) keyboard = InlineKeyboardMarkup( [[InlineKeyboardButton(model, callback_data=f"configia_model_{model}")] for model in models] ) await query.edit_message_text( f"Provider selecionado: {provider}\n\nSelecione o modelo:", reply_markup=keyboard, ) return if data.startswith("configia_model_"): raw_model = data.replace("configia_model_", "").strip() if raw_model == "custom...": await query.edit_message_text("Envie o identificador do modelo que deseja usar.") IA_CONFIG_STEPS = ["custom_model"] return model = raw_model provider = IA_CONFIG_ANSWERS.get("provider", AIProvider.get_provider(model)) _update_env_var("DEFAULT_MODEL", model) global DEFAULT_MODEL, AI_PROVIDER DEFAULT_MODEL = model AI_PROVIDER = provider IA_CONFIG_STEPS = [] IA_CONFIG_ANSWERS = {} is_configia = False _reload_env() await query.edit_message_text( "Configuração de IA atualizada.\n" f"Provider: {(AI_PROVIDER or AIProvider.get_provider(DEFAULT_MODEL)).upper()}\n" f"Modelo: {DEFAULT_MODEL}" ) return async def handle_configia_message(update: Update, context: ContextTypes.DEFAULT_TYPE): global IA_CONFIG_STEPS, IA_CONFIG_ANSWERS, is_configia if not IA_CONFIG_STEPS: return False step = IA_CONFIG_STEPS[0] text = (update.message.text or "").strip() if not text: return False if step == "custom_model": model = text.strip() if not model: await update.message.reply_text("Modelo inválido. Envie o identificador do modelo.") return True provider = IA_CONFIG_ANSWERS.get("provider", AIProvider.get_provider(model)) _update_env_var("DEFAULT_MODEL", model) global DEFAULT_MODEL, AI_PROVIDER DEFAULT_MODEL = model AI_PROVIDER = provider IA_CONFIG_STEPS = [] IA_CONFIG_ANSWERS = {} is_configia = False _reload_env() await update.message.reply_text( "Configuração de IA atualizada.\n" f"Provider: {(AI_PROVIDER or AIProvider.get_provider(DEFAULT_MODEL)).upper()}\n" f"Modelo: {DEFAULT_MODEL}" ) return True return False VISION_CONFIG_STEPS: List[str] = [] VISION_CONFIG_ANSWERS: Dict[str, Any] = {} VISION_CONFIG_PROVIDERS = [ ("openrouter", "OpenRouter"), ("stepfun", "StepFun"), ("qwencloud", "QwenCloud"), ] VISION_CONFIG_MODELS = { "openrouter": [ "meta-llama/llama-4-maverick-17b-128e-instruct", "meta-llama/llama-4-maverick:free", "google/gemini-2.0-flash-exp:free", "custom...", ], "stepfun": [ "step-2-16k", "custom...", ], "qwencloud": [ "qwen-vl-plus", "qwen-vl-max", "custom...", ], } async def veriavision_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) provider = (VISION_PROVIDER or AIProvider.get_provider(VISION_MODEL)).upper() msg = ( "👁 Configuração de visão atual:\n" f"Provider: {provider}\n" f"Modelo: {VISION_MODEL}" ) await update.message.reply_text(msg) async def configiavision_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await auto_detect_chat_id(update) global VISION_CONFIG_STEPS, VISION_CONFIG_ANSWERS VISION_CONFIG_STEPS = ["provider", "model"] VISION_CONFIG_ANSWERS = {} keyboard = InlineKeyboardMarkup( [[InlineKeyboardButton(label, callback_data=f"vision_provider_{value}")] for value, label in VISION_CONFIG_PROVIDERS] ) await update.message.reply_text( "Vamos configurar o modelo de visão (análise de imagens).\n" "Selecione o provider:", reply_markup=keyboard, ) async def configiavision_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query if not query: return await query.answer() data = (query.data or "").strip() global VISION_CONFIG_STEPS, VISION_CONFIG_ANSWERS if data.startswith("vision_provider_"): provider = data.replace("vision_provider_", "").strip() if provider not in {p for p, _ in VISION_CONFIG_PROVIDERS}: return VISION_CONFIG_ANSWERS["provider"] = provider _update_env_var("VISION_PROVIDER", provider) models = VISION_CONFIG_MODELS.get(provider, ["custom..."]) keyboard = InlineKeyboardMarkup( [[InlineKeyboardButton(model, callback_data=f"vision_model_{model}")] for model in models] ) await query.edit_message_text( f"Provider selecionado: {provider}\n\nSelecione o modelo de visão:", reply_markup=keyboard, ) return if data.startswith("vision_model_"): raw_model = data.replace("vision_model_", "").strip() if raw_model == "custom...": await query.edit_message_text("Envie o identificador do modelo de visão que deseja usar.") VISION_CONFIG_STEPS = ["custom_model"] return model = raw_model provider = VISION_CONFIG_ANSWERS.get("provider", AIProvider.get_provider(model)) _update_env_var("VISION_MODEL", model) _update_env_var("VISION_PROVIDER", provider) global VISION_MODEL, VISION_PROVIDER VISION_MODEL = model VISION_PROVIDER = provider VISION_CONFIG_STEPS = [] VISION_CONFIG_ANSWERS = {} _reload_env() await query.edit_message_text( "Modelo de visão atualizado.\n" f"Provider: {(VISION_PROVIDER or AIProvider.get_provider(VISION_MODEL)).upper()}\n" f"Modelo: {VISION_MODEL}" ) return async def handle_configiavision_message(update: Update, context: ContextTypes.DEFAULT_TYPE): global VISION_CONFIG_STEPS, VISION_CONFIG_ANSWERS if not VISION_CONFIG_STEPS: return False step = VISION_CONFIG_STEPS[0] text = (update.message.text or "").strip() if not text: return False if step == "custom_model": model = text.strip() if not model: await update.message.reply_text("Modelo inválido. Envie o identificador do modelo.") return True provider = VISION_CONFIG_ANSWERS.get("provider", AIProvider.get_provider(model)) _update_env_var("VISION_MODEL", model) _update_env_var("VISION_PROVIDER", provider) global VISION_MODEL, VISION_PROVIDER VISION_MODEL = model VISION_PROVIDER = provider VISION_CONFIG_STEPS = [] VISION_CONFIG_ANSWERS = {} _reload_env() await update.message.reply_text( "Modelo de visão atualizado.\n" f"Provider: {(VISION_PROVIDER or AIProvider.get_provider(VISION_MODEL)).upper()}\n" f"Modelo: {VISION_MODEL}" ) return True return False # ===================== MAIN ===================== def _build_fake_update(chat_id: int, text: str) -> Update: chat = Chat(id=chat_id, type="private") user = User(id=chat_id, is_bot=False, first_name="User") message = Message( message_id=0, date=datetime.now(), chat=chat, from_user=user, text=text, ) return Update(update_id=0, message=message) def main(): if not TELEGRAM_BOT_TOKEN: logger.error("TELEGRAM_BOT_TOKEN not set in .env file!") sys.exit(1) if not TELEGRAM_CHAT_ID: logger.info("TELEGRAM_CHAT_ID not set. Using auto-detected chat_id if available.") ensure_contexts_dir() ensure_uploads_dir() global app_instance, proactive_task app_instance = Application.builder().token(TELEGRAM_BOT_TOKEN).connect_timeout(30).read_timeout(30).write_timeout(30).build() app_instance.add_handler(CommandHandler("start", start_command)) app_instance.add_handler(CommandHandler("ajuda", help_command)) app_instance.add_handler(CommandHandler("help", help_command)) app_instance.add_handler(CommandHandler("manualdeuso", manualdeuso_command)) app_instance.add_handler(CommandHandler("perfil", profile_command)) app_instance.add_handler(CommandHandler("editarperfil", start_edit_user_profile)) app_instance.add_handler(CommandHandler("agentprofile", agent_profile_command)) app_instance.add_handler(CommandHandler("editaragentprofile", start_edit_agent_profile)) app_instance.add_handler(CommandHandler("configagentprofile", start_configagentprofile)) app_instance.add_handler(CommandHandler("reset", reset_command)) app_instance.add_handler(CommandHandler("limpar", limpar_command)) app_instance.add_handler(CommandHandler("removerassunto", removerassunto_command)) app_instance.add_handler(CommandHandler("newconfig", newconfig_command)) app_instance.add_handler(CommandHandler("cancelar", cancelar_command)) app_instance.add_handler(CommandHandler("incluirnomeuperfil", incluirnomeuperfil_command)) app_instance.add_handler(CommandHandler("incluirnoperfildoagent", incluirnoperfildoagent_command)) app_instance.add_handler(CommandHandler("onboarding", onboarding_command)) app_instance.add_handler(CommandHandler("debug", debug_command)) app_instance.add_handler(CommandHandler("reload", reload_command)) app_instance.add_handler(CommandHandler("about", about_command)) app_instance.add_handler(CommandHandler("veria", veria_command)) app_instance.add_handler(CommandHandler("configia", configia_command)) app_instance.add_handler(CommandHandler("atualizarsistema", atualizarsistema_command)) app_instance.add_handler(CommandHandler("fusohorario", fusohorario_command)) app_instance.add_handler(CommandHandler("agendamento", agendamento_command)) app_instance.add_handler(CommandHandler("veragendamento", veragendamento_command)) app_instance.add_handler(CommandHandler("useragenda", useragenda_command)) app_instance.add_handler(CommandHandler("veruseragenda", veruseragenda_command)) app_instance.add_handler(CommandHandler("adicionaracoesauto", adicionaracoesauto_command)) app_instance.add_handler(CommandHandler("adicionaratividade", adicionaratividade_command)) app_instance.add_handler(CommandHandler("editaratividade", editaratividade_command)) app_instance.add_handler(CommandHandler("excluiratividade", excluiratividade_command)) app_instance.add_handler(CommandHandler("uploadfile", uploadfile_command)) app_instance.add_handler(CommandHandler("listfiles", listfiles_command)) app_instance.add_handler(CommandHandler("baixar", baixar_command)) app_instance.add_handler(CommandHandler("veriavision", veriavision_command)) app_instance.add_handler(CommandHandler("configiavision", configiavision_command)) app_instance.add_handler(CallbackQueryHandler(configia_callback, pattern=r"^configia_(provider|model)_")) app_instance.add_handler(CallbackQueryHandler(configiavision_callback, pattern=r"^vision_(provider|model)_")) app_instance.add_handler(MessageHandler(filters.VOICE, voice_message_handler)) app_instance.add_handler(MessageHandler(filters.PHOTO, photo_message_handler)) app_instance.add_handler(MessageHandler(filters.Document.ALL, handle_file_upload)) app_instance.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, message_handler)) async def post_init(app: Application): global proactive_task proactive_task = asyncio.create_task(proactive_loop()) effective_chat_id = get_effective_chat_id() logger.info(f"post_init iniciado. TELEGRAM_CHAT_ID configurado: {bool(TELEGRAM_CHAT_ID)} valor={TELEGRAM_CHAT_ID!r} detectado: {effective_chat_id}") if effective_chat_id: try: chat_id = int(effective_chat_id) logger.info(f"Iniciando /start automatico para chat_id={chat_id}") fake_update = _build_fake_update(chat_id, "/start") class SimpleContext: bot = app.bot context = SimpleContext() await start_command(fake_update, context) logger.info("/start automatico executado com sucesso") except Exception as e: err_str = str(e) if "Chat not found" in err_str: logger.info("Auto /start ignorado: chat ainda nao iniciado pelo usuario.") else: logger.error(f"Erro ao executar /start automatico: {e}", exc_info=True) app_instance.post_init = post_init logger.info("PsAgentAI está iniciando... ") app_instance.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main()