Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,283 +1,271 @@
|
|
| 1 |
"""
|
| 2 |
-
🤖 N8n Assistant -
|
| 3 |
- Sem OpenAI
|
| 4 |
-
- LLM:
|
| 5 |
-
- Embeddings: all-MiniLM-L6-v2 (fallback L3-v2)
|
| 6 |
-
-
|
|
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
import os
|
| 10 |
-
import yaml
|
| 11 |
import json
|
|
|
|
| 12 |
import logging
|
| 13 |
-
from typing import
|
| 14 |
|
| 15 |
import gradio as gr
|
|
|
|
| 16 |
|
| 17 |
-
# LlamaIndex
|
| 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 |
-
|
| 23 |
-
|
| 24 |
-
# ------------------------------------------------------------
|
| 25 |
# Logging
|
| 26 |
-
#
|
| 27 |
logging.basicConfig(level=logging.INFO)
|
| 28 |
logger = logging.getLogger("n8n-assistant")
|
| 29 |
|
| 30 |
-
#
|
| 31 |
-
#
|
| 32 |
-
#
|
| 33 |
-
PRIMARY_LLM = "
|
| 34 |
-
FALLBACK_LLM = "google/flan-t5-
|
| 35 |
|
| 36 |
PRIMARY_EMB = "sentence-transformers/all-MiniLM-L6-v2"
|
| 37 |
FALLBACK_EMB = "sentence-transformers/paraphrase-MiniLM-L3-v2"
|
| 38 |
|
| 39 |
-
#
|
| 40 |
-
#
|
| 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 |
-
#
|
| 54 |
-
def
|
| 55 |
-
"""
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
if not os.path.exists(pasta):
|
| 59 |
-
logger.error(f"❌ Pasta não
|
| 60 |
return ""
|
| 61 |
|
|
|
|
| 62 |
for root, _, files in os.walk(pasta):
|
|
|
|
| 63 |
for file in files:
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
| 65 |
try:
|
| 66 |
-
if file.endswith(('.yml', '.yaml')):
|
| 67 |
-
with open(
|
| 68 |
data = yaml.safe_load(f)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
elif file.endswith('.json'):
|
| 73 |
-
with open(caminho_arquivo, 'r', encoding='utf-8') as f:
|
| 74 |
data = json.load(f)
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
elif file.endswith(('.md', '.txt')):
|
| 79 |
-
with open(caminho_arquivo, 'r', encoding='utf-8') as f:
|
| 80 |
texto = f.read()
|
| 81 |
-
texto_final += f"\n\n### Arquivo: {file}\n{texto}"
|
| 82 |
|
|
|
|
| 83 |
except Exception as e:
|
| 84 |
-
logger.warning(f"⚠️ Erro
|
| 85 |
-
continue
|
| 86 |
|
| 87 |
-
|
|
|
|
| 88 |
|
| 89 |
-
def gerar_documentacao(self
|
| 90 |
-
"""Gera
|
| 91 |
try:
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
logger.warning("⚠️ Nenhum conteúdo encontrado para documentação")
|
| 95 |
return False
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
return False
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 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"
|
| 114 |
-
)
|
| 115 |
-
logger.info("✅ Download concluído")
|
| 116 |
return True
|
| 117 |
except Exception as e:
|
| 118 |
-
logger.error(f"❌ Erro
|
| 119 |
return False
|
| 120 |
|
| 121 |
-
#
|
| 122 |
def configurar_embeddings(self) -> bool:
|
| 123 |
-
"""Configura embeddings HuggingFace com fallback"""
|
| 124 |
for emb in (PRIMARY_EMB, FALLBACK_EMB):
|
| 125 |
try:
|
| 126 |
-
|
| 127 |
self.emb_model_used = emb
|
| 128 |
-
logger.info(f"✅ Embeddings
|
| 129 |
return True
|
| 130 |
except Exception as e:
|
| 131 |
-
logger.warning(f"⚠️
|
| 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 |
-
|
| 144 |
-
for model_name in (PRIMARY_LLM, FALLBACK_LLM):
|
| 145 |
try:
|
| 146 |
llm = HuggingFaceLLM(
|
| 147 |
-
model_name=
|
| 148 |
-
tokenizer_name=
|
| 149 |
-
context_window=
|
| 150 |
-
max_new_tokens=
|
| 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
|
| 162 |
"baseado exclusivamente na documentação fornecida. "
|
| 163 |
"Se não souber, diga que não há informações suficientes."
|
| 164 |
),
|
| 165 |
)
|
| 166 |
-
|
| 167 |
-
self.llm_model_used =
|
| 168 |
-
logger.info(f"✅ LLM
|
| 169 |
return True
|
| 170 |
except Exception as e:
|
| 171 |
-
logger.warning(f"⚠️
|
| 172 |
-
|
| 173 |
-
logger.error("❌ Não foi possível configurar o LLM")
|
| 174 |
return False
|
| 175 |
|
| 176 |
-
#
|
| 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
|
| 182 |
return False
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
return False
|
| 191 |
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
self.index = VectorStoreIndex.from_documents(documents)
|
| 195 |
self.query_engine = self.index.as_query_engine()
|
| 196 |
-
logger.info("✅ Índice
|
| 197 |
return True
|
| 198 |
-
|
| 199 |
except Exception as e:
|
| 200 |
logger.error(f"❌ Erro ao criar índice: {e}")
|
| 201 |
return False
|
| 202 |
|
| 203 |
-
#
|
| 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
|
| 210 |
-
|
| 211 |
-
|
| 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
|
| 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: {
|
| 234 |
|
| 235 |
def responder(self, pergunta: str) -> str:
|
| 236 |
-
|
| 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 |
-
|
| 246 |
-
return str(
|
| 247 |
except Exception as e:
|
| 248 |
-
logger.error(f"❌ Erro
|
| 249 |
-
return f"❌ Erro ao processar pergunta: {
|
| 250 |
|
| 251 |
|
| 252 |
-
#
|
| 253 |
# Bootstrap
|
| 254 |
-
#
|
| 255 |
-
logger.info("🚀
|
| 256 |
assistant = N8nAssistant()
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
logger.info(f"✅ {mensagem}")
|
| 261 |
else:
|
| 262 |
-
logger.error(f"❌ {
|
| 263 |
|
| 264 |
-
#
|
| 265 |
# Gradio UI
|
| 266 |
-
#
|
| 267 |
def processar_pergunta(pergunta: str) -> str:
|
| 268 |
-
if not
|
| 269 |
-
return f"❌ Sistema não inicializado: {
|
| 270 |
return assistant.responder(pergunta)
|
| 271 |
|
| 272 |
-
with gr.Blocks(theme=gr.themes.Soft(), title="N8n Assistant
|
| 273 |
gr.Markdown(
|
| 274 |
f"""
|
| 275 |
# 🤖 N8n Assistant (Open Source)
|
| 276 |
-
Assistente
|
| 277 |
-
**Status:** {'✅
|
| 278 |
"""
|
| 279 |
)
|
| 280 |
-
|
| 281 |
with gr.Row():
|
| 282 |
with gr.Column(scale=1):
|
| 283 |
gr.Markdown("### 🤖 N8n Bot")
|
|
@@ -285,38 +273,33 @@ with gr.Blocks(theme=gr.themes.Soft(), title="N8n Assistant (Open Source)") as d
|
|
| 285 |
gr.Markdown("## Como posso ajudar você com o n8n?")
|
| 286 |
with gr.Row():
|
| 287 |
with gr.Column(scale=3):
|
| 288 |
-
|
| 289 |
label="Sua pergunta",
|
| 290 |
-
placeholder="Ex: Como
|
| 291 |
lines=3
|
| 292 |
)
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
limpar_btn = gr.Button("🧹 Limpar")
|
| 296 |
with gr.Column(scale=4):
|
| 297 |
-
|
| 298 |
label="Resposta",
|
| 299 |
-
placeholder="
|
| 300 |
-
lines=
|
| 301 |
)
|
| 302 |
-
|
| 303 |
-
with gr.Accordion("💡 Exemplos de Perguntas", open=False):
|
| 304 |
gr.Markdown(
|
| 305 |
"""
|
| 306 |
-
- Como criar um workflow no n8n?
|
| 307 |
-
- Para que serve o node HTTP Request?
|
| 308 |
-
- Como integrar n8n com Google Sheets?
|
| 309 |
- Como configurar webhooks no n8n?
|
| 310 |
-
-
|
|
|
|
| 311 |
- Como debugar erros nos nodes?
|
| 312 |
-
-
|
| 313 |
-
- Quais nodes usar para automação de email?
|
| 314 |
"""
|
| 315 |
)
|
| 316 |
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
|
| 321 |
if __name__ == "__main__":
|
| 322 |
demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)
|
|
|
|
| 1 |
"""
|
| 2 |
+
🤖 N8n Assistant - Open Source (GRÁTIS, CPU-friendly)
|
| 3 |
- Sem OpenAI
|
| 4 |
+
- LLM: google/flan-t5-base (fallback flan-t5-small)
|
| 5 |
+
- Embeddings: all-MiniLM-L6-v2 (fallback paraphrase-MiniLM-L3-v2)
|
| 6 |
+
- Baixa dataset Jeice/n8n-docs-v2 e gera documentacao.txt
|
| 7 |
+
- Logs detalhados p/ depuração
|
| 8 |
"""
|
| 9 |
|
| 10 |
import os
|
|
|
|
| 11 |
import json
|
| 12 |
+
import yaml
|
| 13 |
import logging
|
| 14 |
+
from typing import Tuple
|
| 15 |
|
| 16 |
import gradio as gr
|
| 17 |
+
from huggingface_hub import snapshot_download
|
| 18 |
|
| 19 |
+
# LlamaIndex
|
| 20 |
+
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings, ServiceContext
|
| 21 |
+
from llama_index.core.settings import Settings as LISettings
|
| 22 |
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
|
| 23 |
from llama_index.llms.huggingface import HuggingFaceLLM
|
| 24 |
|
| 25 |
+
# -------------------------
|
|
|
|
|
|
|
| 26 |
# Logging
|
| 27 |
+
# -------------------------
|
| 28 |
logging.basicConfig(level=logging.INFO)
|
| 29 |
logger = logging.getLogger("n8n-assistant")
|
| 30 |
|
| 31 |
+
# -------------------------
|
| 32 |
+
# Modelos (CPU-friendly)
|
| 33 |
+
# -------------------------
|
| 34 |
+
PRIMARY_LLM = "google/flan-t5-base"
|
| 35 |
+
FALLBACK_LLM = "google/flan-t5-small"
|
| 36 |
|
| 37 |
PRIMARY_EMB = "sentence-transformers/all-MiniLM-L6-v2"
|
| 38 |
FALLBACK_EMB = "sentence-transformers/paraphrase-MiniLM-L3-v2"
|
| 39 |
|
| 40 |
+
# -------------------------
|
| 41 |
+
# App
|
| 42 |
+
# -------------------------
|
| 43 |
class N8nAssistant:
|
|
|
|
|
|
|
| 44 |
def __init__(self):
|
| 45 |
+
self.docs_dir = None
|
| 46 |
self.index = None
|
| 47 |
self.query_engine = None
|
|
|
|
| 48 |
self.inicializado = False
|
| 49 |
self.llm_model_used = None
|
| 50 |
self.emb_model_used = None
|
| 51 |
|
| 52 |
+
# ---------- Dataset ----------
|
| 53 |
+
def baixar_docs(self) -> bool:
|
| 54 |
+
"""Baixa o dataset com a documentação."""
|
| 55 |
+
try:
|
| 56 |
+
logger.info("📥 Baixando dataset Jeice/n8n-docs-v2 ...")
|
| 57 |
+
self.docs_dir = snapshot_download(
|
| 58 |
+
repo_id="Jeice/n8n-docs-v2",
|
| 59 |
+
repo_type="dataset"
|
| 60 |
+
)
|
| 61 |
+
logger.info(f"✅ Dataset baixado em: {self.docs_dir}")
|
| 62 |
+
try:
|
| 63 |
+
logger.info(f"📂 Itens no diretório raiz do dataset: {os.listdir(self.docs_dir)}")
|
| 64 |
+
data_path = os.path.join(self.docs_dir, "data")
|
| 65 |
+
if os.path.isdir(data_path):
|
| 66 |
+
logger.info(f"📂 Pasta /data encontrada. Itens: {os.listdir(data_path)}")
|
| 67 |
+
except Exception as e:
|
| 68 |
+
logger.warning(f"⚠️ Não consegui listar arquivos do dataset: {e}")
|
| 69 |
+
return True
|
| 70 |
+
except Exception as e:
|
| 71 |
+
logger.error(f"❌ Erro ao baixar dataset: {e}")
|
| 72 |
+
return False
|
| 73 |
|
| 74 |
+
# ---------- Consolidação ----------
|
| 75 |
+
def extrair_conteudo_arquivos(self, pasta: str) -> str:
|
| 76 |
+
"""Varre todas as subpastas e agrega .yml/.yaml/.json/.md/.txt em um único texto."""
|
| 77 |
+
extensoes = ('.yml', '.yaml', '.json', '.md', '.txt')
|
| 78 |
+
texto_final = []
|
| 79 |
if not os.path.exists(pasta):
|
| 80 |
+
logger.error(f"❌ Pasta não existe: {pasta}")
|
| 81 |
return ""
|
| 82 |
|
| 83 |
+
total_arquivos = 0
|
| 84 |
for root, _, files in os.walk(pasta):
|
| 85 |
+
logger.info(f"🔎 Explorando: {root} | {len(files)} arquivos")
|
| 86 |
for file in files:
|
| 87 |
+
caminho = os.path.join(root, file)
|
| 88 |
+
if not file.lower().endswith(extensoes):
|
| 89 |
+
continue
|
| 90 |
+
total_arquivos += 1
|
| 91 |
try:
|
| 92 |
+
if file.lower().endswith(('.yml', '.yaml')):
|
| 93 |
+
with open(caminho, 'r', encoding='utf-8') as f:
|
| 94 |
data = yaml.safe_load(f)
|
| 95 |
+
texto = yaml.dump(data, allow_unicode=True, sort_keys=False)
|
| 96 |
+
elif file.lower().endswith('.json'):
|
| 97 |
+
with open(caminho, 'r', encoding='utf-8') as f:
|
|
|
|
|
|
|
| 98 |
data = json.load(f)
|
| 99 |
+
texto = json.dumps(data, ensure_ascii=False, indent=2)
|
| 100 |
+
else: # .md / .txt
|
| 101 |
+
with open(caminho, 'r', encoding='utf-8', errors='ignore') as f:
|
|
|
|
|
|
|
| 102 |
texto = f.read()
|
|
|
|
| 103 |
|
| 104 |
+
texto_final.append(f"\n\n### Arquivo: {os.path.relpath(caminho, pasta)}\n{texto}")
|
| 105 |
except Exception as e:
|
| 106 |
+
logger.warning(f"⚠️ Erro lendo {caminho}: {e}")
|
|
|
|
| 107 |
|
| 108 |
+
logger.info(f"🧾 Total de arquivos agregados: {total_arquivos}")
|
| 109 |
+
return "".join(texto_final)
|
| 110 |
|
| 111 |
+
def gerar_documentacao(self) -> bool:
|
| 112 |
+
"""Gera documentacao.txt a partir do dataset (raiz + /data se existir)."""
|
| 113 |
try:
|
| 114 |
+
if not self.docs_dir:
|
| 115 |
+
logger.error("❌ docs_dir não definido")
|
|
|
|
| 116 |
return False
|
| 117 |
|
| 118 |
+
partes = []
|
| 119 |
+
# raiz do dataset
|
| 120 |
+
partes.append(self.extrair_conteudo_arquivos(self.docs_dir))
|
| 121 |
+
# subpasta /data (comum em datasets do HF)
|
| 122 |
+
data_path = os.path.join(self.docs_dir, "data")
|
| 123 |
+
if os.path.isdir(data_path):
|
| 124 |
+
partes.append(self.extrair_conteudo_arquivos(data_path))
|
| 125 |
|
| 126 |
+
texto = "\n".join([p for p in partes if p and p.strip()])
|
| 127 |
+
if not texto.strip():
|
| 128 |
+
logger.error("❌ Nenhum conteúdo válido encontrado no dataset")
|
| 129 |
+
return False
|
| 130 |
|
| 131 |
+
with open("documentacao.txt", "w", encoding="utf-8") as f:
|
| 132 |
+
f.write(texto)
|
|
|
|
| 133 |
|
| 134 |
+
# Loga um preview
|
| 135 |
+
preview = texto[:1500]
|
| 136 |
+
logger.info(f"📝 documentacao.txt gerado (preview 1500 chars):\n{preview}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
return True
|
| 138 |
except Exception as e:
|
| 139 |
+
logger.error(f"❌ Erro ao gerar documentacao.txt: {e}")
|
| 140 |
return False
|
| 141 |
|
| 142 |
+
# ---------- Modelos ----------
|
| 143 |
def configurar_embeddings(self) -> bool:
|
|
|
|
| 144 |
for emb in (PRIMARY_EMB, FALLBACK_EMB):
|
| 145 |
try:
|
| 146 |
+
LISettings.embed_model = HuggingFaceEmbedding(model_name=emb)
|
| 147 |
self.emb_model_used = emb
|
| 148 |
+
logger.info(f"✅ Embeddings carregados: {emb}")
|
| 149 |
return True
|
| 150 |
except Exception as e:
|
| 151 |
+
logger.warning(f"⚠️ Falhou carregar embeddings {emb}: {e}")
|
|
|
|
| 152 |
return False
|
| 153 |
|
| 154 |
def configurar_llm(self) -> bool:
|
|
|
|
|
|
|
| 155 |
gen_kwargs = {
|
| 156 |
"temperature": 0.2,
|
| 157 |
"do_sample": True,
|
| 158 |
"top_p": 0.9
|
| 159 |
}
|
| 160 |
+
for name in (PRIMARY_LLM, FALLBACK_LLM):
|
|
|
|
| 161 |
try:
|
| 162 |
llm = HuggingFaceLLM(
|
| 163 |
+
model_name=name,
|
| 164 |
+
tokenizer_name=name,
|
| 165 |
+
context_window=2048,
|
| 166 |
+
max_new_tokens=384, # menor = mais leve em CPU
|
| 167 |
generate_kwargs=gen_kwargs,
|
|
|
|
| 168 |
device_map="auto",
|
| 169 |
+
model_kwargs={"torch_dtype": "auto"},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
system_prompt=(
|
| 171 |
"Você é um assistente especialista em n8n. "
|
| 172 |
+
"Responda em português do Brasil, de forma clara e objetiva, "
|
| 173 |
"baseado exclusivamente na documentação fornecida. "
|
| 174 |
"Se não souber, diga que não há informações suficientes."
|
| 175 |
),
|
| 176 |
)
|
| 177 |
+
LISettings.llm = llm
|
| 178 |
+
self.llm_model_used = name
|
| 179 |
+
logger.info(f"✅ LLM carregado: {name}")
|
| 180 |
return True
|
| 181 |
except Exception as e:
|
| 182 |
+
logger.warning(f"⚠️ Falhou carregar LLM {name}: {e}")
|
|
|
|
|
|
|
| 183 |
return False
|
| 184 |
|
| 185 |
+
# ---------- Index ----------
|
| 186 |
def criar_index(self) -> bool:
|
|
|
|
| 187 |
try:
|
| 188 |
if not os.path.exists("documentacao.txt"):
|
| 189 |
+
logger.error("❌ documentacao.txt não existe")
|
| 190 |
return False
|
| 191 |
|
| 192 |
+
# Carrega o único arquivo consolidado
|
| 193 |
+
docs = SimpleDirectoryReader(input_files=["documentacao.txt"]).load_data()
|
| 194 |
+
if not docs:
|
| 195 |
+
logger.error("❌ Nenhum documento carregado de documentacao.txt")
|
| 196 |
+
with open("documentacao.txt", "r", encoding="utf-8") as f:
|
| 197 |
+
logger.error("📄 documentacao.txt (trecho): " + f.read()[:1200])
|
| 198 |
return False
|
| 199 |
|
| 200 |
+
logger.info(f"📚 {len(docs)} documento(s) prontos para indexação")
|
| 201 |
+
self.index = VectorStoreIndex.from_documents(docs)
|
|
|
|
| 202 |
self.query_engine = self.index.as_query_engine()
|
| 203 |
+
logger.info("✅ Índice e QueryEngine criados")
|
| 204 |
return True
|
|
|
|
| 205 |
except Exception as e:
|
| 206 |
logger.error(f"❌ Erro ao criar índice: {e}")
|
| 207 |
return False
|
| 208 |
|
| 209 |
+
# ---------- Orquestração ----------
|
| 210 |
def inicializar(self) -> Tuple[bool, str]:
|
|
|
|
| 211 |
try:
|
|
|
|
| 212 |
if not self.baixar_docs():
|
| 213 |
+
return False, "Erro ao baixar dataset"
|
| 214 |
+
if not self.gerar_documentacao():
|
| 215 |
+
return False, "Erro ao gerar documentacao.txt"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
if not self.criar_index():
|
| 221 |
+
return False, "Erro ao criar índice"
|
|
|
|
| 222 |
self.inicializado = True
|
| 223 |
+
return True, f"Pronto | LLM: {self.llm_model_used} | Emb: {self.emb_model_used}"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
except Exception as e:
|
| 225 |
logger.error(f"❌ Erro na inicialização: {e}")
|
| 226 |
+
return False, f"Erro na inicialização: {e}"
|
| 227 |
|
| 228 |
def responder(self, pergunta: str) -> str:
|
| 229 |
+
if not pergunta.strip():
|
|
|
|
| 230 |
return "⚠️ Por favor, digite uma pergunta."
|
|
|
|
| 231 |
if not self.inicializado or not self.query_engine:
|
| 232 |
return "❌ Sistema não inicializado. Recarregue a página."
|
|
|
|
| 233 |
try:
|
| 234 |
+
logger.info(f"🤔 Pergunta: {pergunta[:120]}")
|
| 235 |
+
resp = self.query_engine.query(pergunta)
|
| 236 |
+
return str(resp)
|
| 237 |
except Exception as e:
|
| 238 |
+
logger.error(f"❌ Erro na resposta: {e}")
|
| 239 |
+
return f"❌ Erro ao processar a pergunta: {e}"
|
| 240 |
|
| 241 |
|
| 242 |
+
# -------------------------
|
| 243 |
# Bootstrap
|
| 244 |
+
# -------------------------
|
| 245 |
+
logger.info("🚀 Subindo N8n Assistant (Open Source, CPU)...")
|
| 246 |
assistant = N8nAssistant()
|
| 247 |
+
ok, status_msg = assistant.inicializar()
|
| 248 |
+
if ok:
|
| 249 |
+
logger.info(f"✅ {status_msg}")
|
|
|
|
| 250 |
else:
|
| 251 |
+
logger.error(f"❌ {status_msg}")
|
| 252 |
|
| 253 |
+
# -------------------------
|
| 254 |
# Gradio UI
|
| 255 |
+
# -------------------------
|
| 256 |
def processar_pergunta(pergunta: str) -> str:
|
| 257 |
+
if not ok:
|
| 258 |
+
return f"❌ Sistema não inicializado: {status_msg}"
|
| 259 |
return assistant.responder(pergunta)
|
| 260 |
|
| 261 |
+
with gr.Blocks(theme=gr.themes.Soft(), title="N8n Assistant") as demo:
|
| 262 |
gr.Markdown(
|
| 263 |
f"""
|
| 264 |
# 🤖 N8n Assistant (Open Source)
|
| 265 |
+
Assistente baseado na documentação oficial do **n8n** (dataset do HF).
|
| 266 |
+
**Status:** {'✅ ' + status_msg if ok else '❌ ' + status_msg}
|
| 267 |
"""
|
| 268 |
)
|
|
|
|
| 269 |
with gr.Row():
|
| 270 |
with gr.Column(scale=1):
|
| 271 |
gr.Markdown("### 🤖 N8n Bot")
|
|
|
|
| 273 |
gr.Markdown("## Como posso ajudar você com o n8n?")
|
| 274 |
with gr.Row():
|
| 275 |
with gr.Column(scale=3):
|
| 276 |
+
pergunta = gr.Textbox(
|
| 277 |
label="Sua pergunta",
|
| 278 |
+
placeholder="Ex: Como configurar um Webhook Trigger no n8n?",
|
| 279 |
lines=3
|
| 280 |
)
|
| 281 |
+
enviar = gr.Button("🚀 Perguntar", variant="primary")
|
| 282 |
+
limpar = gr.Button("🧹 Limpar")
|
|
|
|
| 283 |
with gr.Column(scale=4):
|
| 284 |
+
resposta = gr.Textbox(
|
| 285 |
label="Resposta",
|
| 286 |
+
placeholder="A resposta aparecerá aqui...",
|
| 287 |
+
lines=14
|
| 288 |
)
|
| 289 |
+
with gr.Accordion("💡 Exemplos", open=False):
|
|
|
|
| 290 |
gr.Markdown(
|
| 291 |
"""
|
|
|
|
|
|
|
|
|
|
| 292 |
- Como configurar webhooks no n8n?
|
| 293 |
+
- Para que serve o node HTTP Request?
|
| 294 |
+
- Como integrar com Google Sheets?
|
| 295 |
- Como debugar erros nos nodes?
|
| 296 |
+
- Quais são boas práticas de workflows?
|
|
|
|
| 297 |
"""
|
| 298 |
)
|
| 299 |
|
| 300 |
+
enviar.click(fn=processar_pergunta, inputs=pergunta, outputs=resposta)
|
| 301 |
+
limpar.click(lambda: ("", ""), None, [pergunta, resposta])
|
| 302 |
+
pergunta.submit(fn=processar_pergunta, inputs=pergunta, outputs=resposta)
|
| 303 |
|
| 304 |
if __name__ == "__main__":
|
| 305 |
demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)
|