From 4a1a77ac843894a0248e404b402866d5e9e5006f Mon Sep 17 00:00:00 2001 From: Marco Costa Date: Sat, 10 Oct 2026 00:01:35 -0300 Subject: [PATCH] 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 --- .gitignore | 8 + LICENSE | 16 + README.md | 106 +++++ RULES.md | 46 ++ app.py | 637 +++++++++++++++++++++++++++ config.json | 4 + contexto.json | 4 + iniciar.bat | 30 ++ iniciar.sh | 43 ++ perfil_ia.json | 30 ++ perfil_user.json | 11 + proibidos.json | 16 + requirements.txt | 3 + templates/chat.html | 1003 +++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 1957 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 RULES.md create mode 100644 app.py create mode 100644 config.json create mode 100644 contexto.json create mode 100644 iniciar.bat create mode 100755 iniciar.sh create mode 100644 perfil_ia.json create mode 100644 perfil_user.json create mode 100644 proibidos.json create mode 100644 requirements.txt create mode 100644 templates/chat.html 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...
+ +
+
+ + + +
+ + +
+
+
+ + + + + + +