Jeice commited on
Commit
a6b0739
·
verified ·
1 Parent(s): 0bc097e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +154 -144
app.py CHANGED
@@ -1,72 +1,67 @@
1
  """
2
- 🤖 N8n Assistant - Versão Corrigida (SEM ERRO DE IMAGEM)
3
- Chatbot inteligente para dúvidas sobre n8n - Compatível com Hugging Face Spaces
4
-
5
- CORREÇÃO APLICADA:
6
- - Removido componente Image problemático que causava erro 404
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
- # Configurar logging
 
 
 
 
 
 
 
 
 
19
  logging.basicConfig(level=logging.INFO)
20
- logger = logging.getLogger(__name__)
21
 
22
- # Importações com tratamento de erro
23
- try:
24
- from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
25
- from llama_index.llms.openai import OpenAI
26
- from llama_index.embeddings.openai import OpenAIEmbedding
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 simplificado e funcional"""
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
- def setup_openai(self) -> bool:
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
- """Extrair conteúdo dos arquivos"""
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, dirs, files in os.walk(pasta):
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
- """Gerar arquivo de documentação"""
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 gerada")
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
- """Baixar documentação do HF"""
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
- """Criar índice vetorial"""
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(input_files=["documentacao.txt"]).load_data()
136
-
 
 
137
  if not documents:
138
  logger.error("❌ Nenhum documento carregado")
139
  return False
140
-
141
- # Configurar LLM
142
- Settings.llm = OpenAI(
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
- """Inicializar sistema completo"""
168
  try:
169
- # 1. Configurar OpenAI
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 no download da documentação"
176
-
177
- # 3. Gerar documentação
178
  if not self.gerar_documentacao(self.docs_dir):
179
- return False, "Erro ao processar documentação"
180
-
181
- # 4. Criar índice
 
 
 
 
 
 
182
  if not self.criar_index():
183
- return False, "Erro ao criar índice"
184
-
185
  self.inicializado = True
186
- return True, "Sistema inicializado com sucesso"
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
- """Responder pergunta"""
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[:50]}...")
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
- # Inicializar sistema
211
- logger.info("🚀 Inicializando N8n Assistant...")
 
 
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
- Assistente inteligente para dúvidas sobre **n8n** baseado na documentação oficial.
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)