Update app.py
Browse files
app.py
CHANGED
|
@@ -1,273 +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=
|
|
|
|
| 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=7860)
|