9bccac82f3
When use_agent is enabled, the scheduler runs the message content as a prompt through the LangChain agent and sends Simba's response instead of the raw content. Frontend adds an Ask Simba toggle with visual indicator. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
172 lines
5.8 KiB
Python
172 lines
5.8 KiB
Python
import logging
|
|
from datetime import datetime, timezone
|
|
|
|
from quart import Blueprint, request, jsonify
|
|
|
|
from blueprints.users.decorators import admin_required
|
|
from .models import ScheduledMessage, MessageChannel, MessageStatus, Recurrence
|
|
|
|
scheduled_messages_blueprint = Blueprint(
|
|
"scheduled_messages_api", __name__, url_prefix="/api/scheduled-messages"
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _serialize(msg: ScheduledMessage) -> dict:
|
|
return {
|
|
"id": str(msg.id),
|
|
"recipient": msg.recipient,
|
|
"channel": msg.channel.value,
|
|
"content": msg.content,
|
|
"subject": msg.subject,
|
|
"scheduled_at": msg.scheduled_at.isoformat(),
|
|
"status": msg.status.value,
|
|
"recurrence": msg.recurrence.value,
|
|
"use_agent": msg.use_agent,
|
|
"error_message": msg.error_message,
|
|
"created_at": msg.created_at.isoformat(),
|
|
"updated_at": msg.updated_at.isoformat(),
|
|
}
|
|
|
|
|
|
@scheduled_messages_blueprint.route("/", methods=["GET"])
|
|
@admin_required
|
|
async def list_messages():
|
|
messages = await ScheduledMessage.all().order_by("-scheduled_at")
|
|
return jsonify([_serialize(m) for m in messages])
|
|
|
|
|
|
@scheduled_messages_blueprint.route("/", methods=["POST"])
|
|
@admin_required
|
|
async def create_message():
|
|
data = await request.get_json()
|
|
if not data:
|
|
return jsonify({"error": "Invalid payload"}), 400
|
|
|
|
recipient = (data.get("recipient") or "").strip()
|
|
channel = data.get("channel")
|
|
content = (data.get("content") or "").strip()
|
|
subject = (data.get("subject") or "").strip() or None
|
|
scheduled_at_str = data.get("scheduled_at")
|
|
|
|
recurrence_str = data.get("recurrence", "none")
|
|
|
|
if not recipient or not channel or not content or not scheduled_at_str:
|
|
return jsonify(
|
|
{"error": "recipient, channel, content, and scheduled_at are required"}
|
|
), 400
|
|
|
|
try:
|
|
channel_enum = MessageChannel(channel)
|
|
except ValueError:
|
|
return jsonify(
|
|
{"error": f"Invalid channel: {channel}. Must be 'imessage' or 'email'"}
|
|
), 400
|
|
|
|
try:
|
|
recurrence_enum = Recurrence(recurrence_str)
|
|
except ValueError:
|
|
return jsonify(
|
|
{
|
|
"error": f"Invalid recurrence: {recurrence_str}. Must be 'none', 'daily', 'weekly', or 'monthly'"
|
|
}
|
|
), 400
|
|
|
|
if channel_enum == MessageChannel.EMAIL and not subject:
|
|
return jsonify({"error": "subject is required for email messages"}), 400
|
|
|
|
try:
|
|
scheduled_at = datetime.fromisoformat(scheduled_at_str)
|
|
if scheduled_at.tzinfo is None:
|
|
scheduled_at = scheduled_at.replace(tzinfo=timezone.utc)
|
|
except ValueError:
|
|
return jsonify({"error": "Invalid scheduled_at format"}), 400
|
|
|
|
if scheduled_at <= datetime.now(timezone.utc):
|
|
return jsonify({"error": "scheduled_at must be in the future"}), 400
|
|
|
|
from quart_jwt_extended import get_jwt_identity
|
|
|
|
user_id = get_jwt_identity()
|
|
|
|
use_agent = bool(data.get("use_agent", False))
|
|
|
|
msg = await ScheduledMessage.create(
|
|
recipient=recipient,
|
|
channel=channel_enum,
|
|
content=content,
|
|
subject=subject,
|
|
scheduled_at=scheduled_at,
|
|
recurrence=recurrence_enum,
|
|
use_agent=use_agent,
|
|
created_by_id=user_id,
|
|
)
|
|
return jsonify(_serialize(msg)), 201
|
|
|
|
|
|
@scheduled_messages_blueprint.route("/<msg_id>", methods=["PUT"])
|
|
@admin_required
|
|
async def update_message(msg_id: str):
|
|
msg = await ScheduledMessage.get_or_none(id=msg_id)
|
|
if not msg:
|
|
return jsonify({"error": "Not found"}), 404
|
|
|
|
if msg.status != MessageStatus.PENDING:
|
|
return jsonify({"error": "Can only update pending messages"}), 400
|
|
|
|
data = await request.get_json()
|
|
if not data:
|
|
return jsonify({"error": "Invalid payload"}), 400
|
|
|
|
if "recipient" in data:
|
|
msg.recipient = data["recipient"].strip()
|
|
if "channel" in data:
|
|
try:
|
|
msg.channel = MessageChannel(data["channel"])
|
|
except ValueError:
|
|
return jsonify({"error": f"Invalid channel: {data['channel']}"}), 400
|
|
if "content" in data:
|
|
msg.content = data["content"].strip()
|
|
if "subject" in data:
|
|
msg.subject = data["subject"].strip() or None
|
|
if "recurrence" in data:
|
|
try:
|
|
msg.recurrence = Recurrence(data["recurrence"])
|
|
except ValueError:
|
|
return jsonify({"error": f"Invalid recurrence: {data['recurrence']}"}), 400
|
|
if "use_agent" in data:
|
|
msg.use_agent = bool(data["use_agent"])
|
|
if "scheduled_at" in data:
|
|
try:
|
|
scheduled_at = datetime.fromisoformat(data["scheduled_at"])
|
|
if scheduled_at.tzinfo is None:
|
|
scheduled_at = scheduled_at.replace(tzinfo=timezone.utc)
|
|
if scheduled_at <= datetime.now(timezone.utc):
|
|
return jsonify({"error": "scheduled_at must be in the future"}), 400
|
|
msg.scheduled_at = scheduled_at
|
|
except ValueError:
|
|
return jsonify({"error": "Invalid scheduled_at format"}), 400
|
|
if "status" in data and data["status"] == "cancelled":
|
|
msg.status = MessageStatus.CANCELLED
|
|
|
|
if msg.channel == MessageChannel.EMAIL and not msg.subject:
|
|
return jsonify({"error": "subject is required for email messages"}), 400
|
|
|
|
await msg.save()
|
|
return jsonify(_serialize(msg))
|
|
|
|
|
|
@scheduled_messages_blueprint.route("/<msg_id>", methods=["DELETE"])
|
|
@admin_required
|
|
async def delete_message(msg_id: str):
|
|
msg = await ScheduledMessage.get_or_none(id=msg_id)
|
|
if not msg:
|
|
return jsonify({"error": "Not found"}), 404
|
|
|
|
if msg.status not in (MessageStatus.PENDING, MessageStatus.CANCELLED):
|
|
return jsonify({"error": "Can only delete pending or cancelled messages"}), 400
|
|
|
|
await msg.delete()
|
|
return jsonify({"status": "deleted"})
|