Update app.py
Browse files
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 |
-
#
|
| 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
|
| 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 |
-
•
|
| 28 |
-
|
| 29 |
-
•
|
| 30 |
-
•
|
| 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
|
| 53 |
-
PDF_PATH
|
| 54 |
-
INDEX_FILE
|
| 55 |
CHUNKS_FILE = "r_chunks.npy"
|
| 56 |
|
| 57 |
-
# Modelo de chat
|
| 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
|
| 67 |
embedding_model = SentenceTransformer(EMB_MODEL_NAME)
|
| 68 |
|
| 69 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
def _pdf_to_text_chunks(pdf_path: str, max_chunk_chars: int = 1200) -> List[str]:
|
| 72 |
-
"""
|
| 73 |
-
- Divide por páginas
|
| 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
|
| 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 |
-
#
|
| 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
|
| 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 +
|
| 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
|
| 161 |
-
|
| 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=
|
| 170 |
)
|
| 171 |
-
|
| 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 |
-
"
|
| 185 |
)
|
| 186 |
|
| 187 |
|
| 188 |
-
# =====================
|
| 189 |
|
| 190 |
-
def
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
sys_msg = {"role": "system", "content": make_system_prompt(ctx)}
|
| 200 |
-
usr_msg = {"role": "user", "content":
|
| 201 |
|
| 202 |
-
# streaming para UX fluida
|
| 203 |
try:
|
| 204 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 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 |
-
|
|
|
|
| 227 |
"""
|
| 228 |
|
| 229 |
with gr.Blocks(title=APP_TITLE, css=custom_css, theme=gr.themes.Base()) as demo:
|
| 230 |
-
gr.
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
with gr.
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 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)
|