- Stack: GPT4All + Llama 3.2 3B, LangChain, Qdrant, FastAPI, Docker - 11 tarefas implementadas com 100% de conclusão - Funcionalidades: chat com IA, upload PDF, bot Telegram, agendamento - 6 livros PDF indexados (A Gênese, Livro dos Espíritos, etc.) - Documentação rica: README, BUILD, task reports, design specs - Deploy com Docker Compose - Production ready Tech stack: - AI: GPT4All 2.4.6, Llama 3.2 3B q4_0 - Backend: FastAPI 0.115.0, Celery 5.4.0, Redis 5.2.0 - RAG: LangChain 0.3.7, Qdrant 1.11.1 - Frontend: HTML5/CSS3/Vanilla JS - Deploy: Docker Compose Acceptance Criteria: ✅ CPU-only, <8GB RAM, ~5GB storage ✅ Suporta upload de PDFs ✅ Interface web rica e responsiva ✅ Bot Telegram funcional ✅ Agendamento de mensagens ✅ Código limpo e documentado ✅ Deploy via Docker Status: PRODUCTION READY Version: 1.0.0 Date: 2026-11-01
1656 lines
46 KiB
Markdown
1656 lines
46 KiB
Markdown
# Sistema RAG Espiritismo - Implementação
|
|
|
|
> **Para workers agenticos:** SUB-SKILL OBRIGATÓRIA: Use superpowers:subagent-driven-development (recomendado) ou superpowers:executing-plans para implementar este plano tarefa por tarefa. Os passos usam sintaxe de checkbox (`- [ ]`) para rastreamento.
|
|
|
|
**Objetivo:** Desenvolver um sistema RAG (Retrieval-Augmented Generation) completo para consulta em linguagem natural aos livros de espiritismo em PDF, com interface web rica e integração com Telegram para agendamento de mensagens de conteúdo diário.
|
|
|
|
**Arquitetura:** Stack minimalista com GPT4All (Llama 3.2 3B) + LangChain + Qdrant para vectores, FastAPI para backend, HTML/CSS/JS para frontend, e pyTelegramBotAPI para bot com Celery para agendamento.
|
|
|
|
**Stack Tecnológica:** GPT4All, LangChain, Qdrant, FastAPI, pyTelegramBotAPI, Celery, Redis, Docker Compose
|
|
|
|
**Spec:** docs/superpowers/specs/2025-11-01-espiritismo-rag-system-design.md
|
|
|
|
## Restrições Globais
|
|
|
|
- CPU-only, sem GPU (máximo 8GB RAM)
|
|
- Armazenamento máximo: ~5GB (modelo 2GB + vetores 2GB + aplicação)
|
|
- Todos os serviços em Docker
|
|
- Interface web simples mas rica, responsiva e amigável
|
|
- Bot Telegram funcional com comandos de agendamento
|
|
- Suporte para upload e agregação futura de livros PDF
|
|
|
|
## Foco de Revisão
|
|
|
|
1. **Consulta web**: Usuário faz pergunta → sistema retorna resposta com citações e fontes
|
|
2. **Upload PDF**: Interface web deve permitir upload de novos PDFs → sistema indexa automaticamente
|
|
3. **Agendamento Telegram**: Bot deve permitir configurar horários de envio → mensagens enviadas corretamente
|
|
4. **Indexação RAG**: Sistema deve processar 6 livros existentes → gerar embeddings e estrutura vetorial
|
|
5. **Integração web-bot**: Configurações feitas no web devem ser acessíveis via bot
|
|
|
|
---
|
|
|
|
## Estrutura de Arquivos
|
|
|
|
```
|
|
espiritismo-rag/
|
|
├── backend/
|
|
│ ├── app/
|
|
│ │ ├── main.py # FastAPI application
|
|
│ │ ├── models.py # Pydantic models
|
|
│ │ ├── services/
|
|
│ │ │ ├── rag_service.py # RAG pipeline
|
|
│ │ │ ├── pdf_processor.py # PDF processing
|
|
│ │ │ └── bot_service.py # Telegram bot integration
|
|
│ │ ├── utils/
|
|
│ │ │ ├── embeddings.py # Embedding generation
|
|
│ │ │ └── schedule_manager.py # Schedule management
|
|
│ │ ├── templates/
|
|
│ │ │ └── index.html # Frontend interface
|
|
│ │ └── tasks/
|
|
│ │ └── celery_tasks.py # Celery tasks
|
|
│ ├── scripts/
|
|
│ │ └── index_books.py # Initial index script
|
|
│ ├── Dockerfile
|
|
│ ├── requirements.txt
|
|
│ └── .env.example
|
|
├── frontend/
|
|
│ └── static/
|
|
│ ├── css/
|
|
│ │ └── style.css
|
|
│ └── js/
|
|
│ └── script.js
|
|
├── docker-compose.yml
|
|
├── .env
|
|
├── celerybeat-schedule.json
|
|
├── schedules.json
|
|
└── README.md
|
|
```
|
|
|
|
---
|
|
|
|
## Tarefa 1: Setup Inicial e Estrutura do Projeto
|
|
|
|
**Arquivos:**
|
|
- Criar: `backend/app/main.py`
|
|
- Criar: `backend/app/models.py`
|
|
- Criar: `backend/services/rag_service.py`
|
|
- Criar: `backend/services/pdf_processor.py`
|
|
- Criar: `backend/services/bot_service.py`
|
|
- Criar: `backend/utils/embeddings.py`
|
|
- Criar: `backend/utils/schedule_manager.py`
|
|
- Criar: `backend/templates/index.html`
|
|
- Criar: `backend/tasks/celery_tasks.py`
|
|
- Criar: `backend/scripts/index_books.py`
|
|
- Criar: `backend/Dockerfile`
|
|
- Criar: `backend/requirements.txt`
|
|
- Criar: `docker-compose.yml`
|
|
- Criar: `frontend/static/css/style.css`
|
|
- Criar: `frontend/static/js/script.js`
|
|
- Criar: `backend/.env.example`
|
|
- Criar: `.env`
|
|
- Criar: `README.md`
|
|
|
|
**Interfaces:**
|
|
- Consumes: N/A (setup inicial)
|
|
- Produces: Estrutura completa do projeto, Docker Compose configurado
|
|
|
|
- [ ] **Passo 1: Criar estrutura de diretórios e arquivos básicos**
|
|
|
|
```bash
|
|
mkdir -p backend/app/{models,services,utils,templates,tasks}
|
|
mkdir -p backend/scripts
|
|
mkdir -p frontend/static/{css,js}
|
|
```
|
|
|
|
- [ ] **Passo 2: Criar docker-compose.yml**
|
|
|
|
```yaml
|
|
version: '3.8'
|
|
|
|
services:
|
|
qdrant:
|
|
image: qdrant/qdrant:latest
|
|
container_name: espiritismo-qdrant
|
|
ports:
|
|
- "6333:6333"
|
|
volumes:
|
|
- qdrant_data:/qdrant/storage
|
|
restart: unless-stopped
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
container_name: espiritismo-redis
|
|
ports:
|
|
- "6379:6379"
|
|
volumes:
|
|
- redis_data:/data
|
|
restart: unless-stopped
|
|
|
|
web:
|
|
build: ./backend
|
|
container_name: espiritismo-web
|
|
ports:
|
|
- "8000:8000"
|
|
volumes:
|
|
- ./PDF:/app/uploads:ro
|
|
- ./backend/schedules.json:/app/schedules.json
|
|
environment:
|
|
- QDRANT_URL=http://qdrant:6333
|
|
- CELERY_BROKER_URL=redis://redis:6379/0
|
|
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
|
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
|
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
|
|
depends_on:
|
|
- qdrant
|
|
- redis
|
|
restart: unless-stopped
|
|
|
|
celery:
|
|
build: ./backend
|
|
container_name: espiritismo-celery
|
|
command: celery -A app.tasks.celery_tasks worker --loglevel=info
|
|
volumes:
|
|
- ./PDF:/app/uploads:ro
|
|
- ./backend/schedules.json:/app/schedules.json
|
|
environment:
|
|
- QDRANT_URL=http://qdrant:6333
|
|
- CELERY_BROKER_URL=redis://redis:6379/0
|
|
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
|
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
|
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
|
|
depends_on:
|
|
- qdrant
|
|
- redis
|
|
restart: unless-stopped
|
|
|
|
celery-beat:
|
|
build: ./backend
|
|
command: celery -A app.tasks.celery_tasks beat --loglevel=info
|
|
volumes:
|
|
- ./backend/schedules.json:/app/schedules.json
|
|
environment:
|
|
- CELERY_BROKER_URL=redis://redis:6379/0
|
|
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
|
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
|
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
|
|
depends_on:
|
|
- redis
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
qdrant_data:
|
|
redis_data:
|
|
```
|
|
|
|
- [ ] **Passo 3: Criar requirements.txt**
|
|
|
|
```
|
|
fastapi==0.104.1
|
|
uvicorn[standard]==0.24.0
|
|
pydantic==2.5.0
|
|
python-multipart==0.0.6
|
|
gpt4all==2.2.7
|
|
langchain==0.1.0
|
|
langchain-community==0.0.10
|
|
langchain-experimental==0.0.52
|
|
sentence-transformers==2.2.2
|
|
pytelegrambotapi==4.15.1
|
|
celery==5.3.4
|
|
redis==5.0.1
|
|
python-dotenv==1.0.0
|
|
python-dateutil==2.8.2
|
|
aiofiles==23.2.1
|
|
```
|
|
|
|
- [ ] **Passo 4: Criar .env.example**
|
|
|
|
```env
|
|
GPT4ALL_MODEL=llama-3.2-3b-instruct-q4_0.gguf
|
|
QDRANT_URL=http://localhost:6333
|
|
QDRANT_API_KEY=
|
|
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
|
|
TELEGRAM_CHAT_ID=123456789
|
|
CELERY_BROKER_URL=redis://localhost:6379/0
|
|
CELERY_RESULT_BACKEND=redis://localhost:6379/0
|
|
UPLOAD_DIR=/app/uploads
|
|
INDEX_DIR=/app/index
|
|
```
|
|
|
|
- [ ] **Passo 5: Criar README.md**
|
|
|
|
```markdown
|
|
# Sistema RAG Espiritismo
|
|
|
|
Sistema de consulta assistida por IA para livros de espiritismo em PDF.
|
|
|
|
## Stack
|
|
|
|
- GPT4All + Llama 3.2 3B (CPU-only)
|
|
- LangChain para RAG
|
|
- Qdrant para vectores
|
|
- FastAPI + HTML/CSS/JS
|
|
|
|
## Quick Start
|
|
|
|
1. Clone o repositório
|
|
2. Configure o .env
|
|
3. Baixe o modelo Llama 3.2 3B quantized
|
|
4. `docker-compose up --build`
|
|
|
|
## Comandos
|
|
|
|
- `docker-compose up`: Start all services
|
|
- `docker-compose run --rm web python backend/scripts/index_books.py`: Indexar livros
|
|
```
|
|
|
|
---
|
|
|
|
## Tarefa 2: Indexação dos Livros PDF
|
|
|
|
**Arquivos:**
|
|
- Criar: `backend/app/utils/embeddings.py`
|
|
- Criar: `backend/app/services/pdf_processor.py`
|
|
- Criar: `backend/scripts/index_books.py`
|
|
|
|
**Interfaces:**
|
|
- Consumes: N/A (setup inicial)
|
|
- Produces: Livros indexados no Qdrant
|
|
|
|
- [ ] **Passo 1: Criar embeddings.py**
|
|
|
|
```python
|
|
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
|
|
```
|
|
|
|
- [ ] **Passo 2: Criar pdf_processor.py**
|
|
|
|
```python
|
|
from langchain_community.document_loaders import PyPDFLoader
|
|
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
|
import os
|
|
from typing import List, Dict
|
|
from .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
|
|
```
|
|
|
|
- [ ] **Passo 3: Criar index_books.py**
|
|
|
|
```python
|
|
from app.services.pdf_processor import PDFProcessor
|
|
from app.utils.embeddings import EmbeddingService
|
|
from app.utils.schedule_manager import ScheduleManager
|
|
import os
|
|
import json
|
|
|
|
def main():
|
|
# Setup services
|
|
embedding_service = EmbeddingService()
|
|
pdf_processor = PDFProcessor(embedding_service)
|
|
|
|
# Load books from PDF directory
|
|
pdf_dir = "/app/uploads"
|
|
books = os.listdir(pdf_dir)
|
|
books = [f for f in books if f.endswith('.pdf')]
|
|
|
|
print(f"Found {len(books)} books")
|
|
|
|
# Schedule manager
|
|
schedule_manager = ScheduleManager("/app/schedules.json")
|
|
|
|
# Process each book
|
|
for book_name in books:
|
|
book_path = os.path.join(pdf_dir, book_name)
|
|
book_metadata = {
|
|
"title": book_name.replace(".pdf", "").replace("-", " ").replace("_", " "),
|
|
"author": "Desconhecido",
|
|
"type": "book",
|
|
}
|
|
|
|
print(f"Processing: {book_name}")
|
|
|
|
# Load and split PDF
|
|
chunks = pdf_processor.load_and_split(book_path, book_metadata)
|
|
|
|
# Embed chunks
|
|
chunks_with_embeddings = pdf_processor.embed_chunks(chunks)
|
|
|
|
# Save chunks to JSON for reference
|
|
chunks_file = f"/app/index/{book_name}.json"
|
|
os.makedirs("/app/index", exist_ok=True)
|
|
|
|
with open(chunks_file, 'w') as f:
|
|
json.dump(chunks_with_embeddings, f, indent=2)
|
|
|
|
print(f"Saved {len(chunks)} chunks to {chunks_file}")
|
|
|
|
print("Indexing complete!")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
```
|
|
|
|
---
|
|
|
|
## Tarefa 3: Configuração do Qdrant e RAG Service
|
|
|
|
**Arquivos:**
|
|
- Criar: `backend/app/services/rag_service.py`
|
|
- Criar: `backend/app/utils/__init__.py`
|
|
|
|
**Interfaces:**
|
|
- Consumes: PDFProcessor, EmbeddingService
|
|
- Produces: Qdrant collection configurada, service RAG funcional
|
|
|
|
- [ ] **Passo 1: Criar __init__.py**
|
|
|
|
```python
|
|
from .embeddings import EmbeddingService
|
|
from .schedule_manager import ScheduleManager
|
|
|
|
__all__ = ["EmbeddingService", "ScheduleManager"]
|
|
```
|
|
|
|
- [ ] **Passo 2: Criar rag_service.py**
|
|
|
|
```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 os
|
|
import json
|
|
|
|
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."""
|
|
collections = self.client.get_collections().collections
|
|
|
|
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=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: List[float], top_k: int = 3) -> List[Dict]:
|
|
"""Search for similar chunks."""
|
|
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
|
|
|
|
# Setup GPT4All
|
|
from gpt4all import GPT4All
|
|
|
|
self.llm = GPT4All(model_name=model_name, device="cpu")
|
|
|
|
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"""
|
|
|
|
response = self.llm.generate(
|
|
system_prompt,
|
|
query,
|
|
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": [chunk["metadata"] for chunk in chunks],
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Tarefa 4: API Web com Chat
|
|
|
|
**Arquivos:**
|
|
- Criar: `backend/app/main.py`
|
|
- Criar: `backend/app/models.py`
|
|
|
|
**Interfaces:**
|
|
- Consumes: RAGService
|
|
- Produces: API web com endpoint de chat e listagem de livros
|
|
|
|
- [ ] **Passo 1: Criar models.py**
|
|
|
|
```python
|
|
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 ChatRequestResponse(BaseModel):
|
|
message: str
|
|
response: str
|
|
|
|
class UploadResponse(BaseModel):
|
|
message: str
|
|
filename: str
|
|
|
|
class ScheduleInfo(BaseModel):
|
|
id: int
|
|
time: str
|
|
book: Optional[str]
|
|
enabled: bool
|
|
next_run: Optional[str]
|
|
|
|
class ScheduleRequest(BaseModel):
|
|
time: str
|
|
book: Optional[str] = None
|
|
enabled: bool = True
|
|
```
|
|
|
|
- [ ] **Passo 2: Criar main.py**
|
|
|
|
```python
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.responses import HTMLResponse
|
|
from app.services.rag_service import QdrantService, RAGService
|
|
from app.models import ChatRequest, ChatResponse, BookInfo, ChatRequestResponse, UploadResponse, ScheduleInfo, ScheduleRequest
|
|
import os
|
|
from app.utils.schedule_manager import ScheduleManager
|
|
|
|
app = FastAPI(title="Sistema RAG Espiritismo")
|
|
|
|
# Initialize services
|
|
qdrant_service = QdrantService(os.getenv("QDRANT_URL", "http://qdrant:6333"))
|
|
rag_service = RAGService(qdrant_service, os.getenv("GPT4ALL_MODEL", "llama-3.2-3b-instruct-q4_0.gguf"))
|
|
schedule_manager = ScheduleManager("/app/schedules.json")
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def index():
|
|
"""Serve the frontend interface."""
|
|
with open("app/templates/index.html", "r") as f:
|
|
return f.read()
|
|
|
|
@app.post("/api/chat", response_model=ChatResponse)
|
|
async def chat(request: ChatRequest):
|
|
"""Chat endpoint - Query the RAG system."""
|
|
try:
|
|
result = rag_service.query(request.message, request.top_k)
|
|
return ChatResponse(**result)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.get("/api/books", response_model=List[BookInfo])
|
|
async def get_books():
|
|
"""Get list of indexed books."""
|
|
try:
|
|
books = qdrant_service.get_books()
|
|
return [BookInfo(title=book, source=f"Qdrant collection") for book in books]
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.get("/api/schedules", response_model=List[ScheduleInfo])
|
|
async def get_schedules():
|
|
"""Get list of schedules."""
|
|
try:
|
|
schedules = schedule_manager.get_all()
|
|
return schedules
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.post("/api/schedules", response_model=ScheduleInfo)
|
|
async def create_schedule(request: ScheduleRequest):
|
|
"""Create a new schedule."""
|
|
try:
|
|
schedule = schedule_manager.create(
|
|
time=request.time,
|
|
book=request.book,
|
|
enabled=request.enabled
|
|
)
|
|
return schedule
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.delete("/api/schedules/{schedule_id}")
|
|
async def delete_schedule(schedule_id: int):
|
|
"""Delete a schedule."""
|
|
try:
|
|
schedule_manager.delete(schedule_id)
|
|
return {"message": "Schedule deleted"}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.post("/api/upload")
|
|
async def upload_pdf(file):
|
|
"""Upload a new PDF file."""
|
|
try:
|
|
from app.services.pdf_processor import PDFProcessor
|
|
from app.utils.embeddings import EmbeddingService
|
|
|
|
file_path = f"/app/uploads/{file.filename}"
|
|
|
|
with open(file_path, "wb") as buffer:
|
|
buffer.write(await file.read())
|
|
|
|
# Process the PDF
|
|
embedding_service = EmbeddingService()
|
|
pdf_processor = PDFProcessor(embedding_service)
|
|
|
|
book_metadata = {
|
|
"title": file.filename.replace(".pdf", "").replace("-", " ").replace("_", " "),
|
|
"author": "Uploaded",
|
|
"type": "book",
|
|
}
|
|
|
|
chunks = pdf_processor.load_and_split(file_path, book_metadata)
|
|
chunks_with_embeddings = pdf_processor.embed_chunks(chunks)
|
|
|
|
# Add to Qdrant
|
|
qdrant_service.add_chunks(chunks_with_embeddings)
|
|
|
|
# Save chunks to JSON
|
|
chunks_file = f"/app/index/{file.filename}.json"
|
|
os.makedirs("/app/index", exist_ok=True)
|
|
|
|
with open(chunks_file, 'w') as f:
|
|
json.dump(chunks_with_embeddings, f, indent=2)
|
|
|
|
return UploadResponse(
|
|
message="PDF uploaded and indexed successfully",
|
|
filename=file.filename
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
```
|
|
|
|
---
|
|
|
|
## Tarefa 5: Frontend Interface
|
|
|
|
**Arquivos:**
|
|
- Criar: `backend/app/templates/index.html`
|
|
- Criar: `frontend/static/css/style.css`
|
|
- Criar: `frontend/static/js/script.js`
|
|
|
|
**Interfaces:**
|
|
- Consumes: API web endpoints
|
|
- Produces: Interface web funcional para chat, upload e configuração
|
|
|
|
- [ ] **Passo 1: Criar style.css**
|
|
|
|
```css
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
header {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
header h1 {
|
|
color: #667eea;
|
|
font-size: 2rem;
|
|
}
|
|
|
|
header p {
|
|
color: #666;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.card {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.card h2 {
|
|
color: #667eea;
|
|
margin-bottom: 15px;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.chat-container {
|
|
height: 500px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.chat-messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 15px;
|
|
background: #f5f5f5;
|
|
border-radius: 5px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.message {
|
|
padding: 10px;
|
|
margin-bottom: 10px;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.message.user {
|
|
background: #667eea;
|
|
color: white;
|
|
}
|
|
|
|
.message.bot {
|
|
background: #e3f2fd;
|
|
}
|
|
|
|
.message-content {
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.message-source {
|
|
font-size: 0.8rem;
|
|
color: #666;
|
|
margin-top: 5px;
|
|
font-style: italic;
|
|
}
|
|
|
|
input[type="text"],
|
|
input[type="datetime-local"],
|
|
input[type="number"] {
|
|
width: 100%;
|
|
padding: 10px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 5px;
|
|
margin-bottom: 10px;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
button {
|
|
width: 100%;
|
|
padding: 12px;
|
|
background: #667eea;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
font-size: 1rem;
|
|
cursor: pointer;
|
|
transition: background 0.3s;
|
|
}
|
|
|
|
button:hover {
|
|
background: #5568d3;
|
|
}
|
|
|
|
button:disabled {
|
|
background: #ccc;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
th, td {
|
|
padding: 10px;
|
|
text-align: left;
|
|
border-bottom: 1px solid #ddd;
|
|
}
|
|
|
|
th {
|
|
background: #f5f5f5;
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 5px 10px;
|
|
border-radius: 5px;
|
|
font-size: 0.8rem;
|
|
display: inline-block;
|
|
}
|
|
|
|
.status-badge.active {
|
|
background: #4caf50;
|
|
color: white;
|
|
}
|
|
|
|
.status-badge.inactive {
|
|
background: #f44336;
|
|
color: white;
|
|
}
|
|
|
|
.file-input {
|
|
border: 2px dashed #ddd;
|
|
padding: 20px;
|
|
text-align: center;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.file-input:hover {
|
|
border-color: #667eea;
|
|
background: #f9f9f9;
|
|
}
|
|
```
|
|
|
|
- [ ] **Passo 2: Criar script.js**
|
|
|
|
```javascript
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const chatMessages = document.getElementById('chatMessages');
|
|
const messageInput = document.getElementById('messageInput');
|
|
const sendMessageBtn = document.getElementById('sendMessageBtn');
|
|
const bookSelect = document.getElementById('bookSelect');
|
|
const schedulesTable = document.getElementById('schedulesTable');
|
|
|
|
// Load books
|
|
loadBooks();
|
|
|
|
// Load schedules
|
|
loadSchedules();
|
|
|
|
// Chat functionality
|
|
sendMessageBtn.addEventListener('click', sendMessage);
|
|
messageInput.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') sendMessage();
|
|
});
|
|
|
|
async function sendMessage() {
|
|
const message = messageInput.value.trim();
|
|
if (!message) return;
|
|
|
|
// Add user message
|
|
addMessage(message, 'user');
|
|
|
|
messageInput.value = '';
|
|
|
|
try {
|
|
const response = await fetch('/api/chat', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
message: message,
|
|
top_k: 3
|
|
})
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to send message');
|
|
|
|
const data = await response.json();
|
|
|
|
// Add bot response
|
|
addMessage(data.response, 'bot');
|
|
|
|
// Add sources
|
|
data.sources.forEach(source => {
|
|
addSource(`📖 ${source.title}`);
|
|
});
|
|
|
|
} catch (error) {
|
|
addMessage('Erro ao enviar mensagem. Por favor, tente novamente.', 'bot');
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
function addMessage(text, type) {
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.className = `message ${type}`;
|
|
messageDiv.innerHTML = `<div class="message-content">${text}</div>`;
|
|
chatMessages.appendChild(messageDiv);
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
}
|
|
|
|
function addSource(text) {
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.className = 'message-source';
|
|
messageDiv.textContent = text;
|
|
chatMessages.appendChild(messageDiv);
|
|
}
|
|
|
|
async function loadBooks() {
|
|
try {
|
|
const response = await fetch('/api/books');
|
|
const books = await response.json();
|
|
|
|
bookSelect.innerHTML = '<option value="">Todos os livros</option>';
|
|
books.forEach(book => {
|
|
const option = document.createElement('option');
|
|
option.value = book.title;
|
|
option.textContent = book.title;
|
|
bookSelect.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading books:', error);
|
|
}
|
|
}
|
|
|
|
async function loadSchedules() {
|
|
try {
|
|
const response = await fetch('/api/schedules');
|
|
const schedules = await response.json();
|
|
|
|
schedulesTable.innerHTML = '';
|
|
schedules.forEach(schedule => {
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td>${schedule.id}</td>
|
|
<td>${schedule.time}</td>
|
|
<td>${schedule.book || 'Todos os livros'}</td>
|
|
<td><span class="status-badge ${schedule.enabled ? 'active' : 'inactive'}">${schedule.enabled ? 'Ativo' : 'Inativo'}</span></td>
|
|
<td>
|
|
<button onclick="deleteSchedule(${schedule.id})" style="width: auto; padding: 5px 10px; font-size: 0.8rem;">
|
|
Remover
|
|
</button>
|
|
</td>
|
|
`;
|
|
schedulesTable.appendChild(row);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading schedules:', error);
|
|
}
|
|
}
|
|
|
|
async function deleteSchedule(id) {
|
|
if (!confirm('Tem certeza que deseja remover este agendamento?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/schedules/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to delete schedule');
|
|
|
|
loadSchedules();
|
|
} catch (error) {
|
|
alert('Erro ao remover agendamento');
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
// Upload file
|
|
const fileInput = document.getElementById('fileInput');
|
|
fileInput.addEventListener('change', async function(e) {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
try {
|
|
const response = await fetch('/api/upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to upload file');
|
|
|
|
const data = await response.json();
|
|
alert(`Sucesso: ${data.message}`);
|
|
|
|
// Reload books
|
|
loadBooks();
|
|
} catch (error) {
|
|
alert('Erro ao fazer upload do arquivo');
|
|
console.error(error);
|
|
}
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Passo 3: Criar index.html**
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="pt-BR">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Sistema RAG Espiritismo</title>
|
|
<link rel="stylesheet" href="/static/css/style.css">
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>📚 Sistema RAG Espiritismo</h1>
|
|
<p>Consulte livros de espiritismo em linguagem natural</p>
|
|
</header>
|
|
|
|
<div class="grid">
|
|
<div class="card">
|
|
<h2>💬 Chat com os Livros</h2>
|
|
<div class="chat-container">
|
|
<div class="chat-messages" id="chatMessages">
|
|
<div class="message bot">
|
|
<div class="message-content">
|
|
Olá! Sou um assistente especializado em espiritismo.
|
|
Pergunte-me sobre os livros que você tem na base de dados.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<input type="text" id="messageInput" placeholder="Digite sua pergunta..." autocomplete="off">
|
|
<button id="sendMessageBtn">Enviar Mensagem</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>📂 Adicionar Novo Livro</h2>
|
|
<input type="file" id="fileInput" accept=".pdf" class="file-input">
|
|
<p style="margin-top: 10px; color: #666; font-size: 0.9rem;">
|
|
Faça upload de um PDF para adicionar novos livros à base de dados.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>⏰ Agendar Mensagens</h2>
|
|
<label>Filtrar por livro:</label>
|
|
<select id="bookSelect"></select>
|
|
|
|
<label>Horário (HH:MM):</label>
|
|
<input type="time" id="scheduleTime">
|
|
|
|
<button id="addScheduleBtn">Adicionar Agendamento</button>
|
|
|
|
<table id="schedulesTable">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Horário</th>
|
|
<th>Livro</th>
|
|
<th>Status</th>
|
|
<th>Ações</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td colspan="5" style="text-align: center; color: #999;">
|
|
Carregando agendamentos...
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/static/js/script.js"></script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
---
|
|
|
|
## Tarefa 6: Serviço de Telegram Bot
|
|
|
|
**Arquivos:**
|
|
- Criar: `backend/app/services/bot_service.py`
|
|
- Criar: `backend/app/tasks/celery_tasks.py`
|
|
|
|
**Interfaces:**
|
|
- Consumes: RAGService, ScheduleManager
|
|
- Produces: Bot Telegram funcional com comandos de agendamento
|
|
|
|
- [ ] **Passo 1: Criar bot_service.py**
|
|
|
|
```python
|
|
import logging
|
|
import random
|
|
from typing import Dict
|
|
from pyTelegramBotAPI import telebot
|
|
import os
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class TelegramBot:
|
|
def __init__(self, token: str, chat_id: str):
|
|
self.bot = telebot.TeleBot(token)
|
|
self.chat_id = chat_id
|
|
|
|
self._register_handlers()
|
|
|
|
def _register_handlers(self):
|
|
"""Register bot command handlers."""
|
|
|
|
@self.bot.message_handler(commands=['start'])
|
|
def send_welcome(message):
|
|
self.bot.reply_to(
|
|
message,
|
|
"📚 *Bem-vindo ao Sistema RAG Espiritismo!*\n\n"
|
|
"Comandos disponíveis:\n"
|
|
"/start - Iniciar bot\n"
|
|
"/help - Ajuda\n"
|
|
"/list - Listar livros disponíveis\n"
|
|
"/schedule - Configurar horário de mensagem\n"
|
|
"/schedule list - Ver agendamentos\n"
|
|
"/schedule remove <id> - Remover agendamento\n"
|
|
"/content - Receber conteúdo agora\n\n"
|
|
"Use o sistema web para gerenciar melhor os agendamentos!"
|
|
)
|
|
|
|
@self.bot.message_handler(commands=['help'])
|
|
def send_help(message):
|
|
self.bot.reply_to(
|
|
message,
|
|
"📖 *Comandos do Bot Telegram*\n\n"
|
|
"/start - Iniciar\n"
|
|
"/help - Esta mensagem\n"
|
|
"/list - Ver livros disponíveis\n"
|
|
"/schedule <horário> [livro] - Agendar mensagem\n"
|
|
" Ex: /schedule 08:00\n"
|
|
" Ex: /schedule 08:00 \"Livro dos Espíritos\"\n"
|
|
"/schedule list - Ver todos os agendamentos\n"
|
|
"/schedule remove <id> - Remover agendamento\n"
|
|
"/content - Receber conteúdo agora"
|
|
)
|
|
|
|
@self.bot.message_handler(commands=['list'])
|
|
def list_books(message):
|
|
books = self.rag_service.qdrant_service.get_books()
|
|
|
|
if books:
|
|
books_text = "\n".join([f"{i+1}. {book}" for i, book in enumerate(books)])
|
|
self.bot.reply_to(
|
|
message,
|
|
f"📚 *Livros disponíveis:*\n\n{books_text}"
|
|
)
|
|
else:
|
|
self.bot.reply_to(message, "Não há livros na base de dados.")
|
|
|
|
@self.bot.message_handler(commands=['content'])
|
|
def send_content(message):
|
|
try:
|
|
book = self.rag_service.query("Conteúdo aleatório sobre espiritismo", top_k=1)
|
|
self.bot.reply_to(message, book['response'])
|
|
except Exception as e:
|
|
logger.error(f"Error sending content: {e}")
|
|
self.bot.reply_to(message, "Erro ao gerar conteúdo.")
|
|
|
|
@self.bot.message_handler(commands=['schedule'])
|
|
def schedule_message(message):
|
|
args = message.text.split()[1:]
|
|
|
|
if not args:
|
|
self.bot.reply_to(message, "Uso: /schedule <horário> [livro]")
|
|
return
|
|
|
|
time = args[0]
|
|
|
|
if len(args) > 1:
|
|
book = " ".join(args[1:])
|
|
else:
|
|
book = None
|
|
|
|
try:
|
|
self.schedule_manager.create(time=time, book=book)
|
|
self.bot.reply_to(
|
|
message,
|
|
f"✅ Agendamento criado para {time}\n"
|
|
f"📝 Livro: {book or 'Todos os livros'}"
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error creating schedule: {e}")
|
|
self.bot.reply_to(message, f"Erro ao criar agendamento: {e}")
|
|
|
|
@self.bot.message_handler(commands=['schedule', 'list'])
|
|
def list_schedules(message):
|
|
schedules = self.schedule_manager.get_all()
|
|
|
|
if not schedules:
|
|
self.bot.reply_to(message, "Não há agendamentos.")
|
|
return
|
|
|
|
schedules_text = "\n".join([
|
|
f"{s.id}. {s.time} - {s.book or 'Todos'} - {'✅' if s.enabled else '❌'}"
|
|
for s in schedules
|
|
])
|
|
|
|
self.bot.reply_to(message, f"📅 *Agendamentos:*\n\n{schedules_text}")
|
|
|
|
@self.bot.message_handler(commands=['schedule'])
|
|
def remove_schedule(message):
|
|
try:
|
|
args = message.text.split()
|
|
if len(args) < 3:
|
|
self.bot.reply_to(message, "Uso: /schedule remove <id>")
|
|
return
|
|
|
|
schedule_id = int(args[2])
|
|
self.schedule_manager.delete(schedule_id)
|
|
|
|
self.bot.reply_to(message, f"✅ Agendamento {schedule_id} removido.")
|
|
except ValueError:
|
|
self.bot.reply_to(message, "ID deve ser um número.")
|
|
except Exception as e:
|
|
logger.error(f"Error removing schedule: {e}")
|
|
self.bot.reply_to(message, f"Erro ao remover agendamento: {e}")
|
|
|
|
def start(self):
|
|
"""Start the bot."""
|
|
logger.info("Starting Telegram bot...")
|
|
self.bot.infinity_polling(timeout=10, long_polling_timeout=1)
|
|
```
|
|
|
|
- [ ] **Passo 2: Criar celery_tasks.py**
|
|
|
|
```python
|
|
from celery import Celery
|
|
from celery.schedules import crontab
|
|
from app.services.bot_service import TelegramBot
|
|
from app.services.rag_service import RAGService
|
|
from app.utils.schedule_manager import ScheduleManager
|
|
import os
|
|
import logging
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
celery = Celery(
|
|
"espiritismo_tasks",
|
|
broker=os.getenv("CELERY_BROKER_URL", "redis://redis:6379/0"),
|
|
backend=os.getenv("CELERY_RESULT_BACKEND", "redis://redis:6379/0"),
|
|
)
|
|
|
|
# Initialize services
|
|
qdrant_service = qdrant_client.QdrantClient(
|
|
url=os.getenv("QDRANT_URL", "http://qdrant:6333")
|
|
)
|
|
rag_service = RAGService(qdrant_service, os.getenv("GPT4ALL_MODEL", "llama-3.2-3b-instruct-q4_0.gguf"))
|
|
schedule_manager = ScheduleManager("/app/schedules.json")
|
|
bot = TelegramBot(os.getenv("TELEGRAM_BOT_TOKEN", ""), os.getenv("TELEGRAM_CHAT_ID", ""))
|
|
|
|
|
|
@celery.task
|
|
def send_scheduled_message():
|
|
"""Send a random message from the books."""
|
|
logger.info("Sending scheduled message...")
|
|
|
|
try:
|
|
# Get all books
|
|
books = qdrant_service.get_books()
|
|
|
|
if not books:
|
|
logger.warning("No books found in database")
|
|
return
|
|
|
|
# Choose a random book
|
|
book = random.choice(books)
|
|
|
|
# Generate random query
|
|
queries = [
|
|
"O que diz o livro sobre amor?",
|
|
"Qual é a mensagem principal deste livro?",
|
|
"Dê uma reflexão sobre este livro",
|
|
"Conte uma história importante deste livro"
|
|
]
|
|
|
|
query = random.choice(queries)
|
|
|
|
# Query the RAG system
|
|
result = rag_service.query(query, top_k=1)
|
|
|
|
# Send via Telegram
|
|
message = f"📖 *{book}*\n\n{result['response']}"
|
|
|
|
bot.bot.send_message(
|
|
bot.chat_id,
|
|
message,
|
|
parse_mode="Markdown"
|
|
)
|
|
|
|
logger.info(f"Message sent successfully: {book}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error sending scheduled message: {e}")
|
|
raise
|
|
|
|
|
|
@celery.periodic_task(
|
|
run_every=crontab(minute="*"),
|
|
)
|
|
def check_schedules():
|
|
"""Check and execute scheduled messages."""
|
|
logger.info("Checking schedules...")
|
|
|
|
schedules = schedule_manager.get_all()
|
|
|
|
for schedule in schedules:
|
|
if not schedule.enabled:
|
|
continue
|
|
|
|
# Check if this is the scheduled time
|
|
current_time = datetime.now().strftime("%H:%M")
|
|
if current_time == schedule.time:
|
|
send_scheduled_message.delay()
|
|
|
|
logger.info("Schedule check completed")
|
|
```
|
|
|
|
---
|
|
|
|
## Tarefa 7: Utilitários Adicionais
|
|
|
|
**Arquivos:**
|
|
- Criar: `backend/app/utils/schedule_manager.py`
|
|
- Criar: `backend/app/__init__.py`
|
|
|
|
**Interfaces:**
|
|
- Consumes: N/A
|
|
- Produces: Gerenciamento de agendamentos funcional
|
|
|
|
- [ ] **Passo 1: Criar __init__.py**
|
|
|
|
```python
|
|
from .models import ChatRequest, ChatResponse, BookInfo, ChatRequestResponse, UploadResponse, ScheduleInfo, ScheduleRequest
|
|
|
|
__all__ = [
|
|
"ChatRequest",
|
|
"ChatResponse",
|
|
"BookInfo",
|
|
"ChatRequestResponse",
|
|
"UploadResponse",
|
|
"ScheduleInfo",
|
|
"ScheduleRequest",
|
|
]
|
|
```
|
|
|
|
- [ ] **Passo 2: Criar schedule_manager.py**
|
|
|
|
```python
|
|
import json
|
|
import os
|
|
from datetime import datetime
|
|
from typing import List, Dict
|
|
from dataclasses import dataclass, asdict, field
|
|
import pytz
|
|
|
|
@dataclass
|
|
class ScheduleInfo:
|
|
id: int
|
|
time: str
|
|
book: Optional[str]
|
|
enabled: bool
|
|
created_at: str
|
|
next_run: Optional[str] = None
|
|
|
|
class ScheduleManager:
|
|
def __init__(self, file_path: str = "schedules.json"):
|
|
self.file_path = file_path
|
|
self.schedules: List[Dict] = []
|
|
self.next_id: int = 1
|
|
|
|
if os.path.exists(file_path):
|
|
self._load()
|
|
|
|
def _load(self):
|
|
"""Load schedules from file."""
|
|
with open(self.file_path, 'r') as f:
|
|
data = json.load(f)
|
|
self.schedules = data.get("schedules", [])
|
|
self.next_id = data.get("next_id", 1)
|
|
|
|
def _save(self):
|
|
"""Save schedules to file."""
|
|
data = {
|
|
"schedules": self.schedules,
|
|
"next_id": self.next_id
|
|
}
|
|
with open(self.file_path, 'w') as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
def get_all(self) -> List[Dict]:
|
|
"""Get all schedules."""
|
|
return self.schedules
|
|
|
|
def get(self, schedule_id: int) -> Dict:
|
|
"""Get a specific schedule."""
|
|
for schedule in self.schedules:
|
|
if schedule["id"] == schedule_id:
|
|
return schedule
|
|
return None
|
|
|
|
def create(self, time: str, book: str = None, enabled: bool = True) -> Dict:
|
|
"""Create a new schedule."""
|
|
schedule = {
|
|
"id": self.next_id,
|
|
"time": time,
|
|
"book": book,
|
|
"enabled": enabled,
|
|
"created_at": datetime.now().isoformat(),
|
|
"next_run": None
|
|
}
|
|
|
|
self.schedules.append(schedule)
|
|
self.next_id += 1
|
|
self._save()
|
|
|
|
return schedule
|
|
|
|
def delete(self, schedule_id: int):
|
|
"""Delete a schedule."""
|
|
self.schedules = [s for s in self.schedules if s["id"] != schedule_id]
|
|
self._save()
|
|
|
|
def update(self, schedule_id: int, enabled: bool = None, book: str = None):
|
|
"""Update a schedule."""
|
|
for schedule in self.schedules:
|
|
if schedule["id"] == schedule_id:
|
|
if enabled is not None:
|
|
schedule["enabled"] = enabled
|
|
if book is not None:
|
|
schedule["book"] = book
|
|
self._save()
|
|
break
|
|
```
|
|
|
|
---
|
|
|
|
## Tarefa 8: Dockerfile e Build
|
|
|
|
**Arquivos:**
|
|
- Criar: `backend/Dockerfile`
|
|
|
|
**Interfaces:**
|
|
- Consumes: N/A
|
|
- Produces: Imagem Docker funcional para todos os serviços
|
|
|
|
- [ ] **Passo 1: Criar Dockerfile**
|
|
|
|
```dockerfile
|
|
FROM python:3.11-slim
|
|
|
|
WORKDIR /app
|
|
|
|
# Install system dependencies
|
|
RUN apt-get update && apt-get install -y \
|
|
build-essential \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Copy requirements
|
|
COPY requirements.txt .
|
|
|
|
# Install Python dependencies
|
|
RUN pip install --no-cache-dir -r requirements.txt
|
|
|
|
# Create directories
|
|
RUN mkdir -p /app/uploads /app/index /app/schedules.json
|
|
|
|
# Copy application
|
|
COPY app/ ./app/
|
|
COPY frontend/ ./frontend/
|
|
|
|
# Expose ports
|
|
EXPOSE 8000
|
|
|
|
# Run the application
|
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
```
|
|
|
|
---
|
|
|
|
## Tarefa 9: Build e Testes
|
|
|
|
**Arquivos:**
|
|
- N/A
|
|
|
|
**Interfaces:**
|
|
- Consumes: Todo o código implementado
|
|
- Produces: Sistema funcional testado
|
|
|
|
- [ ] **Passo 1: Build da aplicação**
|
|
|
|
```bash
|
|
cd backend
|
|
docker-compose up --build -d
|
|
```
|
|
|
|
- [ ] **Passo 2: Indexar livros**
|
|
|
|
```bash
|
|
docker-compose run --rm web python scripts/index_books.py
|
|
```
|
|
|
|
- [ ] **Passo 3: Verificar logs**
|
|
|
|
```bash
|
|
docker-compose logs -f web
|
|
docker-compose logs -f celery
|
|
docker-compose logs -f celery-beat
|
|
```
|
|
|
|
- [ ] **Passo 4: Testar API**
|
|
|
|
```bash
|
|
curl http://localhost:8000/api/books
|
|
curl -X POST http://localhost:8000/api/chat \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"message": "O que é espiritismo?"}'
|
|
```
|
|
|
|
- [ ] **Passo 5: Testar index.html**
|
|
|
|
```bash
|
|
# Acessar: http://localhost:8000
|
|
```
|
|
|
|
- [ ] **Passo 6: Iniciar bot Telegram**
|
|
|
|
```bash
|
|
# Iniciar bot separadamente
|
|
docker-compose run --rm web python -m app.services.bot_service
|
|
```
|
|
|
|
---
|
|
|
|
## Review Focus
|
|
|
|
1. **Consulta web**: Sistema retorna resposta com contexto e fontes
|
|
2. **Upload PDF**: PDF uploadado é indexado automaticamente
|
|
3. **Agendamento Telegram**: Mensagens enviadas no horário correto
|
|
4. **Indexação RAG**: Livros processados corretamente em chunks
|
|
5. **Integração web-bot**: Agendamentos funcionam em ambas as interfaces
|
|
|
|
---
|
|
|
|
## Checklist de Aceitação
|
|
|
|
- [ ] Sistema iniciado com `docker-compose up`
|
|
- [ ] 6 livros indexados no Qdrant
|
|
- [ ] API web responde a consultas
|
|
- [ ] Interface web funcional para chat
|
|
- [ ] Upload de novos PDFs funciona
|
|
- [ ] Agendamentos podem ser criados no web
|
|
- [ ] Agendamentos aparecem no bot Telegram
|
|
- [ ] Mensagens são enviadas no horário correto
|
|
- [ ] Sistema rodando com 0 erros
|