Update backend/main.py
Browse files- backend/main.py +34 -49
backend/main.py
CHANGED
|
@@ -25,7 +25,6 @@ load_dotenv()
|
|
| 25 |
# --- 1. Inicialização e Configuração ---
|
| 26 |
app = FastAPI()
|
| 27 |
|
| 28 |
-
# Configuração do CORS
|
| 29 |
app.add_middleware(
|
| 30 |
CORSMiddleware,
|
| 31 |
allow_origins=["*"],
|
|
@@ -34,84 +33,74 @@ app.add_middleware(
|
|
| 34 |
allow_headers=["*"],
|
| 35 |
)
|
| 36 |
|
| 37 |
-
# --- 2.
|
| 38 |
-
# Define o modelo de embedding do Hugging Face (leve para CPU)
|
| 39 |
HF_EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
|
| 40 |
|
| 41 |
-
#
|
| 42 |
-
|
| 43 |
-
model
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
# Inicializa o HuggingFaceEmbeddings na CPU
|
| 46 |
embeddings = HuggingFaceEmbeddings(
|
| 47 |
model_name=HF_EMBEDDING_MODEL,
|
| 48 |
model_kwargs={'device': 'cpu'}
|
| 49 |
)
|
| 50 |
|
| 51 |
-
# Prompt
|
| 52 |
rag_system_prompt = (
|
| 53 |
-
"Você é um assistente
|
| 54 |
-
"
|
| 55 |
-
"
|
| 56 |
-
"Se
|
| 57 |
-
"\n\nCONTEXTO
|
| 58 |
)
|
|
|
|
| 59 |
rag_prompt = ChatPromptTemplate.from_messages(
|
| 60 |
[("system", rag_system_prompt), ("human", "{input}")]
|
| 61 |
)
|
| 62 |
|
| 63 |
-
# A chain será inicializada com o endpoint de upload
|
| 64 |
rag_chain = None
|
| 65 |
|
| 66 |
-
|
| 67 |
# --- 3. Pydantic Model ---
|
| 68 |
class ChatRequest(BaseModel):
|
| 69 |
content: str
|
| 70 |
|
| 71 |
-
|
| 72 |
-
# --- 4. Funções de RAG (Helper Functions) ---
|
| 73 |
-
|
| 74 |
def format_docs(docs):
|
| 75 |
-
"""Formata os documentos recuperados em uma string única para injeção no prompt."""
|
| 76 |
return "\n\n---\n\n".join(
|
| 77 |
-
f"Conteúdo: {doc.page_content}\n(
|
| 78 |
for doc in docs
|
| 79 |
)
|
| 80 |
|
| 81 |
-
|
| 82 |
-
# --- 5. Endpoints da API ---
|
| 83 |
|
| 84 |
@app.post("/upload-document")
|
| 85 |
async def upload_document(file: UploadFile = File(...)):
|
| 86 |
-
"""
|
| 87 |
-
Este endpoint recebe um arquivo, processa-o e inicializa o vetorstore e o retriever.
|
| 88 |
-
"""
|
| 89 |
global rag_chain
|
| 90 |
|
| 91 |
if file.content_type != "application/pdf":
|
| 92 |
-
raise HTTPException(status_code=400, detail="Apenas arquivos PDF são suportados
|
| 93 |
|
| 94 |
-
# 1. Salvar o arquivo temporariamente para o Loader poder ler
|
| 95 |
try:
|
| 96 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
|
| 97 |
content = await file.read()
|
| 98 |
tmp_file.write(content)
|
| 99 |
temp_path = tmp_file.name
|
| 100 |
|
| 101 |
-
# 2. Carregar o documento
|
| 102 |
loader = PyPDFLoader(temp_path)
|
| 103 |
docs = loader.load()
|
| 104 |
|
| 105 |
-
# 3
|
| 106 |
-
text_splitter = RecursiveCharacterTextSplitter(chunk_size=
|
| 107 |
splits = text_splitter.split_documents(docs)
|
| 108 |
|
| 109 |
-
# 4. Vetorizar e Armazenar (Vector Store)
|
| 110 |
vectorstore = FAISS.from_documents(documents=splits, embedding=embeddings)
|
| 111 |
-
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
|
| 112 |
|
| 113 |
-
#
|
| 114 |
-
|
|
|
|
|
|
|
| 115 |
rag_chain = (
|
| 116 |
RunnablePassthrough.assign(
|
| 117 |
context=(lambda x: x["input"]) | retriever | format_docs
|
|
@@ -121,44 +110,40 @@ async def upload_document(file: UploadFile = File(...)):
|
|
| 121 |
| StrOutputParser()
|
| 122 |
)
|
| 123 |
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
except Exception as e:
|
| 127 |
-
print(f"Erro
|
| 128 |
-
|
| 129 |
-
raise HTTPException(status_code=500, detail=f"Falha ao processar o arquivo: {e}")
|
| 130 |
finally:
|
| 131 |
-
# Limpeza: deletar o arquivo temporário
|
| 132 |
if 'temp_path' in locals() and os.path.exists(temp_path):
|
| 133 |
os.remove(temp_path)
|
| 134 |
|
| 135 |
-
|
| 136 |
@app.post("/chat")
|
| 137 |
async def chat(request: ChatRequest):
|
| 138 |
-
"""
|
| 139 |
-
Endpoint de chat que usa o RAG (se inicializado) ou o modelo base.
|
| 140 |
-
"""
|
| 141 |
current_chain = rag_chain
|
| 142 |
|
| 143 |
if current_chain is None:
|
| 144 |
-
# Fallback para a chain original (apenas prompt/sem contexto)
|
| 145 |
base_prompt = ChatPromptTemplate.from_messages(
|
| 146 |
-
[("system", "Você é um assistente
|
| 147 |
)
|
| 148 |
current_chain = base_prompt | model | StrOutputParser()
|
| 149 |
|
| 150 |
-
# Função geradora que produz os pedaços (chunks) da resposta
|
| 151 |
async def stream_generator():
|
| 152 |
try:
|
| 153 |
-
# 'astream' é o método de streaming assíncrono do LangChain
|
| 154 |
async for chunk in current_chain.astream({"input": request.content}):
|
| 155 |
if chunk:
|
| 156 |
yield chunk
|
| 157 |
except Exception as e:
|
| 158 |
-
print(f"Erro
|
| 159 |
yield f"Erro no serviço de IA: {e}"
|
| 160 |
|
| 161 |
-
# Retorna uma resposta de streaming
|
| 162 |
return StreamingResponse(stream_generator(), media_type="text/plain")
|
| 163 |
|
| 164 |
#--- END OF FILE main (1).py ---
|
|
|
|
| 25 |
# --- 1. Inicialização e Configuração ---
|
| 26 |
app = FastAPI()
|
| 27 |
|
|
|
|
| 28 |
app.add_middleware(
|
| 29 |
CORSMiddleware,
|
| 30 |
allow_origins=["*"],
|
|
|
|
| 33 |
allow_headers=["*"],
|
| 34 |
)
|
| 35 |
|
| 36 |
+
# --- 2. Configurações de IA Otimizadas (Baseado no app 26) ---
|
|
|
|
| 37 |
HF_EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
|
| 38 |
|
| 39 |
+
# MUDANÇA 1: Temperatura controlada (0.3 é melhor para fatos/RAG)
|
| 40 |
+
model = ChatGroq(
|
| 41 |
+
model=os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile"),
|
| 42 |
+
temperature=0.3
|
| 43 |
+
)
|
| 44 |
|
|
|
|
| 45 |
embeddings = HuggingFaceEmbeddings(
|
| 46 |
model_name=HF_EMBEDDING_MODEL,
|
| 47 |
model_kwargs={'device': 'cpu'}
|
| 48 |
)
|
| 49 |
|
| 50 |
+
# MUDANÇA 2: System Prompt Melhorado (Inspirado no app 26)
|
| 51 |
rag_system_prompt = (
|
| 52 |
+
"Você é um assistente experiente e prestativo. "
|
| 53 |
+
"Sua tarefa é fornecer respostas detalhadas e ricas em contexto com base nas informações fornecidas. "
|
| 54 |
+
"Ao usar o contexto abaixo, sintetize os pontos principais e explique como eles se relacionam. "
|
| 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 |
)
|
| 58 |
+
|
| 59 |
rag_prompt = ChatPromptTemplate.from_messages(
|
| 60 |
[("system", rag_system_prompt), ("human", "{input}")]
|
| 61 |
)
|
| 62 |
|
|
|
|
| 63 |
rag_chain = None
|
| 64 |
|
|
|
|
| 65 |
# --- 3. Pydantic Model ---
|
| 66 |
class ChatRequest(BaseModel):
|
| 67 |
content: str
|
| 68 |
|
| 69 |
+
# --- 4. Helpers ---
|
|
|
|
|
|
|
| 70 |
def format_docs(docs):
|
|
|
|
| 71 |
return "\n\n---\n\n".join(
|
| 72 |
+
f"Conteúdo: {doc.page_content}\n(Página {doc.metadata.get('page', 'N/A')})"
|
| 73 |
for doc in docs
|
| 74 |
)
|
| 75 |
|
| 76 |
+
# --- 5. Endpoints ---
|
|
|
|
| 77 |
|
| 78 |
@app.post("/upload-document")
|
| 79 |
async def upload_document(file: UploadFile = File(...)):
|
|
|
|
|
|
|
|
|
|
| 80 |
global rag_chain
|
| 81 |
|
| 82 |
if file.content_type != "application/pdf":
|
| 83 |
+
raise HTTPException(status_code=400, detail="Apenas arquivos PDF são suportados.")
|
| 84 |
|
|
|
|
| 85 |
try:
|
| 86 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
|
| 87 |
content = await file.read()
|
| 88 |
tmp_file.write(content)
|
| 89 |
temp_path = tmp_file.name
|
| 90 |
|
|
|
|
| 91 |
loader = PyPDFLoader(temp_path)
|
| 92 |
docs = loader.load()
|
| 93 |
|
| 94 |
+
# MUDANÇA 3: Chunking um pouco maior para pegar mais contexto
|
| 95 |
+
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1200, chunk_overlap=200)
|
| 96 |
splits = text_splitter.split_documents(docs)
|
| 97 |
|
|
|
|
| 98 |
vectorstore = FAISS.from_documents(documents=splits, embedding=embeddings)
|
|
|
|
| 99 |
|
| 100 |
+
# MUDANÇA 4: k=6 (Recupera mais pedaços para o Llama 3.3 analisar)
|
| 101 |
+
retriever = vectorstore.as_retriever(search_kwargs={"k": 6})
|
| 102 |
+
|
| 103 |
+
# Chain com a correção do lambda (dict input)
|
| 104 |
rag_chain = (
|
| 105 |
RunnablePassthrough.assign(
|
| 106 |
context=(lambda x: x["input"]) | retriever | format_docs
|
|
|
|
| 110 |
| StrOutputParser()
|
| 111 |
)
|
| 112 |
|
| 113 |
+
# MUDANÇA 5: Retorno com estatísticas (igual ao app 26)
|
| 114 |
+
return {
|
| 115 |
+
"message": "Processamento concluído!",
|
| 116 |
+
"filename": file.filename,
|
| 117 |
+
"total_pages": len(docs),
|
| 118 |
+
"total_chunks": len(splits)
|
| 119 |
+
}
|
| 120 |
|
| 121 |
except Exception as e:
|
| 122 |
+
print(f"Erro: {e}")
|
| 123 |
+
raise HTTPException(status_code=500, detail=f"Falha ao processar: {e}")
|
|
|
|
| 124 |
finally:
|
|
|
|
| 125 |
if 'temp_path' in locals() and os.path.exists(temp_path):
|
| 126 |
os.remove(temp_path)
|
| 127 |
|
|
|
|
| 128 |
@app.post("/chat")
|
| 129 |
async def chat(request: ChatRequest):
|
|
|
|
|
|
|
|
|
|
| 130 |
current_chain = rag_chain
|
| 131 |
|
| 132 |
if current_chain is None:
|
|
|
|
| 133 |
base_prompt = ChatPromptTemplate.from_messages(
|
| 134 |
+
[("system", "Você é um assistente útil. Nenhum documento foi carregado ainda."), ("human", "{input}")]
|
| 135 |
)
|
| 136 |
current_chain = base_prompt | model | StrOutputParser()
|
| 137 |
|
|
|
|
| 138 |
async def stream_generator():
|
| 139 |
try:
|
|
|
|
| 140 |
async for chunk in current_chain.astream({"input": request.content}):
|
| 141 |
if chunk:
|
| 142 |
yield chunk
|
| 143 |
except Exception as e:
|
| 144 |
+
print(f"Erro stream: {e}")
|
| 145 |
yield f"Erro no serviço de IA: {e}"
|
| 146 |
|
|
|
|
| 147 |
return StreamingResponse(stream_generator(), media_type="text/plain")
|
| 148 |
|
| 149 |
#--- END OF FILE main (1).py ---
|