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:
lgcosta committed 2026-11-01 12:00:08 -03:00
1 parent 67739dcf7e
commit 7af7ba530d
19 files changed
+342 -250

No files matched your search

+111 -101
View File
@@ -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"}
-31
View File
@@ -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
+38 -1
View File
@@ -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
+214
View File
@@ -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)
+45
View File
@@ -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
+181
View File
@@ -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
],
}
+73
View File
@@ -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
},
}
+19
View File
@@ -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
+77
View File
@@ -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