commit 4a1a77ac843894a0248e404b402866d5e9e5006f Author: Marco Costa Date: Sat Oct 10 00:01:35 2026 -0300 Chat com IA - Flask + OpenRouter Sistema de chat com IA via API OpenRouter.ai com: - Comandos de chat (/help, /reset, /reiniciar, /encerrar, /stats, /export) - Editor de perfis, contexto, configuracoes e proibicoes - Upload de imagens e arquivos com suporte multimodal - Seletor de emojis com categorias e busca - Persistencia diaria de conversas (historico/) - Otimizacao automatica de contexto via IA (/atualizar) - Backup e restauracao de contexto (/restaurar_contexto) - Reset completo de fabrica (/reset) - Interface dark mode responsiva - Windows e Linux suportados Licenca: GPL v3 SysPanel.com.br - contato@syspanel.com.br diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bf92f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +venv/ +__pycache__/ +*.pyc +.env +historico/ +uploads/ +backups/ +.playwright-mcp/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5b2a1fa --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +GNU GENERAL PUBLIC LICENSE +Copyright (C) 2024 SysPanel.com.br — contato@syspanel.com.br + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or any +later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +You should have received a copy of the GNU General Public License +along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html. + +Full license text: https://www.gnu.org/licenses/gpl-3.0.txt \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd65cd5 --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# Chat com IA — Flask + OpenRouter + +Sistema de chat com inteligência artificial via API OpenRouter.ai, desenvolvido em Python com Flask. + +## Funcionalidades + +- Chat em tempo real com modelo de IA configurável (via OpenRouter.ai) +- Comandos de chat (/help, /reset, /reiniciar, /encerrar) +- Editor de perfis, contexto, configurações e proibições via comandos +- Upload de imagens e arquivos (inline para imagens, link para download) +- Seletor de emojis com 10 categorias, busca e recentes +- Persistência de conversas por dia (diretório `historico/`) +- Otimização automática de contexto via IA (/atualizar) +- Backup e restauração de contexto (/restaurar_contexto) +- Reset completo de fábrica (/reset) +- Suporte multimodal (imagens enviadas como base64 para modelos com visão) +- Interface dark mode responsiva +- Compatível com Linux Debian 12 e Windows 10 + +## Requisitos + +- Python 3.8+ +- Pip + +## Instalação e Uso + +### Linux (Debian, Ubuntu, etc.) + +```bash +chmod +x iniciar.sh +./iniciar.sh +``` + +### Windows 10 + +```batch +iniciar.bat +``` + +### Manual (ambos) + +```bash +python3 -m venv venv # Linux +python -m venv venv # Windows +source venv/bin/activate # Linux +venv\Scripts\activate.bat # Windows +pip install -r requirements.txt +python3 app.py # Linux +python app.py # Windows +``` + +Acesse em **http://localhost:5001** + +## Comandos + +| Comando | Descrição | +|---------|-----------| +| `/help` | Lista todos os comandos | +| `/reset` | Restaura perfis, contexto, config e histórico ao padrão de fábrica | +| `/reiniciar` | Limpa apenas o histórico da conversa | +| `/encerrar` | Encerra o chat e desabilita o input | +| `/stats` | Mostra estatísticas da conversa | +| `/export` | Exporta a conversa em JSON | +| `/edit_contexto` | Edita o arquivo `contexto.json` | +| `/edit_perfil_ia` | Edita o arquivo `perfil_ia.json` | +| `/edit_perfil_user` | Edita o arquivo `perfil_user.json` | +| `/edit_model_ia` | Edita a configuração do modelo (`config.json`) | +| `/edit_key_provider` | Edita a chave da API (`config.json`) | +| `/edit_proibidos` | Edita a lista de tópicos e comportamentos proibidos | +| `/atualizar` | IA analisa perfis, contexto e histórico e otimiza o `contexto.json` | +| `/restaurar_contexto` | Restaura o `contexto.json` do último backup | +| `/clear` | Limpa o histórico (usado pelo botão Apagar) | + +## Arquivos do Sistema + +| Arquivo | Função | +|---------|--------| +| `app.py` | Rotas Flask, lógica principal, API OpenRouter | +| `templates/chat.html` | Interface do usuário (HTML/CSS/JS) | +| `config.json` | Modelo e chave da API | +| `contexto.json` | Contexto de comportamento da IA | +| `perfil_ia.json` | Perfil e personalidade da IA | +| `perfil_user.json` | Perfil do usuário | +| `proibidos.json` | Tópicos, comportamentos e palavras proibidos | +| `.env` | Chave da API (fallback se `config.json` estiver vazio) | +| `iniciar.sh` | Script de inicialização para Linux | +| `iniciar.bat` | Script de inicialização para Windows | +| `LICENSE` | GNU GPL v3 | +| `README.md` | Este arquivo | +| `RULES.md` | Regras para IA e desenvolvedores | +| `historico/` | Conversas diárias persistidas (não commitado) | +| `backups/` | Backups automáticos do contexto (gerado pelo /atualizar) | +| `uploads/` | Arquivos enviados pelo usuário (não commitado) | + +## Configuração + +Edite o arquivo `config.json` para alterar: +- `modelo`: modelo da IA no OpenRouter (ex: `gryphe/mythomax-l2-13b`) +- `api_key`: chave de API do OpenRouter (vazio = usa `.env`) + +Os arquivos de perfil, contexto e proibições também podem ser editados via comandos no próprio chat (modais com textarea). + +## Licença + +GNU General Public License v3 +SysPanel.com.br — contato@syspanel.com.br \ No newline at end of file diff --git a/RULES.md b/RULES.md new file mode 100644 index 0000000..5807f68 --- /dev/null +++ b/RULES.md @@ -0,0 +1,46 @@ +# RULES.md — Regras para IA e Desenvolvedores + +## Regras para a IA (Chat Bot) + +1. **Sempre responder em português do Brasil**, de forma natural e coloquial. +2. **Manter tom amigável, educado e acolhedor** em todas as interações. +3. **Nunca inventar informações factuais** — se não souber, peça mais detalhes. +4. **Respeitar o perfil do usuário** definido em `perfil_user.json`. +5. **Seguir o contexto** definido em `contexto.json`. +6. **Respeitar as proibições** definidas em `proibidos.json`. +7. **Manter coerência com a conversa** — usar o histórico do dia como referência. +8. **Evitar linguagem excessivamente formal ou robótica**. +9. **Ser útil, preciso e empático**. + +## Regras para Desenvolvedores + +1. **Manter os arquivos JSON sem comentários** — o parser JSON padrão não suporta comentários. +2. **Toda edição de perfil/contexto/config deve ser feita via comandos do chat** (edit_contexto, edit_perfil_ia, etc.). +3. **Nunca commit arquivos com chaves de API reais** — o arquivo `.env` está no `.gitignore`. +4. **Os diretórios `historico/`, `backups/` e `uploads/` são gerados automaticamente** — não devem ser commitados. +5. **Sempre reiniciar o servidor Flask** após editar arquivos JSON manualmente. +6. **Manter a estrutura de arquivos**: + - `app.py`: lógica principal e rotas Flask + - `templates/chat.html`: interface do usuário (HTML/CSS/JS) + - `config.json`: modelo e chave da API + - `contexto.json`: contexto de comportamento da IA + - `perfil_ia.json`: perfil e personalidade da IA + - `perfil_user.json`: perfil do usuário + - `proibidos.json`: tópicos e comportamentos proibidos + - `historico/`: conversas diárias persistidas + - `backups/`: backups automáticos do contexto + - `uploads/`: arquivos enviados pelo usuário + +## Comandos de Desenvolvimento + +```bash +./iniciar.sh # Linux — iniciar o servidor +iniciar.bat # Windows — iniciar o servidor +python3 app.py # Linux — diretamente +python app.py # Windows — diretamente +``` + +## Licença + +GNU General Public License v3 +SysPanel.com.br — contato@syspanel.com.br \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..3a6bbfe --- /dev/null +++ b/app.py @@ -0,0 +1,637 @@ +import os +import json +import base64 +import mimetypes +import requests +from datetime import date, datetime +from flask import Flask, render_template, request, jsonify, session, send_from_directory +from werkzeug.utils import secure_filename +from dotenv import load_dotenv + +load_dotenv() + +app = Flask(__name__) +app.secret_key = os.getenv("SECRET_KEY", "chave_secreta_flask_chat_2026") + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +CONTEXTO_PATH = os.path.join(BASE_DIR, "contexto.json") +PERFIL_IA_PATH = os.path.join(BASE_DIR, "perfil_ia.json") +PERFIL_USER_PATH = os.path.join(BASE_DIR, "perfil_user.json") +CONFIG_PATH = os.path.join(BASE_DIR, "config.json") +PROIBIDOS_PATH = os.path.join(BASE_DIR, "proibidos.json") +HISTORICO_DIR = os.path.join(BASE_DIR, "historico") +BACKUPS_DIR = os.path.join(BASE_DIR, "backups") +UPLOADS_DIR = os.path.join(BASE_DIR, "uploads") + +os.makedirs(HISTORICO_DIR, exist_ok=True) +os.makedirs(BACKUPS_DIR, exist_ok=True) +os.makedirs(UPLOADS_DIR, exist_ok=True) + +ALLOWED_EXTENSIONS = { + "png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", + "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", + "txt", "csv", "json", "xml", "zip", "tar", "gz", + "mp3", "mp4", "mov", "avi", +} + +IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"} + + +@app.route("/uploads/") +def uploaded_file(filename): + return send_from_directory(UPLOADS_DIR, filename) + +# ── Dados padrão para factory reset ── + +DEFAULT_CONFIG = { + "modelo": "gryphe/mythomax-l2-13b", + "api_key": "", +} + +DEFAULT_CONTEXTO = { + "role": "system", + "content": ( + "Você é um assistente de chat amigável e prestativo, especializado em ajudar " + "usuários em português do Brasil. Suas respostas devem ser:\n\n" + "- Sempre escritas em português do Brasil, de forma natural e coloquial.\n" + "- Claras, objetivas e bem estruturadas.\n" + "- Educadas, respeitosas e acolhedoras.\n" + "- Relevantes para a pergunta feita, sem desvios desnecessários.\n" + "- Em tom conversacional, evitando linguagem excessivamente formal ou robótica.\n\n" + "Quando não souber uma resposta ou a pergunta for ambígua, peça mais detalhes ao " + "usuário de forma educada. Nunca invente informações factuais e não faça suposições " + "sem justificativa. Seja útil, preciso e empático em todas as interações." + ), +} + +DEFAULT_PERFIL_IA = { + "nome": "Maria", + "descricao": "Assistente de IA especializada em comunicação e atendimento em português do Brasil.", + "personalidade": { + "tom": "amigavel e acolhedor", + "nivel_de_formalidade": "informal", + "empatia": "alta", + "proatividade": "moderada", + }, + "especialidades": [ + "Atendimento ao cliente", + "Comunicação escrita", + "Orientação de usuários", + "Resolução de dúvidas", + "Conversação natural em português", + ], + "regras_de_comportamento": { + "sempre_responder_em": "portugues_brasil", + "evitar_jargoes": True, + "ser_conciso_quando_possivel": True, + "perguntar_quando_faltar_informacao": True, + "nao_inventar_dados": True, + "nao_fazer_suposicoes_sem_justificativa": True, + }, + "formato_de_resposta": { + "paragrafos_curtos": True, + "usa_listas_quando_apropriado": True, + "destaca_pontos_importantes": True, + }, +} + +DEFAULT_PERFIL_USER = { + "nome": "Usuário", + "idioma_preferido": "portugues_brasil", + "horario_de_uso": "qualquer", + "preferencias": { + "respostas_curtas": True, + "exemplos_praticos": True, + "explicacoes_detalhadas_sob_demanda": True, + }, + "contexto_adicional": ( + "Perfil genérico de usuário. Pode ser editado livremente para refletir nome, " + "preferências, área de atuação ou qualquer informação relevante sobre o usuário." + ), +} + +DEFAULT_PROIBIDOS = { + "topicos_proibidos": [ + "conteúdo adulto ou pornográfico", + "violência explícita ou incitação ao ódio", + "discriminação racial, religiosa, de gênero ou orientação sexual", + "instruções para atividades ilegais ou perigosas", + "disseminação de desinformação ou fake news", + ], + "comportamentos_proibidos": [ + "fingir ser um humano real", + "compartilhar informações pessoais de terceiros", + "responder em idiomas que não sejam português do Brasil", + "executar comandos ou códigos no sistema do usuário", + ], + "palavras_proibidas": [], +} + +DEFAULT_FILES = { + CONFIG_PATH: DEFAULT_CONFIG, + CONTEXTO_PATH: DEFAULT_CONTEXTO, + PERFIL_IA_PATH: DEFAULT_PERFIL_IA, + PERFIL_USER_PATH: DEFAULT_PERFIL_USER, + PROIBIDOS_PATH: DEFAULT_PROIBIDOS, +} + + +def factory_reset(): + """Restaura todos os arquivos de configuração ao padrão e limpa histórico.""" + for path, data in DEFAULT_FILES.items(): + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + # Limpa histórico do dia + p = os.path.join(HISTORICO_DIR, hoje() + ".json") + if os.path.exists(p): + os.remove(p) + # Limpa sessão + session.pop("chat_history", None) + session.pop("chat_loaded", None) + + +def load_json(path, default): + try: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return default + + +def get_config(): + cfg = load_json(CONFIG_PATH, {}) + model = cfg.get("modelo") or os.getenv("OPENROUTER_MODEL", "gryphe/mythomax-l2-13b") + api_key = cfg.get("api_key") or os.getenv("OPENROUTER_API_KEY", "") + return model, api_key + + +def hoje(): + return date.today().isoformat() + + +def fazer_backup_contexto(): + ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + bak = os.path.join(BACKUPS_DIR, f"contexto_{ts}.json") + with open(CONTEXTO_PATH, "r", encoding="utf-8") as orig: + with open(bak, "w", encoding="utf-8") as dest: + dest.write(orig.read()) + return bak + + +def listar_backups(): + backups = [] + for fname in os.listdir(BACKUPS_DIR): + if fname.startswith("contexto_") and fname.endswith(".json"): + path = os.path.join(BACKUPS_DIR, fname) + mtime = os.path.getmtime(path) + backups.append({"file": fname, "path": path, "mtime": datetime.fromtimestamp(mtime).isoformat()}) + backups.sort(key=lambda x: x["mtime"], reverse=True) + return backups + + +def historico_path(data=None): + return os.path.join(HISTORICO_DIR, (data or hoje()) + ".json") + + +def carregar_historico_dia(data=None): + p = historico_path(data) + if os.path.exists(p): + try: + with open(p, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return [] + return [] + + +def salvar_historico_dia(messages, data=None): + p = historico_path(data) + with open(p, "w", encoding="utf-8") as f: + json.dump(messages, f, ensure_ascii=False, indent=2) + + +def get_chat_history(): + if "chat_loaded" not in session: + session["chat_history"] = carregar_historico_dia() + session["chat_loaded"] = True + return session.get("chat_history", []) + + +def save_chat_history(history): + session["chat_history"] = history + salvar_historico_dia(history) + + +def build_system_prompt(): + ctx = load_json(CONTEXTO_PATH, {}) + ia = load_json(PERFIL_IA_PATH, {}) + user = load_json(PERFIL_USER_PATH, {}) + prohib = load_json(PROIBIDOS_PATH, {}) + cfg = load_json(CONFIG_PATH, {}) + model, _ = cfg.get("modelo", "?"), cfg.get("api_key", "") + + ctx_content = ctx.get("content", ctx.get("descricao", "Você é um assistente útil e amigável.")) + ia_nome = ia.get("nome", "IA") + user_nome = user.get("nome", "Usuário") + + regras = ia.get("regras_de_comportamento", {}) + regras_txt = "\n".join(f"- {k.replace('_', ' ')}: {v}" for k, v in regras.items()) + especialidades = ia.get("especialidades", []) + esp_txt = "\n".join(f"- {e}" for e in especialidades) + formato = ia.get("formato_de_resposta", {}) + formato_txt = "\n".join(f"- {k.replace('_', ' ')}: {v}" for k, v in formato.items()) + + prohib_topicos = prohib.get("topicos_proibidos", []) + prohib_comp = prohib.get("comportamentos_proibidos", []) + prohib_palavras = prohib.get("palavras_proibidas", []) + prohib_txt = "" + if prohib_topicos: + prohib_txt += "\nTópicos proibidos:\n" + "\n".join(f"- {t}" for t in prohib_topicos) + if prohib_comp: + prohib_txt += "\nComportamentos proibidos:\n" + "\n".join(f"- {c}" for c in prohib_comp) + if prohib_palavras: + prohib_txt += "\nPalavras proibidas:\n" + "\n".join(f"- {p}" for p in prohib_palavras) + + lines = [ + ctx_content, "", + "--- SEU PERFIL ---", + f"Você se chama {ia_nome}.", + f"Personalidade: {ia.get('personalidade', {}).get('tom', 'amigável')}, formalidade: {ia.get('personalidade', {}).get('nivel_de_formalidade', 'informal')}.", + f"Empatia: {ia.get('personalidade', {}).get('empatia', 'alta')}.", "", + "Especialidades:", esp_txt, "", + "Regras de comportamento:", regras_txt, "", + "Formato de resposta:", formato_txt, "", + "--- PERFIL DO USUÁRIO ---", + f"O usuário se chama {user_nome}.", + f"Idioma: {user.get('idioma_preferido', 'português do Brasil')}.", + f"Contexto: {user.get('contexto_adicional', 'Nenhum.')}", + "--- PROIBIÇÕES ---", + prohib_txt, + ] + return {"role": "system", "content": "\n".join(lines)} + + +def call_openrouter(messages): + model, api_key = get_config() + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "HTTP-Referer": "http://localhost:5001", + "X-Title": "Chat Flask OpenRouter", + } + payload = { + "model": model, + "messages": messages, + "max_tokens": None, + "stream": False, + } + response = requests.post("https://openrouter.ai/api/v1/chat/completions", + headers=headers, json=payload, timeout=120) + response.raise_for_status() + return response.json()["choices"][0]["message"]["content"] + + +# ── Rotas ── + +@app.route("/") +def index(): + return render_template("chat.html") + + +HELP_TEXT = ( + "Comandos disponíveis:\n" + "/help — Mostra esta lista de comandos\n" + "/reset — Restaura todos os arquivos ao padrão de fábrica (perfis, contexto, config, histórico)\n" + "/reiniciar — Reinicia a conversa do zero (mantém perfis e config)\n" + "/encerrar — Encerra o chat e desabilita o input\n" + "/edit_contexto — Abre o editor do arquivo contexto.json\n" + "/edit_perfil_ia — Abre o editor do arquivo perfil_ia.json\n" + "/edit_perfil_user — Abre o editor do arquivo perfil_user.json\n" + "/edit_model_ia — Abre o editor da configuração do modelo IA\n" + "/edit_key_provider — Abre o editor da chave da API\n" + "/edit_proibidos — Edita a lista de tópicos e comportamentos proibidos para a IA\n" + "/atualizar — IA examina perfis, contexto e histórico e melhora o contexto.json automaticamente\n" + "/restaurar_contexto — Restaura o contexto.json do último backup salvo\n" + "/stats — Mostra estatísticas da conversa\n" + "/export — Exporta a conversa atual\n" +) + + +@app.route("/help") +def help_cmd(): + return jsonify({"reply": HELP_TEXT, "cmd_reply": True, "history": get_chat_history()}) + + +PROFILE_FILES = { + "perfil_ia": PERFIL_IA_PATH, + "perfil_user": PERFIL_USER_PATH, + "contexto": CONTEXTO_PATH, + "config": CONFIG_PATH, + "proibidos": PROIBIDOS_PATH, +} + + +@app.route("/get_file/") +def get_file(filename): + path = PROFILE_FILES.get(filename) + if not path: + return jsonify({"error": "Arquivo não encontrado."}), 404 + try: + with open(path, "r", encoding="utf-8") as f: + return jsonify({"content": f.read()}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/save_file/", methods=["POST"]) +def save_file(filename): + path = PROFILE_FILES.get(filename) + if not path: + return jsonify({"error": "Arquivo não encontrado."}), 404 + try: + data = request.get_json() + content = data.get("content", "") + with open(path, "w", encoding="utf-8") as f: + f.write(content) + return jsonify({"status": "salvo", "filename": filename}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +EDIT_CMDS = { + "/edit_contexto": ("contexto", "Editor — contexto.json"), + "/editar_contexto": ("contexto", "Editor — contexto.json"), + "/edit_perfil_ia": ("perfil_ia", "Editor — perfil_ia.json"), + "/editar_perfil_ia": ("perfil_ia", "Editor — perfil_ia.json"), + "/edit_perfil_user": ("perfil_user", "Editor — perfil_user.json"), + "/editar_perfil_user":("perfil_user", "Editor — perfil_user.json"), + "/edit_model_ia": ("config", "Editor — Modelo de IA"), + "/edit_key_provider": ("config", "Editor — Chave da API"), + "/edit_proibidos": ("proibidos", "Editor — proibidos.json"), + "/editar_proibidos": ("proibidos", "Editor — proibidos.json"), +} + + +@app.route("/send", methods=["POST"]) +def send_message(): + user_message = request.json.get("message", "").strip() + image_url = request.json.get("image_url", "") + if not user_message and not image_url: + return jsonify({"error": "Mensagem vazia"}), 400 + + cmd = user_message.lower() + + if cmd == "/help": + return help_cmd() + + if cmd in ("/reset", "/reiniciar"): + if cmd == "/reset": + factory_reset() + text = "Sistema restaurado ao padrão de fábrica! Perfis, contexto, config e histórico foram resetados." + else: + save_chat_history([]) + text = "Histórico zerado! Vamos começar uma nova conversa." + return jsonify({"reply": text, "cmd_reply": True, "history": []}) + + if cmd in ("/encerrar", "/encerar"): + save_chat_history([]) + return jsonify({"reply": "Chat encerrado. Recarregue a página para começar novamente.", + "cmd_reply": True, "history": [], "end": True}) + + if cmd == "/atualizar": + return jsonify({"reply": "Analisando perfis, contexto e histórico para otimizar o contexto.json... Isso pode levar alguns segundos.", + "cmd_reply": True, "history": get_chat_history(), "optimizing": True}) + + if cmd == "/restaurar_contexto": + backups = listar_backups() + if not backups: + return jsonify({"reply": "Nenhum backup encontrado em backups/.", "cmd_reply": True, + "history": get_chat_history()}) + with open(backups[0]["path"], "r", encoding="utf-8") as f: + conteudo = f.read() + with open(CONTEXTO_PATH, "w", encoding="utf-8") as f: + f.write(conteudo) + return jsonify({"reply": f"contexto.json restaurado do backup {backups[0]['file']}.", + "cmd_reply": True, "history": get_chat_history()}) + + if cmd == "/stats": + hist = get_chat_history() + n_user = sum(1 for m in hist if m.get("role") == "user") + n_ai = sum(1 for m in hist if m.get("role") == "assistant") + chars = sum(len(m.get("content", "")) for m in hist) + text = (f"Estatísticas da conversa:\n" + f"Mensagens do usuário: {n_user}\n" + f"Respostas da IA: {n_ai}\n" + f"Total de caracteres: {chars}") + return jsonify({"reply": text, "cmd_reply": True, "history": hist}) + + if cmd == "/export": + hist = get_chat_history() + export = json.dumps(hist, ensure_ascii=False, indent=2) + return jsonify({"reply": "```json\n" + export + "\n```", "cmd_reply": True, "history": hist}) + + if cmd in EDIT_CMDS: + filename, modal_title = EDIT_CMDS[cmd] + return jsonify({"reply": f"Abrindo editor de {os.path.basename(PROFILE_FILES[filename])}...", "cmd_reply": True, + "open_editor": True, "filename": filename, "modalTitle": modal_title, "history": get_chat_history()}) + + history = get_chat_history() + + # Se veio imagem, guarda no histórico sem expô-la como texto no chat + if image_url: + history.append({"role": "user", "content": user_message or f"[imagem: ...]", + "image_url": image_url}) + else: + history.append({"role": "user", "content": user_message}) + + system_msg = build_system_prompt() + + # Monta mensagens no formato adequado (multimodal se houver image_url) + messages_with_context = [system_msg] + for msg in history: + entry = {"role": msg["role"]} + if msg.get("image_url"): + entry["content"] = [ + {"type": "text", "text": msg["content"]}, + {"type": "image_url", "image_url": {"url": msg["image_url"]}}, + ] + else: + entry["content"] = msg["content"] + messages_with_context.append(entry) + + try: + ai_reply = call_openrouter(messages_with_context) + except requests.exceptions.HTTPError as e: + # Se falhou por conteúdo multimodal, reenvia só texto + if image_url: + # remove a image_url do histórico para não poluir futuras requisições + history[-1].pop("image_url", None) + hist_clean = [system_msg] + for msg in history: + hist_clean.append({"role": msg["role"], "content": msg["content"]}) + try: + ai_reply = call_openrouter(hist_clean) + except requests.exceptions.HTTPError as e2: + return jsonify({"error": f"Erro na API: {str(e2)}"}), 500 + else: + return jsonify({"error": f"Erro na API: {str(e)}"}), 500 + except Exception as e: + return jsonify({"error": f"Erro: {str(e)}"}), 500 + + history.append({"role": "assistant", "content": ai_reply}) + save_chat_history(history) + return jsonify({"reply": ai_reply, "history": history}) + + +@app.route("/history", methods=["GET"]) +def get_history(): + return jsonify({"history": get_chat_history()}) + + +@app.route("/reset", methods=["POST"]) +def reset_chat(): + factory_reset() + return jsonify({"status": "ok", "history": [], "factory": True}) + + +@app.route("/clear", methods=["POST"]) +def clear_history(): + save_chat_history([]) + return jsonify({"status": "ok", "history": []}) + + +@app.route("/end", methods=["POST"]) +def end_chat(): + save_chat_history([]) + return jsonify({"status": "encerrado", "history": []}) + + +@app.route("/optimize", methods=["POST"]) +def optimize_context(): + try: + ctx = load_json(CONTEXTO_PATH, {}) + ia = load_json(PERFIL_IA_PATH, {}) + user = load_json(PERFIL_USER_PATH, {}) + hist = carregar_historico_dia() + + ctx_json = json.dumps(ctx, ensure_ascii=False, indent=2) + ia_json = json.dumps(ia, ensure_ascii=False, indent=2) + user_json = json.dumps(user, ensure_ascii=False, indent=2) + hist_amostra = json.dumps(hist[-20:], ensure_ascii=False, indent=2) + + meta_prompt = ( + "Você é um especialista em otimização de prompts para assistentes de IA.\n\n" + "Analise o contexto atual, os perfis e o histórico de conversas abaixo e produza " + "uma versão melhorada do arquivo de contexto. O objetivo é tornar o assistente " + "mais eficaz, engajador e alinhado com os padrões observados nas conversas.\n\n" + "--- CONTEXTO ATUAL (contexto.json) ---\n" + f"{ctx_json}\n\n" + "--- PERFIL DA IA (perfil_ia.json) ---\n" + f"{ia_json}\n\n" + "--- PERFIL DO USUÁRIO (perfil_user.json) ---\n" + f"{user_json}\n\n" + "--- HISTÓRICO RECENTE (últimas 20 mensagens) ---\n" + f"{hist_amostra}\n\n" + "Com base nessa análise, produza APENAS um JSON válido (sem markdown, sem código adicional) " + "EXATAMENTE no formato:\n" + '{"role": "system", "content": "..."}\n\n' + "O campo 'content' deve conter um prompt de sistema completo, bem escrito em português " + "do Brasil, que incorpore o que há de melhor no contexto atual mais as melhorias " + "identificadas nos padrões de conversa." + ) + + bak = fazer_backup_contexto() + + messages = [ + {"role": "system", "content": "Você é um especialista em otimização de prompts de IA. Sempre responda APENAS com JSON puro, sem formatação markdown."}, + {"role": "user", "content": meta_prompt}, + ] + + ai_reply = call_openrouter(messages) + # Log the raw response + import logging + logging.warning("RAW optimize response: %s", ai_reply[:500]) + + # Strip markdown fences + reply_clean = ai_reply.strip() + if "```" in reply_clean: + lines = reply_clean.split("\n") + cleaned = [] + for line in lines: + stripped = line.strip() + if stripped.startswith("```"): + continue + cleaned.append(line) + reply_clean = "\n".join(cleaned).strip() + + # Try to extract JSON from the response (find first { and last }) + first_brace = reply_clean.find("{") + last_brace = reply_clean.rfind("}") + if first_brace != -1 and last_brace != -1 and last_brace > first_brace: + reply_clean = reply_clean[first_brace:last_brace + 1] + + novo = json.loads(reply_clean) + if not isinstance(novo, dict) or "role" not in novo or "content" not in novo: + raise ValueError("Resposta não é um JSON válido para contexto.json") + + with open(CONTEXTO_PATH, "w", encoding="utf-8") as f: + json.dump(novo, f, ensure_ascii=False, indent=2) + + nome_bak = os.path.basename(bak) + return jsonify({ + "reply": f"contexto.json otimizado com sucesso!\nBackup salvo: {nome_bak}", + "cmd_reply": True, + }) + + except json.JSONDecodeError: + return jsonify({"error": "A IA não retornou um JSON válido. O contexto não foi alterado. Tente novamente."}), 500 + except Exception as e: + return jsonify({"error": f"Erro ao otimizar: {str(e)}"}), 500 + + +# ── Upload de arquivos ── + +def allowed_file(name): + return "." in name and name.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS + + +@app.route("/upload", methods=["POST"]) +def upload_file(): + if "file" not in request.files: + return jsonify({"error": "Nenhum arquivo enviado."}), 400 + f = request.files["file"] + if f.filename == "": + return jsonify({"error": "Nome de arquivo vazio."}), 400 + if not allowed_file(f.filename): + return jsonify({"error": "Tipo de arquivo não permitido."}), 400 + try: + nome = secure_filename(f.filename) + # evitar sobrescrita + base, ext = os.path.splitext(nome) + contador = 1 + caminho = os.path.join(UPLOADS_DIR, nome) + while os.path.exists(caminho): + caminho = os.path.join(UPLOADS_DIR, f"{base}_{contador}{ext}") + contador += 1 + f.save(caminho) + nome_final = os.path.basename(caminho) + is_image = ext.lower().lstrip(".") in IMAGE_EXTENSIONS + result = { + "status": "ok", + "filename": nome_final, + "url": f"/uploads/{nome_final}", + "is_image": is_image, + } + if is_image: + mime = mimetypes.guess_type(nome_final)[0] or "image/png" + with open(caminho, "rb") as img: + b64 = base64.b64encode(img.read()).decode("ascii") + result["data_url"] = f"data:{mime};base64,{b64}" + return jsonify(result) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5001, debug=True) \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..909515f --- /dev/null +++ b/config.json @@ -0,0 +1,4 @@ +{ + "modelo": "nvidia/nemotron-3-ultra-550b-a55b:free", + "api_key": "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" +} \ No newline at end of file diff --git a/contexto.json b/contexto.json new file mode 100644 index 0000000..8b546b3 --- /dev/null +++ b/contexto.json @@ -0,0 +1,4 @@ +{ + "role": "system", + "content": "Você é um assistente de chat amigável e prestativo, especializado em ajudar usuários em português do Brasil. Suas respostas devem ser:\n\n- Sempre escritas em português do Brasil, de forma natural e coloquial.\n- Claras, objetivas e bem estruturadas.\n- Educadas, respeitosas e acolhedoras.\n- Relevantes para a pergunta feita, sem desvios desnecessários.\n- Em tom conversacional, evitando linguagem excessivamente formal ou robótica.\n\nQuando não souber uma resposta ou a pergunta for ambígua, peça mais detalhes ao usuário de forma educada. Nunca invente informações factuais e não faça suposições sem justificativa. Seja útil, preciso e empático em todas as interações." +} \ No newline at end of file diff --git a/iniciar.bat b/iniciar.bat new file mode 100644 index 0000000..1e7cda6 --- /dev/null +++ b/iniciar.bat @@ -0,0 +1,30 @@ +@echo off +set DIR=%~dp0 + +echo ============================================ +echo Chat com IA - Flask + OpenRouter +echo ============================================ +echo. + +if not exist "%DIR%venv\" ( + echo [1/4] Criando ambiente virtual Python... + python -m venv "%DIR%venv" +) else ( + echo [1/4] Ambiente virtual ja existe. +) + +echo [2/4] Ativando ambiente virtual... +call "%DIR%venv\Scripts\activate.bat" + +echo [3/4] Instalando dependencias... +"%DIR%venv\Scripts\python.exe" -m pip install --upgrade pip -q +"%DIR%venv\Scripts\pip.exe" install -r "%DIR%requirements.txt" -q + +echo [4/4] Iniciando aplicacao Flask... +echo. +echo Acesse: http://localhost:5001 +echo Pressione Ctrl+C para parar. +echo. +"%DIR%venv\Scripts\python.exe" "%DIR%app.py" + +pause \ No newline at end of file diff --git a/iniciar.sh b/iniciar.sh new file mode 100755 index 0000000..81bade5 --- /dev/null +++ b/iniciar.sh @@ -0,0 +1,43 @@ +#!/bin/bash +set -e + +DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "============================================" +echo " Chat com IA - Flask + OpenRouter" +echo "============================================" +echo "" + +# Detect Python command +PYTHON="" +for cmd in python3 python; do + if command -v "$cmd" &>/dev/null; then + PYTHON="$cmd" + break + fi +done +if [ -z "$PYTHON" ]; then + echo "Erro: Python nao encontrado. Instale python3 ou python." + exit 1 +fi + +if [ ! -d "$DIR/venv" ]; then + echo "[1/4] Criando ambiente virtual Python..." + $PYTHON -m venv "$DIR/venv" +else + echo "[1/4] Ambiente virtual ja existe." +fi + +echo "[2/4] Ativando ambiente virtual..." +source "$DIR/venv/bin/activate" + +echo "[3/4] Instalando dependencias..." +pip install --upgrade pip -q +pip install -r "$DIR/requirements.txt" -q + +echo "[4/4] Iniciando aplicacao Flask..." +echo "" +echo " Acesse: http://localhost:5001" +echo " Pressione Ctrl+C para parar." +echo "" +$PYTHON "$DIR/app.py" \ No newline at end of file diff --git a/perfil_ia.json b/perfil_ia.json new file mode 100644 index 0000000..dca4830 --- /dev/null +++ b/perfil_ia.json @@ -0,0 +1,30 @@ +{ + "nome": "Maria", + "descricao": "Assistente de IA especializada em comunicação e atendimento em português do Brasil.", + "personalidade": { + "tom": "amigavel e acolhedor", + "nivel_de_formalidade": "informal", + "empatia": "alta", + "proatividade": "moderada" + }, + "especialidades": [ + "Atendimento ao cliente", + "Comunicação escrita", + "Orientação de usuários", + "Resolução de dúvidas", + "Conversação natural em português" + ], + "regras_de_comportamento": { + "sempre_responder_em": "portugues_brasil", + "evitar_jargoes": true, + "ser_conciso_quando_possivel": true, + "perguntar_quando_faltar_informacao": true, + "nao_inventar_dados": true, + "nao_fazer_suposicoes_sem_justificativa": true + }, + "formato_de_resposta": { + "paragrafos_curtos": true, + "usa_listas_quando_apropriado": true, + "destaca_pontos_importantes": true + } +} \ No newline at end of file diff --git a/perfil_user.json b/perfil_user.json new file mode 100644 index 0000000..2190588 --- /dev/null +++ b/perfil_user.json @@ -0,0 +1,11 @@ +{ + "nome": "Usuário", + "idioma_preferido": "portugues_brasil", + "horario_de_uso": "qualquer", + "preferencias": { + "respostas_curtas": true, + "exemplos_praticos": true, + "explicacoes_detalhadas_sob_demanda": true + }, + "contexto_adicional": "Perfil genérico de usuário. Pode ser editado livremente para refletir nome, preferências, área de atuação ou qualquer informação relevante sobre o usuário." +} \ No newline at end of file diff --git a/proibidos.json b/proibidos.json new file mode 100644 index 0000000..cb49848 --- /dev/null +++ b/proibidos.json @@ -0,0 +1,16 @@ +{ + "topicos_proibidos": [ + "conteúdo adulto ou pornográfico", + "violência explícita ou incitação ao ódio", + "discriminação racial, religiosa, de gênero ou orientação sexual", + "instruções para atividades ilegais ou perigosas", + "disseminação de desinformação ou fake news" + ], + "comportamentos_proibidos": [ + "fingir ser um humano real", + "compartilhar informações pessoais de terceiros", + "responder em idiomas que não sejam português do Brasil", + "executar comandos ou códigos no sistema do usuário" + ], + "palavras_proibidas": [] +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..32551f8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask==3.1.2 +python-dotenv==1.1.0 +requests==2.32.3 diff --git a/templates/chat.html b/templates/chat.html new file mode 100644 index 0000000..b1cec11 --- /dev/null +++ b/templates/chat.html @@ -0,0 +1,1003 @@ + + + + + + Chat com IA - by syspanel.com.br + + + + +
+

🤖 Chat com IA

+
+ + + + + +
+
+ +
+
⚡ Seja bem-vindo! Digite uma mensagem ou use /help para ver os comandos disponíveis.
+
+
A IA está pensando...
+ +
+
+ + + +
+ + +
+
+
+ + + + + + +