Usa o modelo multilingual, ignora trechos curtos e o template do Llama 3.2 para a resposta sair em uma frase completa. O chat deixa a API livre durante a geração, o upload indexa o PDF e os agendamentos não são gravados duas vezes.
184 lines
6.3 KiB
Python
184 lines
6.3 KiB
Python
from qdrant_client import QdrantClient
|
|
from qdrant_client.models import Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue
|
|
import numpy as np
|
|
from typing import List, Dict, Optional
|
|
import json
|
|
import os
|
|
import time
|
|
import uuid
|
|
|
|
class QdrantService:
|
|
def __init__(self, url: str, collection_name: str = "espiritismo_books"):
|
|
self.client = QdrantClient(url=url, prefer_grpc=False)
|
|
self.collection_name = collection_name
|
|
self.embedding_dim = 384
|
|
|
|
self._ensure_collection()
|
|
|
|
def _ensure_collection(self):
|
|
"""Create collection if it doesn't exist."""
|
|
last_error = None
|
|
for attempt in range(15):
|
|
try:
|
|
collections = self.client.get_collections().collections
|
|
break
|
|
except Exception as exc:
|
|
last_error = exc
|
|
print(f"Qdrant indisponível ({attempt + 1}/15): {exc}")
|
|
time.sleep(2)
|
|
else:
|
|
raise RuntimeError(f"Não foi possível conectar ao Qdrant: {last_error}")
|
|
|
|
collection_exists = any(
|
|
c.name == self.collection_name for c in collections
|
|
)
|
|
|
|
if not collection_exists:
|
|
self.client.create_collection(
|
|
collection_name=self.collection_name,
|
|
vectors_config=VectorParams(
|
|
size=self.embedding_dim,
|
|
distance=Distance.COSINE,
|
|
),
|
|
)
|
|
print(f"Created collection: {self.collection_name}")
|
|
else:
|
|
print(f"Collection {self.collection_name} already exists")
|
|
|
|
def add_chunks(self, chunks: List[Dict]):
|
|
"""Add chunks to the collection."""
|
|
points = [
|
|
PointStruct(
|
|
id=str(chunk.get("id") or uuid.uuid4()),
|
|
vector=chunk["embedding"],
|
|
payload={
|
|
"content": chunk["content"],
|
|
"metadata": chunk["metadata"],
|
|
},
|
|
)
|
|
for chunk in chunks
|
|
]
|
|
|
|
for start in range(0, len(points), 128):
|
|
batch = points[start:start + 128]
|
|
self.client.upsert(collection_name=self.collection_name, points=batch)
|
|
if points:
|
|
print(f"Added {len(points)} chunks to Qdrant")
|
|
|
|
def search(self, query_embedding, top_k: int = 3) -> List[Dict]:
|
|
"""Search for similar chunks."""
|
|
if hasattr(query_embedding, "tolist"):
|
|
query_embedding = query_embedding.tolist()
|
|
results = self.client.search(
|
|
collection_name=self.collection_name,
|
|
query_vector=query_embedding,
|
|
limit=top_k,
|
|
)
|
|
|
|
return [
|
|
{
|
|
"content": hit.payload["content"],
|
|
"metadata": hit.payload["metadata"],
|
|
"score": hit.score,
|
|
}
|
|
for hit in results
|
|
]
|
|
|
|
def get_books(self) -> List[str]:
|
|
"""Get list of books in the collection."""
|
|
books = set()
|
|
offset = None
|
|
while True:
|
|
points, offset = self.client.scroll(
|
|
collection_name=self.collection_name,
|
|
limit=256,
|
|
offset=offset,
|
|
with_vectors=False,
|
|
)
|
|
for point in points:
|
|
metadata = (point.payload or {}).get("metadata") or {}
|
|
books.add(metadata.get("title", "Unknown"))
|
|
if offset is None:
|
|
break
|
|
|
|
return sorted(books)
|
|
|
|
def get_collection_info(self) -> Dict:
|
|
"""Get information about the collection."""
|
|
info = self.client.get_collection(self.collection_name)
|
|
return {
|
|
"status": info.status,
|
|
"vectors_count": info.vectors_count,
|
|
"points_count": info.points_count,
|
|
}
|
|
|
|
|
|
class RAGService:
|
|
def __init__(self, qdrant_service: QdrantService, model_name: str = "llama-3.2-3b-instruct-q4_0.gguf"):
|
|
self.qdrant = qdrant_service
|
|
self.model_name = model_name
|
|
self.llm = None
|
|
|
|
def _get_llm(self):
|
|
if self.llm is None:
|
|
if not os.path.isfile(self.model_name):
|
|
raise FileNotFoundError(
|
|
f"Modelo não encontrado em {self.model_name}. "
|
|
"Baixe llama-3.2-3b-instruct-q4_0.gguf para backend/models/."
|
|
)
|
|
from gpt4all import GPT4All
|
|
|
|
model_dir = os.path.dirname(self.model_name) or "."
|
|
model_file = os.path.basename(self.model_name)
|
|
self.llm = GPT4All(
|
|
model_name=model_file,
|
|
model_path=model_dir,
|
|
device="cpu",
|
|
allow_download=False,
|
|
)
|
|
return self.llm
|
|
|
|
def generate_response(self, query: str, context: List[str], top_k: int = 3) -> str:
|
|
"""Generate a response based on the query and context."""
|
|
context_text = "\n\n".join(context)
|
|
|
|
system_prompt = f"""Você responde perguntas sobre espiritismo usando apenas o contexto.
|
|
Escreva uma ou duas frases completas, em português. Não repita a pergunta nem as instruções.
|
|
Se o contexto não bastar, responda apenas: Não encontrei essa informação nos livros indexados.
|
|
|
|
Contexto:
|
|
{context_text}"""
|
|
template = (
|
|
"<|start_header_id|>user<|end_header_id|>\n\n"
|
|
"{0}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n"
|
|
)
|
|
with self._get_llm().chat_session(system_prompt=system_prompt, prompt_template=template):
|
|
response = self.llm.generate(query, max_tokens=160, temp=0.1)
|
|
return response.strip()
|
|
|
|
def query(self, query: str, top_k: int = 3) -> Dict:
|
|
"""Query the RAG system."""
|
|
# Get embeddings
|
|
from app.utils.embeddings import EmbeddingService
|
|
embedding_service = EmbeddingService()
|
|
|
|
query_embedding = embedding_service.embed_query(query)
|
|
|
|
# Search
|
|
chunks = self.qdrant.search(query_embedding, top_k)
|
|
|
|
# Generate response
|
|
context = [chunk["content"] for chunk in chunks]
|
|
|
|
response = self.generate_response(query, context, top_k)
|
|
|
|
return {
|
|
"query": query,
|
|
"response": response,
|
|
"context": context,
|
|
"sources": [
|
|
{"metadata": chunk["metadata"], "score": chunk["score"]}
|
|
for chunk in chunks
|
|
],
|
|
}
|