Files
PsAgentIA/psagentia_agent.py
marco e489b6a2e2 feat: initial project setup with psagentia agent implementation
Add core Python agent with configuration management, environment setup,
and deployment documentation. Includes user/agent profiles, startup
scripts, and dependency management for the psagentia system.
2026-09-03 16:11:46 -03:00

3571 lines
138 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
PsAgentIA - Personal Agent IA Conversacional
Sistema de Personal Agent IA Conversacional via Telegram
"""
import os
import sys
import json
import random
import re
import asyncio
import logging
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
from telegram.ext import (
Application,
CommandHandler,
MessageHandler,
filters,
ContextTypes,
)
from telegram.constants import ParseMode
# 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")
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"))
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
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; PsAgentIA/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:
config = load_config()
tz = config.get("timezone", "")
return tz or "UTC"
def set_timezone(tz: str) -> bool:
try:
if ZoneInfo is not None:
ZoneInfo(tz)
config = load_config()
config["timezone"] = tz
save_config(config)
return True
except Exception:
return False
def _get_timezone_info(tz_name: str):
if ZoneInfo is None:
return None
try:
return ZoneInfo(tz_name)
except Exception:
return None
# ===================== 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__)), "uploadsfiles")
UPLOADS_META_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "uploads_metadata.json")
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_uploaded_file(query: str) -> Optional[Dict[str, Any]]:
meta = load_uploads_metadata()
q = query.lower().strip()
for filename, info in meta.items():
if q in filename.lower() or q in info.get("original_name", "").lower():
return info
return None
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 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": "PsAgentIA",
"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 PsAgentIA 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", "PsAgentIA")
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", "PsAgentIA"))
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 = update.message.text.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:
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/PsAgentIA"
headers["X-Title"] = "PsAgentIA"
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 =====================
PSAGENTIA_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", "PsAgentIA")
def build_system_prompt(profile: Dict[str, Any]) -> str:
agent = load_agent_profile()
agent_name = agent.get("name", "PsAgentIA")
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 = PSAGENTIA_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']}"
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", "PsAgentIA")
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", "PsAgentIA")
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)
text = update.message.text.strip()
parts = text.split(maxsplit=1)
if len(parts) < 2 or not parts[1].strip():
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"🕐 Fuso horário atual: {tz}\n"
f"Hora atual: {time_str}\n\n"
f"Para alterar: /fusohorario <fuso>\n"
f"Exemplo: /fusohorario America/Sao_Paulo\n\n"
f"Fusos comuns:\n"
f"• America/Sao_Paulo\n"
f"• America/New_York\n"
f"• Europe/London\n"
f"• Europe/Lisbon\n"
f"• Asia/Tokyo\n"
f"• UTC"
)
return
new_tz = parts[1].strip()
if set_timezone(new_tz):
tz_info = _get_timezone_info(new_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"✅ Fuso horário alterado para {new_tz}\n"
f"Hora atual: {time_str}"
)
else:
await update.message.reply_text(
f"❌ Fuso horário inválido: {new_tz}\n\n"
f"Use /fusohorario para ver o fuso atual.\n"
f"Exemplos válidos: America/Sao_Paulo, America/New_York, Europe/London, UTC, etc."
)
# ===================== 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_psagentia_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_psagentia_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", "PsAgentIA")
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", "PsAgentIA")
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_psagentia_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)
if not files:
await update.message.reply_text(
"📂 Nenhum arquivo enviado ainda.\n"
"Envie um .pdf, .txt, .docx, .png ou .jpg para eu analisar!"
)
return
lines = ["📂 **Seus arquivos enviados:**", ""]
for filename, info in files.items():
orig = info.get("original_name", filename)
summary = info.get("summary", "")
preview = summary[:80] + "..." if len(summary) > 80 else summary
lines.append("📄 **" + orig + "**")
if preview:
lines.append(" _" + preview + "_")
lines.append("")
lines.append("Pergunte sobre qualquer arquivo pelo nome!")
await update.message.reply_text("\n".join(lines))
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
safe_name = datetime.now().strftime("%Y%m%d_%H%M%S") + "_" + file_id[:8] + ext
filepath = os.path.join(UPLOADS_DIR, safe_name)
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(safe_name, original_name, filepath, update.effective_chat.id)
meta = load_uploads_metadata()
info = meta.get(safe_name, {})
summary = info.get("summary", "")
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 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)
/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
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', 'PsAgentIA')}
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', 'PsAgentIA')}
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 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_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 = {}
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):
await auto_detect_chat_id(update)
user_message = update.message.text.strip()
if not user_message:
return
global is_onboarding, is_newconfig, is_configagentprofile, is_edit_user_profile, is_edit_agent_profile, is_activity_crud, is_agendamento, is_useragenda
if is_activity_crud:
if await handle_activity_crud_answer(update, context):
return
if is_onboarding:
handled = await handle_onboarding_answer(update, context)
if handled:
return
if is_newconfig:
handled = await handle_newconfig_answer(update, context)
if handled:
return
if is_configagentprofile:
handled = await handle_configagentprofile_answer(update, context)
if handled:
return
if is_edit_user_profile:
handled = await handle_edit_user_profile_answer(update, context)
if handled:
return
if is_edit_agent_profile:
handled = await handle_edit_agent_profile_answer(update, context)
if handled:
return
if is_agendamento:
handled = await handle_agendamento_answer(update, context)
if handled:
return
if is_useragenda:
handled = await handle_useragenda_answer(update, context)
if handled:
return
profile_updated = await handle_profile_update_if_triggered(update, context)
if profile_updated:
return
add_to_context("user", user_message)
# Check if user query references an uploaded file
file_context = ""
uploads = load_uploads_metadata()
for fname, finfo in uploads.items():
orig = finfo.get("original_name", "")
if fname.lower() in user_message.lower() or orig.lower() in user_message.lower():
full = finfo.get("full_text", "")
if full:
file_context = "\n\n[CONTEUDO DO ARQUIVO: " + orig + "]\n" + full[:3000] + "\n[FIM DO ARQUIVO]"
if len(full) > 3000:
file_context += "\n[O conteudo foi truncado para 3000 caracteres]"
break
if file_context:
modified = user_message + file_context
else:
modified = user_message
response = await get_psagentia_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 PsAgentIA:\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!")
# ===================== 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("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("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("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(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("PsAgentIA está iniciando... ")
app_instance.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()