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.
3571 lines
138 KiB
Python
Executable File
3571 lines
138 KiB
Python
Executable File
#!/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()
|