Geoeasy commited on
Commit
8b4671d
·
verified ·
1 Parent(s): 94f5502

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +115 -118
app.py CHANGED
@@ -1,7 +1,5 @@
1
  import os
2
- import io
3
  import re
4
- import time
5
  from pathlib import Path
6
  from typing import List, Tuple
7
 
@@ -9,52 +7,32 @@ import numpy as np
9
  import faiss
10
  import gradio as gr
11
 
12
- # Para leitura do PDF
13
  try:
14
  from pypdf import PdfReader # pypdf é leve e confiável para extração de texto
15
  except Exception:
16
- # fallback simples se pypdf não estiver disponível
17
  PdfReader = None
18
 
19
- # Embeddings e LLM (NVIDIA API estilo OpenAI)
20
  from sentence_transformers import SentenceTransformer
21
  from openai import OpenAI, OpenAIError
22
 
23
  """
24
- ===============================================================================
25
  DFSORT RAG – Assistente em Português (Gradio)
26
- -------------------------------------------------------------------------------
27
- Objetivo: responder sobre DFSORT (IBM z/OS) usando **apenas** o PDF fornecido como
28
- base de conhecimento (RAG Retrieval Augmented Generation).
29
- Tudo em português: interface, comentários e mensagens do sistema.
30
- Sem conteúdos de CV ou outros temas. Foco total em DFSORT.
31
- • O app cria o índice (FAISS + embeddings MiniLM) automaticamente na primeira execução.
32
-
33
- Como usar
34
- 1) Garanta que o PDF esteja disponível. Por padrão este script usa:
35
- - PDF_PATH = "ice2ca11.pdf" (você pode alterar o caminho abaixo)
36
- 2) Execute o script. Na primeira execução, ele extrai o texto do PDF e cria:
37
- - r_docs.index (FAISS)
38
- - r_chunks.npy (lista de trechos do PDF)
39
- 3) Interaja no chat. O modelo responde **somente** com base nos trechos recuperados.
40
-
41
- Requisitos (pip):
42
- pip install gradio pypdf faiss-cpu sentence-transformers openai
43
-
44
- ==========================================================================
45
- ATENÇÃO SOBRE KEYS
46
- - Configure a variável de ambiente NV_API_KEY com a sua chave da NVIDIA
47
- (API OpenAI-compatible em https://integrate.api.nvidia.com/v1).
48
- ==========================================================================
49
  """
50
 
51
  # ===================== Configurações =====================
52
- APP_TITLE = "DFSORT RAG (PDF)"
53
- PDF_PATH = "ice2ca11.pdf" # use o PDF fornecido; altere se necessário
54
- INDEX_FILE = "r_docs.index"
55
  CHUNKS_FILE = "r_chunks.npy"
56
 
57
- # Modelo de chat na NVIDIA (pode trocar por outro suportado)
58
  CHAT_MODEL = "meta/llama3-8b-instruct"
59
  NV_API_KEY = os.environ.get("NV_API_KEY")
60
  if not NV_API_KEY:
@@ -63,16 +41,18 @@ if not NV_API_KEY:
63
  client = OpenAI(base_url="https://integrate.api.nvidia.com/v1", api_key=NV_API_KEY)
64
 
65
  # Modelo de embeddings (baixa no primeiro uso)
66
- EMB_MODEL_NAME = "all-MiniLM-L6-v2"
67
  embedding_model = SentenceTransformer(EMB_MODEL_NAME)
68
 
69
- # ===================== Pipeline de Indexação =====================
 
 
 
 
70
 
71
  def _pdf_to_text_chunks(pdf_path: str, max_chunk_chars: int = 1200) -> List[str]:
72
- """ o PDF e cria chunks de texto amigáveis ao RAG.
73
- - Divide por páginas e quebras duplas de linha.
74
- - Faz um 'merge' simples até atingir ~max_chunk_chars.
75
- - Remove linhas vazias e normaliza espaços.
76
  """
77
  path = Path(pdf_path)
78
  if not path.exists():
@@ -80,7 +60,7 @@ def _pdf_to_text_chunks(pdf_path: str, max_chunk_chars: int = 1200) -> List[str]
80
 
81
  raw_pages: List[str] = []
82
  if PdfReader is None:
83
- # fallback: ler bytes e tentar split muito simples (não ideal)
84
  with open(path, "rb") as f:
85
  data = f.read()
86
  text = data.decode(errors="ignore")
@@ -94,21 +74,17 @@ def _pdf_to_text_chunks(pdf_path: str, max_chunk_chars: int = 1200) -> List[str]
94
  raw = ""
95
  raw_pages.append(raw)
96
 
97
- # limpeza e chunking
98
  blocks: List[str] = []
99
  for page_txt in raw_pages:
100
  if not page_txt:
101
  continue
102
- # normalizações leves
103
  t = re.sub(r"[ \t]+", " ", page_txt)
104
  t = re.sub(r"\n{2,}", "\n\n", t).strip()
105
- # quebra por parágrafos duplos ou linhas
106
  parts = re.split(r"\n\n+|\n• |\n- ", t)
107
  blocks.extend(p.strip() for p in parts if p and p.strip())
108
 
109
- # juntar em chunks de tamanho alvo
110
  chunks: List[str] = []
111
- buf = []
112
  size = 0
113
  for b in blocks:
114
  if size + len(b) + 1 > max_chunk_chars:
@@ -122,19 +98,19 @@ def _pdf_to_text_chunks(pdf_path: str, max_chunk_chars: int = 1200) -> List[str]
122
  if buf:
123
  chunks.append("\n".join(buf))
124
 
125
- # reforço: remover pedaços muito curtos
126
  chunks = [c.strip() for c in chunks if len(c.strip()) > 50]
127
  return chunks
128
 
129
 
130
  def build_or_load_index(pdf_path: str, index_path: str, chunks_path: str) -> Tuple[faiss.IndexFlatIP, np.ndarray]:
131
- """Cria ou carrega o índice FAISS e os chunks."""
132
  if Path(index_path).exists() and Path(chunks_path).exists():
133
  index = faiss.read_index(index_path)
134
  chunks = np.load(chunks_path, allow_pickle=True)
135
  return index, chunks
136
 
137
- # construir
138
  chunks_list = _pdf_to_text_chunks(pdf_path)
139
  emb = embedding_model.encode(chunks_list, convert_to_numpy=True, normalize_embeddings=True)
140
  d = emb.shape[1]
@@ -145,34 +121,28 @@ def build_or_load_index(pdf_path: str, index_path: str, chunks_path: str) -> Tup
145
  return index, np.array(chunks_list, dtype=object)
146
 
147
 
148
- # ===================== Recuperação + Chat =====================
149
 
150
  def retrieve_context(query: str, index: faiss.IndexFlatIP, chunks: np.ndarray, k: int = 6) -> str:
151
  q = embedding_model.encode([query], convert_to_numpy=True, normalize_embeddings=True)
152
  scores, idxs = index.search(q, k)
153
- parts = []
154
  for i in idxs[0]:
155
  if 0 <= i < len(chunks):
156
  parts.append(str(chunks[i]))
157
  return "\n---\n".join(parts)
158
 
159
 
160
- def nv_stream(messages, temperature: float, top_p: float, max_tokens: int):
161
- """Streaming de resposta do modelo NVIDIA (compatível com OpenAI)."""
162
- assistant_reply = ""
163
- stream = client.chat.completions.create(
164
  model=CHAT_MODEL,
165
  messages=messages,
166
  temperature=temperature,
167
  top_p=top_p,
168
  max_tokens=max_tokens,
169
- stream=True,
170
  )
171
- for chunk in stream:
172
- delta = chunk.choices[0].delta
173
- if hasattr(delta, "content") and delta.content:
174
- assistant_reply += delta.content
175
- yield assistant_reply
176
 
177
 
178
  def make_system_prompt(ctx: str) -> str:
@@ -181,32 +151,44 @@ def make_system_prompt(ctx: str) -> str:
181
  "Responda **apenas** com base no contexto recuperado do PDF.\n"
182
  "Se a informação não estiver no contexto, diga que não sabe.\n\n"
183
  f"=== Contexto (trechos do PDF) ===\n{ctx}\n\n"
184
- "Ao mostrar exemplos, prefira JCL/SYSIN claros e curtos."
185
  )
186
 
187
 
188
- # ===================== UI (Gradio) =====================
189
 
190
- def chatbot_ui(user_input: str, temperature: float, top_p: float, max_tokens: int, k: int):
191
- if not user_input or not user_input.strip():
192
- return ""
193
- # garanta índice carregado
194
  global faiss_index, pdf_chunks
195
  if faiss_index is None or pdf_chunks is None:
196
  faiss_index, pdf_chunks = build_or_load_index(PDF_PATH, INDEX_FILE, CHUNKS_FILE)
197
 
198
- ctx = retrieve_context(user_input, faiss_index, pdf_chunks, k=k)
 
 
 
 
 
 
 
 
 
199
  sys_msg = {"role": "system", "content": make_system_prompt(ctx)}
200
- usr_msg = {"role": "user", "content": user_input}
201
 
202
- # streaming para UX fluida
203
  try:
204
- out = ""
205
- for partial in nv_stream([sys_msg, usr_msg], temperature, top_p, max_tokens):
206
- out = partial
207
- return out
208
  except OpenAIError as e:
209
- return f"⚠️ Erro da API: {e.__class__.__name__}: {e}"
 
 
 
 
 
 
 
 
 
 
210
 
211
 
212
  def rebuild_index_action():
@@ -215,59 +197,74 @@ def rebuild_index_action():
215
  return "✅ Índice reconstruído com sucesso a partir do PDF."
216
 
217
 
218
- # Estado global carregado sob demanda
219
- faiss_index = None
220
- pdf_chunks = None
221
-
222
-
223
  custom_css = r"""
224
  :root { --primary:#2156d9; --bg:#f8fafc; --ink:#0f172a; }
225
  body { background: var(--bg); color: var(--ink); }
226
- #chatbox { height: 60vh; overflow-y: auto; }
 
227
  """
228
 
229
  with gr.Blocks(title=APP_TITLE, css=custom_css, theme=gr.themes.Base()) as demo:
230
- gr.Markdown(f"## {APP_TITLE}")
231
- gr.Markdown(
232
- "Este assistente responde sobre **DFSORT** usando apenas o PDF como fonte. "
233
- "Se algo não estiver no PDF, ele informa que não sabe."
234
- )
235
-
236
- with gr.Row():
237
- with gr.Column(scale=3):
238
- chat = gr.ChatInterface(
239
- fn=lambda msg, hist, t, p, mt, k: chatbot_ui(msg, t, p, mt, k),
240
- additional_inputs=[
241
- gr.Slider(0, 1, 0.4, label="Temperature"),
242
- gr.Slider(0, 1, 0.95, label="Top-p"),
243
- gr.Slider(128, 4096, 768, step=64, label="Max Tokens"),
244
- gr.Slider(2, 12, 6, step=1, label="Trechos (k)")
245
- ],
246
- multimodal=False,
247
- title="Chat DFSORT (RAG)",
248
- textbox=gr.Textbox(placeholder="Pergunte algo sobre DFSORT… ex.: Como uso INCLUDE COND?"),
249
- cache_examples=False,
250
- )
251
- with gr.Column(scale=2):
252
- gr.Markdown("### Controlo do índice")
253
- gr.Markdown(f"PDF atual: `{PDF_PATH}`")
254
- btn_rebuild = gr.Button("Reconstruir índice a partir do PDF")
255
- msg = gr.Markdown()
256
- btn_rebuild.click(lambda: rebuild_index_action(), [], [msg])
257
-
258
- gr.Markdown("---")
259
- gr.Markdown("### Dicas de consulta (direto do PDF)")
260
- gr.Markdown(
261
- "- Ex.: `Ordenar por 10 bytes a partir da posição 1 (CH, A).`\n"
262
- "- Ex.: `Como faço para eliminar duplicados com SUM FIELDS=NONE?`\n"
263
- "- Ex.: `JOINKEYS: explique o uso de REFORMAT.`\n"
264
- "- Ex.: `Exemplo de OUTFIL com cabeçalho e REMOVECC.`"
265
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
  if __name__ == "__main__":
268
- # cria índice na primeira execução
269
  if not Path(INDEX_FILE).exists() or not Path(CHUNKS_FILE).exists():
270
  print("[i] Construindo índice a partir do PDF…")
271
  faiss_index, pdf_chunks = build_or_load_index(PDF_PATH, INDEX_FILE, CHUNKS_FILE)
272
  print("[i] Índice criado.")
273
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
  import os
 
2
  import re
 
3
  from pathlib import Path
4
  from typing import List, Tuple
5
 
 
7
  import faiss
8
  import gradio as gr
9
 
10
+ # Leitura do PDF
11
  try:
12
  from pypdf import PdfReader # pypdf é leve e confiável para extração de texto
13
  except Exception:
 
14
  PdfReader = None
15
 
16
+ # Embeddings e LLM (API NVIDIA estilo OpenAI)
17
  from sentence_transformers import SentenceTransformer
18
  from openai import OpenAI, OpenAIError
19
 
20
  """
 
21
  DFSORT RAG – Assistente em Português (Gradio)
22
+ ---------------------------------------------
23
+ Interface totalmente em português.
24
+ Botões "Enviar" e "Limpar" no chat.
25
+ Página enquadrada (layout responsivo) para tudo ficar visível.
26
+ RAG simples: FAISS + MiniLM sobre o PDF fornecido (somente ele como fonte).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  """
28
 
29
  # ===================== Configurações =====================
30
+ APP_TITLE = "DFSORT RAG (PDF)"
31
+ PDF_PATH = "ice2ca11.pdf" # ajuste se o PDF tiver outro nome/caminho
32
+ INDEX_FILE = "r_docs.index"
33
  CHUNKS_FILE = "r_chunks.npy"
34
 
35
+ # Modelo de chat (NVIDIA OpenAI-compatible)
36
  CHAT_MODEL = "meta/llama3-8b-instruct"
37
  NV_API_KEY = os.environ.get("NV_API_KEY")
38
  if not NV_API_KEY:
 
41
  client = OpenAI(base_url="https://integrate.api.nvidia.com/v1", api_key=NV_API_KEY)
42
 
43
  # Modelo de embeddings (baixa no primeiro uso)
44
+ EMB_MODEL_NAME = "all-MiniLM-L6-v2"
45
  embedding_model = SentenceTransformer(EMB_MODEL_NAME)
46
 
47
+ # Estado global (carregado sob demanda)
48
+ faiss_index = None
49
+ pdf_chunks = None
50
+
51
+ # ===================== Indexação a partir do PDF =====================
52
 
53
  def _pdf_to_text_chunks(pdf_path: str, max_chunk_chars: int = 1200) -> List[str]:
54
+ """Extrai texto do PDF e cria chunks (~max_chunk_chars) para o RAG.
55
+ - Divide por páginas; normaliza espaços/linhas; agrega em blocos.
 
 
56
  """
57
  path = Path(pdf_path)
58
  if not path.exists():
 
60
 
61
  raw_pages: List[str] = []
62
  if PdfReader is None:
63
+ # fallback tosco se pypdf faltar (não recomendado)
64
  with open(path, "rb") as f:
65
  data = f.read()
66
  text = data.decode(errors="ignore")
 
74
  raw = ""
75
  raw_pages.append(raw)
76
 
 
77
  blocks: List[str] = []
78
  for page_txt in raw_pages:
79
  if not page_txt:
80
  continue
 
81
  t = re.sub(r"[ \t]+", " ", page_txt)
82
  t = re.sub(r"\n{2,}", "\n\n", t).strip()
 
83
  parts = re.split(r"\n\n+|\n• |\n- ", t)
84
  blocks.extend(p.strip() for p in parts if p and p.strip())
85
 
 
86
  chunks: List[str] = []
87
+ buf: List[str] = []
88
  size = 0
89
  for b in blocks:
90
  if size + len(b) + 1 > max_chunk_chars:
 
98
  if buf:
99
  chunks.append("\n".join(buf))
100
 
101
+ # remover pedaços muito curtos
102
  chunks = [c.strip() for c in chunks if len(c.strip()) > 50]
103
  return chunks
104
 
105
 
106
  def build_or_load_index(pdf_path: str, index_path: str, chunks_path: str) -> Tuple[faiss.IndexFlatIP, np.ndarray]:
107
+ """Cria/carrega índice FAISS e os chunks a partir do PDF."""
108
  if Path(index_path).exists() and Path(chunks_path).exists():
109
  index = faiss.read_index(index_path)
110
  chunks = np.load(chunks_path, allow_pickle=True)
111
  return index, chunks
112
 
113
+ # construir do zero
114
  chunks_list = _pdf_to_text_chunks(pdf_path)
115
  emb = embedding_model.encode(chunks_list, convert_to_numpy=True, normalize_embeddings=True)
116
  d = emb.shape[1]
 
121
  return index, np.array(chunks_list, dtype=object)
122
 
123
 
124
+ # ===================== Recuperação + LLM =====================
125
 
126
  def retrieve_context(query: str, index: faiss.IndexFlatIP, chunks: np.ndarray, k: int = 6) -> str:
127
  q = embedding_model.encode([query], convert_to_numpy=True, normalize_embeddings=True)
128
  scores, idxs = index.search(q, k)
129
+ parts: List[str] = []
130
  for i in idxs[0]:
131
  if 0 <= i < len(chunks):
132
  parts.append(str(chunks[i]))
133
  return "\n---\n".join(parts)
134
 
135
 
136
+ def nv_complete(messages, temperature: float, top_p: float, max_tokens: int) -> str:
137
+ resp = client.chat.completions.create(
 
 
138
  model=CHAT_MODEL,
139
  messages=messages,
140
  temperature=temperature,
141
  top_p=top_p,
142
  max_tokens=max_tokens,
143
+ stream=False,
144
  )
145
+ return resp.choices[0].message.content.strip()
 
 
 
 
146
 
147
 
148
  def make_system_prompt(ctx: str) -> str:
 
151
  "Responda **apenas** com base no contexto recuperado do PDF.\n"
152
  "Se a informação não estiver no contexto, diga que não sabe.\n\n"
153
  f"=== Contexto (trechos do PDF) ===\n{ctx}\n\n"
154
+ "Quando der exemplos, forneça JCL/SYSIN curtos e claros."
155
  )
156
 
157
 
158
+ # ===================== Handlers do Chat =====================
159
 
160
+ def ensure_index_loaded():
 
 
 
161
  global faiss_index, pdf_chunks
162
  if faiss_index is None or pdf_chunks is None:
163
  faiss_index, pdf_chunks = build_or_load_index(PDF_PATH, INDEX_FILE, CHUNKS_FILE)
164
 
165
+
166
+ def on_send(user_msg, history, temperature, top_p, max_tokens, k):
167
+ """Envia a pergunta, roda o RAG e devolve o histórico atualizado."""
168
+ ensure_index_loaded()
169
+ history = history or []
170
+ user_msg = (user_msg or "").strip()
171
+ if not user_msg:
172
+ return history, ""
173
+
174
+ ctx = retrieve_context(user_msg, faiss_index, pdf_chunks, k=int(k))
175
  sys_msg = {"role": "system", "content": make_system_prompt(ctx)}
176
+ usr_msg = {"role": "user", "content": user_msg}
177
 
 
178
  try:
179
+ answer = nv_complete([sys_msg, usr_msg], float(temperature), float(top_p), int(max_tokens))
 
 
 
180
  except OpenAIError as e:
181
+ answer = f"⚠️ Erro da API: {e.__class__.__name__}: {e}"
182
+
183
+ history = history + [
184
+ {"role": "user", "content": user_msg},
185
+ {"role": "assistant", "content": answer},
186
+ ]
187
+ return history, "" # limpa o textbox
188
+
189
+
190
+ def on_clear():
191
+ return [], ""
192
 
193
 
194
  def rebuild_index_action():
 
197
  return "✅ Índice reconstruído com sucesso a partir do PDF."
198
 
199
 
200
+ # ===================== UI (Gradio) =====================
 
 
 
 
201
  custom_css = r"""
202
  :root { --primary:#2156d9; --bg:#f8fafc; --ink:#0f172a; }
203
  body { background: var(--bg); color: var(--ink); }
204
+ .container { max-width: 1200px; margin: 0 auto; }
205
+ #chatbox { height: 70vh; overflow-y: auto; border:1px solid #cbd5e1; border-radius:8px; padding:0.5rem; }
206
  """
207
 
208
  with gr.Blocks(title=APP_TITLE, css=custom_css, theme=gr.themes.Base()) as demo:
209
+ with gr.Column(elem_classes="container"):
210
+ gr.Markdown(f"## {APP_TITLE}")
211
+ gr.Markdown(
212
+ "Assistente **RAG** sobre **DFSORT**, usando **apenas** o PDF fornecido. "
213
+ "Se algo não estiver no PDF, eu aviso que não sei."
214
+ )
215
+
216
+ with gr.Row():
217
+ # ===== Coluna principal (chat) =====
218
+ with gr.Column(scale=3):
219
+ chatbot = gr.Chatbot(type="messages", elem_id="chatbox", height=560)
220
+ state_history = gr.State([]) # guarda o histórico no formato messages
221
+
222
+ user_box = gr.Textbox(placeholder="Pergunte algo sobre DFSORT… ex.: Como uso INCLUDE COND?", lines=2)
223
+ with gr.Row():
224
+ btn_send = gr.Button("Enviar", variant="primary")
225
+ btn_clear = gr.Button("Limpar")
226
+
227
+ with gr.Row():
228
+ temperature = gr.Slider(0, 1, 0.4, step=0.05, label="Temperature")
229
+ top_p = gr.Slider(0, 1, 0.95, step=0.01, label="Top-p")
230
+ with gr.Row():
231
+ max_tokens = gr.Slider(128, 4096, 768, step=64, label="Max Tokens")
232
+ k_chunks = gr.Slider(2, 12, 6, step=1, label="Trechos (k)")
233
+
234
+ # Enviar via botão e Enter
235
+ btn_send.click(
236
+ on_send,
237
+ inputs=[user_box, state_history, temperature, top_p, max_tokens, k_chunks],
238
+ outputs=[chatbot, user_box],
239
+ )
240
+ user_box.submit(
241
+ on_send,
242
+ inputs=[user_box, state_history, temperature, top_p, max_tokens, k_chunks],
243
+ outputs=[chatbot, user_box],
244
+ )
245
+ btn_clear.click(on_clear, outputs=[chatbot, user_box])
246
+
247
+ # ===== Coluna lateral (controle do índice e dicas) =====
248
+ with gr.Column(scale=2):
249
+ gr.Markdown("### Controlo do índice")
250
+ gr.Markdown(f"PDF atual(DFSORT Application Programming Guide)): `{PDF_PATH}`")
251
+ btn_rebuild = gr.Button("Reconstruir índice a partir do PDF")
252
+ msg = gr.Markdown()
253
+ btn_rebuild.click(lambda: rebuild_index_action(), [], [msg])
254
+
255
+ gr.Markdown("---")
256
+ gr.Markdown("### Dicas de consulta")
257
+ gr.Markdown(
258
+ "- Ex.: `Ordenar por 10 bytes a partir da posição 1 (CH, A).`\n"
259
+ "- Ex.: `Como faço para eliminar duplicados com SUM FIELDS=NONE?`\n"
260
+ "- Ex.: `JOINKEYS: explique o uso de REFORMAT.`\n"
261
+ "- Ex.: `Exemplo de OUTFIL com cabeçalho e REMOVECC.`"
262
+ )
263
 
264
  if __name__ == "__main__":
265
+ # cria índice na primeira execução (se não existir)
266
  if not Path(INDEX_FILE).exists() or not Path(CHUNKS_FILE).exists():
267
  print("[i] Construindo índice a partir do PDF…")
268
  faiss_index, pdf_chunks = build_or_load_index(PDF_PATH, INDEX_FILE, CHUNKS_FILE)
269
  print("[i] Índice criado.")
270
+ demo.launch(server_name="0.0.0.0", server_port=7860)