Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,72 +1,67 @@
|
|
| 1 |
"""
|
| 2 |
-
🤖 N8n Assistant - Versão
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
-
|
| 7 |
-
- Mantida toda funcionalidade do sistema
|
| 8 |
"""
|
| 9 |
|
| 10 |
import os
|
| 11 |
import yaml
|
| 12 |
import json
|
| 13 |
import logging
|
| 14 |
-
import time
|
| 15 |
from typing import Optional, Tuple
|
|
|
|
| 16 |
import gradio as gr
|
| 17 |
|
| 18 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
logging.basicConfig(level=logging.INFO)
|
| 20 |
-
logger = logging.getLogger(
|
| 21 |
|
| 22 |
-
#
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
from huggingface_hub import snapshot_download
|
| 28 |
-
logger.info("✅ Bibliotecas importadas com sucesso")
|
| 29 |
-
except ImportError as e:
|
| 30 |
-
logger.error(f"❌ Erro ao importar bibliotecas: {e}")
|
| 31 |
-
raise
|
| 32 |
|
|
|
|
|
|
|
| 33 |
|
|
|
|
|
|
|
|
|
|
| 34 |
class N8nAssistant:
|
| 35 |
-
"""Assistente N8n
|
| 36 |
-
|
| 37 |
def __init__(self):
|
| 38 |
self.index = None
|
| 39 |
self.query_engine = None
|
| 40 |
self.docs_dir = None
|
| 41 |
self.inicializado = False
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
"""Configurar OpenAI"""
|
| 45 |
-
try:
|
| 46 |
-
api_key = os.getenv("OPENAI_API_KEY")
|
| 47 |
-
if not api_key:
|
| 48 |
-
logger.error("❌ OPENAI_API_KEY não encontrada")
|
| 49 |
-
return False
|
| 50 |
-
|
| 51 |
-
os.environ["OPENAI_API_KEY"] = api_key
|
| 52 |
-
logger.info("✅ OpenAI configurada")
|
| 53 |
-
return True
|
| 54 |
-
except Exception as e:
|
| 55 |
-
logger.error(f"❌ Erro OpenAI: {e}")
|
| 56 |
-
return False
|
| 57 |
|
|
|
|
| 58 |
def extrair_conteudo_arquivos(self, pasta: str) -> str:
|
| 59 |
-
"""
|
| 60 |
texto_final = ""
|
| 61 |
-
|
| 62 |
if not os.path.exists(pasta):
|
| 63 |
logger.error(f"❌ Pasta não encontrada: {pasta}")
|
| 64 |
return ""
|
| 65 |
|
| 66 |
-
for root,
|
| 67 |
for file in files:
|
| 68 |
caminho_arquivo = os.path.join(root, file)
|
| 69 |
-
|
| 70 |
try:
|
| 71 |
if file.endswith(('.yml', '.yaml')):
|
| 72 |
with open(caminho_arquivo, 'r', encoding='utf-8') as f:
|
|
@@ -92,28 +87,27 @@ class N8nAssistant:
|
|
| 92 |
return texto_final
|
| 93 |
|
| 94 |
def gerar_documentacao(self, pasta_origem: str) -> bool:
|
| 95 |
-
"""
|
| 96 |
try:
|
| 97 |
texto = self.extrair_conteudo_arquivos(pasta_origem)
|
| 98 |
-
|
| 99 |
if not texto.strip():
|
| 100 |
-
logger.warning("⚠️ Nenhum conteúdo encontrado")
|
| 101 |
return False
|
| 102 |
-
|
| 103 |
with open("documentacao.txt", 'w', encoding='utf-8') as f:
|
| 104 |
f.write(texto)
|
| 105 |
-
|
| 106 |
-
logger.info("✅ Documentação
|
| 107 |
return True
|
| 108 |
-
|
| 109 |
except Exception as e:
|
| 110 |
logger.error(f"❌ Erro ao gerar documentação: {e}")
|
| 111 |
return False
|
| 112 |
|
| 113 |
def baixar_docs(self) -> bool:
|
| 114 |
-
"""
|
| 115 |
try:
|
| 116 |
-
logger.info("📥 Baixando documentação...")
|
| 117 |
self.docs_dir = snapshot_download(
|
| 118 |
repo_id="Jeice/n8n-docs-v2",
|
| 119 |
repo_type="dataset"
|
|
@@ -121,94 +115,144 @@ class N8nAssistant:
|
|
| 121 |
logger.info("✅ Download concluído")
|
| 122 |
return True
|
| 123 |
except Exception as e:
|
| 124 |
-
logger.error(f"❌ Erro no download: {e}")
|
| 125 |
return False
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
def criar_index(self) -> bool:
|
| 128 |
-
"""
|
| 129 |
try:
|
| 130 |
-
# Carregar documentos
|
| 131 |
if not os.path.exists("documentacao.txt"):
|
| 132 |
logger.error("❌ documentacao.txt não encontrado")
|
| 133 |
return False
|
| 134 |
-
|
| 135 |
-
documents = SimpleDirectoryReader(
|
| 136 |
-
|
|
|
|
|
|
|
| 137 |
if not documents:
|
| 138 |
logger.error("❌ Nenhum documento carregado")
|
| 139 |
return False
|
| 140 |
-
|
| 141 |
-
#
|
| 142 |
-
|
| 143 |
-
model="gpt-3.5-turbo",
|
| 144 |
-
temperature=0.1,
|
| 145 |
-
system_prompt=(
|
| 146 |
-
"Você é um assistente especialista em n8n. "
|
| 147 |
-
"Responda sempre em português do Brasil, de forma clara e objetiva, "
|
| 148 |
-
"baseado exclusivamente na documentação fornecida. "
|
| 149 |
-
"Se não souber, diga que não há informações suficientes."
|
| 150 |
-
)
|
| 151 |
-
)
|
| 152 |
-
Settings.embed_model = OpenAIEmbedding()
|
| 153 |
-
|
| 154 |
-
# Criar índice
|
| 155 |
-
logger.info("🧠 Criando índice...")
|
| 156 |
self.index = VectorStoreIndex.from_documents(documents)
|
| 157 |
self.query_engine = self.index.as_query_engine()
|
| 158 |
-
|
| 159 |
-
logger.info("✅ Índice criado")
|
| 160 |
return True
|
| 161 |
-
|
| 162 |
except Exception as e:
|
| 163 |
logger.error(f"❌ Erro ao criar índice: {e}")
|
| 164 |
return False
|
| 165 |
|
|
|
|
| 166 |
def inicializar(self) -> Tuple[bool, str]:
|
| 167 |
-
"""
|
| 168 |
try:
|
| 169 |
-
# 1
|
| 170 |
-
if not self.setup_openai():
|
| 171 |
-
return False, "Erro na configuração OpenAI"
|
| 172 |
-
|
| 173 |
-
# 2. Baixar docs
|
| 174 |
if not self.baixar_docs():
|
| 175 |
-
return False, "Erro
|
| 176 |
-
|
| 177 |
-
#
|
| 178 |
if not self.gerar_documentacao(self.docs_dir):
|
| 179 |
-
return False, "Erro ao processar documentação"
|
| 180 |
-
|
| 181 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
if not self.criar_index():
|
| 183 |
-
return False, "Erro ao criar índice"
|
| 184 |
-
|
| 185 |
self.inicializado = True
|
| 186 |
-
return True,
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
| 188 |
except Exception as e:
|
| 189 |
logger.error(f"❌ Erro na inicialização: {e}")
|
| 190 |
return False, f"Erro: {str(e)}"
|
| 191 |
|
| 192 |
def responder(self, pergunta: str) -> str:
|
| 193 |
-
"""
|
| 194 |
if not pergunta or not pergunta.strip():
|
| 195 |
return "⚠️ Por favor, digite uma pergunta."
|
| 196 |
-
|
| 197 |
if not self.inicializado or not self.query_engine:
|
| 198 |
return "❌ Sistema não inicializado. Recarregue a página."
|
| 199 |
-
|
| 200 |
try:
|
| 201 |
-
logger.info(f"🤔 Pergunta: {pergunta[:
|
| 202 |
response = self.query_engine.query(pergunta)
|
| 203 |
return str(response)
|
| 204 |
-
|
| 205 |
except Exception as e:
|
| 206 |
logger.error(f"❌ Erro ao responder: {e}")
|
| 207 |
return f"❌ Erro ao processar pergunta: {str(e)}"
|
| 208 |
|
| 209 |
|
| 210 |
-
#
|
| 211 |
-
|
|
|
|
|
|
|
| 212 |
assistant = N8nAssistant()
|
| 213 |
sucesso, mensagem = assistant.inicializar()
|
| 214 |
|
|
@@ -217,37 +261,28 @@ if sucesso:
|
|
| 217 |
else:
|
| 218 |
logger.error(f"❌ {mensagem}")
|
| 219 |
|
| 220 |
-
|
|
|
|
|
|
|
| 221 |
def processar_pergunta(pergunta: str) -> str:
|
| 222 |
-
"""Wrapper para Gradio"""
|
| 223 |
if not sucesso:
|
| 224 |
return f"❌ Sistema não inicializado: {mensagem}"
|
| 225 |
return assistant.responder(pergunta)
|
| 226 |
|
| 227 |
-
|
| 228 |
-
# Interface Gradio - SEM COMPONENTE IMAGE PROBLEMÁTICO
|
| 229 |
-
with gr.Blocks(theme=gr.themes.Soft(), title="N8n Assistant") as demo:
|
| 230 |
-
|
| 231 |
-
# Cabeçalho
|
| 232 |
gr.Markdown(
|
| 233 |
f"""
|
| 234 |
-
# 🤖 N8n Assistant
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
"""
|
| 239 |
-
#**Status:** {'✅ Sistema Pronto' if sucesso else '❌ ' + mensagem}
|
| 240 |
)
|
| 241 |
-
|
| 242 |
-
# Layout principal - SEM IMAGEM
|
| 243 |
with gr.Row():
|
| 244 |
with gr.Column(scale=1):
|
| 245 |
-
# REMOVIDO: componente gr.Image que causava erro 404
|
| 246 |
gr.Markdown("### 🤖 N8n Bot")
|
| 247 |
-
|
| 248 |
with gr.Column(scale=4):
|
| 249 |
gr.Markdown("## Como posso ajudar você com o n8n?")
|
| 250 |
-
|
| 251 |
with gr.Row():
|
| 252 |
with gr.Column(scale=3):
|
| 253 |
input_box = gr.Textbox(
|
|
@@ -255,19 +290,16 @@ with gr.Blocks(theme=gr.themes.Soft(), title="N8n Assistant") as demo:
|
|
| 255 |
placeholder="Ex: Como criar um workflow no n8n?",
|
| 256 |
lines=3
|
| 257 |
)
|
| 258 |
-
|
| 259 |
with gr.Row():
|
| 260 |
enviar_btn = gr.Button("🚀 Perguntar", variant="primary")
|
| 261 |
limpar_btn = gr.Button("🧹 Limpar")
|
| 262 |
-
|
| 263 |
with gr.Column(scale=4):
|
| 264 |
output_box = gr.Textbox(
|
| 265 |
label="Resposta",
|
| 266 |
placeholder="Sua resposta aparecerá aqui...",
|
| 267 |
lines=12
|
| 268 |
)
|
| 269 |
-
|
| 270 |
-
# Exemplos
|
| 271 |
with gr.Accordion("💡 Exemplos de Perguntas", open=False):
|
| 272 |
gr.Markdown(
|
| 273 |
"""
|
|
@@ -281,32 +313,10 @@ with gr.Blocks(theme=gr.themes.Soft(), title="N8n Assistant") as demo:
|
|
| 281 |
- Quais nodes usar para automação de email?
|
| 282 |
"""
|
| 283 |
)
|
| 284 |
-
|
| 285 |
-
# Eventos
|
| 286 |
-
enviar_btn.click(
|
| 287 |
-
fn=processar_pergunta,
|
| 288 |
-
inputs=input_box,
|
| 289 |
-
outputs=output_box
|
| 290 |
-
)
|
| 291 |
-
|
| 292 |
-
limpar_btn.click(
|
| 293 |
-
lambda: ("", ""),
|
| 294 |
-
None,
|
| 295 |
-
[input_box, output_box]
|
| 296 |
-
)
|
| 297 |
-
|
| 298 |
-
input_box.submit(
|
| 299 |
-
fn=processar_pergunta,
|
| 300 |
-
inputs=input_box,
|
| 301 |
-
outputs=output_box
|
| 302 |
-
)
|
| 303 |
|
|
|
|
|
|
|
|
|
|
| 304 |
|
| 305 |
-
# Lançar aplicação
|
| 306 |
if __name__ == "__main__":
|
| 307 |
-
demo.launch(
|
| 308 |
-
server_name="0.0.0.0",
|
| 309 |
-
server_port=7860,
|
| 310 |
-
show_error=True
|
| 311 |
-
)
|
| 312 |
-
|
|
|
|
| 1 |
"""
|
| 2 |
+
🤖 N8n Assistant - Versão Open Source (GRÁTIS)
|
| 3 |
+
- Sem OpenAI
|
| 4 |
+
- LLM: microsoft/Phi-3.5-mini-instruct (fallback flan-t5-base)
|
| 5 |
+
- Embeddings: all-MiniLM-L6-v2 (fallback L3-v2)
|
| 6 |
+
- Compatível com Hugging Face Spaces (CPU)
|
|
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
import os
|
| 10 |
import yaml
|
| 11 |
import json
|
| 12 |
import logging
|
|
|
|
| 13 |
from typing import Optional, Tuple
|
| 14 |
+
|
| 15 |
import gradio as gr
|
| 16 |
|
| 17 |
+
# LlamaIndex (open source stacks)
|
| 18 |
+
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
|
| 19 |
+
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
|
| 20 |
+
from llama_index.llms.huggingface import HuggingFaceLLM
|
| 21 |
+
|
| 22 |
+
from huggingface_hub import snapshot_download
|
| 23 |
+
|
| 24 |
+
# ------------------------------------------------------------
|
| 25 |
+
# Logging
|
| 26 |
+
# ------------------------------------------------------------
|
| 27 |
logging.basicConfig(level=logging.INFO)
|
| 28 |
+
logger = logging.getLogger("n8n-assistant")
|
| 29 |
|
| 30 |
+
# ------------------------------------------------------------
|
| 31 |
+
# Configs de modelos (primários + fallbacks)
|
| 32 |
+
# ------------------------------------------------------------
|
| 33 |
+
PRIMARY_LLM = "microsoft/Phi-3.5-mini-instruct"
|
| 34 |
+
FALLBACK_LLM = "google/flan-t5-base" # muito leve
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
PRIMARY_EMB = "sentence-transformers/all-MiniLM-L6-v2"
|
| 37 |
+
FALLBACK_EMB = "sentence-transformers/paraphrase-MiniLM-L3-v2"
|
| 38 |
|
| 39 |
+
# ------------------------------------------------------------
|
| 40 |
+
# Classe principal
|
| 41 |
+
# ------------------------------------------------------------
|
| 42 |
class N8nAssistant:
|
| 43 |
+
"""Assistente N8n open-source e funcional"""
|
| 44 |
+
|
| 45 |
def __init__(self):
|
| 46 |
self.index = None
|
| 47 |
self.query_engine = None
|
| 48 |
self.docs_dir = None
|
| 49 |
self.inicializado = False
|
| 50 |
+
self.llm_model_used = None
|
| 51 |
+
self.emb_model_used = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
+
# --------- Utilitários de dados ----------
|
| 54 |
def extrair_conteudo_arquivos(self, pasta: str) -> str:
|
| 55 |
+
"""Extrai conteúdo textual dos arquivos .yml/.yaml/.json/.md/.txt"""
|
| 56 |
texto_final = ""
|
| 57 |
+
|
| 58 |
if not os.path.exists(pasta):
|
| 59 |
logger.error(f"❌ Pasta não encontrada: {pasta}")
|
| 60 |
return ""
|
| 61 |
|
| 62 |
+
for root, _, files in os.walk(pasta):
|
| 63 |
for file in files:
|
| 64 |
caminho_arquivo = os.path.join(root, file)
|
|
|
|
| 65 |
try:
|
| 66 |
if file.endswith(('.yml', '.yaml')):
|
| 67 |
with open(caminho_arquivo, 'r', encoding='utf-8') as f:
|
|
|
|
| 87 |
return texto_final
|
| 88 |
|
| 89 |
def gerar_documentacao(self, pasta_origem: str) -> bool:
|
| 90 |
+
"""Gera um único arquivo 'documentacao.txt' com todo o conteúdo unificado"""
|
| 91 |
try:
|
| 92 |
texto = self.extrair_conteudo_arquivos(pasta_origem)
|
|
|
|
| 93 |
if not texto.strip():
|
| 94 |
+
logger.warning("⚠️ Nenhum conteúdo encontrado para documentação")
|
| 95 |
return False
|
| 96 |
+
|
| 97 |
with open("documentacao.txt", 'w', encoding='utf-8') as f:
|
| 98 |
f.write(texto)
|
| 99 |
+
|
| 100 |
+
logger.info("✅ Documentação consolidada em documentacao.txt")
|
| 101 |
return True
|
| 102 |
+
|
| 103 |
except Exception as e:
|
| 104 |
logger.error(f"❌ Erro ao gerar documentação: {e}")
|
| 105 |
return False
|
| 106 |
|
| 107 |
def baixar_docs(self) -> bool:
|
| 108 |
+
"""Baixa a documentação do HF dataset"""
|
| 109 |
try:
|
| 110 |
+
logger.info("📥 Baixando documentação do dataset Jeice/n8n-docs-v2 ...")
|
| 111 |
self.docs_dir = snapshot_download(
|
| 112 |
repo_id="Jeice/n8n-docs-v2",
|
| 113 |
repo_type="dataset"
|
|
|
|
| 115 |
logger.info("✅ Download concluído")
|
| 116 |
return True
|
| 117 |
except Exception as e:
|
| 118 |
+
logger.error(f"❌ Erro no download do dataset: {e}")
|
| 119 |
return False
|
| 120 |
|
| 121 |
+
# --------- Configuração de modelos ----------
|
| 122 |
+
def configurar_embeddings(self) -> bool:
|
| 123 |
+
"""Configura embeddings HuggingFace com fallback"""
|
| 124 |
+
for emb in (PRIMARY_EMB, FALLBACK_EMB):
|
| 125 |
+
try:
|
| 126 |
+
Settings.embed_model = HuggingFaceEmbedding(model_name=emb)
|
| 127 |
+
self.emb_model_used = emb
|
| 128 |
+
logger.info(f"✅ Embeddings configurados: {emb}")
|
| 129 |
+
return True
|
| 130 |
+
except Exception as e:
|
| 131 |
+
logger.warning(f"⚠️ Falha ao carregar embeddings {emb}: {e}")
|
| 132 |
+
logger.error("❌ Não foi possível configurar embeddings")
|
| 133 |
+
return False
|
| 134 |
+
|
| 135 |
+
def configurar_llm(self) -> bool:
|
| 136 |
+
"""Configura LLM HuggingFace com fallback, otimizado para CPU"""
|
| 137 |
+
# parâmetros neutros/seguros para CPU
|
| 138 |
+
gen_kwargs = {
|
| 139 |
+
"temperature": 0.2,
|
| 140 |
+
"do_sample": True,
|
| 141 |
+
"top_p": 0.9
|
| 142 |
+
}
|
| 143 |
+
# tentar primário depois fallback
|
| 144 |
+
for model_name in (PRIMARY_LLM, FALLBACK_LLM):
|
| 145 |
+
try:
|
| 146 |
+
llm = HuggingFaceLLM(
|
| 147 |
+
model_name=model_name,
|
| 148 |
+
tokenizer_name=model_name,
|
| 149 |
+
context_window=4096,
|
| 150 |
+
max_new_tokens=512,
|
| 151 |
+
generate_kwargs=gen_kwargs,
|
| 152 |
+
# device_map="auto" funciona em CPU/GPU no Space
|
| 153 |
+
device_map="auto",
|
| 154 |
+
model_kwargs={
|
| 155 |
+
# dtype padrão (evitar float16 em CPU)
|
| 156 |
+
"torch_dtype": "auto"
|
| 157 |
+
},
|
| 158 |
+
# system_prompt para orientar o estilo de resposta
|
| 159 |
+
system_prompt=(
|
| 160 |
+
"Você é um assistente especialista em n8n. "
|
| 161 |
+
"Responda sempre em português do Brasil, de forma clara e objetiva, "
|
| 162 |
+
"baseado exclusivamente na documentação fornecida. "
|
| 163 |
+
"Se não souber, diga que não há informações suficientes."
|
| 164 |
+
),
|
| 165 |
+
)
|
| 166 |
+
Settings.llm = llm
|
| 167 |
+
self.llm_model_used = model_name
|
| 168 |
+
logger.info(f"✅ LLM configurado: {model_name}")
|
| 169 |
+
return True
|
| 170 |
+
except Exception as e:
|
| 171 |
+
logger.warning(f"⚠️ Falha ao carregar LLM {model_name}: {e}")
|
| 172 |
+
|
| 173 |
+
logger.error("❌ Não foi possível configurar o LLM")
|
| 174 |
+
return False
|
| 175 |
+
|
| 176 |
+
# --------- Indexação ----------
|
| 177 |
def criar_index(self) -> bool:
|
| 178 |
+
"""Cria o índice vetorial a partir de documentacao.txt"""
|
| 179 |
try:
|
|
|
|
| 180 |
if not os.path.exists("documentacao.txt"):
|
| 181 |
logger.error("❌ documentacao.txt não encontrado")
|
| 182 |
return False
|
| 183 |
+
|
| 184 |
+
documents = SimpleDirectoryReader(
|
| 185 |
+
input_files=["documentacao.txt"]
|
| 186 |
+
).load_data()
|
| 187 |
+
|
| 188 |
if not documents:
|
| 189 |
logger.error("❌ Nenhum documento carregado")
|
| 190 |
return False
|
| 191 |
+
|
| 192 |
+
# Criar índice + query engine
|
| 193 |
+
logger.info("🧠 Criando índice (VectorStoreIndex) ...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
self.index = VectorStoreIndex.from_documents(documents)
|
| 195 |
self.query_engine = self.index.as_query_engine()
|
| 196 |
+
logger.info("✅ Índice criado e query_engine pronto")
|
|
|
|
| 197 |
return True
|
| 198 |
+
|
| 199 |
except Exception as e:
|
| 200 |
logger.error(f"❌ Erro ao criar índice: {e}")
|
| 201 |
return False
|
| 202 |
|
| 203 |
+
# --------- Orquestração ----------
|
| 204 |
def inicializar(self) -> Tuple[bool, str]:
|
| 205 |
+
"""Pipeline completo de inicialização (open-source)"""
|
| 206 |
try:
|
| 207 |
+
# 1) Baixar docs
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
if not self.baixar_docs():
|
| 209 |
+
return False, "Erro ao baixar a documentação (dataset)"
|
| 210 |
+
|
| 211 |
+
# 2) Consolidar documentação
|
| 212 |
if not self.gerar_documentacao(self.docs_dir):
|
| 213 |
+
return False, "Erro ao processar/consolidar a documentação"
|
| 214 |
+
|
| 215 |
+
# 3) Configurar embeddings e LLM (open source)
|
| 216 |
+
if not self.configurar_embeddings():
|
| 217 |
+
return False, "Erro ao configurar embeddings"
|
| 218 |
+
if not self.configurar_llm():
|
| 219 |
+
return False, "Erro ao configurar LLM"
|
| 220 |
+
|
| 221 |
+
# 4) Criar índice
|
| 222 |
if not self.criar_index():
|
| 223 |
+
return False, "Erro ao criar o índice"
|
| 224 |
+
|
| 225 |
self.inicializado = True
|
| 226 |
+
return True, (
|
| 227 |
+
f"Sistema inicializado com sucesso | "
|
| 228 |
+
f"LLM: {self.llm_model_used} | Embeddings: {self.emb_model_used}"
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
except Exception as e:
|
| 232 |
logger.error(f"❌ Erro na inicialização: {e}")
|
| 233 |
return False, f"Erro: {str(e)}"
|
| 234 |
|
| 235 |
def responder(self, pergunta: str) -> str:
|
| 236 |
+
"""Executa a consulta no query_engine"""
|
| 237 |
if not pergunta or not pergunta.strip():
|
| 238 |
return "⚠️ Por favor, digite uma pergunta."
|
| 239 |
+
|
| 240 |
if not self.inicializado or not self.query_engine:
|
| 241 |
return "❌ Sistema não inicializado. Recarregue a página."
|
| 242 |
+
|
| 243 |
try:
|
| 244 |
+
logger.info(f"🤔 Pergunta: {pergunta[:120]}...")
|
| 245 |
response = self.query_engine.query(pergunta)
|
| 246 |
return str(response)
|
|
|
|
| 247 |
except Exception as e:
|
| 248 |
logger.error(f"❌ Erro ao responder: {e}")
|
| 249 |
return f"❌ Erro ao processar pergunta: {str(e)}"
|
| 250 |
|
| 251 |
|
| 252 |
+
# ------------------------------------------------------------
|
| 253 |
+
# Bootstrap
|
| 254 |
+
# ------------------------------------------------------------
|
| 255 |
+
logger.info("🚀 Inicializando N8n Assistant (Open Source)...")
|
| 256 |
assistant = N8nAssistant()
|
| 257 |
sucesso, mensagem = assistant.inicializar()
|
| 258 |
|
|
|
|
| 261 |
else:
|
| 262 |
logger.error(f"❌ {mensagem}")
|
| 263 |
|
| 264 |
+
# ------------------------------------------------------------
|
| 265 |
+
# Gradio UI
|
| 266 |
+
# ------------------------------------------------------------
|
| 267 |
def processar_pergunta(pergunta: str) -> str:
|
|
|
|
| 268 |
if not sucesso:
|
| 269 |
return f"❌ Sistema não inicializado: {mensagem}"
|
| 270 |
return assistant.responder(pergunta)
|
| 271 |
|
| 272 |
+
with gr.Blocks(theme=gr.themes.Soft(), title="N8n Assistant (Open Source)") as demo:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
gr.Markdown(
|
| 274 |
f"""
|
| 275 |
+
# 🤖 N8n Assistant (Open Source)
|
| 276 |
+
Assistente para dúvidas sobre **n8n** baseado na documentação oficial e em modelos **open-source**.
|
| 277 |
+
**Status:** {'✅ Sistema Pronto' if sucesso else '❌ ' + mensagem}
|
|
|
|
| 278 |
"""
|
|
|
|
| 279 |
)
|
| 280 |
+
|
|
|
|
| 281 |
with gr.Row():
|
| 282 |
with gr.Column(scale=1):
|
|
|
|
| 283 |
gr.Markdown("### 🤖 N8n Bot")
|
|
|
|
| 284 |
with gr.Column(scale=4):
|
| 285 |
gr.Markdown("## Como posso ajudar você com o n8n?")
|
|
|
|
| 286 |
with gr.Row():
|
| 287 |
with gr.Column(scale=3):
|
| 288 |
input_box = gr.Textbox(
|
|
|
|
| 290 |
placeholder="Ex: Como criar um workflow no n8n?",
|
| 291 |
lines=3
|
| 292 |
)
|
|
|
|
| 293 |
with gr.Row():
|
| 294 |
enviar_btn = gr.Button("🚀 Perguntar", variant="primary")
|
| 295 |
limpar_btn = gr.Button("🧹 Limpar")
|
|
|
|
| 296 |
with gr.Column(scale=4):
|
| 297 |
output_box = gr.Textbox(
|
| 298 |
label="Resposta",
|
| 299 |
placeholder="Sua resposta aparecerá aqui...",
|
| 300 |
lines=12
|
| 301 |
)
|
| 302 |
+
|
|
|
|
| 303 |
with gr.Accordion("💡 Exemplos de Perguntas", open=False):
|
| 304 |
gr.Markdown(
|
| 305 |
"""
|
|
|
|
| 313 |
- Quais nodes usar para automação de email?
|
| 314 |
"""
|
| 315 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
|
| 317 |
+
enviar_btn.click(fn=processar_pergunta, inputs=input_box, outputs=output_box)
|
| 318 |
+
limpar_btn.click(lambda: ("", ""), None, [input_box, output_box])
|
| 319 |
+
input_box.submit(fn=processar_pergunta, inputs=input_box, outputs=output_box)
|
| 320 |
|
|
|
|
| 321 |
if __name__ == "__main__":
|
| 322 |
+
demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|