fix: make docker compose build and start the API
The image context was backend/, so COPY frontend/ failed. Build from the repo root, place Python modules on the import path, and pin packages that exist on PyPI. The web process no longer loads the LLM at startup.
This commit is contained in:
1 parent
67739dcf7e
commit
7af7ba530d
19 files changed
+342
-250
No files matched your search
+111
-101
@@ -1,24 +1,37 @@
|
||||
from fastapi import FastAPI, File, UploadFile, HTTPException
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.services.rag_service import RAGService
|
||||
from app.services.bot_service import BotService
|
||||
from app.utils.schedule_manager import ScheduleManager
|
||||
from app.models import ChatRequest, ChatResponse, BookInfo, UploadResponse, ScheduleInfo, ScheduleRequest
|
||||
import os
|
||||
import json
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
# Initialize services
|
||||
qdrant_service = QdrantService(os.getenv("QDRANT_URL", "http://qdrant:6333"))
|
||||
rag_service = RAGService(qdrant_service, model_name=os.getenv("GPT4ALL_MODEL", "llama-3.2-3b-instruct-q4_0.gguf"))
|
||||
from fastapi import FastAPI, File, HTTPException, UploadFile
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app.models import (
|
||||
BookInfo,
|
||||
ChatRequest,
|
||||
ChatResponse,
|
||||
ScheduleInfo,
|
||||
ScheduleRequest,
|
||||
UploadResponse,
|
||||
)
|
||||
from app.services.bot_service import BotService
|
||||
from app.services.rag_service import QdrantService, RAGService
|
||||
from app.utils.schedule_manager import ScheduleManager
|
||||
|
||||
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
SCHEDULES_PATH = os.getenv("SCHEDULES_PATH", "/app/schedules.json")
|
||||
UPLOAD_DIR = os.getenv("UPLOAD_DIR", "/app/uploads")
|
||||
QDRANT_URL = os.getenv("QDRANT_URL", "http://qdrant:6333")
|
||||
MODEL_PATH = os.getenv("GPT4ALL_MODEL", "/app/models/llama-3.2-3b-instruct-q4_0.gguf")
|
||||
|
||||
qdrant_service: QdrantService | None = None
|
||||
rag_service: RAGService | None = None
|
||||
bot_service = BotService(os.getenv("TELEGRAM_BOT_TOKEN", ""), os.getenv("TELEGRAM_CHAT_ID", ""))
|
||||
schedule_manager = ScheduleManager(os.getenv("SCHEDULES_PATH", "/app/schedules.json"))
|
||||
schedule_manager = ScheduleManager(SCHEDULES_PATH)
|
||||
|
||||
app = FastAPI(title="Espiritismo RAG API")
|
||||
|
||||
# CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
@@ -27,128 +40,125 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Mount static files
|
||||
app.mount("/static", StaticFiles(directory="../frontend/static"), name="static")
|
||||
app.mount(
|
||||
"/static",
|
||||
StaticFiles(directory=os.path.join(BASE_DIR, "frontend", "static")),
|
||||
name="static",
|
||||
)
|
||||
|
||||
|
||||
def get_qdrant() -> QdrantService:
|
||||
global qdrant_service
|
||||
if qdrant_service is None:
|
||||
qdrant_service = QdrantService(QDRANT_URL)
|
||||
return qdrant_service
|
||||
|
||||
|
||||
def get_rag() -> RAGService:
|
||||
global rag_service
|
||||
if rag_service is None:
|
||||
rag_service = RAGService(get_qdrant(), model_name=MODEL_PATH)
|
||||
return rag_service
|
||||
|
||||
|
||||
# Load existing schedules from file
|
||||
def load_schedules():
|
||||
if os.path.exists("/app/schedules.json"):
|
||||
with open("/app/schedules.json", "r") as f:
|
||||
return json.load(f)
|
||||
if os.path.isfile(SCHEDULES_PATH):
|
||||
with open(SCHEDULES_PATH, "r", encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
return data if isinstance(data, list) else []
|
||||
return []
|
||||
|
||||
|
||||
def save_schedules(schedules: list):
|
||||
parent = os.path.dirname(SCHEDULES_PATH)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
with open(SCHEDULES_PATH, "w", encoding="utf-8") as handle:
|
||||
json.dump(schedules, handle, indent=2)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def index():
|
||||
"""Serve the index page."""
|
||||
with open("../backend/templates/index.html", "r") as f:
|
||||
return f.read()
|
||||
template = os.path.join(BASE_DIR, "templates", "index.html")
|
||||
with open(template, "r", encoding="utf-8") as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
@app.get("/api/books", response_model=List[BookInfo])
|
||||
async def get_books():
|
||||
"""Get list of books in the collection."""
|
||||
books = qdrant_service.get_books()
|
||||
books = get_qdrant().get_books()
|
||||
return [{"title": book, "source": book} for book in books]
|
||||
|
||||
|
||||
@app.post("/api/chat", response_model=ChatResponse)
|
||||
async def chat(request: ChatRequest):
|
||||
"""Query the RAG system."""
|
||||
try:
|
||||
result = rag_service.query(request.message, request.top_k)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
return get_rag().query(request.message, request.top_k)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@app.post("/api/upload", response_model=UploadResponse)
|
||||
async def upload_pdf(file: UploadFile = File(...)):
|
||||
"""Upload a new PDF file."""
|
||||
upload_dir = "/app/uploads"
|
||||
|
||||
if not file.filename.endswith('.pdf'):
|
||||
if not file.filename or not file.filename.endswith(".pdf"):
|
||||
raise HTTPException(status_code=400, detail="Only PDF files are allowed")
|
||||
|
||||
file_path = os.path.join(upload_dir, file.filename)
|
||||
|
||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||
file_path = os.path.join(UPLOAD_DIR, os.path.basename(file.filename))
|
||||
try:
|
||||
with open(file_path, "wb") as f:
|
||||
content = await file.read()
|
||||
f.write(content)
|
||||
|
||||
content = await file.read()
|
||||
with open(file_path, "wb") as handle:
|
||||
handle.write(content)
|
||||
return UploadResponse(message="File uploaded successfully", filename=file.filename)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@app.get("/api/schedules", response_model=List[ScheduleInfo])
|
||||
async def get_schedules():
|
||||
"""Get all schedules."""
|
||||
schedules = load_schedules()
|
||||
return [ScheduleInfo(**s) for s in schedules]
|
||||
return [ScheduleInfo(**item) for item in load_schedules()]
|
||||
|
||||
|
||||
@app.post("/api/schedules", response_model=ScheduleInfo)
|
||||
async def create_schedule(request: ScheduleRequest):
|
||||
"""Create a new schedule."""
|
||||
schedules = load_schedules()
|
||||
new_schedule = {
|
||||
"id": len(schedules) + 1,
|
||||
"time": request.time,
|
||||
"book": request.book,
|
||||
"enabled": request.enabled,
|
||||
}
|
||||
schedules.append(new_schedule)
|
||||
|
||||
with open("/app/schedules.json", "w") as f:
|
||||
json.dump(schedules, f, indent=2)
|
||||
|
||||
# Start celery beat if needed
|
||||
new_schedule = schedule_manager.add_schedule(request.model_dump())
|
||||
if not any(item.get("id") == new_schedule["id"] for item in schedules):
|
||||
schedules.append(new_schedule)
|
||||
save_schedules(schedules)
|
||||
try:
|
||||
bot_service.add_schedule(new_schedule)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not add schedule to bot: {e}")
|
||||
|
||||
except Exception as exc:
|
||||
print(f"Warning: Could not add schedule to bot: {exc}")
|
||||
return ScheduleInfo(**new_schedule)
|
||||
|
||||
|
||||
@app.put("/api/schedules/{schedule_id}", response_model=ScheduleInfo)
|
||||
async def update_schedule(schedule_id: int, request: ScheduleRequest):
|
||||
"""Update an existing schedule."""
|
||||
schedules = load_schedules()
|
||||
updated = schedule_manager.update_schedule(schedule_id, request.model_dump())
|
||||
if updated is None:
|
||||
raise HTTPException(status_code=404, detail="Schedule not found")
|
||||
save_schedules(schedule_manager.list_schedules())
|
||||
try:
|
||||
bot_service.update_schedule(updated)
|
||||
except Exception as exc:
|
||||
print(f"Warning: Could not update schedule in bot: {exc}")
|
||||
return ScheduleInfo(**updated)
|
||||
|
||||
for i, s in enumerate(schedules):
|
||||
if s["id"] == schedule_id:
|
||||
schedules[i] = {
|
||||
"id": schedule_id,
|
||||
"time": request.time,
|
||||
"book": request.book,
|
||||
"enabled": request.enabled,
|
||||
}
|
||||
|
||||
with open("/app/schedules.json", "w") as f:
|
||||
json.dump(schedules, f, indent=2)
|
||||
|
||||
try:
|
||||
bot_service.update_schedule(schedules[i])
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not update schedule in bot: {e}")
|
||||
|
||||
return ScheduleInfo(**schedules[i])
|
||||
|
||||
raise HTTPException(status_code=404, detail="Schedule not found")
|
||||
|
||||
@app.delete("/api/schedules/{schedule_id}")
|
||||
async def delete_schedule(schedule_id: int):
|
||||
"""Delete a schedule."""
|
||||
schedules = load_schedules()
|
||||
|
||||
for i, s in enumerate(schedules):
|
||||
if s["id"] == schedule_id:
|
||||
deleted = schedules.pop(i)
|
||||
|
||||
with open("/app/schedules.json", "w") as f:
|
||||
json.dump(schedules, f, indent=2)
|
||||
|
||||
try:
|
||||
bot_service.remove_schedule(deleted)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not remove schedule from bot: {e}")
|
||||
|
||||
return {"message": "Schedule deleted"}
|
||||
|
||||
raise HTTPException(status_code=404, detail="Schedule not found")
|
||||
current = schedule_manager.get_schedule(schedule_id)
|
||||
if current is None or not schedule_manager.delete_schedule(schedule_id):
|
||||
raise HTTPException(status_code=404, detail="Schedule not found")
|
||||
save_schedules(schedule_manager.list_schedules())
|
||||
try:
|
||||
bot_service.remove_schedule(current)
|
||||
except Exception as exc:
|
||||
print(f"Warning: Could not remove schedule from bot: {exc}")
|
||||
return {"message": "Schedule deleted"}
|
||||
@@ -1,31 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
top_k: int = 3
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
query: str
|
||||
response: str
|
||||
sources: List[Dict]
|
||||
|
||||
class BookInfo(BaseModel):
|
||||
title: str
|
||||
source: str
|
||||
|
||||
class UploadResponse(BaseModel):
|
||||
message: str
|
||||
filename: str
|
||||
|
||||
class ScheduleInfo(BaseModel):
|
||||
id: int
|
||||
time: str
|
||||
book: Optional[str]
|
||||
enabled: bool
|
||||
next_run: Optional[str] = None
|
||||
|
||||
class ScheduleRequest(BaseModel):
|
||||
time: str
|
||||
book: Optional[str] = None
|
||||
enabled: bool = True
|
||||
@@ -1 +1,38 @@
|
||||
# Pydantic models package
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
top_k: int = 3
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
query: str
|
||||
response: str
|
||||
sources: List[Dict]
|
||||
|
||||
|
||||
class BookInfo(BaseModel):
|
||||
title: str
|
||||
source: str
|
||||
|
||||
|
||||
class UploadResponse(BaseModel):
|
||||
message: str
|
||||
filename: str
|
||||
|
||||
|
||||
class ScheduleInfo(BaseModel):
|
||||
id: int
|
||||
time: str
|
||||
book: Optional[str] = None
|
||||
enabled: bool
|
||||
next_run: Optional[str] = None
|
||||
|
||||
|
||||
class ScheduleRequest(BaseModel):
|
||||
time: str
|
||||
book: Optional[str] = None
|
||||
enabled: bool = True
|
||||
@@ -0,0 +1,214 @@
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
from typing import Dict, List
|
||||
|
||||
import telebot
|
||||
|
||||
class BotService:
|
||||
def __init__(self, token: str, chat_id: str):
|
||||
self.chat_id = chat_id
|
||||
self.bot = telebot.TeleBot(token) if token else None
|
||||
if self.bot is not None:
|
||||
self._register_handlers()
|
||||
|
||||
# Store pending schedules
|
||||
self.pending_schedules: List[Dict] = []
|
||||
|
||||
def _register_handlers(self):
|
||||
"""Register message handlers."""
|
||||
|
||||
@self.bot.message_handler(commands=['start', 'help'])
|
||||
def send_welcome(message):
|
||||
self.bot.reply_to(
|
||||
message,
|
||||
"📚 Sistema RAG Espiritismo\n\n"
|
||||
"Comandos disponíveis:\n"
|
||||
"/start - Iniciar\n"
|
||||
"/help - Ajuda\n"
|
||||
"/status - Ver status\n"
|
||||
"/books - Listar livros\n"
|
||||
)
|
||||
|
||||
@self.bot.message_handler(commands=['status'])
|
||||
def send_status(message):
|
||||
status_text = f"""
|
||||
📚 Status do Sistema RAG Espiritismo
|
||||
|
||||
Bot: Ativo
|
||||
Chat ID: {self.chat_id}
|
||||
|
||||
Direcione uma pergunta sobre espiritismo para eu responder!
|
||||
"""
|
||||
self.bot.reply_to(message, status_text)
|
||||
|
||||
@self.bot.message_handler(commands=['books'])
|
||||
def list_books(message):
|
||||
books = [
|
||||
"📖 Livros disponíveis:\n\n",
|
||||
"• A Gênese (Allan Kardec)\n",
|
||||
"• Livro dos Espíritos (Allan Kardec)\n",
|
||||
"• Livro dos Médiums (Allan Kardec)\n",
|
||||
"• O Céu e o Inferno (Manuel Quintão)\n",
|
||||
"• O Evangelho Segundo o Espiritismo (Allan Kardec)\n",
|
||||
"• O Que é o Espiritismo (Allan Kardec)\n"
|
||||
]
|
||||
self.bot.reply_to(message, "".join(books))
|
||||
|
||||
@self.bot.message_handler(func=lambda msg: True)
|
||||
def handle_question(message):
|
||||
"""Handle user questions."""
|
||||
question = message.text
|
||||
|
||||
# Send loading indicator
|
||||
self.bot.reply_to(
|
||||
message,
|
||||
"🔍 Processando sua pergunta..."
|
||||
)
|
||||
|
||||
# Send to RAG service asynchronously
|
||||
self._query_rag_service(question, message.chat.id)
|
||||
|
||||
def _query_rag_service(self, question: str, chat_id: int):
|
||||
"""Query RAG service and send response."""
|
||||
try:
|
||||
from app.services.rag_service import QdrantService, RAGService
|
||||
|
||||
qdrant = QdrantService(os.getenv("QDRANT_URL", "http://qdrant:6333"))
|
||||
rag = RAGService(
|
||||
qdrant,
|
||||
model_name=os.getenv("GPT4ALL_MODEL", "/app/models/llama-3.2-3b-instruct-q4_0.gguf"),
|
||||
)
|
||||
|
||||
result = rag.query(question, top_k=3)
|
||||
|
||||
# Format response
|
||||
response_text = f"""
|
||||
✅ Resposta:\n\n{result['response']}
|
||||
|
||||
📚 Fontes:
|
||||
{self._format_sources(result['sources'])}
|
||||
"""
|
||||
|
||||
self.bot.send_message(chat_id, response_text)
|
||||
|
||||
except Exception as e:
|
||||
self.bot.send_message(
|
||||
chat_id,
|
||||
f"❌ Erro ao processar sua pergunta: {str(e)}"
|
||||
)
|
||||
|
||||
def _format_sources(self, sources: List[Dict]) -> str:
|
||||
"""Format source metadata."""
|
||||
formatted = []
|
||||
for source in sources:
|
||||
meta = source.get('metadata', {})
|
||||
title = meta.get('title', 'Desconhecido')
|
||||
chunk = meta.get('chunk_id', '?')
|
||||
formatted.append(f"• {title} (chunk {chunk})")
|
||||
return "\n".join(formatted)
|
||||
|
||||
def add_schedule(self, schedule: Dict):
|
||||
"""Add schedule to pending list and database."""
|
||||
self.pending_schedules.append(schedule)
|
||||
self._save_schedules()
|
||||
|
||||
def update_schedule(self, schedule: Dict):
|
||||
"""Update schedule in pending list and database."""
|
||||
for i, s in enumerate(self.pending_schedules):
|
||||
if s['id'] == schedule['id']:
|
||||
self.pending_schedules[i] = schedule
|
||||
break
|
||||
self._save_schedules()
|
||||
|
||||
def remove_schedule(self, schedule: Dict):
|
||||
"""Remove schedule from pending list and database."""
|
||||
self.pending_schedules = [s for s in self.pending_schedules if s['id'] != schedule['id']]
|
||||
self._save_schedules()
|
||||
|
||||
def _save_schedules(self):
|
||||
"""Save schedules to JSON file."""
|
||||
schedules_path = "/app/schedules.json"
|
||||
|
||||
# Load existing schedules
|
||||
schedules = []
|
||||
if os.path.exists(schedules_path):
|
||||
with open(schedules_path, "r") as f:
|
||||
schedules = json.load(f)
|
||||
|
||||
# Update pending schedules
|
||||
for pending in self.pending_schedules:
|
||||
found = False
|
||||
for i, s in enumerate(schedules):
|
||||
if s['id'] == pending['id']:
|
||||
schedules[i] = pending
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
schedules.append(pending)
|
||||
|
||||
# Write back
|
||||
with open(schedules_path, "w") as f:
|
||||
json.dump(schedules, f, indent=2)
|
||||
|
||||
def start_polling(self):
|
||||
"""Start the bot polling."""
|
||||
if self.bot is None:
|
||||
print("TELEGRAM_BOT_TOKEN ausente; polling não iniciado")
|
||||
return
|
||||
print("Starting Telegram bot polling...")
|
||||
self.bot.infinity_polling(timeout=10, long_polling_timeout=5)
|
||||
|
||||
def send_scheduled_message(self, schedule: Dict):
|
||||
"""Send scheduled message to Telegram."""
|
||||
try:
|
||||
from app.services.rag_service import QdrantService, RAGService
|
||||
|
||||
qdrant = QdrantService(os.getenv("QDRANT_URL", "http://qdrant:6333"))
|
||||
rag = RAGService(
|
||||
qdrant,
|
||||
model_name=os.getenv("GPT4ALL_MODEL", "/app/models/llama-3.2-3b-instruct-q4_0.gguf"),
|
||||
)
|
||||
response = rag.query(schedule.get('book') or 'espiritismo', top_k=3)
|
||||
message = f"""
|
||||
⏰ Mensagem Agendada - {schedule.get('book', 'Todos os livros')}
|
||||
|
||||
{response['response']}
|
||||
|
||||
📚 Fontes:
|
||||
{self._format_sources(response['sources'])}
|
||||
"""
|
||||
if self.bot is None:
|
||||
print("TELEGRAM_BOT_TOKEN ausente; mensagem agendada não enviada")
|
||||
return
|
||||
self.bot.send_message(self.chat_id, message)
|
||||
except Exception as e:
|
||||
print(f"Error sending scheduled message: {e}")
|
||||
|
||||
def generate_daily_content(self):
|
||||
"""Generate daily content for all books."""
|
||||
books = [
|
||||
"A Gênese",
|
||||
"Livro dos Espíritos",
|
||||
"Livro dos Médiums",
|
||||
"O Céu e o Inferno",
|
||||
"O Evangelho Segundo o Espiritismo",
|
||||
"O Que é o Espiritismo"
|
||||
]
|
||||
|
||||
selected_books = random.sample(books, 2)
|
||||
book_names = "\n".join(f"• {book}" for book in selected_books)
|
||||
|
||||
message = f"""
|
||||
🌟 Conteúdo Diário de Espiritismo
|
||||
|
||||
📚 Livros selecionados:
|
||||
{book_names}
|
||||
|
||||
Este conteúdo foi gerado automaticamente pelo Sistema RAG Espiritismo.
|
||||
"""
|
||||
|
||||
if self.bot is None:
|
||||
print("TELEGRAM_BOT_TOKEN ausente; conteúdo diário não enviado")
|
||||
return
|
||||
self.bot.send_message(self.chat_id, message)
|
||||
@@ -0,0 +1,45 @@
|
||||
from langchain_community.document_loaders import PyPDFLoader
|
||||
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
||||
import os
|
||||
from typing import List, Dict
|
||||
from app.utils.embeddings import EmbeddingService
|
||||
|
||||
class PDFProcessor:
|
||||
def __init__(self, embedding_service: EmbeddingService):
|
||||
self.embedding_service = embedding_service
|
||||
self.text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=500,
|
||||
chunk_overlap=50,
|
||||
length_function=len,
|
||||
)
|
||||
|
||||
def load_and_split(self, file_path: str, metadata: Dict) -> List[Dict]:
|
||||
"""Load and split a PDF file."""
|
||||
loader = PyPDFLoader(file_path)
|
||||
documents = loader.load()
|
||||
|
||||
chunks = self.text_splitter.split_documents(documents)
|
||||
|
||||
# Convert to dictionaries with metadata
|
||||
chunk_data = []
|
||||
for i, doc in enumerate(chunks):
|
||||
chunk_data.append({
|
||||
"content": doc.page_content,
|
||||
"metadata": {
|
||||
**metadata,
|
||||
"chunk_id": i,
|
||||
"source": file_path,
|
||||
}
|
||||
})
|
||||
|
||||
return chunk_data
|
||||
|
||||
def embed_chunks(self, chunks: List[Dict]) -> List[Dict]:
|
||||
"""Add embeddings to chunks."""
|
||||
texts = [chunk["content"] for chunk in chunks]
|
||||
embeddings = self.embedding_service.embed(texts)
|
||||
|
||||
for chunk, embedding in zip(chunks, embeddings):
|
||||
chunk["embedding"] = embedding.tolist()
|
||||
|
||||
return chunks
|
||||
@@ -0,0 +1,181 @@
|
||||
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 os
|
||||
import json
|
||||
import time
|
||||
|
||||
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 = []
|
||||
|
||||
for i, chunk in enumerate(chunks):
|
||||
point = PointStruct(
|
||||
id=chunk.get("id") or i,
|
||||
vector=chunk["embedding"],
|
||||
payload={
|
||||
"content": chunk["content"],
|
||||
"metadata": chunk["metadata"],
|
||||
}
|
||||
)
|
||||
points.append(point)
|
||||
|
||||
if points:
|
||||
self.client.upsert(
|
||||
collection_name=self.collection_name,
|
||||
points=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."""
|
||||
points = self.client.scroll(
|
||||
collection_name=self.collection_name,
|
||||
limit=10000,
|
||||
)[0]
|
||||
|
||||
books = set()
|
||||
for point in points:
|
||||
metadata = point.payload["metadata"]
|
||||
books.add(metadata.get("title", "Unknown"))
|
||||
|
||||
return sorted(list(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ê é um assistente especializado em espiritismo. Use as informações abaixo para responder à pergunta do usuário.
|
||||
|
||||
Informações:
|
||||
{context_text}
|
||||
|
||||
Instruções:
|
||||
- Responda de forma clara e concisa
|
||||
- Use o contexto fornecido
|
||||
- Quando citar passagens, mencione o livro e capítulo (se disponível)
|
||||
- Se não souber a resposta, diga que não encontrou na base de dados"""
|
||||
|
||||
prompt = f"{system_prompt}\n\nPergunta: {query}\nResposta:"
|
||||
response = self._get_llm().generate(prompt, max_tokens=500)
|
||||
|
||||
return response
|
||||
|
||||
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
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
from celery import Celery
|
||||
from celery.schedules import crontab
|
||||
from app.services.bot_service import BotService
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
celery = Celery(
|
||||
'tasks',
|
||||
broker=os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0"),
|
||||
backend=os.getenv("CELERY_RESULT_BACKEND", "redis://localhost:6379/0"),
|
||||
)
|
||||
|
||||
bot_service = BotService(
|
||||
os.getenv("TELEGRAM_BOT_TOKEN", ""),
|
||||
os.getenv("TELEGRAM_CHAT_ID", "")
|
||||
)
|
||||
|
||||
# Load schedules from JSON
|
||||
def load_schedules() -> List[Dict]:
|
||||
schedules_path = "/app/schedules.json"
|
||||
if os.path.exists(schedules_path):
|
||||
with open(schedules_path, "r") as f:
|
||||
return json.load(f)
|
||||
return []
|
||||
|
||||
# Save schedules to JSON
|
||||
def save_schedules(schedules: List[Dict]):
|
||||
with open("/app/schedules.json", "w") as f:
|
||||
json.dump(schedules, f, indent=2)
|
||||
|
||||
@celery.task
|
||||
def send_scheduled_message(schedule_id: int = None):
|
||||
"""Send one schedule, or every enabled schedule when id is omitted."""
|
||||
schedules = load_schedules()
|
||||
if schedule_id is None:
|
||||
selected = [item for item in schedules if item.get("enabled", True)]
|
||||
else:
|
||||
selected = [item for item in schedules if item.get("id") == schedule_id and item.get("enabled", True)]
|
||||
|
||||
if not selected:
|
||||
print(f"Schedule {schedule_id} not found")
|
||||
return
|
||||
|
||||
for schedule in selected:
|
||||
try:
|
||||
bot_service.send_scheduled_message(schedule)
|
||||
print(f"Sent scheduled message: {schedule['time']}")
|
||||
except Exception as e:
|
||||
print(f"Error sending scheduled message: {e}")
|
||||
|
||||
@celery.task
|
||||
def generate_daily_content():
|
||||
"""Generate daily content for all books."""
|
||||
try:
|
||||
bot_service.generate_daily_content()
|
||||
print("Sent daily content")
|
||||
except Exception as e:
|
||||
print(f"Error generating daily content: {e}")
|
||||
|
||||
# Periodic tasks
|
||||
celery.conf.beat_schedule = {
|
||||
'send-scheduled-messages': {
|
||||
'task': 'app.tasks.celery_tasks.send_scheduled_message',
|
||||
'schedule': crontab(minute=0), # Every hour
|
||||
'args': (None,), # Process all enabled schedules
|
||||
},
|
||||
'generate-daily-content': {
|
||||
'task': 'app.tasks.celery_tasks.generate_daily_content',
|
||||
'schedule': crontab(hour=8, minute=0), # Daily at 8 AM
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
from sentence_transformers import SentenceTransformer
|
||||
import numpy as np
|
||||
|
||||
class EmbeddingService:
|
||||
def __init__(self, model_name="all-MiniLM-L6-v2"):
|
||||
self.model = SentenceTransformer(model_name)
|
||||
self.dimension = 384
|
||||
|
||||
def embed(self, texts: list[str]) -> np.ndarray:
|
||||
"""Generate embeddings for a list of texts."""
|
||||
return self.model.encode(texts, convert_to_numpy=True)
|
||||
|
||||
def embed_query(self, text: str) -> np.ndarray:
|
||||
"""Generate embedding for a single query."""
|
||||
return self.model.encode([text], convert_to_numpy=True)[0]
|
||||
|
||||
def get_dimension(self) -> int:
|
||||
"""Return embedding dimension."""
|
||||
return self.dimension
|
||||
@@ -0,0 +1,77 @@
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
class ScheduleManager:
|
||||
def __init__(self, schedules_path: str = "/app/schedules.json"):
|
||||
self.schedules_path = schedules_path
|
||||
self.schedules = self._load_schedules()
|
||||
|
||||
def _load_schedules(self) -> List[Dict]:
|
||||
"""Load schedules from JSON file."""
|
||||
if os.path.exists(self.schedules_path):
|
||||
try:
|
||||
with open(self.schedules_path, 'r') as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
return []
|
||||
|
||||
def save_schedules(self):
|
||||
"""Save schedules to JSON file."""
|
||||
os.makedirs(os.path.dirname(self.schedules_path), exist_ok=True)
|
||||
with open(self.schedules_path, 'w') as f:
|
||||
json.dump(self.schedules, f, indent=2)
|
||||
|
||||
def add_schedule(self, schedule: Dict) -> Dict:
|
||||
"""Add a new schedule."""
|
||||
schedule_id = max([s['id'] for s in self.schedules], default=0) + 1
|
||||
new_schedule = {
|
||||
'id': schedule_id,
|
||||
'time': schedule['time'],
|
||||
'book': schedule.get('book'),
|
||||
'enabled': schedule.get('enabled', True),
|
||||
}
|
||||
self.schedules.append(new_schedule)
|
||||
self.save_schedules()
|
||||
return new_schedule
|
||||
|
||||
def update_schedule(self, schedule_id: int, updates: Dict) -> Optional[Dict]:
|
||||
"""Update an existing schedule."""
|
||||
for i, schedule in enumerate(self.schedules):
|
||||
if schedule['id'] == schedule_id:
|
||||
self.schedules[i].update(updates)
|
||||
self.save_schedules()
|
||||
return self.schedules[i]
|
||||
return None
|
||||
|
||||
def delete_schedule(self, schedule_id: int) -> bool:
|
||||
"""Delete a schedule."""
|
||||
for i, schedule in enumerate(self.schedules):
|
||||
if schedule['id'] == schedule_id:
|
||||
self.schedules.pop(i)
|
||||
self.save_schedules()
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_schedule(self, schedule_id: int) -> Optional[Dict]:
|
||||
"""Get a specific schedule."""
|
||||
for schedule in self.schedules:
|
||||
if schedule['id'] == schedule_id:
|
||||
return schedule
|
||||
return None
|
||||
|
||||
def list_schedules(self) -> List[Dict]:
|
||||
"""List all schedules."""
|
||||
return self.schedules
|
||||
|
||||
def get_enabled_schedules(self) -> List[Dict]:
|
||||
"""Get all enabled schedules."""
|
||||
return [s for s in self.schedules if s.get('enabled', True)]
|
||||
|
||||
def is_schedule_ready(self, schedule: Dict) -> bool:
|
||||
"""Check if a schedule is ready to be sent."""
|
||||
schedule_time = datetime.fromisoformat(schedule['time'].replace('Z', '+00:00'))
|
||||
current_time = datetime.now()
|
||||
return current_time >= schedule_time
|
||||
Reference in new issue
Block a user