Geoeasy commited on
Commit
15ca720
·
verified ·
1 Parent(s): bd14dc0

Upload 5 files

Browse files
Files changed (6) hide show
  1. .gitattributes +2 -0
  2. app.py +273 -0
  3. ice2ca11.pdf +3 -0
  4. r_chunks.npy +3 -0
  5. r_docs.index +3 -0
  6. requirements.txt +17 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ ice2ca11.pdf filter=lfs diff=lfs merge=lfs -text
37
+ r_docs.index filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import re
4
+ import time
5
+ from pathlib import Path
6
+ from typing import List, Tuple
7
+
8
+ 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:
61
+ raise RuntimeError("🔒 NV_API_KEY não definido. Configure em Settings → Variables & Secrets.")
62
+
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
+ """Lê 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():
79
+ raise FileNotFoundError(f"PDF não encontrado: {pdf_path}")
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")
87
+ raw_pages = re.split(r"\f|\n\s*\n", text)
88
+ else:
89
+ reader = PdfReader(str(path))
90
+ for pg in reader.pages:
91
+ try:
92
+ raw = pg.extract_text() or ""
93
+ except Exception:
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:
115
+ if buf:
116
+ chunks.append("\n".join(buf))
117
+ buf = [b]
118
+ size = len(b)
119
+ else:
120
+ buf.append(b)
121
+ size += len(b) + 1
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]
141
+ index = faiss.IndexFlatIP(d)
142
+ index.add(emb)
143
+ faiss.write_index(index, index_path)
144
+ np.save(chunks_path, np.array(chunks_list, dtype=object))
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:
179
+ return (
180
+ "Você é um assistente especializado em DFSORT (IBM z/OS).\n"
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():
213
+ global faiss_index, pdf_chunks
214
+ faiss_index, pdf_chunks = build_or_load_index(PDF_PATH, INDEX_FILE, CHUNKS_FILE)
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=7861)
ice2ca11.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a6601d98cbfc2ebe917d8758fdf7c24e4d1a59e0e2b8ff27707e470624995031
3
+ size 6202478
r_chunks.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bfde06a3044c293d72c60301f7e49a40ec575289316250d23751a7f4860a0103
3
+ size 2060551
r_docs.index ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2052db52f49efd135545d64366b9ee74df937fc8f2c2449bfc8a8ff0d8a24f39
3
+ size 1446957
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Núcleo científico
2
+ numpy>=1.24
3
+
4
+ # FAISS (indexação vetorial)
5
+ faiss-cpu>=1.7.4
6
+
7
+ # Interface web
8
+ gradio>=4.0
9
+
10
+ # Leitura de PDF
11
+ pypdf>=4.0
12
+
13
+ # Embeddings
14
+ sentence-transformers>=2.2.2
15
+
16
+ # Cliente OpenAI (compatível com API da NVIDIA)
17
+ openai>=1.0.0