Jeice commited on
Commit
da5f153
·
verified ·
1 Parent(s): 609b1ba

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +155 -172
app.py CHANGED
@@ -1,283 +1,271 @@
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:
68
  data = yaml.safe_load(f)
69
- texto = yaml.dump(data, allow_unicode=True)
70
- texto_final += f"\n\n### Arquivo: {file}\n{texto}"
71
-
72
- elif file.endswith('.json'):
73
- with open(caminho_arquivo, 'r', encoding='utf-8') as f:
74
  data = json.load(f)
75
- texto = json.dumps(data, indent=2, ensure_ascii=False)
76
- texto_final += f"\n\n### Arquivo: {file}\n{texto}"
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 ao ler {file}: {e}")
85
- continue
86
 
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"
114
- )
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
-
259
- if sucesso:
260
- logger.info(f"✅ {mensagem}")
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")
@@ -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
- input_box = gr.Textbox(
289
  label="Sua pergunta",
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
  """
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
- - Quais são as melhores práticas para workflows?
 
311
  - Como debugar erros nos nodes?
312
- - Como usar condições nos workflows?
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)
 
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)