Update backend/main.py
Browse files- backend/main.py +27 -25
backend/main.py
CHANGED
|
@@ -12,7 +12,8 @@ from langchain_groq import ChatGroq
|
|
| 12 |
from fastapi.responses import StreamingResponse
|
| 13 |
|
| 14 |
# RAG Imports
|
| 15 |
-
|
|
|
|
| 16 |
from langchain_community.embeddings import HuggingFaceEmbeddings
|
| 17 |
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 18 |
from langchain_community.vectorstores import FAISS
|
|
@@ -35,7 +36,7 @@ app.add_middleware(
|
|
| 35 |
# --- 2. Configurações de IA ---
|
| 36 |
HF_EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
|
| 37 |
|
| 38 |
-
#
|
| 39 |
model = ChatGroq(
|
| 40 |
model="llama-3.3-70b-versatile",
|
| 41 |
temperature=0.3
|
|
@@ -50,6 +51,7 @@ rag_system_prompt = (
|
|
| 50 |
"Você é um assistente experiente e prestativo. "
|
| 51 |
"Sua tarefa é fornecer respostas detalhadas e ricas em contexto com base nas informações fornecidas. "
|
| 52 |
"Ao usar o contexto abaixo, sintetize os pontos principais e explique como eles se relacionam. "
|
|
|
|
| 53 |
"Se a resposta não estiver no contexto, diga honestamente que não sabe, não invente informações."
|
| 54 |
"\n\nCONTEXTO DO DOCUMENTO:\n{context}"
|
| 55 |
)
|
|
@@ -58,9 +60,8 @@ rag_prompt = ChatPromptTemplate.from_messages(
|
|
| 58 |
[("system", rag_system_prompt), ("human", "{input}")]
|
| 59 |
)
|
| 60 |
|
| 61 |
-
# Variáveis globais para armazenar a "inteligência" do RAG
|
| 62 |
rag_chain = None
|
| 63 |
-
global_retriever = None
|
| 64 |
|
| 65 |
# --- 3. Modelo de Dados ---
|
| 66 |
class ChatRequest(BaseModel):
|
|
@@ -69,7 +70,7 @@ class ChatRequest(BaseModel):
|
|
| 69 |
# --- 4. Helpers ---
|
| 70 |
def format_docs(docs):
|
| 71 |
return "\n\n---\n\n".join(
|
| 72 |
-
f"📄 Conteúdo: {doc.page_content}\n(🔖 Fonte:
|
| 73 |
for doc in docs
|
| 74 |
)
|
| 75 |
|
|
@@ -79,34 +80,46 @@ def format_docs(docs):
|
|
| 79 |
async def upload_document(file: UploadFile = File(...)):
|
| 80 |
global rag_chain, global_retriever
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
try:
|
| 86 |
-
|
|
|
|
|
|
|
| 87 |
content = await file.read()
|
| 88 |
tmp_file.write(content)
|
| 89 |
temp_path = tmp_file.name
|
| 90 |
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
docs = loader.load()
|
| 93 |
|
|
|
|
|
|
|
| 94 |
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1200, chunk_overlap=200)
|
| 95 |
splits = text_splitter.split_documents(docs)
|
| 96 |
|
|
|
|
| 97 |
vectorstore = FAISS.from_documents(documents=splits, embedding=embeddings)
|
| 98 |
-
|
| 99 |
-
# MUDANÇA: Salvamos o retriever globalmente
|
| 100 |
global_retriever = vectorstore.as_retriever(search_kwargs={"k": 6})
|
| 101 |
|
| 102 |
-
# A chain agora é simples, pois faremos a recuperação manual no endpoint chat
|
| 103 |
rag_chain = rag_prompt | model | StrOutputParser()
|
| 104 |
|
| 105 |
return {
|
| 106 |
"message": "Processamento concluído!",
|
| 107 |
"filename": file.filename,
|
| 108 |
-
"
|
| 109 |
-
"
|
| 110 |
}
|
| 111 |
|
| 112 |
except Exception as e:
|
|
@@ -118,44 +131,33 @@ async def upload_document(file: UploadFile = File(...)):
|
|
| 118 |
|
| 119 |
@app.post("/chat")
|
| 120 |
async def chat(request: ChatRequest):
|
| 121 |
-
"""
|
| 122 |
-
Endpoint de chat com Auditoria (envia contexto ao final)
|
| 123 |
-
"""
|
| 124 |
current_chain = rag_chain
|
| 125 |
context_str = ""
|
| 126 |
-
docs_source = []
|
| 127 |
|
| 128 |
-
# 1. Recuperação Manual de Contexto (Se o RAG estiver ativo)
|
| 129 |
if global_retriever:
|
| 130 |
try:
|
| 131 |
-
# Busca os documentos relevantes
|
| 132 |
docs_source = global_retriever.invoke(request.content)
|
| 133 |
context_str = format_docs(docs_source)
|
| 134 |
except Exception as e:
|
| 135 |
print(f"Erro na recuperação: {e}")
|
| 136 |
context_str = "Erro ao recuperar contexto."
|
| 137 |
else:
|
| 138 |
-
# Fallback se não houver PDF
|
| 139 |
current_chain = (
|
| 140 |
ChatPromptTemplate.from_messages([("system", "Você é um assistente útil."), ("human", "{input}")])
|
| 141 |
| model
|
| 142 |
| StrOutputParser()
|
| 143 |
)
|
| 144 |
|
| 145 |
-
# 2. Gerador de Streaming com "Payload Oculto"
|
| 146 |
async def stream_generator():
|
| 147 |
try:
|
| 148 |
-
# Passa o contexto manualmente para o prompt
|
| 149 |
input_data = {"input": request.content}
|
| 150 |
if context_str:
|
| 151 |
input_data["context"] = context_str
|
| 152 |
|
| 153 |
-
# Stream da resposta da IA
|
| 154 |
async for chunk in current_chain.astream(input_data):
|
| 155 |
if chunk:
|
| 156 |
yield chunk
|
| 157 |
|
| 158 |
-
# MUDANÇA: Ao final, enviamos um separador e o contexto para auditoria
|
| 159 |
if context_str:
|
| 160 |
debug_data = f"\n\n###__DEBUG__###\n**Auditoria de Contexto (RAG):**\n\n{context_str}"
|
| 161 |
yield debug_data
|
|
|
|
| 12 |
from fastapi.responses import StreamingResponse
|
| 13 |
|
| 14 |
# RAG Imports
|
| 15 |
+
# MUDANÇA: Adicionado TextLoader para arquivos de texto
|
| 16 |
+
from langchain_community.document_loaders import PyPDFLoader, TextLoader
|
| 17 |
from langchain_community.embeddings import HuggingFaceEmbeddings
|
| 18 |
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 19 |
from langchain_community.vectorstores import FAISS
|
|
|
|
| 36 |
# --- 2. Configurações de IA ---
|
| 37 |
HF_EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
|
| 38 |
|
| 39 |
+
# Modelo Groq
|
| 40 |
model = ChatGroq(
|
| 41 |
model="llama-3.3-70b-versatile",
|
| 42 |
temperature=0.3
|
|
|
|
| 51 |
"Você é um assistente experiente e prestativo. "
|
| 52 |
"Sua tarefa é fornecer respostas detalhadas e ricas em contexto com base nas informações fornecidas. "
|
| 53 |
"Ao usar o contexto abaixo, sintetize os pontos principais e explique como eles se relacionam. "
|
| 54 |
+
"Se o contexto for código (Python/MD), explique o funcionamento ou use como referência."
|
| 55 |
"Se a resposta não estiver no contexto, diga honestamente que não sabe, não invente informações."
|
| 56 |
"\n\nCONTEXTO DO DOCUMENTO:\n{context}"
|
| 57 |
)
|
|
|
|
| 60 |
[("system", rag_system_prompt), ("human", "{input}")]
|
| 61 |
)
|
| 62 |
|
|
|
|
| 63 |
rag_chain = None
|
| 64 |
+
global_retriever = None
|
| 65 |
|
| 66 |
# --- 3. Modelo de Dados ---
|
| 67 |
class ChatRequest(BaseModel):
|
|
|
|
| 70 |
# --- 4. Helpers ---
|
| 71 |
def format_docs(docs):
|
| 72 |
return "\n\n---\n\n".join(
|
| 73 |
+
f"📄 Conteúdo: {doc.page_content}\n(🔖 Fonte: {doc.metadata.get('source', 'Desconhecida')})"
|
| 74 |
for doc in docs
|
| 75 |
)
|
| 76 |
|
|
|
|
| 80 |
async def upload_document(file: UploadFile = File(...)):
|
| 81 |
global rag_chain, global_retriever
|
| 82 |
|
| 83 |
+
# 1. Validação de extensão
|
| 84 |
+
filename = file.filename.lower()
|
| 85 |
+
allowed_extensions = [".pdf", ".txt", ".md", ".py"]
|
| 86 |
+
|
| 87 |
+
if not any(filename.endswith(ext) for ext in allowed_extensions):
|
| 88 |
+
raise HTTPException(status_code=400, detail=f"Extensão não suportada. Use: {allowed_extensions}")
|
| 89 |
|
| 90 |
try:
|
| 91 |
+
# Salva arquivo temporário com a extensão correta (importante para o Loader)
|
| 92 |
+
file_ext = os.path.splitext(filename)[1]
|
| 93 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=file_ext) as tmp_file:
|
| 94 |
content = await file.read()
|
| 95 |
tmp_file.write(content)
|
| 96 |
temp_path = tmp_file.name
|
| 97 |
|
| 98 |
+
# 2. Seleção do Loader baseado na extensão
|
| 99 |
+
if filename.endswith(".pdf"):
|
| 100 |
+
loader = PyPDFLoader(temp_path)
|
| 101 |
+
else:
|
| 102 |
+
# Para .txt, .md, .py usamos o TextLoader com UTF-8
|
| 103 |
+
loader = TextLoader(temp_path, encoding="utf-8")
|
| 104 |
+
|
| 105 |
docs = loader.load()
|
| 106 |
|
| 107 |
+
# 3. Chunking
|
| 108 |
+
# Se for código (.py), talvez chunks menores sejam melhores, mas manteremos o padrão por enquanto
|
| 109 |
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1200, chunk_overlap=200)
|
| 110 |
splits = text_splitter.split_documents(docs)
|
| 111 |
|
| 112 |
+
# 4. Vetorização
|
| 113 |
vectorstore = FAISS.from_documents(documents=splits, embedding=embeddings)
|
|
|
|
|
|
|
| 114 |
global_retriever = vectorstore.as_retriever(search_kwargs={"k": 6})
|
| 115 |
|
|
|
|
| 116 |
rag_chain = rag_prompt | model | StrOutputParser()
|
| 117 |
|
| 118 |
return {
|
| 119 |
"message": "Processamento concluído!",
|
| 120 |
"filename": file.filename,
|
| 121 |
+
"total_chunks": len(splits),
|
| 122 |
+
"type": file_ext
|
| 123 |
}
|
| 124 |
|
| 125 |
except Exception as e:
|
|
|
|
| 131 |
|
| 132 |
@app.post("/chat")
|
| 133 |
async def chat(request: ChatRequest):
|
|
|
|
|
|
|
|
|
|
| 134 |
current_chain = rag_chain
|
| 135 |
context_str = ""
|
|
|
|
| 136 |
|
|
|
|
| 137 |
if global_retriever:
|
| 138 |
try:
|
|
|
|
| 139 |
docs_source = global_retriever.invoke(request.content)
|
| 140 |
context_str = format_docs(docs_source)
|
| 141 |
except Exception as e:
|
| 142 |
print(f"Erro na recuperação: {e}")
|
| 143 |
context_str = "Erro ao recuperar contexto."
|
| 144 |
else:
|
|
|
|
| 145 |
current_chain = (
|
| 146 |
ChatPromptTemplate.from_messages([("system", "Você é um assistente útil."), ("human", "{input}")])
|
| 147 |
| model
|
| 148 |
| StrOutputParser()
|
| 149 |
)
|
| 150 |
|
|
|
|
| 151 |
async def stream_generator():
|
| 152 |
try:
|
|
|
|
| 153 |
input_data = {"input": request.content}
|
| 154 |
if context_str:
|
| 155 |
input_data["context"] = context_str
|
| 156 |
|
|
|
|
| 157 |
async for chunk in current_chain.astream(input_data):
|
| 158 |
if chunk:
|
| 159 |
yield chunk
|
| 160 |
|
|
|
|
| 161 |
if context_str:
|
| 162 |
debug_data = f"\n\n###__DEBUG__###\n**Auditoria de Contexto (RAG):**\n\n{context_str}"
|
| 163 |
yield debug_data
|