- Add VISION_MODEL and VISION_PROVIDER env options with openrouter default and provider auto-detection from the model id - Add /veriavision, /configiavision and /about commands - Support natural-language editing of files inside workspace/ and a /baixar command to download them - Replace config-based timezone with system-local time detection - Add module version/author/license metadata - Document development conventions in new RULES.md and update README.md/DEPLOY.md with the new commands and env vars
5009 lines
193 KiB
Python
Executable File
5009 lines
193 KiB
Python
Executable File
#!/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'<a[^>]+class="result__a"[^>]*href="([^"]+)"[^>]*>(.*?)</a>'
|
||
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 <nome>` 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 <nome_do_arquivo>")
|
||
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 <texto> — 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 <texto> — 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 <nome> — 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 <assunto> — 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 <texto> - 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 <texto> - 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 <assunto> - 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 <assunto>\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()
|