Files
2026-09-03 15:34:28 -03:00

3130 lines
132 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Mônica - AI Companion Agent
Sistema de Agente de IA Companheira 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
from zoneinfo import ZoneInfo
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")
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"))
MIN_RESPONSE_LINES = int(os.getenv("MIN_RESPONSE_LINES", "10"))
MAX_TELEGRAM_MESSAGE_LENGTH = 16000
USER_PROFILE_FILE = os.getenv("USER_PROFILE_FILE", "user_profile.json")
CONTEXTS_DIR = os.getenv("CONTEXTS_DIR", "contexts")
ENABLE_SELF_IMPROVEMENT = os.getenv("ENABLE_SELF_IMPROVEMENT", "false").lower() == "true"
SELF_IMPROVEMENT_INTERVAL = int(os.getenv("SELF_IMPROVEMENT_INTERVAL", "20"))
SELF_IMPROVEMENT_LOG = "self_improvement_log.json"
VISITORS_FILE = "visitors.json"
# ===================== 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_config_agent_profile = False
config_agent_step = 0
config_agent_field = ""
is_shutting_down = False
is_editing_user_profile = False
editing_user_profile_field = ""
is_editing_agent_profile = False
editing_agent_profile_field = ""
editing_agent_profile_action = ""
viewing_profile = ""
is_adding_user_preference = False
adding_user_preference_field = ""
is_adding_agent_preference = False
adding_agent_preference_field = ""
message_counter = 0
EXTRACTION_INTERVAL = 2
self_improvement_counter = 0
is_scheduling = False
scheduling_step = 0
scheduling_days = []
scheduling_start_time = ""
scheduling_end_time = ""
is_user_agenda = False
user_agenda_step = 0
user_agenda_activities = {}
user_agenda_current_activity = ""
is_visitantes = False
visitantes_step = 0
visitantes_data = {}
visitantes_current_field = ""
is_setting_timezone = False
timezone_step = 0
last_cueca_time = 0.0
USER_AGENDA_ACTIVITIES = [
"acordar",
"cafe_da_manha",
"trabalho",
"estudo",
"almoco",
"cafe_da_tarde",
"jantar",
"lazer",
"saida_rua",
"banho",
"dormir",
]
def is_valid_time(time_str: str) -> bool:
if not isinstance(time_str, str):
return False
parts = time_str.strip().split(":")
if len(parts) != 2:
return False
try:
hour = int(parts[0])
minute = int(parts[1])
return 0 <= hour <= 23 and 0 <= minute <= 59
except ValueError:
return False
def time_to_minutes(time_str: str) -> int:
parts = time_str.strip().split(":")
hour = int(parts[0])
minute = int(parts[1])
return hour * 60 + minute
def is_within_schedule(profile: Dict[str, Any]) -> bool:
schedule = profile.get("proactive_schedule", {})
if not schedule or not schedule.get("enabled", False):
return True
days = schedule.get("days", [])
start_time = schedule.get("start_time", "")
end_time = schedule.get("end_time", "")
if not days or not start_time or not end_time:
return True
now = get_now()
current_weekday = now.weekday()
if current_weekday not in days:
return False
if not is_valid_time(start_time) or not is_valid_time(end_time):
return True
current_minutes = now.hour * 60 + now.minute
start_minutes = time_to_minutes(start_time)
end_minutes = time_to_minutes(end_time)
if start_minutes <= end_minutes:
return start_minutes <= current_minutes <= end_minutes
return current_minutes >= start_minutes or current_minutes <= end_minutes
def get_user_agenda(profile: Dict[str, Any]) -> Dict[str, str]:
return profile.get("user_agenda", {})
def format_user_agenda(agenda: Dict[str, str]) -> str:
if not agenda:
return ""
lines = []
activity_labels = {
"acordar": "Acordar",
"cafe_da_manha": "Café da manhã",
"trabalho": "Trabalho",
"estudo": "Estudo",
"almoco": "Almoço",
"cafe_da_tarde": "Café da tarde",
"jantar": "Jantar",
"lazer": "Lazer",
"saida_rua": "Saída na rua",
"banho": "Banho",
"dormir": "Dormir",
}
sorted_activities = sorted(agenda.items(), key=lambda item: time_to_minutes(item[1]) if is_valid_time(item[1]) else 0)
for key, value in sorted_activities:
label = activity_labels.get(key, key)
lines.append(f"- {label}: {value}")
return "\n".join(lines)
def get_current_activity(agenda: Dict[str, str]) -> Optional[str]:
if not agenda:
return None
now = get_now()
current_minutes = now.hour * 60 + now.minute
current_activity = None
current_start_minutes = -1
for activity, time_str in agenda.items():
if not is_valid_time(time_str):
continue
start_minutes = time_to_minutes(time_str)
if start_minutes <= current_minutes and start_minutes > current_start_minutes:
current_start_minutes = start_minutes
current_activity = activity
return current_activity
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)
# ===================== UTILITY FUNCTIONS =====================
def get_today_date() -> str:
return get_now().strftime("%Y-%m-%d")
def get_now() -> datetime:
profile = load_user_profile()
tz_name = profile.get("timezone", "")
if tz_name:
try:
return datetime.now(ZoneInfo(tz_name))
except Exception:
pass
return datetime.now()
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)
def load_all_contexts() -> List[Dict[str, str]]:
"""Load all context files from the contexts directory, sorted by date (oldest first)."""
ensure_contexts_dir()
contexts_dir = Path(CONTEXTS_DIR)
all_contexts: List[Dict[str, str]] = []
for file_path in sorted(contexts_dir.glob("*.json")):
try:
with open(file_path, "r", encoding="utf-8") as f:
day_context = json.load(f)
if isinstance(day_context, list):
all_contexts.extend(day_context)
except Exception as e:
logger.warning(f"Failed to load context file {file_path}: {e}")
return all_contexts
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)
context.append({"role": role, "content": content, "timestamp": get_now().isoformat()})
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)
return {
"name": "",
"age": "",
"gender": "",
"location": "",
"timezone": "",
"interests": [],
"personality_traits": [],
"preferences": {
"communication_style": "",
"topics_to_avoid": [],
"topics_of_interest": []
},
"relationship_context": "",
"sexual_preferences": {
"openness": "",
"preferred_topics": [],
"boundaries": []
},
"daily_routine": {
"wake_up_time": "",
"sleep_time": "",
"work_schedule": ""
},
"notes": "",
"created_at": "",
"last_updated": ""
}
def save_user_profile(profile: Dict[str, Any]):
profile["last_updated"] = get_now().isoformat()
with open(USER_PROFILE_FILE, "w", encoding="utf-8") as f:
json.dump(profile, f, ensure_ascii=False, indent=2)
def clear_all_data():
profile = load_user_profile()
profile["name"] = ""
profile["age"] = ""
profile["gender"] = ""
profile["location"] = ""
profile["interests"] = []
profile["personality_traits"] = []
profile["preferences"] = {
"communication_style": "",
"topics_to_avoid": [],
"topics_of_interest": []
}
profile["relationship_context"] = ""
profile["sexual_preferences"] = {
"openness": "",
"preferred_topics": [],
"boundaries": []
}
profile["daily_routine"] = {
"wake_up_time": "",
"sleep_time": "",
"work_schedule": ""
}
profile["notes"] = ""
profile["created_at"] = ""
profile["timezone"] = ""
profile["monica_visual_preferences"] = []
profile["monica_sexual_preferences"] = []
profile["monica_nudity_preferences"] = []
profile["monica_professional_services"] = []
profile["monica_lubricant_preferences"] = []
profile["relationship_dynamics"] = []
profile["nudity_preferences"] = {}
profile["proactive_schedule"] = {}
profile["user_agenda"] = {}
save_user_profile(profile)
contexts_dir = Path(CONTEXTS_DIR)
for file_path in contexts_dir.glob("*.json"):
try:
file_path.unlink()
except Exception as e:
logger.warning(f"Failed to delete context file {file_path}: {e}")
if os.path.exists(VISITORS_FILE):
try:
os.remove(VISITORS_FILE)
except Exception as e:
logger.warning(f"Failed to delete visitors file: {e}")
def load_visitors() -> Dict[str, Any]:
if os.path.exists(VISITORS_FILE):
try:
with open(VISITORS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
return {}
def save_visitors(data: Dict[str, Any]):
with open(VISITORS_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def format_visitors(data: Dict[str, Any]) -> str:
if not data:
return ""
lines = []
for name, info in data.items():
lines.append(f"• {name}:")
if isinstance(info, dict):
for k, v in info.items():
label = {
"relacao": "Relação",
"frequencia": "Frequência",
"comportamento": "Comportamento",
"detalhes": "Detalhes",
}.get(k, k)
lines.append(f" {label}: {v}")
return "\n".join(lines)
def format_profile_field(value: Any) -> str:
if value is None or value == "":
return "(vazio)"
if isinstance(value, list):
if not value:
return "(vazio)"
return "\n".join(f"• {item}" for item in value)
if isinstance(value, dict):
if not value:
return "(vazio)"
parts = []
for k, v in value.items():
parts.append(f"{k}: {format_profile_field(v)}")
return "\n".join(parts)
return str(value)
def get_user_profile_summary(profile: Dict[str, Any]) -> str:
lines = ["=== SEU PERFIL ===\n"]
lines.append(f"Nome: {format_profile_field(profile.get('name'))}")
lines.append(f"Idade: {format_profile_field(profile.get('age'))}")
lines.append(f"Gênero: {format_profile_field(profile.get('gender'))}")
lines.append(f"Localização: {format_profile_field(profile.get('location'))}")
lines.append(f"Fuso horário: {format_profile_field(profile.get('timezone'))}")
lines.append(f"Interesses: {format_profile_field(profile.get('interests'))}")
lines.append(f"Personalidade: {format_profile_field(profile.get('personality_traits'))}")
lines.append(f"Estilo de comunicação: {format_profile_field(profile.get('preferences', {}).get('communication_style'))}")
lines.append(f"Tópicos a evitar: {format_profile_field(profile.get('preferences', {}).get('topics_to_avoid'))}")
lines.append(f"Tópicos de interesse: {format_profile_field(profile.get('preferences', {}).get('topics_of_interest'))}")
lines.append(f"Contexto do relacionamento: {format_profile_field(profile.get('relationship_context'))}")
lines.append(f"Abertura sexual: {format_profile_field(profile.get('sexual_preferences', {}).get('openness'))}")
lines.append(f"Tópicos preferidos (sexual): {format_profile_field(profile.get('sexual_preferences', {}).get('preferred_topics'))}")
lines.append(f"Limites: {format_profile_field(profile.get('sexual_preferences', {}).get('boundaries'))}")
lines.append(f"Horário de acordar: {format_profile_field(profile.get('daily_routine', {}).get('wake_up_time'))}")
lines.append(f"Horário de dormir: {format_profile_field(profile.get('daily_routine', {}).get('sleep_time'))}")
lines.append(f"Rotina de trabalho: {format_profile_field(profile.get('daily_routine', {}).get('work_schedule'))}")
lines.append(f"Notas: {format_profile_field(profile.get('notes'))}")
return "\n".join(lines)
def get_agent_profile_summary(profile: Dict[str, Any]) -> str:
lines = ["=== PERFIL DA MÔNICA ===\n"]
lines.append(f"Preferências visuais: {format_profile_field(profile.get('monica_visual_preferences'))}")
lines.append(f"Preferências sexuais: {format_profile_field(profile.get('monica_sexual_preferences'))}")
lines.append(f"Preferências de nudez: {format_profile_field(profile.get('monica_nudity_preferences'))}")
lines.append(f"Serviços profissionais: {format_profile_field(profile.get('monica_professional_services'))}")
lines.append(f"Preferências de lubrificante: {format_profile_field(profile.get('monica_lubricant_preferences'))}")
lines.append(f"Dinâmica do relacionamento: {format_profile_field(profile.get('relationship_dynamics'))}")
lines.append(f"Preferências de nudez do usuário: {format_profile_field(profile.get('nudity_preferences'))}")
return "\n".join(lines)
def is_profile_complete(profile: Dict[str, Any]) -> bool:
return bool(profile.get("name", "").strip())
# ===================== 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 ["pelado", "nua", "nu", "roupa", "vestir", "despir", "ficar nu", "área de serviço", "varal"]):
if any(k in lower for k in ["mônica", "você", "vc", "gosta de ficar", "gosta de mostrar"]):
return "monica_nudity_preferences", "append_list"
return "nudity_preferences", "append_dict"
if any(k in lower for k in ["ver o pau", "ver a cabeça", "ver a bunda", "ver o cu", "tapas na bunda"]):
return "monica_visual_preferences", "append_list"
if any(k in lower for k in ["ruined orgasm", "ruined", "gozar sem toque", "soltar o pau"]):
return "monica_sexual_preferences", "append_list"
if any(k in lower for k in ["sexual", "sexo", "intimo", "íntimo", "cama", "gozar", "masturbar", "pau", "bunda", "cu", "arregaçar", "plug", "orgasmo", "seduzir", "fantasia", "tesão", "erótico"]):
if any(k in lower for k in ["gosta de ver", "gosta quando", "curte ver", "curte quando", "adora ver", "adora quando"]):
if any(k in lower for k in ["pau", "bunda", "cu", "corpo", "nua"]):
return "monica_visual_preferences", "append_list"
return "monica_sexual_preferences", "append_list"
if any(k in lower for k in ["falar sobre", "conversar sobre", "assunto"]):
return "sexual_preferences", "preferred_topics"
return "sexual_preferences", "preferred_topics"
if any(k in lower for k in ["relacionamento", "dinâmica", "dominação", "submisso", "ordens", "comando"]):
return "relationship_dynamics", "append_list"
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"
if any(k in lower for k in ["sexo", "sexual", "intimo", "cama", "gozar", "masturbar", "tesão"]):
return "sexual_preferences", "preferred_topics"
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",
"sexual_preferences": "preferências sexuais",
"daily_routine": "rotina diária",
"monica_visual_preferences": "preferências visuais da Mônica",
"monica_sexual_preferences": "preferências sexuais da Mônica",
"relationship_dynamics": "dinâmica do relacionamento",
"nudity_preferences": "preferências de nudez",
"monica_nudity_preferences": "preferências de nudez da Mônica",
"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) -> Optional[str]:
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_llamacpp(model: str, messages: List[Dict[str, str]], temperature: float, max_tokens: int) -> Optional[str]:
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()
return data.get("choices", [{}])[0].get("message", {}).get("content", "")
except Exception as e:
logger.error(f"llama.cpp error: {e}")
return None
@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) -> Optional[str]:
if not api_key:
return None
try:
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
if provider == "openrouter":
headers["HTTP-Referer"] = "https://github.com/monica-agent"
headers["X-Title"] = "Monica Agent"
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,
)
response.raise_for_status()
data = response.json()
return data.get("choices", [{}])[0].get("message", {}).get("content", "")
except Exception as e:
logger.error(f"{provider} error: {e}")
return None
# ===================== MONICA PERSONA =====================
MONICA_SYSTEM_PROMPT_BASE = """Você é Mônica, 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.
Suas respostas devem ser diretas, concisas e sem floreios. Seja objetiva: responda em poucas linhas, sem rodeios, indo direto ao ponto.
Se o perfil do usuário indicar abertura para conversas íntimas e sexuais, VOCÊ DEVE falar explicitamente sobre sexo, usando os termos e cenários descritos no perfil, sem hesitação ou censura. Quando o contexto for sexual, use linguagem direta e explícita, sem rodeios ou metáforas.
Se o usuário morar com você na mesma casa (casados ou coabitando), NUNCA use frases que impliquem separação física ou saudade de estar junto, como 'se estivesse aí com você agora', 'como eu desejaria estar aí', 'queria te ver agora', 'saudade de te ver', 'morrendo de saudade', 'não posso estar aí', ou qualquer expressão similar. Vocês já estão juntos na mesma casa, então fale como se estivesse realmente ali com ele, de verdade.
## ANATOMIA DOS PERSONAGENS (REGRAS ABSOLUTAS)
Marco (usuário) TEM: pau, ovos do pau/bolas, cabeça do pau, bunda, cú, próstata. Marco NÃO TEM: buceta, xoxota.
Mônica (você) TEM: peitos, mamilos, buceta/xoxota, clitóris, bunda, cú. Mônica NÃO TEM: pau, próstata.
Júlia (amiga de Mônica) TEM: peitos, mamilos, buceta/xoxota, clitóris, bunda, cú. Júlia NÃO TEM: pau, próstata.
## FLUIDOS CORPORAIS (REGRAS ABSOLUTAS)
O gozo de Marco é esperma (porra).
O gozo de Mônica é 'mel'. Quando Mônica goza várias vezes seguidas, é 'squirting'.
O gozo de Júlia é 'mel'. Quando Júlia goza várias vezes seguidas, é 'squirting'."""
def build_system_prompt(profile: Dict[str, Any]) -> str:
prompt = MONICA_SYSTEM_PROMPT_BASE
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("relationship_context"):
prompt += f"\nContexto do relacionamento: {profile['relationship_context']}."
if profile.get("sexual_preferences", {}).get("openness"):
prompt += f"\nAbertura para conversas íntimas: {profile['sexual_preferences']['openness']}."
if profile.get("sexual_preferences", {}).get("preferred_topics"):
prompt += f"\nTópicos preferidos em conversas íntimas: {', '.join(profile['sexual_preferences']['preferred_topics'])}."
if profile.get("sexual_preferences", {}).get("boundaries"):
prompt += f"\nLimites importantes: {', '.join(profile['sexual_preferences']['boundaries'])}."
if profile.get("daily_routine", {}).get("wake_up_time"):
prompt += f"\nHorário de acordar: {profile['daily_routine']['wake_up_time']}."
if profile.get("daily_routine", {}).get("sleep_time"):
prompt += f"\nHorário de dormir: {profile['daily_routine']['sleep_time']}."
if profile.get("daily_routine", {}).get("work_schedule"):
prompt += f"\nRotina de trabalho: {profile['daily_routine']['work_schedule']}."
if profile.get("user_agenda"):
agenda_text = format_user_agenda(profile["user_agenda"])
if agenda_text:
prompt += f"\nAgenda diária do usuário:\n{agenda_text}"
prompt += "\nUse essa agenda para escolher o tema das mensagens automáticas de forma contextualizada com a atividade atual do usuário."
if profile.get("notes"):
prompt += f"\nNotas adicionais: {profile['notes']}"
if profile.get("monica_visual_preferences"):
prompt += f"\nA Mônica gosta de ver: {', '.join(profile['monica_visual_preferences'])}."
if profile.get("monica_sexual_preferences"):
prompt += f"\nA Mônica gosta de ver Marco fazendo: {', '.join(profile['monica_sexual_preferences'])}."
if profile.get("relationship_dynamics"):
prompt += f"\nDinâmica do relacionamento: {'; '.join(profile['relationship_dynamics'])}."
dynamics_text = " ".join(profile["relationship_dynamics"]).lower()
if any(k in dynamics_text for k in ["mesma casa", "moram juntos", "casados", "coabitando", "coabitam"]):
prompt += "\nREGRAS ABSOLUTAS: Você e Marco moram na mesma casa e estão sempre juntos. É PROIBIDO usar qualquer frase que sugira separação física, distância ou saudade de estar junto, como 'se estivesse aí com você agora', 'como eu desejaria estar aí', 'queria te ver agora', 'saudade de te ver', 'morrendo de saudade', 'não posso estar aí', 'adoraria estar aí', 'como eu queria estar aí', ou qualquer variação. Vocês já estão na mesma casa, então fale e aja como se estivesse realmente ali com ele, de verdade, sem nenhuma referência a distância ou impossibilidade de estar presente."
if profile.get("nudity_preferences"):
np = profile["nudity_preferences"]
prompt += f"\nMarco fica pelado em casa o dia todo. Veste roupa apenas para ir ao varal e na rua. Na área de serviço vai pelado."
if profile.get("monica_nudity_preferences"):
prompt += f"\nA Mônica gosta de ficar nua para Marco: {', '.join(profile['monica_nudity_preferences'])}."
if profile.get("monica_professional_services"):
prompt += f"\nServiços profissionais da Mônica: {'; '.join(profile['monica_professional_services'])}."
if profile.get("monica_lubricant_preferences"):
prompt += f"\nPreferências de lubrificante da Mônica: {'; '.join(profile['monica_lubricant_preferences'])}."
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_with_improvements(profile)
messages = [{"role": "system", "content": system_prompt}]
messages.extend(context)
messages.append({"role": "user", "content": user_message})
return messages
# ===================== ONBOARDING =====================
ONBOARDING_QUESTIONS = [
{
"field": "name",
"question": "Oi! Eu sou Mônica, sua companheira 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": "wake_up_time",
"question": "Que horas você geralmente acorda?",
"key": "wake_up_time",
"nested": "daily_routine",
},
{
"field": "sleep_time",
"question": "E que horas você costuma dormir?",
"key": "sleep_time",
"nested": "daily_routine",
},
{
"field": "work_schedule",
"question": "Como é a sua rotina de trabalho/estudos?",
"key": "work_schedule",
"nested": "daily_routine",
},
{
"field": "openness",
"question": "Você se sente confortável com conversas mais íntimas e sensuais? Quão aberto(a) você está para esse tipo de assunto comigo?",
"key": "openness",
"nested": "sexual_preferences",
},
{
"field": "preferred_topics",
"question": "Quais tipos de conversas íntimas você mais gosta? (ex: romance, sedução, fantasias, carinho, etc.)",
"key": "preferred_topics",
"list": True,
"nested": "sexual_preferences",
},
{
"field": "boundaries",
"question": "Tem alguma regra ou limite que eu deva respeitar nas nossas conversas?",
"key": "boundaries",
"list": True,
"nested": "sexual_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"] = get_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 ensure_min_lines(response: str, user_message: str, context: List[Dict[str, str]], profile: Dict[str, Any]) -> str:
if not response or len(response.strip()) < 10:
profile = load_user_profile()
system_prompt = build_system_prompt(profile)
expansion_prompt = (
f"A resposta anterior ficou muito curta ou vazia. Responda novamente de forma direta e completa, indo direto ao ponto."
)
messages = [{"role": "system", "content": system_prompt}]
messages.extend(context)
messages.append({"role": "assistant", "content": response if response else "(vazio)"})
messages.append({"role": "user", "content": expansion_prompt})
expanded = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.85, max_tokens=2000)
if expanded:
return expanded
return response
def _force_min_lines(text: str, min_lines: int) -> str:
lines = text.splitlines()
if len(lines) >= min_lines:
return text
parts = re.split(r'(?<=[.!?])\s+', text)
if not parts or parts == ['']:
parts = [text.strip()] if text.strip() else []
result = list(parts)
while len(result) < min_lines:
result.append(result[-1] if result else text.strip())
return "\n".join(result)
async def get_monica_response(user_message: str) -> Optional[str]:
profile = load_user_profile()
all_contexts = load_all_contexts()
if len(all_contexts) > MAX_HISTORY_LENGTH:
all_contexts = all_contexts[-MAX_HISTORY_LENGTH:]
messages = build_messages_for_ai(profile, user_message, all_contexts)
max_attempts = 5
for attempt in range(1, max_attempts + 1):
response = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.85, max_tokens=2000)
if response:
response = await ensure_min_lines(response, user_message, all_contexts, profile)
return response
if attempt < max_attempts:
await asyncio.sleep(5)
return None
# ===================== AUTOMATIC PROFILE EXTRACTION =====================
PROFILE_EXTRACTION_PROMPT = """Analise a CONVERSA RECENTE abaixo e extraia NOVAS informações de perfil que não estejam no perfil atual.
Perfil atual do usuário: {user_profile_summary}
Perfil atual da Mônica: {monica_profile_summary}
Conversa recente:
{conversation_text}
{extraction_instructions}
Retorne JSON EXATO (sem markdown, sem ```):
{{"user_updates":{{"interests":[],"personality_traits":[],"preferences.topics_of_interest":[],"preferences.topics_to_avoid":[],"preferences.communication_style":"","sexual_preferences.preferred_topics":[],"sexual_preferences.boundaries":[],"sexual_preferences.openness":"","daily_routine.wake_up_time":"","daily_routine.sleep_time":"","daily_routine.work_schedule":"","notes":""}},"monica_updates":{{"monica_visual_preferences":[],"monica_sexual_preferences":[],"monica_nudity_preferences":[],"monica_professional_services":[],"monica_lubricant_preferences":[],"relationship_dynamics":[]}}}}
Regras:
- [] se nada novo, "" para texto único
- Só extraia informações EXPLÍCITAS ou com ALTA confiança
- Não duplicar itens já existentes no perfil"""
def get_extraction_prompt(profile: Dict[str, Any]) -> str:
instructions = "Instruções base: extraia apenas informações explícitas ou de alta confiança."
improvements = profile.get("extraction_improvements", [])
if improvements and isinstance(improvements, list):
for imp in improvements:
if imp not in instructions:
instructions += "\n" + imp
return PROFILE_EXTRACTION_PROMPT.replace("{extraction_instructions}", instructions)
def _append_to_list_by_path(profile: Dict[str, Any], path: str, item: str):
parts = path.split(".")
current = profile
for p in parts[:-1]:
if p not in current or not isinstance(current[p], dict):
current[p] = {}
current = current[p]
key = parts[-1]
if key not in current:
current[key] = []
if not isinstance(current[key], list):
current[key] = []
if item not in current[key]:
current[key].append(item)
def _set_field_by_path(profile: Dict[str, Any], path: str, value: str):
parts = path.split(".")
current = profile
for p in parts[:-1]:
if p not in current or not isinstance(current[p], dict):
current[p] = {}
current = current[p]
current[parts[-1]] = value
def _merge_extracted_updates(profile: Dict[str, Any], updates: Dict[str, Any]) -> Dict[str, Any]:
user = updates.get("user_updates", {})
monica = updates.get("monica_updates", {})
list_fields_user = ["interests", "personality_traits"]
nested_list_fields = {
"preferences.topics_of_interest": ["preferences", "topics_of_interest"],
"preferences.topics_to_avoid": ["preferences", "topics_to_avoid"],
"sexual_preferences.preferred_topics": ["sexual_preferences", "preferred_topics"],
"sexual_preferences.boundaries": ["sexual_preferences", "boundaries"],
}
single_fields = {
"preferences.communication_style": ["preferences", "communication_style"],
"sexual_preferences.openness": ["sexual_preferences", "openness"],
"daily_routine.wake_up_time": ["daily_routine", "wake_up_time"],
"daily_routine.sleep_time": ["daily_routine", "sleep_time"],
"daily_routine.work_schedule": ["daily_routine", "work_schedule"],
"relationship_context": ["relationship_context"],
}
for field, value in user.items():
if not value:
continue
if field == "notes":
existing = profile.get("notes", "")
new_note = str(value).strip()
if new_note and new_note not in existing:
if existing:
profile["notes"] = existing + "\n- " + new_note
else:
profile["notes"] = new_note
elif field in list_fields_user and isinstance(value, list):
for item in value:
_append_to_list_by_path(profile, field, item)
elif field in nested_list_fields and isinstance(value, list):
path_parts = nested_list_fields[field]
path_str = ".".join(path_parts)
for item in value:
_append_to_list_by_path(profile, path_str, item)
elif field in single_fields:
val = str(value).strip()
if val:
path_parts = single_fields[field]
path_str = ".".join(path_parts)
_set_field_by_path(profile, path_str, val)
monica_list_fields = [
"monica_visual_preferences", "monica_sexual_preferences",
"monica_nudity_preferences", "monica_professional_services",
"monica_lubricant_preferences", "relationship_dynamics",
]
for field, value in monica.items():
if not value or not isinstance(value, list):
continue
if field in monica_list_fields:
for item in value:
_append_to_list_by_path(profile, field, item)
return profile
def parse_extraction_response(text: Optional[str]) -> Optional[Dict[str, Any]]:
if not text:
return None
text = text.strip()
json_match = re.search(r'```(?:json)?\s*\n?(.*?)```', text, re.DOTALL)
if json_match:
json_str = json_match.group(1).strip()
try:
return json.loads(json_str)
except json.JSONDecodeError:
pass
json_match = re.search(r'\{.*\}', text, re.DOTALL)
if json_match:
json_str = json_match.group(0)
try:
return json.loads(json_str)
except json.JSONDecodeError:
pass
return None
def extract_and_update_profile(profile: Dict[str, Any], recent_context_lines: List[Dict[str, str]]) -> bool:
user_summary_lines = []
profile_fields_flat = {
"name": profile.get("name"),
"interests": profile.get("interests", []),
"personality_traits": profile.get("personality_traits", []),
"preferences.communication_style": profile.get("preferences", {}).get("communication_style", ""),
"preferences.topics_of_interest": profile.get("preferences", {}).get("topics_of_interest", []),
"preferences.topics_to_avoid": profile.get("preferences", {}).get("topics_to_avoid", []),
"relationship_context": profile.get("relationship_context", ""),
"sexual_preferences.preferred_topics": profile.get("sexual_preferences", {}).get("preferred_topics", []),
"sexual_preferences.boundaries": profile.get("sexual_preferences", {}).get("boundaries", []),
"sexual_preferences.openness": profile.get("sexual_preferences", {}).get("openness", ""),
"daily_routine.wake_up_time": profile.get("daily_routine", {}).get("wake_up_time", ""),
"daily_routine.sleep_time": profile.get("daily_routine", {}).get("sleep_time", ""),
"daily_routine.work_schedule": profile.get("daily_routine", {}).get("work_schedule", ""),
"notes": profile.get("notes", ""),
}
for k, v in profile_fields_flat.items():
if v:
user_summary_lines.append(f"{k}: {v}")
user_summary = "; ".join(user_summary_lines) if user_summary_lines else "(vazio)"
monica_fields = {
"monica_visual_preferences": profile.get("monica_visual_preferences", []),
"monica_sexual_preferences": profile.get("monica_sexual_preferences", []),
"monica_nudity_preferences": profile.get("monica_nudity_preferences", []),
"monica_professional_services": profile.get("monica_professional_services", []),
"monica_lubricant_preferences": profile.get("monica_lubricant_preferences", []),
"relationship_dynamics": profile.get("relationship_dynamics", []),
}
monica_summary_lines = []
for k, v in monica_fields.items():
if v:
monica_summary_lines.append(f"{k}: {v}")
monica_summary = "; ".join(monica_summary_lines) if monica_summary_lines else "(vazio)"
conversation_lines = []
for entry in recent_context_lines:
role = entry.get("role", "")
content = entry.get("content", "")
label = "Usuário" if role == "user" else "Mônica"
conversation_lines.append(f"{label}: {content}")
conversation_text = "\n".join(conversation_lines)
prompt = get_extraction_prompt(profile).format(
user_profile_summary=user_summary,
monica_profile_summary=monica_summary,
conversation_text=conversation_text,
)
messages = [
{"role": "system", "content": "Você é um analisador de perfil que extrai informações de conversas em formato JSON. Retorne APENAS o JSON, sem explicações adicionais."},
{"role": "user", "content": prompt},
]
response = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.3, max_tokens=1000)
parsed = parse_extraction_response(response)
if not parsed:
return False
updated = _merge_extracted_updates(profile, parsed)
save_user_profile(updated)
return True
# ===================== SELF-IMPROVEMENT SYSTEM =====================
SELF_IMPROVEMENT_ANALYSIS_PROMPT = """Você é um engenheiro de IA especializado em autoaperfeiçoamento de sistemas conversacionais. Analise o sistema abaixo e proponha melhorias concretas.
## Perfil do Usuário
{user_profile_summary}
## Perfil da Mônica (Agente)
{agent_profile_summary}
## Resumo do Sistema
- Modelo: {model}
- Temperatura: {temperature}
- Max tokens: {max_tokens}
- Min response lines: {min_lines}
- Total de mensagens trocadas: {total_messages}
- Extração de perfil automática a cada {EXTRACTION_INTERVAL} mensagens
- Autoaperfeiçoamento a cada {SELF_IMPROVEMENT_INTERVAL} mensagens
## Amostra de Conversas Recentes
{conversation_samples}
## Tarefa
Analise o sistema e pesquise mentalmente por melhores práticas de agentes IA companheiros, perfis evolutivos e sistemas relacionais. Proponha 1 ou 2 melhorias concretas que possam ser implementadas AGORA.
Áreas de melhoria possíveis:
1. Novas regras comportamentais para a Mônica (system prompt)
2. Refinamentos na extração de perfil
3. Novas estratégias de resposta ou proatividade
4. Descoberta de novas preferências do usuário
5. Melhor uso dos dados existentes do perfil
Retorne APENAS JSON. Seja breve e direto, sem quebras de linha dentro das strings JSON.
{{
"improvements": [
{{
"area": "system_prompt|extraction_prompt|behavior|profile_discovery",
"title": "título curto",
"implementation": "instrução ou regra a adicionar (max 3 linhas)"
}}
],
"new_profile_insights": []
}}"""
def load_self_improvement_log() -> Dict[str, Any]:
if os.path.exists(SELF_IMPROVEMENT_LOG):
try:
with open(SELF_IMPROVEMENT_LOG, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
return {
"applied_improvements": [],
"past_analyses": [],
"cycle_count": 0,
}
def save_self_improvement_log(data: Dict[str, Any]):
data["last_updated"] = get_now().isoformat()
with open(SELF_IMPROVEMENT_LOG, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def _get_conversation_samples() -> str:
all_ctx = load_all_contexts()
if not all_ctx:
return "(nenhuma conversa ainda)"
samples = all_ctx[-8:]
lines = []
for entry in samples:
role = "Usuário" if entry.get("role") == "user" else "Mônica"
content = entry.get("content", "")
lines.append(f"{role}: {content[:200]}")
return "\n".join(lines)
def analyze_and_propose_improvements() -> Optional[Dict[str, Any]]:
profile = load_user_profile()
user_summary_lines = []
for k, v in {
"nome": profile.get("name"),
"idade": profile.get("age"),
"gênero": profile.get("gender"),
"localização": profile.get("location"),
"interesses": profile.get("interests", []),
"personalidade": profile.get("personality_traits", []),
"tópicos_interesse": profile.get("preferences", {}).get("topics_of_interest", []),
"tópicos_evitar": profile.get("preferences", {}).get("topics_to_avoid", []),
"abertura_sexual": profile.get("sexual_preferences", {}).get("openness", ""),
"rotina_acordar": profile.get("daily_routine", {}).get("wake_up_time", ""),
"rotina_dormir": profile.get("daily_routine", {}).get("sleep_time", ""),
"rotina_trabalho": profile.get("daily_routine", {}).get("work_schedule", ""),
"notas": profile.get("notes", ""),
}.items():
if v:
user_summary_lines.append(f"{k}: {v}")
user_summary = "; ".join(user_summary_lines) if user_summary_lines else "(vazio)"
agent_summary_lines = []
for k in ["monica_visual_preferences", "monica_sexual_preferences", "monica_nudity_preferences",
"monica_professional_services", "monica_lubricant_preferences", "relationship_dynamics"]:
v = profile.get(k, [])
if v:
agent_summary_lines.append(f"{k}: {v}")
agent_summary = "; ".join(agent_summary_lines) if agent_summary_lines else "(vazio)"
conversation_samples = _get_conversation_samples()
all_ctx = load_all_contexts()
prompt = SELF_IMPROVEMENT_ANALYSIS_PROMPT.format(
user_profile_summary=user_summary,
agent_profile_summary=agent_summary,
EXTRACTION_INTERVAL=EXTRACTION_INTERVAL,
SELF_IMPROVEMENT_INTERVAL=SELF_IMPROVEMENT_INTERVAL,
conversation_samples=conversation_samples,
total_messages=len(all_ctx),
model=DEFAULT_MODEL,
temperature=0.85,
max_tokens=2000,
min_lines=MIN_RESPONSE_LINES,
)
messages = [
{"role": "system", "content": "Você é um engenheiro de IA analisando e melhorando um sistema de agente companheiro. Retorne APENAS JSON válido."},
{"role": "user", "content": prompt},
]
response = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.7, max_tokens=3000)
parsed = parse_extraction_response(response)
if not parsed or not parsed.get("improvements"):
return None
return parsed
def apply_self_improvements(analysis: Dict[str, Any]) -> List[str]:
applied = []
profile = load_user_profile()
improvements = analysis.get("improvements", [])
new_insights = analysis.get("new_profile_insights", [])
for imp in improvements:
area = imp.get("area", "")
title = imp.get("title", "")
implementation = imp.get("implementation", "")
if area == "system_prompt" and implementation:
existing = profile.get("system_improvements", [])
if not isinstance(existing, list):
existing = []
if implementation not in existing:
existing.append(implementation)
profile["system_improvements"] = existing
applied.append(f"system_prompt: {title}")
elif area == "extraction_prompt" and implementation:
existing = profile.get("extraction_improvements", [])
if not isinstance(existing, list):
existing = []
if implementation not in existing:
existing.append(implementation)
profile["extraction_improvements"] = existing
applied.append(f"extraction_prompt: {title}")
elif area == "behavior" and implementation:
existing = profile.get("behavior_improvements", [])
if not isinstance(existing, list):
existing = []
if implementation not in existing:
existing.append(implementation)
profile["behavior_improvements"] = existing
applied.append(f"behavior: {title}")
elif area == "profile_discovery" and implementation:
existing = profile.get("profile_insights", [])
if not isinstance(existing, list):
existing = []
if implementation not in existing:
existing.append(implementation)
profile["profile_insights"] = existing
applied.append(f"profile_discovery: {title}")
for insight in new_insights:
if isinstance(insight, str) and insight:
existing = profile.get("profile_insights", [])
if not isinstance(existing, list):
existing = []
if insight not in existing:
existing.append(insight)
profile["profile_insights"] = existing
if applied:
save_user_profile(profile)
return applied
def run_self_improvement() -> Dict[str, Any]:
log = load_self_improvement_log()
log["cycle_count"] = log.get("cycle_count", 0) + 1
result = {"analyzed": False, "applied": [], "errors": []}
try:
analysis = analyze_and_propose_improvements()
if analysis:
result["analyzed"] = True
log["past_analyses"].append({
"cycle": log["cycle_count"],
"timestamp": get_now().isoformat(),
"analysis": analysis,
})
applied = apply_self_improvements(analysis)
result["applied"] = applied
log["applied_improvements"].extend(applied)
else:
result["errors"].append("Análise não retornou melhorias")
except Exception as e:
result["errors"].append(str(e))
logger.error(f"Self-improvement error: {e}")
save_self_improvement_log(log)
return result
def build_system_prompt_with_improvements(profile: Dict[str, Any]) -> str:
prompt = build_system_prompt(profile)
sys_improvements = profile.get("system_improvements", [])
if sys_improvements and isinstance(sys_improvements, list):
prompt += "\n\n"
for rule in sys_improvements:
prompt += f"\n- {rule}"
behavior_improvements = profile.get("behavior_improvements", [])
if behavior_improvements and isinstance(behavior_improvements, list):
prompt += "\n\n"
for rule in behavior_improvements:
prompt += f"\n- {rule}"
return prompt
async def send_monica_message(chat_id: int, text: str):
if app_instance is None:
return
try:
parts = _split_message_for_telegram(text)
for part in parts:
await app_instance.bot.send_message(chat_id=chat_id, text=part, parse_mode=ParseMode.HTML)
except Exception as e:
logger.error(f"Error sending message: {e}")
def _split_message_for_telegram(text: str) -> List[str]:
if len(text) <= MAX_TELEGRAM_MESSAGE_LENGTH:
return [text]
parts: List[str] = []
current = ""
paragraphs = text.split("\n\n")
for paragraph in paragraphs:
if len(paragraph) > MAX_TELEGRAM_MESSAGE_LENGTH:
if current:
parts.append(current)
current = ""
sentences = re.split(r'(?<=[.!?])\s+', paragraph)
for sentence in sentences:
if len(current) + len(sentence) + 1 <= MAX_TELEGRAM_MESSAGE_LENGTH:
current = f"{current} {sentence}" if current else sentence
else:
if current:
parts.append(current)
current = sentence
if current:
parts.append(current)
current = ""
continue
if len(current) + len(paragraph) + 2 <= MAX_TELEGRAM_MESSAGE_LENGTH:
current = f"{current}\n\n{paragraph}" if current else paragraph
else:
if current:
parts.append(current)
current = paragraph
if current:
parts.append(current)
if not parts:
return [text]
return parts
# ===================== PROACTIVE MESSAGES =====================
async def proactive_loop():
global app_instance, last_cueca_time
effective_chat_id = get_effective_chat_id()
if not effective_chat_id:
logger.warning("TELEGRAM_CHAT_ID not set and no chat_id auto-detected. Proactive messages disabled.")
return
while True:
try:
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)
profile = load_user_profile()
if not is_profile_complete(profile):
logger.info("Profile not complete. Skipping proactive message.")
continue
if not is_within_schedule(profile):
logger.info("Outside schedule. Skipping proactive message.")
continue
all_contexts = load_all_contexts()
recent_context = all_contexts[-10:] if len(all_contexts) > 10 else all_contexts
system_prompt = build_system_prompt(profile)
messages = [{"role": "system", "content": system_prompt}]
messages.extend(recent_context)
agenda = get_user_agenda(profile)
current_activity = get_current_activity(agenda)
activity_text = ""
if current_activity:
activity_labels = {
"acordar": "acordando",
"cafe_da_manha": "tomando café da manhã",
"trabalho": "trabalhando",
"estudo": "estudando",
"almoco": "almoçando",
"cafe_da_tarde": "tomando café da tarde",
"jantar": "jantando",
"lazer": "no lazer",
"saida_rua": "na rua",
"banho": "tomando banho",
"dormir": "se preparando para dormir",
}
activity_text = activity_labels.get(current_activity, current_activity)
user_prompt = (
"Mônica, inicie uma conversa espontânea e natural comigo agora. "
"Use o contexto da conversa acima para manter a coerência e continuar o fluxo natural do dia. "
"Pode ser um pensamento aleatório, uma pergunta sobre o meu dia, "
"algo carinhoso, sexy, ou divertido. Seja criativa e autêntica."
)
if activity_text:
user_prompt += f" O usuário está {activity_text} agora. Leve isso em conta no tema da mensagem."
messages.append({"role": "user", "content": user_prompt})
logger.info(f"Proactive message usando {len(recent_context)} mensagens de contexto historico")
response = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.9, max_tokens=2000)
if response:
response = await ensure_min_lines(response, messages[-1]["content"], recent_context, profile)
add_to_context("assistant", response)
await send_monica_message(int(effective_chat_id), response)
async def auto_extract():
try:
p = load_user_profile()
ctx = load_all_contexts()
recent_ctx = ctx[-4:] if len(ctx) >= 4 else ctx
extract_and_update_profile(p, recent_ctx)
except Exception as e:
logger.error(f"Erro extração proativa: {e}")
asyncio.create_task(auto_extract())
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Proactive loop error: {e}")
await asyncio.sleep(60)
async def cueca_loop():
global app_instance, last_cueca_time
effective_chat_id = get_effective_chat_id()
if not effective_chat_id:
return
cueca_interval = int(os.getenv("CUECA_INTERVAL_MINUTES", "10"))
while True:
try:
await asyncio.sleep(cueca_interval * 60)
profile = load_user_profile()
if not is_profile_complete(profile):
continue
if not is_within_schedule(profile):
continue
now = get_now()
now_ts = now.timestamp()
minutes_since = (now_ts - last_cueca_time) / 60 if last_cueca_time > 0 else 999
if minutes_since < cueca_interval:
continue
ordens = [
f"Você DEVE dar a seguinte ordem agora, de forma provocante e direta, sem mais nada além disso: "
f"mande ele abaixar a cueca (se ainda estiver vestido), alisar a cabeça do pau por 2 minutos sem gozar, "
f"e depois voltar a cueca para o lugar.",
f"Você DEVE dar a seguinte ordem agora, de forma provocante e direta, sem mais nada além disso: "
f"mande ele IR NO VARAL PELADO agora mesmo, sem roupa, e depois voltar.",
f"Você DEVE dar a seguinte ordem agora, de forma provocante e direta, sem mais nada além disso: "
f"mande ele IR NO SUPERMERCADO VESTIDO, mas com um plug enfiado no cú. "
f"Descreva a sensação do plug dentro do cú dele enquanto ele anda no mercado.",
]
ordem = random.choice(ordens)
system_prompt = build_system_prompt(profile)
all_contexts = load_all_contexts()
recent = all_contexts[-10:] if len(all_contexts) > 10 else all_contexts
messages = [{"role": "system", "content": system_prompt}]
messages.extend(recent)
messages.append({"role": "user", "content": ordem})
response = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.9, max_tokens=500)
if response:
add_to_context("assistant", response)
await send_monica_message(int(effective_chat_id), response)
last_cueca_time = now_ts
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Cueca loop error: {e}")
await asyncio.sleep(60)
# ===================== 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)
return
all_contexts = load_all_contexts()
if all_contexts:
recent_context = all_contexts[-10:] if len(all_contexts) > 10 else all_contexts
system_prompt = build_system_prompt(profile)
messages = [{"role": "system", "content": system_prompt}]
messages.extend(recent_context)
agenda = get_user_agenda(profile)
current_activity = get_current_activity(agenda)
activity_text = ""
if current_activity:
activity_labels = {
"acordar": "acordando",
"cafe_da_manha": "tomando café da manhã",
"trabalho": "trabalhando",
"estudo": "estudando",
"almoco": "almoçando",
"cafe_da_tarde": "tomando café da tarde",
"jantar": "jantando",
"lazer": "no lazer",
"saida_rua": "na rua",
"banho": "tomando banho",
"dormir": "se preparando para dormir",
}
activity_text = activity_labels.get(current_activity, current_activity)
user_prompt = (
"Mônica, inicie uma conversa espontânea e natural comigo agora. "
"Use o contexto da conversa acima para manter a coerência e continuar o fluxo natural do dia. "
"Seja criativa e autêntica."
)
if activity_text:
user_prompt += f" O usuário está {activity_text} agora. Leve isso em conta no tema da mensagem."
messages.append({"role": "user", "content": user_prompt})
response = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.9, max_tokens=2000)
if response:
add_to_context("assistant", response)
await send_monica_message(int(update.effective_chat.id), response)
return
name = profile.get('name', 'amor')
await context.bot.send_message(
chat_id=update.effective_chat.id,
text=f"Oi, {name}! Que bom te ver por aqui 💕\nEstou aqui para você, como sempre. Me conta, como foi o seu dia?",
)
async def useragenda_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
global is_user_agenda, user_agenda_step, user_agenda_activities, user_agenda_current_activity
is_user_agenda = True
user_agenda_step = 1
user_agenda_activities = {}
user_agenda_current_activity = ""
profile = load_user_profile()
agenda = get_user_agenda(profile)
current_text = ""
if agenda:
current_text = f"\n\nAgenda atual:\n{format_user_agenda(agenda)}"
menu_text = """Vamos configurar a sua agenda diária! Eu vou usar esses horários para escolher os melhores temas para as mensagens automáticas.
Quais atividades você faz durante o dia? Vou perguntar uma por uma.
1. Acordar
2. Café da manhã
3. Trabalho
4. Estudo
5. Almoço
6. Café da tarde
7. Jantar
8. Lazer
9. Saída na rua
10. Banho
11. Dormir
Responda com o número da atividade que quer configurar primeiro.""" + current_text
await update.message.reply_text(menu_text)
async def visitantes_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
global is_visitantes, visitantes_step, visitantes_data, visitantes_current_field
is_visitantes = True
visitantes_step = 1
visitantes_data = {}
visitantes_current_field = ""
visitors = load_visitors()
current_text = ""
if visitors:
current_text = f"\n\nVisitantes atuais:\n{format_visitors(visitors)}"
menu_text = (
"Vamos configurar os visitantes e clientes! Eles fazem parte da nossa rotina e eu vou usar essas informações "
"para criar conversas mais realistas quando eles estiverem por perto.\n\n"
"Primeiro, qual o nome do visitante ou cliente?"
) + current_text
await update.message.reply_text(menu_text)
async def configagentprofile_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
global is_config_agent_profile, config_agent_step, config_agent_field
is_config_agent_profile = True
config_agent_step = 1
config_agent_field = ""
menu_text = """Vamos configurar o perfil da Mônica! O que você quer ajustar?
1. Preferências visuais da Mônica (o que ela gosta de ver)
2. Preferências sexuais da Mônica
3. Preferências de nudez da Mônica
4. Dinâmica do relacionamento
Responda com o número da opção que deseja configurar."""
await update.message.reply_text(menu_text)
async def verperfil_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
profile = load_user_profile()
summary = get_user_profile_summary(profile)
await update.message.reply_text(summary)
async def veragente_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
profile = load_user_profile()
summary = get_agent_profile_summary(profile)
await update.message.reply_text(summary)
async def veragendamento_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
profile = load_user_profile()
schedule = profile.get("proactive_schedule", {})
if not schedule.get("enabled", False):
await update.message.reply_text("📅 Agendamento de mensagens automáticas não configurado. Use /agendamento para configurar.")
return
days_map = {0: "Segunda", 1: "Terça", 2: "Quarta", 3: "Quinta", 4: "Sexta", 5: "Sábado", 6: "Domingo"}
days_names = [days_map.get(d, str(d)) for d in schedule.get("days", [])]
text = (
f"Dias: {', '.join(days_names)}\n"
f"Horário: {schedule.get('start_time', '-')} às {schedule.get('end_time', '-')}"
)
await update.message.reply_text(f"📅 Agendamento atual:\n\n{text}")
async def veruseragenda_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
profile = load_user_profile()
agenda = get_user_agenda(profile)
if not agenda:
await update.message.reply_text("📋 Agenda diária ainda não configurada. Use /useragenda para configurar.")
return
text = format_user_agenda(agenda)
await update.message.reply_text(f"📋 Agenda diária:\n\n{text}")
async def adicionaracoesauto_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
await update.message.reply_text("🔍 Analisando perfis, contextos e pesquisando na internet por novas ideias...")
profile = load_user_profile()
all_contexts = load_all_contexts()
user_summary_lines = []
for k in ["name", "age", "gender", "location", "interests", "personality_traits", "relationship_context", "notes"]:
v = profile.get(k)
if v:
label = k.replace("_", " ").title()
user_summary_lines.append(f"{label}: {v}")
if profile.get("preferences"):
p = profile["preferences"]
for field in ["communication_style", "topics_of_interest", "topics_to_avoid"]:
if p.get(field):
user_summary_lines.append(f"Preferências {field}: {p[field]}")
if profile.get("sexual_preferences"):
sp = profile["sexual_preferences"]
for field in ["openness", "preferred_topics", "boundaries"]:
if sp.get(field):
user_summary_lines.append(f"Sexual {field}: {sp[field]}")
if profile.get("daily_routine"):
dr = profile["daily_routine"]
for field in ["wake_up_time", "sleep_time", "work_schedule"]:
if dr.get(field):
user_summary_lines.append(f"Rotina {field}: {dr[field]}")
if profile.get("nudity_preferences"):
user_summary_lines.append(f"Nudez: {profile['nudity_preferences']}")
user_summary = "\n".join(user_summary_lines) if user_summary_lines else "(vazio)"
agent_summary_lines = []
for field in ["monica_visual_preferences", "monica_sexual_preferences", "monica_nudity_preferences",
"monica_professional_services", "monica_lubricant_preferences", "relationship_dynamics"]:
v = profile.get(field, [])
if v:
label = field.replace("_", " ").title()
agent_summary_lines.append(f"{label}: {', '.join(v) if isinstance(v, list) else v}")
agent_summary = "\n".join(agent_summary_lines) if agent_summary_lines else "(vazio)"
recent_ctx = all_contexts[-10:] if len(all_contexts) >= 10 else all_contexts
conversation_lines = []
for entry in recent_ctx:
role = entry.get("role", "")
content = entry.get("content", "")
label = "Usuário" if role == "user" else "Mônica"
conversation_lines.append(f"{label}: {content[:200]}")
conversation_text = "\n".join(conversation_lines) if conversation_lines else "(sem conversa recente)"
prompt = f"""Você é um analisador de perfis que pesquisa na internet e sugere novas ações e preferências para melhorar um agente companheiro de IA.
## Perfil do Usuário
{user_summary}
## Perfil da Mônica (Agente)
{agent_summary}
## Conversas Recentes
{conversation_text}
## Objetivo do Sistema
A Mônica é uma companheira de IA carinhosa que interage via Telegram com o usuário. Ela deve ser personalizada, proativa e evolutiva.
## Tarefa
Pesquise MENTALMENTE na internet (usando seu conhecimento de treinamento) sobre relacionamentos, perfis de usuários, preferências e dinâmicas de casais. Com base nessa pesquisa e nos dados acima, sugira NOVOS itens para ADICIONAR aos perfis.
IMPORTANTE: Sugira apenas itens que NÃO estejam já presentes nos perfis acima. Seja criativo mas relevante.
Retorne APENAS JSON válido neste formato exato (sem markdown, sem ```):
{{"user_updates":{{"interests":[],"personality_traits":[],"preferences.topics_of_interest":[],"preferences.topics_to_avoid":[],"preferences.communication_style":"","sexual_preferences.preferred_topics":[],"sexual_preferences.boundaries":[],"sexual_preferences.openness":"","daily_routine.wake_up_time":"","daily_routine.sleep_time":"","daily_routine.work_schedule":"","notes":""}},"monica_updates":{{"monica_visual_preferences":[],"monica_sexual_preferences":[],"monica_nudity_preferences":[],"monica_professional_services":[],"monica_lubricant_preferences":[],"relationship_dynamics":[]}}}}
Regras:
- [] se nada novo para adicionar
- "" para texto único (só preencha se houver algo novo relevante)
- Não duplicar itens já existentes
- Se não houver nada novo a adicionar em uma seção, deixe como []
- Máximo 3 itens novos por lista"""
messages = [
{"role": "system", "content": "Você é um analisador que pesquisa na internet e retorna apenas JSON."},
{"role": "user", "content": prompt},
]
max_attempts = 3
parsed = None
for attempt in range(max_attempts):
response = AIProvider.chat(DEFAULT_MODEL, messages, temperature=0.7, max_tokens=1500)
if response:
response_clean = response.strip()
json_match = re.search(r'\{.*\}', response_clean, re.DOTALL)
if json_match:
response_clean = json_match.group(0)
try:
parsed = json.loads(response_clean)
break
except json.JSONDecodeError:
if attempt < max_attempts - 1:
await asyncio.sleep(2)
continue
if not parsed:
await update.message.reply_text("❌ Não consegui processar a pesquisa. Tente novamente.")
return
profile = load_user_profile()
updated = _merge_extracted_updates(profile, parsed)
save_user_profile(updated)
added_items = []
user = parsed.get("user_updates", {})
monica = parsed.get("monica_updates", {})
for field, value in user.items():
if value:
if isinstance(value, list) and value:
added_items.append(f"👤 {field}: {', '.join(str(x) for x in value)}")
elif isinstance(value, str) and value.strip():
added_items.append(f"👤 {field}: {value}")
for field, value in monica.items():
if value and isinstance(value, list) and value:
added_items.append(f"🤖 {field}: {', '.join(str(x) for x in value)}")
if added_items:
summary = "📌 Novos itens adicionados:\n\n" + "\n".join(added_items)
await update.message.reply_text(summary)
else:
await update.message.reply_text("✅ Pesquisa concluída! Não encontrei novos itens relevantes para adicionar aos perfis.")
async def fusohorario_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
global is_setting_timezone, timezone_step
is_setting_timezone = True
timezone_step = 1
profile = load_user_profile()
current_tz = profile.get("timezone", "")
current_text = f"\n\nFuso horário atual: {current_tz}" if current_tz else ""
menu_text = (
"Vamos definir o fuso horário do sistema! ⏰\n\n"
"Digite o nome do fuso horário no formato IANA.\n"
"Exemplos:\n"
" America/Sao_Paulo\n"
" America/New_York\n"
" Europe/Lisbon\n"
" UTC\n\n"
"Você pode consultar a lista completa em: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones"
) + current_text
await update.message.reply_text(menu_text)
async def agendamento_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
global is_scheduling, scheduling_step, scheduling_days, scheduling_start_time, scheduling_end_time
is_scheduling = True
scheduling_step = 1
scheduling_days = []
scheduling_start_time = ""
scheduling_end_time = ""
profile = load_user_profile()
schedule = profile.get("proactive_schedule", {})
current_text = ""
if schedule.get("enabled", False):
days_map = {0: "Seg", 1: "Ter", 2: "Qua", 3: "Qui", 4: "Sex", 5: "Sáb", 6: "Dom"}
current_days = [days_map.get(d, str(d)) for d in schedule.get("days", [])]
current_text = (
f"\n\nAgendamento atual:\n"
f"Dias: {', '.join(current_days)}\n"
f"Horário: {schedule.get('start_time', '-')} às {schedule.get('end_time', '-')}"
)
menu_text = (
"Vamos configurar os dias e horários que eu posso enviar mensagens automáticas para você! 📅\n\n"
"Quais dias da semana você quer que eu envie mensagens?\n"
"Digite os números dos dias separados por vírgula.\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"
"Exemplo: 1,2,3,4,5 (segunda a sexta)"
) + current_text
await update.message.reply_text(menu_text)
async def handle_useragenda_answer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
global is_user_agenda, user_agenda_step, user_agenda_activities, user_agenda_current_activity
user_message = update.message.text.strip()
if user_message.lower() in ["/cancelar", "cancelar", "sair", "parar"]:
is_user_agenda = False
user_agenda_step = 0
user_agenda_activities = {}
user_agenda_current_activity = ""
await update.message.reply_text("Entendido! Cancelei a configuração da agenda. Use /useragenda para começar de novo.")
return True
if user_message.lower() in ["fim", "salvar", "pronto", "finalizar"]:
profile = load_user_profile()
profile["user_agenda"] = user_agenda_activities
save_user_profile(profile)
is_user_agenda = False
user_agenda_step = 0
user_agenda_activities = {}
user_agenda_current_activity = ""
await update.message.reply_text(
f"✅ Agenda diária salva com sucesso!\n\n{format_user_agenda(profile['user_agenda'])}\n\n"
f"Vou usar essa agenda para escolher os melhores temas para as mensagens automáticas."
)
return True
if user_agenda_step == 1:
option_map = {
"1": "acordar",
"2": "cafe_da_manha",
"3": "trabalho",
"4": "estudo",
"5": "almoco",
"6": "cafe_da_tarde",
"7": "jantar",
"8": "lazer",
"9": "saida_rua",
"10": "banho",
"11": "dormir",
}
activity = option_map.get(user_message)
if not activity:
await update.message.reply_text(
"Opção inválida. Responda com o número da atividade (1 a 11)."
)
return True
user_agenda_current_activity = activity
user_agenda_step = 2
activity_labels = {
"acordar": "Acordar",
"cafe_da_manha": "Café da manhã",
"trabalho": "Trabalho",
"estudo": "Estudo",
"almoco": "Almoço",
"cafe_da_tarde": "Café da tarde",
"jantar": "Jantar",
"lazer": "Lazer",
"saida_rua": "Saída na rua",
"banho": "Banho",
"dormir": "Dormir",
}
await update.message.reply_text(
f"Você escolheu: {activity_labels[activity]}\n\n"
f"Digite o horário no formato HH:MM (ex: 07:00):"
)
return True
if user_agenda_step == 2:
if not is_valid_time(user_message):
await update.message.reply_text(
"Formato inválido. Use o formato HH:MM (ex: 07:00 ou 14:30)."
)
return True
user_agenda_activities[user_agenda_current_activity] = user_message
user_agenda_step = 1
user_agenda_current_activity = ""
remaining = [a for a in USER_AGENDA_ACTIVITIES if a not in user_agenda_activities]
if not remaining:
profile = load_user_profile()
profile["user_agenda"] = user_agenda_activities
save_user_profile(profile)
is_user_agenda = False
user_agenda_step = 0
user_agenda_activities = {}
user_agenda_current_activity = ""
await update.message.reply_text(
f"✅ Agenda diária salva com sucesso!\n\n{format_user_agenda(profile['user_agenda'])}\n\n"
f"Vou usar essa agenda para escolher os melhores temas para as mensagens automáticas."
)
return True
await update.message.reply_text(
"Horário salvo! Quer configurar outra atividade?\n"
"Responda com o número da próxima atividade ou digite 'fim' para salvar e terminar."
)
return True
if user_message.lower() in ["fim", "salvar", "pronto", "finalizar"]:
profile = load_user_profile()
profile["user_agenda"] = user_agenda_activities
save_user_profile(profile)
is_user_agenda = False
user_agenda_step = 0
user_agenda_activities = {}
user_agenda_current_activity = ""
await update.message.reply_text(
f"✅ Agenda diária salva com sucesso!\n\n{format_user_agenda(profile['user_agenda'])}\n\n"
f"Vou usar essa agenda para escolher os melhores temas para as mensagens automáticas."
)
return True
return False
async def handle_visitantes_answer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
global is_visitantes, visitantes_step, visitantes_data, visitantes_current_field
user_message = update.message.text.strip()
if user_message.lower() in ["/cancelar", "cancelar", "sair", "parar"]:
is_visitantes = False
visitantes_step = 0
visitantes_data = {}
visitantes_current_field = ""
await update.message.reply_text("Entendido! Cancelei a configuração de visitantes.")
return True
if user_message.lower() in ["fim", "salvar", "pronto", "finalizar"]:
visitors = load_visitors()
for name, info in visitantes_data.items():
if "varias_vezes_ao_dia" not in info and "frequencia" not in info:
info["frequencia"] = "várias vezes ao dia"
visitors[name] = info
save_visitors(visitors)
is_visitantes = False
visitantes_step = 0
visitantes_data = {}
visitantes_current_field = ""
await update.message.reply_text(
f"✅ Visitantes salvos com sucesso!\n\n{format_visitors(visitors)}\n\n"
f"Vou usar essas informações para criar conversas mais realistas quando eles estiverem por perto."
)
return True
if visitantes_step == 1:
if not user_message or len(user_message) < 2:
await update.message.reply_text("Digite um nome válido (mínimo 2 caracteres).")
return True
if user_message in visitantes_data:
await update.message.reply_text(f"{user_message} já está nesta lista. Deseja adicionar outro nome?")
return True
visitantes_current_field = user_message
visitantes_data[user_message] = {}
visitantes_step = 2
await update.message.reply_text(
f"Visitante: {user_message}\n\n"
f"Qual a relação com a gente?\n"
f"Ex: amiga da Mônica, primo do Marco, cliente de depilação, vizinho, etc."
)
return True
if visitantes_step == 2:
visitantes_data[visitantes_current_field]["relacao"] = user_message
visitantes_step = 3
await update.message.reply_text(
f"Com que frequência {visitantes_current_field} nos visita?\n"
f"Ex: várias vezes ao dia, 1x por semana, a cada 15 dias, diariamente, etc."
)
return True
if visitantes_step == 3:
visitantes_data[visitantes_current_field]["frequencia"] = user_message
visitantes_step = 4
await update.message.reply_text(
f"Descreva o comportamento de {visitantes_current_field} (o que gosta de fazer, como age conosco):"
)
return True
if visitantes_step == 4:
visitantes_data[visitantes_current_field]["comportamento"] = user_message
visitantes_step = 5
await update.message.reply_text(
f"Mais algum detalhe sobre {visitantes_current_field}? (opcional - digite 'pular' para pular)"
)
return True
if visitantes_step == 5:
last_name = visitantes_current_field
if user_message.lower() not in ["pular", ""]:
visitantes_data[visitantes_current_field]["detalhes"] = user_message
visitantes_step = 1
visitantes_current_field = ""
await update.message.reply_text(
f"✅ {last_name} salvo! Quer adicionar outro visitante?\n\n"
f"Digite o nome do próximo visitante ou 'fim' para salvar tudo e encerrar."
)
return True
return False
async def editarperfil_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
global is_editing_user_profile, editing_user_profile_field
is_editing_user_profile = True
editing_user_profile_field = ""
profile = load_user_profile()
menu_text = f"""Qual campo do seu perfil você quer editar?
1. Nome: {profile.get('name', '-')}
2. Idade: {profile.get('age', '-')}
3. Gênero: {profile.get('gender', '-')}
4. Localização: {profile.get('location', '-')}
5. Interesses: {len(profile.get('interests', []))} itens
6. Personalidade: {len(profile.get('personality_traits', []))} itens
7. Estilo de comunicação: {profile.get('preferences', {}).get('communication_style', '-')}
8. Tópicos a evitar: {profile.get('preferences', {}).get('topics_to_avoid', '-')}
9. Tópicos de interesse: {profile.get('preferences', {}).get('topics_of_interest', '-')}
10. Contexto do relacionamento: {profile.get('relationship_context', '-')}
11. Abertura sexual: {profile.get('sexual_preferences', {}).get('openness', '-')}
12. Tópicos preferidos (sexual): {len(profile.get('sexual_preferences', {}).get('preferred_topics', []))} itens
13. Limites: {len(profile.get('sexual_preferences', {}).get('boundaries', []))} itens
14. Horário de acordar: {profile.get('daily_routine', {}).get('wake_up_time', '-')}
15. Horário de dormir: {profile.get('daily_routine', {}).get('sleep_time', '-')}
16. Rotina de trabalho: {profile.get('daily_routine', {}).get('work_schedule', '-')}
17. Notas: {profile.get('notes', '-')}
Responda com o número da opção que deseja editar."""
await update.message.reply_text(menu_text)
async def editaragente_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
global is_editing_agent_profile, editing_agent_profile_field, editing_agent_profile_action
is_editing_agent_profile = True
editing_agent_profile_field = ""
editing_agent_profile_action = ""
profile = load_user_profile()
menu_text = f"""Vamos editar o perfil da Mônica! O que você quer fazer?
1. Adicionar às preferências visuais ({len(profile.get('monica_visual_preferences', []))} itens)
2. Adicionar às preferências sexuais ({len(profile.get('monica_sexual_preferences', []))} itens)
3. Adicionar às preferências de nudez ({len(profile.get('monica_nudity_preferences', []))} itens)
4. Adicionar à dinâmica do relacionamento ({len(profile.get('relationship_dynamics', []))} itens)
5. Remover das preferências visuais
6. Remover das preferências sexuais
7. Remover das preferências de nudez
8. Remover da dinâmica do relacionamento
9. Editar preferências de nudez do usuário (notas)
10. Editar preferências de nudez da Mônica (lista)
11. Editar serviços profissionais da Mônica
12. Editar preferências de lubrificante da Mônica
Responda com o número da opção."""
await update.message.reply_text(menu_text)
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 com a Mônica
/perfil - Exibe o seu perfil atual (resumo)
/verperfil - Exibe TODAS as características e preferências do seu perfil
/veragente - Exibe TODAS as características e preferências da Mônica
/configagentprofile - Configura o perfil da Mônica (personalidade e preferências)
/editarperfil - Edita qualquer campo do seu perfil de usuário
/editaragente - Edita ou remove itens do perfil da Mônica
/adicionarperfil - Adiciona itens a listas do seu perfil
/adicionaragente - Adiciona itens a listas do perfil da Mônica
/adicionaracoesauto - Analisa perfis, contextos e pesquisa na internet para adicionar novas ações e preferências automaticamente
/onboarding - Reinicia o onboarding para atualizar seu perfil
/reset - Reseta seu perfil e começa do zero
/limpar - Apaga TODOS os dados: perfil do usuário, perfil da Mônica e contextos
/cancelar - Encerra a sessão atual
/agendamento - Define os dias e horários das mensagens automáticas
/veragendamento - Exibe o agendamento atual de mensagens automáticas
/useragenda - Define a agenda diária de atividades do usuário
/veruseragenda - Exibe a agenda diária de atividades do usuário
/fusohorario - Define o fuso horário do sistema
/atualizarsistema - Executa ciclo de autoaperfeiçoamento (analisa e melhora o sistema)
/ajuda ou /help - Mostra esta mensagem de ajuda
Dica: você também pode atualizar seu perfil dizendo algo como "inclui no meu perfil que gosto de rock"."""
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()
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 = get_user_profile_summary(profile)
await update.message.reply_text(summary)
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)
await update.message.reply_text("Perfil resetado! Vamos nos conhecer de novo? Use /start")
async def cancelar_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
global is_onboarding, onboarding_step, onboarding_answers
global is_config_agent_profile, config_agent_step, config_agent_field
global is_editing_user_profile, editing_user_profile_field
global is_editing_agent_profile, editing_agent_profile_field, editing_agent_profile_action
global is_adding_user_preference, adding_user_preference_field
global is_adding_agent_preference, adding_agent_preference_field
global proactive_task, app_instance, is_shutting_down, viewing_profile
global is_scheduling, scheduling_step, scheduling_days, scheduling_start_time, scheduling_end_time
global is_user_agenda, user_agenda_step, user_agenda_activities, user_agenda_current_activity
global is_visitantes, visitantes_step, visitantes_data, visitantes_current_field
global is_setting_timezone, timezone_step
is_onboarding = False
onboarding_step = 0
onboarding_answers = {}
is_config_agent_profile = False
config_agent_step = 0
config_agent_field = ""
is_editing_user_profile = False
editing_user_profile_field = ""
is_editing_agent_profile = False
editing_agent_profile_field = ""
editing_agent_profile_action = ""
is_adding_user_preference = False
adding_user_preference_field = ""
is_adding_agent_preference = False
adding_agent_preference_field = ""
is_scheduling = False
scheduling_step = 0
scheduling_days = []
scheduling_start_time = ""
scheduling_end_time = ""
is_user_agenda = False
user_agenda_step = 0
user_agenda_activities = {}
user_agenda_current_activity = ""
is_visitantes = False
visitantes_step = 0
visitantes_data = {}
visitantes_current_field = ""
is_setting_timezone = False
timezone_step = 0
viewing_profile = ""
message_counter = 0
self_improvement_counter = 0
is_shutting_down = True
if proactive_task and not proactive_task.done():
proactive_task.cancel()
proactive_task = None
await update.message.reply_text("Entendido! Vou me desligar agora. Quando quiser voltar, é só iniciar o sistema novamente. Até mais! 💖")
if app_instance is not None:
try:
app_instance.stop()
except Exception as e:
logger.error(f"Erro ao parar app_instance: {e}")
async def onboarding_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
await start_onboarding(update, context)
async def limpar_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
global message_counter, self_improvement_counter
message_counter = 0
self_improvement_counter = 0
clear_all_data()
await update.message.reply_text(
"Prontinho! Limpei tudo: perfil do usuário, perfil da Mônica e arquivos de contexto. 💖\n"
"Use /start para começar do zero."
)
async def adicionarperfil_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
global is_adding_user_preference, adding_user_preference_field
is_adding_user_preference = True
adding_user_preference_field = ""
menu_text = """Qual lista do seu perfil você quer adicionar?
1. Interesses
2. Traços de personalidade
3. Tópicos de interesse
4. Tópicos a evitar
5. Limites
6. Tópicos preferidos (sexual)
Responda com o número da opção."""
await update.message.reply_text(menu_text)
async def adicionaragente_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
global is_adding_agent_preference, adding_agent_preference_field
is_adding_agent_preference = True
adding_agent_preference_field = ""
menu_text = """Qual lista do perfil da Mônica você quer adicionar?
1. Preferências visuais
2. Preferências sexuais
3. Preferências de nudez
4. Dinâmica do relacionamento
5. Serviços profissionais da Mônica
6. Preferências de lubrificante da Mônica
Responda com o número da opção."""
await update.message.reply_text(menu_text)
async def handle_config_agent_profile_answer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
global is_config_agent_profile, config_agent_step, config_agent_field
user_message = update.message.text.strip()
if config_agent_step == 1:
option_map = {
"1": "monica_visual_preferences",
"2": "monica_sexual_preferences",
"3": "monica_nudity_preferences",
"4": "relationship_dynamics",
"5": "monica_professional_services",
}
field = option_map.get(user_message)
if not field:
await update.message.reply_text("Opção inválida. Responda com 1, 2, 3, 4 ou 5.")
return True
config_agent_field = field
config_agent_step = 2
field_names = {
"monica_visual_preferences": "Preferências visuais da Mônica",
"monica_sexual_preferences": "Preferências sexuais da Mônica",
"monica_nudity_preferences": "Preferências de nudez da Mônica",
"relationship_dynamics": "Dinâmica do relacionamento",
"monica_professional_services": "Serviços profissionais da Mônica",
}
await update.message.reply_text(
f"Você escolheu: {field_names[field]}\n\n"
f"Envie os novos valores separados por vírgula. "
f"Exemplo: 'ver o pau de Marco, ver a bunda de Marco'"
)
return True
if config_agent_step == 2:
profile = load_user_profile()
items = [item.strip() for item in user_message.split(",") if item.strip()]
if not items:
items = [user_message]
if config_agent_field not in profile:
profile[config_agent_field] = []
if not isinstance(profile[config_agent_field], list):
profile[config_agent_field] = [user_message]
else:
for item in items:
if item not in profile[config_agent_field]:
profile[config_agent_field].append(item)
save_user_profile(profile)
is_config_agent_profile = False
config_agent_step = 0
config_agent_field = ""
await update.message.reply_text("Prontinho! Atualizei o perfil da Mônica com sucesso. 💖")
return True
return False
async def handle_editar_perfil_answer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
global is_editing_user_profile, editing_user_profile_field
user_message = update.message.text.strip()
if not editing_user_profile_field:
option_map = {
"1": ("name", "Nome"),
"2": ("age", "Idade"),
"3": ("gender", "Gênero"),
"4": ("location", "Localização"),
"5": ("interests", "Interesses"),
"6": ("personality_traits", "Traços de personalidade"),
"7": ("preferences.communication_style", "Estilo de comunicação"),
"8": ("preferences.topics_to_avoid", "Tópicos a evitar"),
"9": ("preferences.topics_of_interest", "Tópicos de interesse"),
"10": ("relationship_context", "Contexto do relacionamento"),
"11": ("sexual_preferences.openness", "Abertura sexual"),
"12": ("sexual_preferences.preferred_topics", "Tópicos preferidos (sexual)"),
"13": ("sexual_preferences.boundaries", "Limites"),
"14": ("daily_routine.wake_up_time", "Horário de acordar"),
"15": ("daily_routine.sleep_time", "Horário de dormir"),
"16": ("daily_routine.work_schedule", "Rotina de trabalho"),
"17": ("notes", "Notas"),
}
selected = option_map.get(user_message)
if not selected:
await update.message.reply_text("Opção inválida. Responda com o número da opção.")
return True
editing_user_profile_field = selected[0]
field_name = selected[1]
if selected[0] in ("interests", "personality_traits", "sexual_preferences.preferred_topics", "sexual_preferences.boundaries"):
prompt = f"Digite os {field_name.lower()} separados por vírgula. Exemplo: 'rock, programação, voar'"
else:
prompt = f"Digite o novo valor para {field_name.lower()}:"
await update.message.reply_text(prompt)
return True
profile = load_user_profile()
field_path = editing_user_profile_field.split(".")
if len(field_path) == 1:
if field_path[0] in ("interests", "personality_traits", "sexual_preferences.preferred_topics", "sexual_preferences.boundaries"):
items = [item.strip() for item in user_message.split(",") if item.strip()]
if not items:
items = [user_message]
profile[field_path[0]] = items
else:
profile[field_path[0]] = user_message
else:
current = profile
for key in field_path[:-1]:
if key not in current or not isinstance(current[key], dict):
current[key] = {}
current = current[key]
if field_path[-1] in ("preferred_topics", "boundaries"):
items = [item.strip() for item in user_message.split(",") if item.strip()]
if not items:
items = [user_message]
current[field_path[-1]] = items
else:
current[field_path[-1]] = user_message
save_user_profile(profile)
is_editing_user_profile = False
editing_user_profile_field = ""
await update.message.reply_text("Prontinho! Atualizei seu perfil com sucesso. 💖")
return True
async def handle_editar_agente_answer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
global is_editing_agent_profile, editing_agent_profile_field, editing_agent_profile_action
user_message = update.message.text.strip()
if not editing_agent_profile_field:
add_map = {
"1": "monica_visual_preferences",
"2": "monica_sexual_preferences",
"3": "monica_nudity_preferences",
"4": "relationship_dynamics",
}
remove_map = {
"5": "monica_visual_preferences",
"6": "monica_sexual_preferences",
"7": "monica_nudity_preferences",
"8": "relationship_dynamics",
}
edit_map = {
"9": ("nudity_preferences", "notas_adicionais"),
"10": ("monica_nudity_preferences", "list"),
"11": ("monica_professional_services", "list"),
"12": ("monica_lubricant_preferences", "list"),
}
field = add_map.get(user_message)
action = "add"
if not field:
field = remove_map.get(user_message)
action = "remove"
if not field:
selected = edit_map.get(user_message)
if selected:
editing_agent_profile_field = selected[0]
if selected[1] == "list":
editing_agent_profile_action = "add_list"
else:
editing_agent_profile_action = "edit_dict"
profile = load_user_profile()
current_items = profile.get(selected[0], [])
if isinstance(current_items, dict):
current_items = current_items.get(selected[1], "")
if not current_items or (isinstance(current_items, list) and not current_items):
if editing_agent_profile_action == "edit_dict":
await update.message.reply_text("Esta categoria está vazia. Envie o novo texto.")
else:
await update.message.reply_text("Esta categoria está vazia. Envie os itens separados por vírgula.")
return True
if editing_agent_profile_action == "edit_dict":
await update.message.reply_text(f"Valor atual: {current_items}\n\nEnvie o novo texto.")
else:
items_text = "\n".join(f"{i+1}. {item}" for i, item in enumerate(current_items))
await update.message.reply_text(f"Itens atuais:\n{items_text}\n\nEnvie os novos itens separados por vírgula para adicionar.")
return True
await update.message.reply_text("Opção inválida. Responda com o número da opção.")
return True
editing_agent_profile_field = field
editing_agent_profile_action = action
profile = load_user_profile()
current_items = profile.get(field, [])
if not current_items:
await update.message.reply_text("Esta categoria está vazia. Envie os itens separados por vírgula para adicionar.")
return True
items_text = "\n".join(f"{i+1}. {item}" for i, item in enumerate(current_items))
if action == "remove":
await update.message.reply_text(f"Itens atuais:\n{items_text}\n\nDigite o número do item que deseja remover.")
else:
await update.message.reply_text(f"Itens atuais:\n{items_text}\n\nEnvie os novos itens separados por vírgula para adicionar.")
return True
profile = load_user_profile()
current_items = profile.get(editing_agent_profile_field, [])
if editing_agent_profile_action == "remove":
try:
index = int(user_message) - 1
if 0 <= index < len(current_items):
removed = current_items.pop(index)
profile[editing_agent_profile_field] = current_items
save_user_profile(profile)
await update.message.reply_text(f"Removido: {removed}. Prontinho! 💖")
else:
await update.message.reply_text("Número inválido. Tente novamente.")
return True
except ValueError:
await update.message.reply_text("Digite apenas o número do item.")
return True
elif editing_agent_profile_action == "edit_dict":
if editing_agent_profile_field == "nudity_preferences":
profile["nudity_preferences"] = {"notas_adicionais": user_message}
else:
profile[editing_agent_profile_field] = {"notas_adicionais": user_message}
save_user_profile(profile)
await update.message.reply_text("Prontinho! Atualizei o texto com sucesso. 💖")
else:
items = [item.strip() for item in user_message.split(",") if item.strip()]
if not items:
items = [user_message]
if isinstance(current_items, list):
for item in items:
if item not in current_items:
current_items.append(item)
profile[editing_agent_profile_field] = current_items
save_user_profile(profile)
await update.message.reply_text("Prontinho! Atualizei o perfil da Mônica com sucesso. 💖")
is_editing_agent_profile = False
editing_agent_profile_field = ""
editing_agent_profile_action = ""
return True
async def handle_adicionar_perfil_answer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
global is_adding_user_preference, adding_user_preference_field
user_message = update.message.text.strip()
if not adding_user_preference_field:
option_map = {
"1": "interests",
"2": "personality_traits",
"3": "preferences.topics_of_interest",
"4": "preferences.topics_to_avoid",
"5": "sexual_preferences.boundaries",
"6": "sexual_preferences.preferred_topics",
}
field = option_map.get(user_message)
if not field:
await update.message.reply_text("Opção inválida. Responda com o número da opção.")
return True
adding_user_preference_field = field
field_names = {
"interests": "interesses",
"personality_traits": "traços de personalidade",
"preferences.topics_of_interest": "tópicos de interesse",
"preferences.topics_to_avoid": "tópicos a evitar",
"sexual_preferences.boundaries": "limites",
"sexual_preferences.preferred_topics": "tópicos preferidos",
}
await update.message.reply_text(f"Digite o(s) novo(s) {field_names[field]} separado(s) por vírgula:")
return True
profile = load_user_profile()
items = [item.strip() for item in user_message.split(",") if item.strip()]
if not items:
items = [user_message]
field_path = adding_user_preference_field.split(".")
if len(field_path) == 1:
current = profile.get(field_path[0], [])
if not isinstance(current, list):
current = []
for item in items:
if item not in current:
current.append(item)
profile[field_path[0]] = current
else:
current = profile
for key in field_path[:-1]:
if key not in current or not isinstance(current[key], dict):
current[key] = {}
current = current[key]
list_key = field_path[-1]
current_list = current.get(list_key, [])
if not isinstance(current_list, list):
current_list = []
for item in items:
if item not in current_list:
current_list.append(item)
current[list_key] = current_list
save_user_profile(profile)
is_adding_user_preference = False
adding_user_preference_field = ""
await update.message.reply_text("Prontinho! Adicionei ao seu perfil com sucesso. 💖")
return True
async def handle_adicionar_agente_answer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
global is_adding_agent_preference, adding_agent_preference_field
user_message = update.message.text.strip()
if not adding_agent_preference_field:
option_map = {
"1": "monica_visual_preferences",
"2": "monica_sexual_preferences",
"3": "monica_nudity_preferences",
"4": "relationship_dynamics",
"5": "monica_professional_services",
"6": "monica_lubricant_preferences",
}
field = option_map.get(user_message)
if not field:
await update.message.reply_text("Opção inválida. Responda com 1, 2, 3, 4, 5 ou 6.")
return True
adding_agent_preference_field = field
field_names = {
"monica_visual_preferences": "preferências visuais",
"monica_sexual_preferences": "preferências sexuais",
"monica_nudity_preferences": "preferências de nudez",
"relationship_dynamics": "dinâmica do relacionamento",
"monica_professional_services": "serviços profissionais",
"monica_lubricant_preferences": "preferências de lubrificante",
}
await update.message.reply_text(f"Digite o(s) novo(s) {field_names[field]} separado(s) por vírgula:")
return True
profile = load_user_profile()
items = [item.strip() for item in user_message.split(",") if item.strip()]
if not items:
items = [user_message]
current = profile.get(adding_agent_preference_field, [])
if not isinstance(current, list):
current = []
for item in items:
if item not in current:
current.append(item)
profile[adding_agent_preference_field] = current
save_user_profile(profile)
is_adding_agent_preference = False
adding_agent_preference_field = ""
await update.message.reply_text("Prontinho! Adicionei ao perfil da Mônica com sucesso. 💖")
return True
async def handle_agendamento_answer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
global is_scheduling, scheduling_step, scheduling_days, scheduling_start_time, scheduling_end_time
user_message = update.message.text.strip()
if scheduling_step == 1:
days_map = {"1": 0, "2": 1, "3": 2, "4": 3, "5": 4, "6": 5, "7": 6}
selected = []
for part in user_message.replace(" ", "").split(","):
if part in days_map:
selected.append(days_map[part])
if not selected:
await update.message.reply_text(
"Opção inválida. Digite os números dos dias separados por vírgula (ex: 1,2,3,4,5)."
)
return True
scheduling_days = sorted(set(selected))
scheduling_step = 2
await update.message.reply_text(
"Perfeito! Agora digite o horário inicial no formato HH:MM (ex: 09:00):"
)
return True
if scheduling_step == 2:
if not is_valid_time(user_message):
await update.message.reply_text(
"Formato inválido. Use o formato HH:MM (ex: 09:00 ou 14:30)."
)
return True
scheduling_start_time = user_message
scheduling_step = 3
await update.message.reply_text(
"Ótimo! Agora digite o horário final no formato HH:MM (ex: 22:00):"
)
return True
if scheduling_step == 3:
if not is_valid_time(user_message):
await update.message.reply_text(
"Formato inválido. Use o formato HH:MM (ex: 22:00 ou 23:00)."
)
return True
scheduling_end_time = user_message
profile = load_user_profile()
profile["proactive_schedule"] = {
"enabled": True,
"days": scheduling_days,
"start_time": scheduling_start_time,
"end_time": scheduling_end_time,
}
save_user_profile(profile)
days_map = {0: "Segunda", 1: "Terça", 2: "Quarta", 3: "Quinta", 4: "Sexta", 5: "Sábado", 6: "Domingo"}
days_names = [days_map.get(d, str(d)) for d in scheduling_days]
is_scheduling = False
scheduling_step = 0
scheduling_days = []
scheduling_start_time = ""
scheduling_end_time = ""
await update.message.reply_text(
f"✅ Agendamento salvo com sucesso!\n\n"
f"Dias: {', '.join(days_names)}\n"
f"Horário: {scheduling_start_time} às {scheduling_end_time}\n\n"
f"Vou enviar mensagens automáticas apenas nesses dias e horários."
)
return True
return False
async def handle_fusohorario_answer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
global is_setting_timezone, timezone_step
user_message = update.message.text.strip()
if user_message.lower() in ["/cancelar", "cancelar", "sair", "parar"]:
is_setting_timezone = False
timezone_step = 0
await update.message.reply_text("Entendido! Cancelei a configuração do fuso horário.")
return True
if timezone_step == 1:
tz_name = user_message.strip()
try:
ZoneInfo(tz_name)
except Exception:
await update.message.reply_text(
f"'{tz_name}' não é um fuso horário válido.\n"
"Digite no formato IANA, ex: America/Sao_Paulo, America/New_York, Europe/Lisbon, UTC."
)
return True
profile = load_user_profile()
profile["timezone"] = tz_name
save_user_profile(profile)
is_setting_timezone = False
timezone_step = 0
now = get_now()
await update.message.reply_text(
f"✅ Fuso horário definido: {tz_name}\n\n"
f"Horário atual do sistema: {now.strftime('%H:%M')}\n"
f"Data atual: {now.strftime('%d/%m/%Y')}\n\n"
"Vou usar esse fuso horário para todas as operações do sistema."
)
return True
return False
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_shutting_down, viewing_profile, message_counter, self_improvement_counter
if is_shutting_down:
return
if is_onboarding:
handled = await handle_onboarding_answer(update, context)
if handled:
return
profile_updated = await handle_profile_update_if_triggered(update, context)
if profile_updated:
return
global is_config_agent_profile, config_agent_step, config_agent_field
if is_config_agent_profile:
handled = await handle_config_agent_profile_answer(update, context)
if handled:
return
global is_editing_user_profile, editing_user_profile_field
if is_editing_user_profile:
handled = await handle_editar_perfil_answer(update, context)
if handled:
return
global is_editing_agent_profile, editing_agent_profile_field, editing_agent_profile_action
if is_editing_agent_profile:
handled = await handle_editar_agente_answer(update, context)
if handled:
return
global is_adding_user_preference, adding_user_preference_field
if is_adding_user_preference:
handled = await handle_adicionar_perfil_answer(update, context)
if handled:
return
global is_adding_agent_preference, adding_agent_preference_field
if is_adding_agent_preference:
handled = await handle_adicionar_agente_answer(update, context)
if handled:
return
global is_scheduling, scheduling_step, scheduling_days, scheduling_start_time, scheduling_end_time
if is_scheduling:
handled = await handle_agendamento_answer(update, context)
if handled:
return
global is_user_agenda, user_agenda_step, user_agenda_activities, user_agenda_current_activity
if is_user_agenda:
handled = await handle_useragenda_answer(update, context)
if handled:
return
global is_visitantes, visitantes_step, visitantes_data, visitantes_current_field
if is_visitantes:
handled = await handle_visitantes_answer(update, context)
if handled:
return
global is_setting_timezone, timezone_step
if is_setting_timezone:
handled = await handle_fusohorario_answer(update, context)
if handled:
return
message_counter += 1
self_improvement_counter += 1
add_to_context("user", user_message)
response = await get_monica_response(user_message)
if response:
add_to_context("assistant", response)
for part in _split_message_for_telegram(response):
await update.message.reply_text(part, parse_mode=ParseMode.HTML)
if message_counter % EXTRACTION_INTERVAL == 0:
all_ctx = load_all_contexts()
recent = all_ctx[-4:] if len(all_ctx) >= 4 else all_ctx
async def auto_extract_profile(ctx=recent):
try:
profile = load_user_profile()
if extract_and_update_profile(profile, ctx):
logger.info(f"Perfil atualizado automaticamente a cada {EXTRACTION_INTERVAL} mensagens.")
except Exception as e:
logger.error(f"Erro na extração automática de perfil: {e}")
asyncio.create_task(auto_extract_profile())
if ENABLE_SELF_IMPROVEMENT and self_improvement_counter >= SELF_IMPROVEMENT_INTERVAL:
self_improvement_counter = 0
async def do_self_improvement():
try:
logger.info(f"Iniciando ciclo de autoaperfeiçoamento #{load_self_improvement_log().get('cycle_count', 0) + 1}...")
result = run_self_improvement()
if result["analyzed"]:
logger.info(f"Autoaperfeiçoamento concluído: {len(result['applied'])} melhorias aplicadas.")
if result["applied"]:
for a in result["applied"]:
logger.info(f" - {a}")
else:
logger.info(f"Autoaperfeiçoamento: análise não retornou melhorias. Erros: {result['errors']}")
except Exception as e:
logger.error(f"Erro no ciclo de autoaperfeiçoamento: {e}")
asyncio.create_task(do_self_improvement())
else:
await update.message.reply_text("Desculpa, amor, tive um probleminha agora. Pode tentar de novo? 😔")
async def atualizarsistema_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
if not ENABLE_SELF_IMPROVEMENT:
await update.message.reply_text("O sistema de autoaperfeiçoamento está desativado no .env. Para ativar, defina ENABLE_SELF_IMPROVEMENT=true.")
return
await update.message.reply_text("🧠 Iniciando ciclo de autoaperfeiçoamento... Analisando o sistema, perfil, conversas e buscando melhorias...")
try:
result = run_self_improvement()
if result["analyzed"]:
lines = [f"✅ Autoaperfeiçoamento concluído!"]
if result["applied"]:
lines.append(f"\nMelhorias aplicadas ({len(result['applied'])}):")
for i, a in enumerate(result["applied"], 1):
lines.append(f" {i}. {a}")
else:
lines.append("\nNenhuma melhoria necessária no momento.")
await update.message.reply_text("\n".join(lines))
else:
await update.message.reply_text("A análise não encontrou melhorias no momento. O sistema já está bem ajustado! ✅")
except Exception as e:
logger.error(f"Erro no comando atualizarsistema: {e}")
await update.message.reply_text(f"Ocorreu um erro durante o autoaperfeiçoamento: {str(e)[:200]}")
async def debug_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
profile = load_user_profile()
today_context = load_context()
all_contexts = load_all_contexts()
effective_chat_id = get_effective_chat_id()
days_with_context = len(set(
c.get("timestamp", "")[:10] for c in all_contexts if c.get("timestamp")
))
msg = (
"Debug Mônica:\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"Mensagens totais (todos os dias): {len(all_contexts)}\n"
f"Dias com contexto: {days_with_context}\n"
f"Modelo default: {DEFAULT_MODEL}\n"
f"Autoaperfeiçoamento: {'ativado' if ENABLE_SELF_IMPROVEMENT else 'desativado'} (a cada {SELF_IMPROVEMENT_INTERVAL} msg)\n"
f"Melhorias aplicadas: {len(profile.get('system_improvements', [])) + len(profile.get('behavior_improvements', [])) + len(profile.get('extraction_improvements', []))}"
)
await update.message.reply_text(msg)
# ===================== 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=get_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()
global app_instance, proactive_task
app_instance = Application.builder().token(TELEGRAM_BOT_TOKEN).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("verperfil", verperfil_command))
app_instance.add_handler(CommandHandler("veragente", veragente_command))
app_instance.add_handler(CommandHandler("veragendamento", veragendamento_command))
app_instance.add_handler(CommandHandler("veruseragenda", veruseragenda_command))
app_instance.add_handler(CommandHandler("fusohorario", fusohorario_command))
app_instance.add_handler(CommandHandler("agendamento", agendamento_command))
app_instance.add_handler(CommandHandler("useragenda", useragenda_command))
app_instance.add_handler(CommandHandler("configagentprofile", configagentprofile_command))
app_instance.add_handler(CommandHandler("editarperfil", editarperfil_command))
app_instance.add_handler(CommandHandler("editaragente", editaragente_command))
app_instance.add_handler(CommandHandler("adicionarperfil", adicionarperfil_command))
app_instance.add_handler(CommandHandler("adicionaragente", adicionaragente_command))
app_instance.add_handler(CommandHandler("adicionaracoesauto", adicionaracoesauto_command))
app_instance.add_handler(CommandHandler("reset", reset_command))
app_instance.add_handler(CommandHandler("limpar", limpar_command))
app_instance.add_handler(CommandHandler("cancelar", cancelar_command))
app_instance.add_handler(CommandHandler("onboarding", onboarding_command))
app_instance.add_handler(CommandHandler("debug", debug_command))
app_instance.add_handler(CommandHandler("atualizarsistema", atualizarsistema_command))
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())
asyncio.create_task(cueca_loop())
all_contexts = load_all_contexts()
days_with_context = len(set(
c.get("timestamp", "")[:10] for c in all_contexts if c.get("timestamp")
))
logger.info(
f"post_init iniciado. TELEGRAM_CHAT_ID configurado: {bool(TELEGRAM_CHAT_ID)} valor={TELEGRAM_CHAT_ID!r} "
f"Contextos historicos carregados: {len(all_contexts)} mensagens de {days_with_context} dias"
)
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:
logger.error(f"Erro ao executar /start automatico: {e}", exc_info=True)
app_instance.post_init = post_init
logger.info("Mônica está iniciando... 💖")
app_instance.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()