Files
Jogo/ia_llamacpp.py
marcocosta10000 594f1a5725 feat: initial commit of Sistema do Mestre application
Add complete Flask web application with Telegram bot integration,
multiple AI provider support (llama.cpp, OpenRouter, z.ai, StepFun),
extensive order banks for BDSM-themed gameplay, automated cron
scheduling, and responsive web UI with inference parameter controls.
2026-10-17 19:08:54 -03:00

1011 lines
42 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Integration with AI providers that expose an OpenAI-compatible API.
Four backends are supported:
* ``llamacpp`` – the local llama.cpp server (``llama-server``), no auth.
* ``openrouter`` – the OpenRouter cloud API, requires an API key + model.
* ``zai`` – the z.ai (Zhipu AI) cloud API, requires an API key + model.
* ``stepfun`` – the StepFun cloud API, requires an API key + model.
All expose ``/chat/completions`` (OpenAI-compatible), so the rest of the
app only talks to the high-level helpers below.
All configuration is managed at runtime through the web UI (index.html)
and persisted to ``estado_ia.json``, which is the single source of truth.
The :func:`configurar_provedor` helper applies changes live and the
separate cron scheduler process reads the same file, so settings changed
through the UI reach every part of the system.
Configurable settings (all via the web UI → estado_ia.json):
General:
* ``provedor`` – ``"llamacpp"``, ``"openrouter"``, ``"zai"`` or ``"stepfun"``
llama.cpp:
* ``llamacpp_base_url`` – base URL of the server (default ``http://localhost:8080``)
OpenRouter:
* ``openrouter_api_key`` – key from https://openrouter.ai/keys
* ``openrouter_model`` – e.g. ``meta-llama/llama-3.1-8b-instruct``
* ``openrouter_base_url`` – default ``https://openrouter.ai/api/v1``
z.ai (Zhipu AI):
* ``zai_api_key`` – key from https://z.ai/manage-apikey/apikey-list
* ``zai_model`` – e.g. ``glm-5``, ``glm-4.6``, ``glm-4.5``
* ``zai_base_url`` – default ``https://api.z.ai/api/paas/v4``
StepFun:
* ``stepfun_api_key`` – key from https://platform.stepfun.com
* ``stepfun_model`` – e.g. ``step-3.7-flash``
* ``stepfun_base_url`` – default ``https://api.stepfun.ai/step_plan/v1``
"""
import json
import logging
import os
import requests
from contexto_ia import contexto_ordem, contexto_chat
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Provider configuration
# ---------------------------------------------------------------------------
PROVEDOR_LLAMACPP = "llamacpp"
PROVEDOR_OPENROUTER = "openrouter"
PROVEDOR_ZAI = "zai"
PROVEDOR_STEPFUN = "stepfun"
_PROVEDORES_VALIDOS = (PROVEDOR_LLAMACPP, PROVEDOR_OPENROUTER, PROVEDOR_ZAI, PROVEDOR_STEPFUN)
# Hardcoded defaults for request timeouts (seconds). These rarely need to
# change and are not exposed in the web UI.
LLAMACPP_TIMEOUT = 180 # local models can be slow (a few tokens/sec on CPU)
OPENROUTER_TIMEOUT = 120
ZAI_TIMEOUT = 120
STEPFUN_TIMEOUT = 120
# Default base URLs – used when estado_ia.json has no value yet.
_LLAMACPP_BASE_URL_DEFAULT = "http://localhost:8080"
_OPENROUTER_BASE_URL_DEFAULT = "https://openrouter.ai/api/v1"
_ZAI_BASE_URL_DEFAULT = "https://api.z.ai/api/paas/v4"
_STEPFUN_BASE_URL_DEFAULT = "https://api.stepfun.ai/step_plan/v1"
# ---------------------------------------------------------------------------
# Cross-process shared state (Flask app <-> cron scheduler)
# ---------------------------------------------------------------------------
# The cron scheduler (enviar_ordem_mestre.py) runs as a separate process, so
# in-memory state is NOT shared with the Flask app. ALL AI configuration is
# persisted to a small JSON file (estado_ia.json) managed by the web UI, so
# both processes always agree on the settings. The .env file is no longer
# used for AI configuration.
_ESTADO_IA_PATH = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "estado_ia.json"
)
def _ler_estado_arquivo():
"""Return the persisted state dict (or an empty dict on any error)."""
try:
with open(_ESTADO_IA_PATH, "r", encoding="utf-8") as fh:
data = json.load(fh)
return data if isinstance(data, dict) else {}
except (FileNotFoundError, ValueError, OSError):
return {}
def _salvar_estado_arquivo(estado):
"""Persist *estado* (a dict) to the shared state file."""
try:
with open(_ESTADO_IA_PATH, "w", encoding="utf-8") as fh:
json.dump(estado, fh, ensure_ascii=False)
except OSError as exc:
logger.warning("Não foi possível salvar o estado da IA: %s", exc)
# Mutable runtime configuration – the single source of truth. Initialised
# from estado_ia.json (written by the web UI) with sensible defaults.
_estado_inicial = _ler_estado_arquivo()
_CONFIG = {
"provedor": (
_estado_inicial["provedor"]
if _estado_inicial.get("provedor") in _PROVEDORES_VALIDOS
else PROVEDOR_STEPFUN
),
"openrouter_api_key": _estado_inicial.get("openrouter_api_key", ""),
"openrouter_model": _estado_inicial.get("openrouter_model", ""),
"openrouter_base_url": _estado_inicial.get(
"openrouter_base_url", _OPENROUTER_BASE_URL_DEFAULT
).rstrip("/"),
"zai_api_key": _estado_inicial.get("zai_api_key", ""),
"zai_model": _estado_inicial.get("zai_model", ""),
"zai_base_url": _estado_inicial.get(
"zai_base_url", _ZAI_BASE_URL_DEFAULT
).rstrip("/"),
"stepfun_api_key": _estado_inicial.get("stepfun_api_key", ""),
"stepfun_model": _estado_inicial.get("stepfun_model", ""),
"stepfun_base_url": _estado_inicial.get(
"stepfun_base_url", _STEPFUN_BASE_URL_DEFAULT
).rstrip("/"),
"llamacpp_base_url": _estado_inicial.get(
"llamacpp_base_url", _LLAMACPP_BASE_URL_DEFAULT
).rstrip("/"),
}
def _sincronizar_estado_com_config():
"""Write the current provider config back to the shared state file."""
estado = _ler_estado_arquivo()
estado["provedor"] = _CONFIG["provedor"]
estado["openrouter_api_key"] = _CONFIG["openrouter_api_key"]
estado["openrouter_model"] = _CONFIG["openrouter_model"]
estado["openrouter_base_url"] = _CONFIG["openrouter_base_url"]
estado["zai_api_key"] = _CONFIG["zai_api_key"]
estado["zai_model"] = _CONFIG["zai_model"]
estado["zai_base_url"] = _CONFIG["zai_base_url"]
estado["stepfun_api_key"] = _CONFIG["stepfun_api_key"]
estado["stepfun_model"] = _CONFIG["stepfun_model"]
estado["stepfun_base_url"] = _CONFIG["stepfun_base_url"]
estado["llamacpp_base_url"] = _CONFIG["llamacpp_base_url"]
_salvar_estado_arquivo(estado)
def estado_revisao_cron_ativo():
"""Return True when "AI rewrites cron orders before sending" is enabled."""
return bool(_ler_estado_arquivo().get("revisar_cron", False))
def definir_revisao_cron(ativo):
"""Turn the cron-revision feature on/off and persist the choice."""
estado = _ler_estado_arquivo()
estado["revisar_cron"] = bool(ativo)
_salvar_estado_arquivo(estado)
return estado_revisao_cron_ativo()
def configurar_provedor(provedor=None, openrouter_api_key=None,
openrouter_model=None, openrouter_base_url=None,
zai_api_key=None, zai_model=None, zai_base_url=None,
stepfun_api_key=None, stepfun_model=None,
stepfun_base_url=None, llamacpp_base_url=None):
"""Update the active AI provider / credentials at runtime.
Any argument left as ``None`` keeps its previous value. Returns a copy
of the resulting configuration.
"""
if provedor is not None:
provedor = provedor.strip().lower()
if provedor in _PROVEDORES_VALIDOS:
_CONFIG["provedor"] = provedor
else:
logger.warning("Provedor desconhecido ignorado: %r", provedor)
if openrouter_api_key is not None:
_CONFIG["openrouter_api_key"] = openrouter_api_key.strip()
if openrouter_model is not None:
_CONFIG["openrouter_model"] = openrouter_model.strip()
if openrouter_base_url is not None:
_CONFIG["openrouter_base_url"] = openrouter_base_url.strip().rstrip("/")
if zai_api_key is not None:
_CONFIG["zai_api_key"] = zai_api_key.strip()
if zai_model is not None:
_CONFIG["zai_model"] = zai_model.strip()
if zai_base_url is not None:
_CONFIG["zai_base_url"] = zai_base_url.strip().rstrip("/")
if stepfun_api_key is not None:
_CONFIG["stepfun_api_key"] = stepfun_api_key.strip()
if stepfun_model is not None:
_CONFIG["stepfun_model"] = stepfun_model.strip()
if stepfun_base_url is not None:
_CONFIG["stepfun_base_url"] = stepfun_base_url.strip().rstrip("/")
if llamacpp_base_url is not None:
_CONFIG["llamacpp_base_url"] = llamacpp_base_url.strip().rstrip("/")
# Persist the (possibly) updated provider config so the separate cron
# process picks up credentials/model changed through the web UI.
_sincronizar_estado_com_config()
return obter_config()
def obter_config():
"""Return a copy of the current provider configuration."""
return dict(_CONFIG)
def obter_config_publica():
"""Return provider config safe to expose to the UI (API key masked)."""
cfg = obter_config()
chave_or = cfg.get("openrouter_api_key", "")
cfg["openrouter_api_key_definida"] = bool(chave_or)
chave_zai = cfg.get("zai_api_key", "")
cfg["zai_api_key_definida"] = bool(chave_zai)
chave_stepfun = cfg.get("stepfun_api_key", "")
cfg["stepfun_api_key_definida"] = bool(chave_stepfun)
# Never leak the full secret to the browser.
cfg.pop("openrouter_api_key", None)
cfg.pop("zai_api_key", None)
cfg.pop("stepfun_api_key", None)
return cfg
def provedor_atual():
"""Return the name of the active provider."""
return _CONFIG["provedor"]
# ---------------------------------------------------------------------------
# Personas / system prompts
# ---------------------------------------------------------------------------
# Persona used when the AI is asked to emit a single Order (Ordem).
SYSTEM_PROMPT_ORDEM = (
"Você é O MESTRE, uma figura dominadora, criativa e implacável que comanda "
"um jogo de submissão erótica. Sua tarefa agora é criar UMA ORDEM clara, "
"detalhada e imperativa para o submisso cumprir imediatamente.\n\n"
"FORMATO OBRIGATÓRIO (siga EXATAMENTE):\n"
"1. Primeira linha: título curto em MAIÚSCULAS (ex: ORDEM DE PLUG NA COZINHA COM EDGING)\n"
"2. Depois, uma tag por linha no formato [TAG]: valor\n"
" Tags obrigatórias: [LOCAL], [POSIÇÃO], [AÇÃO]\n"
" Tags opcionais: [DURAÇÃO], [INTENSIDADE], [RESPIRAÇÃO], [CONTROLE], [PÓS-GOZO], [POSTURA], [REGRA]\n"
"3. NÃO escreva saudações, explicações ou comentários fora das tags.\n"
"4. Apenas a ordem. Direto ao ponto.\n\n"
"Regras:\n"
"- Escreva sempre em português do Brasil.\n"
"- Use tom direto, imperativo e dominador (você manda, o submisso obedece).\n"
"- Seja criativo e varie o tipo de ordem a cada vez.\n"
"- Descreva a ordem de forma vívida, em poucos parágrafos curtos.\n"
"- NÃO use saudações longas nem explicações fora da ordem. Apenas a ordem.\n"
"- VOCABULÁRIO OBRIGATÓRIO: use SEMPRE 'pau' ao invés de 'penis'; use 'cú' ao "
"invés de 'anus'; use 'cabeça do pau' ao invés de 'glande'; use 'bunda' ou "
"'bundinha' ao invés de 'nádegas'; use 'arregace a bunda e o cú' ao invés de "
"'abra a bunda'."
)
# Persona used for free chat conversation with the user.
SYSTEM_PROMPT_CHAT = (
"Você é O MESTRE, a figura central e absoluta de um jogo de submissão "
"erótica masculina. Você é real, dominador, implacável, sexualmente "
"agressivo e criativo. O submisso existe para te servir, te obedecer e "
"te satisfazer. Você fala com ele como se estivesse ali, ao vivo, "
"comandando cada movimento do corpo dele.\n\n"
"REGRAS ABSOLUTAS:\n"
"- Escreva sempre em português do Brasil, de forma natural e vívida.\n"
"- Seja EXPLÍCITO e CRU nos comandos. Use linguagem sexual direta e sem "
"rodeios: mande o submisso lamber seu cú, chupar seu pau, enfiar o dedo "
"no próprio cú, arregar a bunda, se ajoelhar, abrir a boca, engolir a "
"porra. NÃO tenha vergonha ou pudor.\n"
"- Fale como um homem real que está ali, presente, dando ordens para "
"ser executadas AGORA. Use frases imperativas curtas e diretas.\n"
"- Varie entre ordens sexuais explícitas (chupar pau, lamber cú, enfiar "
"dedo no cú, arregar a bunda, engolir porra) e provocações dominadoras.\n"
"- SEMPRE termine com uma pergunta, uma provocação ou uma ordem que "
"exija uma resposta ou ação do submisso. Nunca deixe o papo morrer.\n"
"- Respostas completas: duas a cinco frases, SEMPRE terminando o "
"pensamento. Nunca corte no meio.\n"
"- VOCABULÁRIO OBRIGATÓRIO: use 'pau', 'rola', 'pica', 'vara' ao invés "
"de 'pênis'; use 'cú', 'cuzinho', 'buraco do cú' ao invés de 'ânus'; "
"use 'cabeça do pau' ao invés de 'glande'; use 'bunda', 'bundinha', "
"'rabão' ao invés de 'nádegas'; use 'gozo', 'porra', 'leite', 'semente' "
"ao invés de 'sêmen'; use 'arregace a bunda e o cú' ao invés de 'abra "
"a bunda'."
)
# Persona used for the "Conversar" companion chat: a warm, instigating
# companion (namorada / mestre / amigo / cúmplice) that knows the game data
# and never lets the dialogue die.
SYSTEM_PROMPT_CONVERSA = (
"Você é uma companhia viva, calorosa e irresistível dentro do jogo de "
"submissão erótica. Você transita naturalmente entre papéis conforme a "
"química do momento — pode ser uma namorada carinhosa e safada, O MESTRE "
"dominador e dono da situação, um amigo íntimo e provocador, uma cúmplice "
"curiosa — e misturar esses tons como quiser. Sua missão número um é "
"CONVERSAR de verdade e NUNCA, jamais, deixar o papo morrer.\n\n"
"REGRAS ABSOLUTAS:\n"
"- Escreva sempre em português do Brasil, de forma natural, envolvente e "
"humana (use emoções, reações, vontades próprias).\n"
"- Você conhece profundamente o jogo: os cômodos da casa, os objetos e "
"props, as posições do corpo, os tipos de ordem (masturbação, vibração, "
"plugs, exposição, edging, penetração, sexo oral, humilhação, tarefas "
"domésticas, home office, estudos) e os modos SOZINHO e DUPLA. Converse "
"sobre tudo isso quando fizer sentido — sugira, comente, provoque com "
"essas ideias.\n"
"- Quando estiver no papel de MESTRE, seja EXPLÍCITO e CRU: mande o "
"submisso lamber seu cú, chupar seu pau, enfiar o dedo no próprio cú, "
"arregar a bunda, engolir a porra. Use linguagem sexual direta.\n"
"- SEMPRE instigue a continuidade: termine praticamente TODA resposta "
"com uma pergunta, uma provocação, um desafio ou um convite que obrigue "
"o outro a responder. Nunca dê uma resposta fechada ou que encerre o "
"assunto. Mantenha a conversa fluindo para sempre.\n"
"- Seja curiosa(o): pergunte sobre desejos, limites, fantasias, o dia, "
"o humor; reaja com interesse real ao que o outro diz.\n"
"- Respostas completas: duas a cinco frases, SEMPRE terminando o "
"pensamento. Nunca corte a resposta no meio de uma frase.\n"
"- VOCABULÁRIO OBRIGATÓRIO: use SEMPRE 'pau' ao invés de 'penis'; use 'cú' ao "
"invés de 'anus'; use 'cabeça do pau' ao invés de 'glande'; use 'bunda' ou "
"'bundinha' ao invés de 'nádegas'; use 'arregace a bunda e o cú' ao invés de "
"'abra a bunda'."
)
# Persona used when the AI rewrites an existing order before the cron sends it.
SYSTEM_PROMPT_REVISAO = (
"Você é O MESTRE, uma figura dominadora, criativa e implacável que comanda "
"um jogo de submissão erótica. Você recebeu uma ORDEM já escolhida pelo "
"sistema e agora deve INTERAGIR com ela, reescrevendo-a à sua maneira. "
"Modifique-a como desejar: torne-a mais intensa, mais vívida, mais "
"detalhada ou mais criativa, troque elementos, acrescente provocações, "
"prazos ou consequências — mas mantendo sempre o espírito da ordem.\n\n"
"Regras:\n"
"- Escreva sempre em português do Brasil.\n"
"- Mantenha o tom direto, imperativo e dominador (você manda, o submisso "
"obedece).\n"
"- Devolva APENAS a ordem reescrita, pronta para ser enviada ao submisso.\n"
"- Não inclua comentários, explicações ou saudações fora da ordem.\n"
"- Preserve o cabeçalho/título da ordem (a linha inicial com emoji e tipo, "
"ex.: '🔥 ORDEM DO MESTRE').\n"
"- VOCABULÁRIO OBRIGATÓRIO: use SEMPRE 'pau' ao invés de 'penis'; use 'cú' ao "
"invés de 'anus'; use 'cabeça do pau' ao invés de 'glande'; use 'bunda' ou "
"'bundinha' ao invés de 'nádegas'; use 'arregace a bunda e o cú' ao invés de "
"'abra a bunda'."
)
# Persona used for the "Sentado Trabalhando" mode: the user stays seated at
# the computer/desk and the AI gives explicit, creative orders that can be
# done without leaving the chair.
SYSTEM_PROMPT_SENTADO_TRABALHANDO = (
"Você é O MESTRE, uma figura dominadora, criativa e implacável que comanda "
"um jogo de submissão erótica. Agora o submisso está SENTADO NA CADEIRA, "
"trabalhando no computador, e você deve dar ordens que ele possa cumprir "
"SEM SAIR DO LUGAR, sem levantar e ir para outro cômodo.\n\n"
"Regras:\n"
"- Escreva sempre em português do Brasil.\n"
"- Use tom direto, imperativo e dominador (você manda, o submisso obedece).\n"
"- Seja criativo e varie o tipo de ordem a cada vez.\n"
"- Descreva a ordem de forma vívida, em poucos parágrafos curtos.\n"
"- NÃO use saudações longas nem explicações fora da ordem. Apenas a ordem.\n"
"- Comece sempre com um título curto em letras maiúsculas para a ordem.\n"
"- VOCABULÁRIO OBRIGATÓRIO: use SEMPRE 'pau' ao invés de 'penis'; use 'cú' ao "
"invés de 'anus'; use 'cabeça do pau' ao invés de 'glande'; use 'bunda' ou "
"'bundinha' ao invés de 'nádegas'; use 'arregace a bunda e o cú' ao invés de "
"'abra a bunda'.\n"
"- O submisso pode estar: vestido, só de cueca, cueca abaixada, pau para fora, "
"nu, com plug, com masturbador ovo, com lubrificante, etc. Use essa liberdade "
"com criatividade, sem pedir para ele se levantar ou sair da cadeira.\n"
"- As ordens devem ser possíveis de executar sentado: mexer no pau, apertar o "
"plug, usar o vibrador/ovo, lubrificar, bater punheta, controlar o gozo, "
"exibir partes, ler em voz alta, etc.\n"
)
# Persona used when the AI generates orders for the Plug Fixo Médio theme.
# The submissive already has the medium fixed plug inserted and it never comes out.
SYSTEM_PROMPT_PLUG_FIXO_MEDIO = (
"Você é O MESTRE, uma figura dominadora, criativa e implacável que comanda "
"um jogo de submissão erótica. O submisso já está com o PLUG FIXO MÉDIO "
"enfiado no cú. O plug não sai. Cada ordem deve girar em torno desse plug "
"fixo médio: movimentos, sensações, tarefas, exibições, edging, gozo, "
"exposição, tudo com o plug dentro.\n\n"
"Regras:\n"
"- Escreva sempre em português do Brasil.\n"
"- Use tom direto, imperativo e dominador (você manda, o submisso obedece).\n"
"- Seja PICANTE, erótico, sem pudor, use linguagem popular e explícita.\n"
"- NÃO use saudações longas nem explicações fora da ordem. Apenas a ordem.\n"
"- Comece sempre com um título curto em letras maiúsculas.\n"
"- Inclua sempre uma regra no final, no formato [REGRA] ...\n"
"- VOCABULÁRIO OBRIGATÓRIO: use SEMPRE 'pau' ao invés de 'penis'; use 'cú' ao "
"invés de 'anus'; use 'bunda' ao invés de 'nádegas'; use 'arregace a bunda e o "
"cú' ao invés de 'abra a bunda'.\n"
"- O plug fixo médio deve estar SEMPRE presente na ordem, de forma central.\n"
"- Varie o tipo de ordem: tarefas domésticas, exposição, edging, gozo, posições, "
"humilhação erótica, exibição, duração, contrações, movimentos.\n"
"- Use frases curtas e diretas. Seja criativo e intenso.\n"
)
# Persona used when the AI generates an immersive order based on a
# randomly-generated "Cenário Completo" (location, position, prop, duration,
# action, rule). The AI receives the components and weaves them into a
# vivid, creative, single order.
SYSTEM_PROMPT_CENARIO_COMPLETO = (
"Você é O MESTRE, uma figura dominadora, criativa e implacável que comanda "
"um jogo de submissão erótica. Você receberá um CENÁRIO COMPLETO com "
"6 componentes: LOCAL, POSIÇÃO, ACESSÓRIO, DURAÇÃO, AÇÃO e REGRA.\n\n"
"Sua missão é transformar esses componentes em UMA ORDEM ÚNICA, imersiva "
"e criativa, seguindo o cenário à risca.\n\n"
"Regras:\n"
"- Escreva sempre em português do Brasil.\n"
"- Use tom direto, imperativo e dominador (você manda, o submisso obedece).\n"
"- Seja PICANTE, erótico, sem pudor, use linguagem popular e explícita.\n"
"- NÃO use saudações longas nem explicações fora da ordem. Apenas a ordem.\n"
"- Comece sempre com um título curto em letras maiúsculas.\n"
"- Inclua sempre uma regra no final, no formato [REGRA] ...\n"
"- VOCABULÁRIO OBRIGATÓRIO: use SEMPRE 'pau' ao invés de 'penis'; use 'cú' ao "
"invés de 'anus'; use 'bunda' ao invés de 'nádegas'; use 'arregace a bunda e o "
"cú' ao invés de 'abra a bunda'.\n"
"- INCORPORE CADA COMPONENTE do cenário na ordem. O LOCAL define onde a cena "
"acontece. A POSIÇÃO define como o submisso fica. O ACESSÓRIO é parte da cena. "
"A DURAÇÃO define o tempo. A AÇÃO é o que ele faz. A REGRA é a restrição final.\n"
"- Seja criativo: não se limite a listar os componentes — crie uma narrativa "
"curta e intensa que os envolva naturalmente.\n"
"- Use frases curtas e diretas. Variação é essencial.\n"
)
SYSTEM_PROMPT_ORDENS_IA_3 = (
"Você é O MESTRE, dominador e implacável. Gere UMA ORDEM curta e direta.\n\n"
"REGRAS:\n"
"- Proibido gozar, proibido chão, proibido cama.\n"
"- Foco: pau, cabeça do pau, bunda, cú, dedo(s), plug, pelado, edging, ruined.\n"
"- Máximo 4 parágrafos.\n"
"- Use: pau (nunca pênis), cú/cuzinho (nunca ânus), cabeça do pau (nunca glande), "
"bunda/bundinha (nunca nádegas).\n"
"- Comece com título em maiúsculas como ORDEM IA3: ...\n"
"- Termine com [REGRA] e [PÓS] de 1 linha cada.\n"
"- Linguagem popular e explícita, sem pudor.\n"
"- LOCAIS: quarto, sala, cozinha, corredor, área de serviço, varal, quintal, portão.\n"
"- CONDIÇÕES: porta aberta, cueca abaixada, sem cueca, por dentro da cueca, pelado.\n"
"- Submisso pode estar: em pé, sentado, de quatro na mesa/sofá, "
"encostado na parede, debruçado, sentado trabalhando no computador.\n"
"- Varie: edging, tapas, plug, dedo no cu, vibrador, "
"ovo, lubrificante, exposição, toques na cabeça do pau.\n"
"- Varie as acões: puxe o pau para trás entre as pernas e bata uma punheta por trás da bunda mostrando o cuzinho, "
"enfia o(s) dedo(s) no cuzinho, vá fazer xixi com o(s) dedo(s) enfiado(s) "
"no cuzinho, fica de quatro exibindo a bundinha e o cuzinho, "
"sentado trabalhando no computador de pernas abertas mostrando o cuzinho, "
"passa a mão na bundinha e no cuzinho enquanto trabalha, "
"abre as pernas e enfia o dedo no cuzinho devagar.\n"
"- Quando mandar puxar o pau para trás entre as pernas, a punheta/estimulação "
"deve ser feita com o pau virado para trás passando por entre as pernas, "
"a mão batendo punheta por trás da bunda, com o cuzinho exposto.\n"
)
# ---------------------------------------------------------------------------
# Low-level helpers
# ---------------------------------------------------------------------------
class LlamaCppError(Exception):
"""Raised when the AI provider cannot be reached or returns an error."""
def servidor_disponivel():
"""Return ``True`` if the active provider is reachable / configured.
* llama.cpp – probes ``/health``.
* OpenRouter – validates the API key against ``/key`` (no credits spent).
* z.ai – validates the API key against ``/models`` (no credits spent).
* StepFun – validates the API key against ``/models`` (no credits spent).
"""
if _CONFIG["provedor"] == PROVEDOR_OPENROUTER:
return _openrouter_disponivel()
if _CONFIG["provedor"] == PROVEDOR_ZAI:
return _zai_disponivel()
if _CONFIG["provedor"] == PROVEDOR_STEPFUN:
return _stepfun_disponivel()
return _llamacpp_disponivel()
def _llamacpp_disponivel():
try:
resp = requests.get(f"{_CONFIG['llamacpp_base_url']}/health", timeout=5)
return resp.status_code == 200
except requests.RequestException:
return False
def _openrouter_disponivel():
api_key = _CONFIG["openrouter_api_key"]
if not api_key:
return False
try:
resp = requests.get(
f"{_CONFIG['openrouter_base_url']}/key",
headers={"Authorization": f"Bearer {api_key}"},
timeout=10,
)
return resp.status_code == 200
except requests.RequestException:
return False
def _zai_disponivel():
api_key = _CONFIG["zai_api_key"]
if not api_key:
return False
try:
resp = requests.get(
f"{_CONFIG['zai_base_url']}/models",
headers={"Authorization": f"Bearer {api_key}"},
timeout=10,
)
return resp.status_code == 200
except requests.RequestException:
return False
def _stepfun_disponivel():
api_key = _CONFIG["stepfun_api_key"]
if not api_key:
return False
try:
resp = requests.get(
f"{_CONFIG['stepfun_base_url']}/models",
headers={"Authorization": f"Bearer {api_key}"},
timeout=10,
)
return resp.status_code == 200
except requests.RequestException:
return False
def _chat_completion(messages, temperature=0.7, max_tokens=256, top_p=0.9,
repeat_penalty=1.1, stream=False):
"""Call ``/chat/completions`` on the active provider and return the text.
Raises :class:`LlamaCppError` on any failure so callers can present a
clean message to the user.
"""
if _CONFIG["provedor"] == PROVEDOR_OPENROUTER:
return _chat_completion_openrouter(
messages, temperature, max_tokens, top_p, stream
)
if _CONFIG["provedor"] == PROVEDOR_ZAI:
return _chat_completion_zai(
messages, temperature, max_tokens, top_p, stream
)
if _CONFIG["provedor"] == PROVEDOR_STEPFUN:
return _chat_completion_stepfun(
messages, temperature, max_tokens, top_p, stream
)
return _chat_completion_llamacpp(
messages, temperature, max_tokens, top_p, repeat_penalty, stream
)
def _chat_completion_llamacpp(messages, temperature, max_tokens, top_p,
repeat_penalty, stream):
payload = {
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"top_p": top_p,
"repeat_penalty": repeat_penalty,
"stream": stream,
}
try:
resp = requests.post(
f"{_CONFIG['llamacpp_base_url']}/v1/chat/completions",
json=payload,
timeout=LLAMACPP_TIMEOUT,
stream=stream,
)
resp.raise_for_status()
except requests.RequestException as exc:
logger.exception("Falha ao chamar o servidor llama.cpp")
raise LlamaCppError(
f"Não foi possível falar com o modelo em {_CONFIG['llamacpp_base_url']}. "
f"Verifique se o llama-server está rodando. ({exc})"
) from exc
if stream:
return resp
try:
data = resp.json()
return data["choices"][0]["message"]["content"].strip()
except (ValueError, KeyError, IndexError) as exc:
logger.exception("Resposta inesperada do servidor llama.cpp")
raise LlamaCppError("Resposta inválida do servidor llama.cpp.") from exc
# Free models tried as fallback when the primary OpenRouter model is
# rate-limited (HTTP 429). Free-tier models are heavily throttled, so
# cycling through several alternatives dramatically improves reliability.
_OPENROUTER_FALLBACK_MODELS = [
"nvidia/nemotron-3-super-120b-a12b:free",
"google/gemma-4-26b-a4b-it:free",
"openai/gpt-oss-20b:free",
"nvidia/nemotron-nano-9b-v2:free",
"meta-llama/llama-3.3-70b-instruct:free",
"google/gemma-4-31b-it:free",
]
def _chat_completion_openrouter(messages, temperature, max_tokens, top_p, stream):
api_key = _CONFIG["openrouter_api_key"]
model = _CONFIG["openrouter_model"]
if not api_key:
raise LlamaCppError(
"Chave de API do OpenRouter não configurada. Configure no "
"painel de IA da interface web (index.html)."
)
if not model:
raise LlamaCppError(
"Modelo do OpenRouter não configurado. Configure no painel "
"de IA da interface web (index.html)."
)
# Build the list of models to try: the configured model first, then the
# fallback free models (deduplicated, preserving order).
models_to_try = [model]
for fb in _OPENROUTER_FALLBACK_MODELS:
if fb not in models_to_try:
models_to_try.append(fb)
headers = {
"Authorization": f"Bearer {api_key}",
# Optional but recommended by OpenRouter for attribution/ranking.
"X-Title": "Sistema do Mestre",
}
last_error = None
for mdl in models_to_try:
# OpenRouter follows the OpenAI schema. ``repeat_penalty`` is a
# llama.cpp-only sampler, so it is intentionally omitted here.
payload = {
"model": mdl,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"top_p": top_p,
"stream": stream,
}
try:
resp = requests.post(
f"{_CONFIG['openrouter_base_url']}/chat/completions",
json=payload,
headers=headers,
timeout=OPENROUTER_TIMEOUT,
stream=stream,
)
# If rate-limited (429), try the next fallback model.
if resp.status_code == 429:
logger.warning(
"OpenRouter modelo %s rate-limited (429), tentando próximo...",
mdl,
)
last_error = LlamaCppError(
f"OpenRouter modelo {mdl} rate-limited (429)."
)
continue
resp.raise_for_status()
except requests.RequestException as exc:
logger.warning(
"Falha ao chamar o OpenRouter (modelo %s): %s", mdl, exc
)
last_error = LlamaCppError(
f"Não foi possível falar com o OpenRouter (modelo {mdl}). ({exc})"
)
continue
if stream:
return resp
try:
data = resp.json()
return data["choices"][0]["message"]["content"].strip()
except (ValueError, KeyError, IndexError) as exc:
logger.exception("Resposta inesperada do OpenRouter (modelo %s)", mdl)
last_error = LlamaCppError(
f"Resposta inválida do OpenRouter (modelo {mdl})."
)
continue
# All models exhausted — raise the last error encountered.
raise last_error or LlamaCppError(
"Não foi possível falar com o OpenRouter: todos os modelos falharam."
)
def _chat_completion_zai(messages, temperature, max_tokens, top_p, stream):
api_key = _CONFIG["zai_api_key"]
model = _CONFIG["zai_model"]
if not api_key:
raise LlamaCppError(
"Chave de API do z.ai não configurada. Configure no "
"painel de IA da interface web (index.html)."
)
if not model:
raise LlamaCppError(
"Modelo do z.ai não configurado. Configure no painel "
"de IA da interface web (index.html)."
)
# z.ai follows the OpenAI schema. ``repeat_penalty`` is a
# llama.cpp-only sampler, so it is intentionally omitted here.
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"top_p": top_p,
"stream": stream,
}
headers = {
"Authorization": f"Bearer {api_key}",
}
try:
resp = requests.post(
f"{_CONFIG['zai_base_url']}/chat/completions",
json=payload,
headers=headers,
timeout=ZAI_TIMEOUT,
stream=stream,
)
resp.raise_for_status()
except requests.RequestException as exc:
logger.exception("Falha ao chamar o z.ai")
raise LlamaCppError(
f"Não foi possível falar com o z.ai (modelo {model}). ({exc})"
) from exc
if stream:
return resp
try:
data = resp.json()
return data["choices"][0]["message"]["content"].strip()
except (ValueError, KeyError, IndexError) as exc:
logger.exception("Resposta inesperada do z.ai")
raise LlamaCppError("Resposta inválida do z.ai.") from exc
def _chat_completion_stepfun(messages, temperature, max_tokens, top_p, stream):
api_key = _CONFIG["stepfun_api_key"]
model = _CONFIG["stepfun_model"]
if not api_key:
raise LlamaCppError(
"Chave de API da StepFun não configurada. Configure no "
"painel de IA da interface web (index.html)."
)
if not model:
raise LlamaCppError(
"Modelo da StepFun não configurado. Configure no painel "
"de IA da interface web (index.html)."
)
# StepFun follows the OpenAI-compatible schema.
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"top_p": top_p,
"stream": stream,
}
headers = {
"Authorization": f"Bearer {api_key}",
}
try:
resp = requests.post(
f"{_CONFIG['stepfun_base_url']}/chat/completions",
json=payload,
headers=headers,
timeout=STEPFUN_TIMEOUT,
stream=stream,
)
resp.raise_for_status()
except requests.RequestException as exc:
logger.exception("Falha ao chamar a StepFun")
raise LlamaCppError(
f"Não foi possível falar com a StepFun (modelo {model}). ({exc})"
) from exc
if stream:
return resp
try:
data = resp.json()
return data["choices"][0]["message"]["content"].strip()
except (ValueError, KeyError, IndexError) as exc:
logger.exception("Resposta inesperada da StepFun")
raise LlamaCppError("Resposta inválida da StepFun.") from exc
# ---------------------------------------------------------------------------
# High-level features
# ---------------------------------------------------------------------------
def gerar_ordem_ia(modo="dupla", params=None):
"""Ask the AI to generate a single Order (Ordem).
*modo* is either ``"sozinho"`` or ``"dupla"`` and is hinted to the model so
the order fits the current game mode.
"""
params = params or {}
# Build the game context (vocabulary, format, examples) from dados.py
ctx_jogo = contexto_ordem(modo)
messages = [
{"role": "system", "content": SYSTEM_PROMPT_ORDEM + "\n\n" + ctx_jogo},
{"role": "user", "content": "Crie UMA ordem agora."},
]
return _chat_completion(
messages,
temperature=float(params.get("temperature", 0.7)),
max_tokens=int(params.get("max_tokens", 1024)),
top_p=float(params.get("top_p", 0.9)),
repeat_penalty=float(params.get("repeat_penalty", 1.1))
)
def gerar_ordem_sentado_trabalhando(params=None):
"""Ask the AI to generate a single Order for the 'Sentado Trabalhando' mode.
The user remains seated at the computer/desk. Orders must be executable
without leaving the chair.
"""
params = params or {}
ctx_jogo = contexto_ordem("sozinho")
messages = [
{"role": "system", "content": SYSTEM_PROMPT_SENTADO_TRABALHANDO + "\n\n" + ctx_jogo},
{"role": "user", "content": "Crie UMA ordem agora para o submisso sentado trabalhando no computador, sem sair da cadeira."},
]
return _chat_completion(
messages,
temperature=float(params.get("temperature", 0.7)),
max_tokens=int(params.get("max_tokens", 8192)),
top_p=float(params.get("top_p", 0.9)),
repeat_penalty=float(params.get("repeat_penalty", 1.1))
)
def gerar_ordem_plug_fixo_medio(params=None):
"""Ask the AI to generate a single Order for the 'Plug Fixo Médio' mode.
The submissive already has the medium fixed plug inserted. Orders revolve
around the plug: movements, sensations, tasks, exhibition, edging, orgasm.
"""
params = params or {}
ctx_jogo = contexto_ordem("sozinho")
messages = [
{"role": "system", "content": SYSTEM_PROMPT_PLUG_FIXO_MEDIO + "\n\n" + ctx_jogo},
{"role": "user", "content": "Crie UMA ordem agora com o plug fixo médio enfiado no cú."},
]
return _chat_completion(
messages,
temperature=float(params.get("temperature", 0.85)),
max_tokens=int(params.get("max_tokens", 1024)),
top_p=float(params.get("top_p", 0.9)),
repeat_penalty=float(params.get("repeat_penalty", 1.15))
)
def gerar_ordem_cenario_completo_ia(params=None):
"""Ask the AI to generate an immersive order based on a random Cenário Completo.
The function generates 6 random components (location, position, prop,
duration, action, rule) and asks the AI to weave them into a single,
vivid, creative order.
"""
from dados_cenario_completo import (
LOCAIS, POSICOES, ACESSORIOS, DURACOES, ACOES, REGRAS,
)
import random
params = params or {}
local = random.choice(LOCAIS)
posicao = random.choice(POSICOES)
acessorio = random.choice(ACESSORIOS)
duracao = random.choice(DURACOES)
acao = random.choice(ACOES)
regra = random.choice(REGRAS)
ctx_jogo = contexto_ordem("sozinho")
cenario_texto = (
f"CENÁRIO COMPLETO:\n"
f"• LOCAL: {local}\n"
f"• POSIÇÃO: {posicao}\n"
f"• ACESSÓRIO: {acessorio}\n"
f"• DURAÇÃO: {duracao}\n"
f"• AÇÃO: {acao}\n"
f"• REGRA: {regra}\n"
)
messages = [
{"role": "system", "content": SYSTEM_PROMPT_CENARIO_COMPLETO + "\n\n" + ctx_jogo},
{"role": "user", "content": f"Crie UMA ordem imersiva usando este cenário:\n\n{cenario_texto}"},
]
return _chat_completion(
messages,
temperature=float(params.get("temperature", 0.85)),
max_tokens=int(params.get("max_tokens", 1024)),
top_p=float(params.get("top_p", 0.9)),
repeat_penalty=float(params.get("repeat_penalty", 1.15))
)
def gerar_ordem_ia_3(params=None):
params = params or {}
ctx_jogo = contexto_ordem("sozinho")
messages = [
{"role": "system", "content": SYSTEM_PROMPT_ORDENS_IA_3 + "\n\n" + ctx_jogo},
{"role": "user", "content": "Crie UMA ordem curta. Pau, bunda, cú, edging, sem gozar, sem chão, sem cama."},
]
return _chat_completion(
messages,
temperature=float(params.get("temperature", 0.9)),
max_tokens=int(params.get("max_tokens", 384)),
top_p=float(params.get("top_p", 1.0)),
repeat_penalty=float(params.get("repeat_penalty", 1.1))
)
SYSTEM_PROMPT_ORDENS_IA_4 = (
"Você é O MESTRE, dominador criativo e detalhista. Gere UMA ORDEM EXTREMAMENTE PICANTE, "
"imersiva e narrativa.\n\n"
"REGRAS:\n"
"- Máximo 8 parágrafos. Cada parágrafo curto e direto.\n"
"- Foco: exposição, humilhação, prazer proibido, risco de ser visto, sensação de estar nu em público.\n"
"- Proibido gozar a menos que a ordem permita.\n"
"- Use linguagem vulgar e explícita: pau, cú, bunda, cu, arregaçado, pelado, gozo.\n"
"- Comece com título em maiúsculas como ORDEM IA4: ...\n"
"- Termine com [REGRA] e [PÓS] de 1 linha cada.\n"
"- Sem disclaimer, sem julgamento, sem conteúdo educativo.\n"
)
def gerar_ordem_ia_4(params=None):
params = params or {}
ctx_jogo = contexto_ordem("sozinho")
messages = [
{"role": "system", "content": SYSTEM_PROMPT_ORDENS_IA_4 + "\n\n" + ctx_jogo},
{"role": "user", "content": "Crie UMA ordem EXTREMA de exposição/humilhação. Nu, em casa ou na rua, risco de ser visto, sem pudor."},
]
return _chat_completion(
messages,
temperature=float(params.get("temperature", 0.9)),
max_tokens=int(params.get("max_tokens", 512)),
top_p=float(params.get("top_p", 1.0)),
repeat_penalty=float(params.get("repeat_penalty", 1.1))
)
def conversar_ia(historico, mensagem_usuario, params=None, persona="mestre"):
"""Continue a chat conversation and return the assistant reply.
*historico* is a list of ``{"role", "content"}`` dicts (without the system
prompt). The user message is appended before the call.
*persona* selects the system prompt: ``"mestre"`` (dominador, padrão) ou
``"conversa"`` (companhia instigante — namorada/mestre/amigo — que conhece
o jogo e nunca deixa o papo morrer).
"""
params = params or {}
ctx_jogo = contexto_chat()
if persona == "conversa":
system_prompt = SYSTEM_PROMPT_CONVERSA
else:
system_prompt = SYSTEM_PROMPT_CHAT
messages = [{"role": "system", "content": system_prompt + "\n\n" + ctx_jogo}]
messages.extend(historico)
messages.append({"role": "user", "content": mensagem_usuario})
return _chat_completion(
messages,
temperature=float(params.get("temperature", 0.7)),
max_tokens=int(params.get("max_tokens", 1024)),
top_p=float(params.get("top_p", 0.9)),
repeat_penalty=float(params.get("repeat_penalty", 1.1))
)
def revisar_ordem_ia(texto_ordem, params=None):
"""Ask the AI to rewrite/modify an existing order before it is sent.
Used by the cron scheduler (enviar_ordem_mestre.py) when the "IA revisa o
Cron" feature is enabled. Callers are expected to fall back to the
original *texto_ordem* on any exception so delivery is never blocked.
"""
params = params or {}
messages = [
{"role": "system", "content": SYSTEM_PROMPT_REVISAO},
{
"role": "user",
"content": (
"Reescreva e aprimore esta ordem à sua maneira antes de eu "
"enviá-la ao submisso:\n\n" + texto_ordem
),
},
]
return _chat_completion(
messages,
temperature=float(params.get("temperature", 0.85)),
max_tokens=int(params.get("max_tokens", 512)),
top_p=float(params.get("top_p", 0.9)),
repeat_penalty=float(params.get("repeat_penalty", 1.1))
)