feat: implementar sistema RAG Espiritismo completo

- 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
This commit is contained in:
lgcosta committed 2026-11-01 10:56:17 -03:00
commit b9c0c66d21
56 files changed
+5793

No files matched your search

+172
View File
@@ -0,0 +1,172 @@
* {
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;
}
+263
View File
@@ -0,0 +1,263 @@
// API base URL
const API_BASE = '/api';
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadBooks();
loadSchedules();
// Event listeners
document.getElementById('send-btn').addEventListener('click', sendChatMessage);
document.getElementById('chat-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendChatMessage();
});
document.getElementById('upload-btn').addEventListener('click', uploadPDF);
document.getElementById('add-schedule-btn').addEventListener('click', addSchedule);
});
async function sendChatMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) return;
// Add user message to chat
addMessage(message, 'user');
input.value = '';
// Show loading
showLoading();
try {
const response = await fetch(`${API_BASE}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, top_k: 3 }),
});
if (!response.ok) {
throw new Error('Erro ao enviar mensagem');
}
const data = await response.json();
// Remove loading
hideLoading();
// Add bot response
addMessage(data.response, 'bot');
// Add sources
if (data.sources && data.sources.length > 0) {
const sourceText = data.sources.map(s => `📖 ${s.metadata.title} (chunk ${s.metadata.chunk_id})`).join('\n');
addMessage(`Fontes: ${sourceText}`, 'bot', true);
}
} catch (error) {
hideLoading();
addMessage(`❌ Erro: ${error.message}`, 'bot');
}
}
function addMessage(content, type, isSource = false) {
const container = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;
if (isSource) {
messageDiv.style.fontSize = '0.85rem';
}
messageDiv.innerHTML = `<div class="message-content">${content}</div>`;
container.appendChild(messageDiv);
container.scrollTop = container.scrollHeight;
}
function showLoading() {
const container = document.getElementById('chat-messages');
const loadingDiv = document.createElement('div');
loadingDiv.id = 'chat-loading';
loadingDiv.className = 'message bot';
loadingDiv.innerHTML = '<div class="message-content">Carregando...</div>';
container.appendChild(loadingDiv);
container.scrollTop = container.scrollHeight;
}
function hideLoading() {
const loading = document.getElementById('chat-loading');
if (loading) loading.remove();
}
async function loadBooks() {
try {
const response = await fetch(`${API_BASE}/books`);
const books = await response.json();
const booksList = document.getElementById('books-list');
booksList.innerHTML = '';
if (books.length === 0) {
booksList.innerHTML = '<p style="color: #666;">Nenhum livro encontrado.</p>';
return;
}
books.forEach(book => {
const bookDiv = document.createElement('div');
bookDiv.className = 'book-item';
bookDiv.innerHTML = `
<div class="book-icon">📖</div>
<div class="book-name">${book.title}</div>
`;
booksList.appendChild(bookDiv);
});
// Update book dropdown
const select = document.getElementById('schedule-book');
select.innerHTML = '<option value="">Todos os livros</option>';
books.forEach(book => {
const option = document.createElement('option');
option.value = book.title;
option.textContent = book.title;
select.appendChild(option);
});
} catch (error) {
console.error('Erro ao carregar livros:', error);
}
}
async function uploadPDF() {
const fileInput = document.getElementById('file-input');
const file = fileInput.files[0];
if (!file) {
alert('Selecione um arquivo PDF primeiro');
return;
}
const statusDiv = document.getElementById('upload-status');
statusDiv.textContent = 'Enviando...';
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${API_BASE}/upload`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Erro ao fazer upload');
}
const data = await response.json();
statusDiv.textContent = `✅ ${data.filename} enviado com sucesso!`;
statusDiv.style.color = '#4caf50';
setTimeout(() => {
statusDiv.textContent = '';
fileInput.value = '';
}, 3000);
// Reload books
loadBooks();
} catch (error) {
statusDiv.textContent = `❌ Erro: ${error.message}`;
statusDiv.style.color = '#f44336';
}
}
async function loadSchedules() {
try {
const response = await fetch(`${API_BASE}/schedules`);
const schedules = await response.json();
renderSchedules(schedules);
} catch (error) {
console.error('Erro ao carregar agendamentos:', error);
}
}
function renderSchedules(schedules) {
const container = document.getElementById('schedules-list');
container.innerHTML = '';
if (schedules.length === 0) {
container.innerHTML = '<p style="color: #666;">Nenhum agendamento.</p>';
return;
}
schedules.forEach(schedule => {
const div = document.createElement('div');
div.className = 'schedule-item';
div.innerHTML = `
<div class="schedule-info">
<span class="schedule-time">🕐 ${formatDateTime(schedule.time)}</span>
<span class="schedule-book">${schedule.book || 'Todos os livros'}</span>
</div>
<button class="schedule-delete" data-id="${schedule.id}">🗑️</button>
`;
container.appendChild(div);
// Add delete handler
div.querySelector('.schedule-delete').addEventListener('click', () => deleteSchedule(schedule.id));
});
}
async function addSchedule() {
const timeInput = document.getElementById('schedule-time');
const bookSelect = document.getElementById('schedule-book');
if (!timeInput.value) {
alert('Selecione uma data e hora');
return;
}
try {
const response = await fetch(`${API_BASE}/schedules`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
time: timeInput.value,
book: bookSelect.value || null,
enabled: true,
}),
});
if (!response.ok) {
throw new Error('Erro ao criar agendamento');
}
const schedule = await response.json();
loadSchedules();
timeInput.value = '';
bookSelect.value = '';
} catch (error) {
alert(`Erro: ${error.message}`);
}
}
async function deleteSchedule(scheduleId) {
if (!confirm('Tem certeza que deseja excluir este agendamento?')) {
return;
}
try {
const response = await fetch(`${API_BASE}/schedules/${scheduleId}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Erro ao excluir agendamento');
}
loadSchedules();
} catch (error) {
alert(`Erro: ${error.message}`);
}
}
function formatDateTime(dateString) {
const date = new Date(dateString);
return date.toLocaleString('pt-BR');
}