diff --git a/blueprints/scheduled_messages/__init__.py b/blueprints/scheduled_messages/__init__.py index 96f0bfd..10a921e 100644 --- a/blueprints/scheduled_messages/__init__.py +++ b/blueprints/scheduled_messages/__init__.py @@ -23,6 +23,7 @@ def _serialize(msg: ScheduledMessage) -> dict: "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(), @@ -89,6 +90,8 @@ async def create_message(): user_id = get_jwt_identity() + use_agent = bool(data.get("use_agent", False)) + msg = await ScheduledMessage.create( recipient=recipient, channel=channel_enum, @@ -96,6 +99,7 @@ async def create_message(): subject=subject, scheduled_at=scheduled_at, recurrence=recurrence_enum, + use_agent=use_agent, created_by_id=user_id, ) return jsonify(_serialize(msg)), 201 @@ -131,6 +135,8 @@ async def update_message(msg_id: str): 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"]) diff --git a/blueprints/scheduled_messages/models.py b/blueprints/scheduled_messages/models.py index f1a02f3..0be6081 100644 --- a/blueprints/scheduled_messages/models.py +++ b/blueprints/scheduled_messages/models.py @@ -36,6 +36,7 @@ class ScheduledMessage(Model): recurrence = fields.CharEnumField( enum_type=Recurrence, max_length=20, default=Recurrence.NONE ) + use_agent = fields.BooleanField(default=False) error_message = fields.TextField(null=True) created_by = fields.ForeignKeyField( "models.User", related_name="scheduled_messages" diff --git a/blueprints/scheduled_messages/scheduler.py b/blueprints/scheduled_messages/scheduler.py index 54c177b..4e3da45 100644 --- a/blueprints/scheduled_messages/scheduler.py +++ b/blueprints/scheduled_messages/scheduler.py @@ -16,6 +16,19 @@ RECURRENCE_DELTAS = { } +async def _run_agent(prompt: str) -> str: + """Run a prompt through the LangChain agent and return the response text.""" + from blueprints.conversation.agents import main_agent + from blueprints.conversation.prompts import SIMBA_SYSTEM_PROMPT + + messages_payload = [ + {"role": "system", "content": SIMBA_SYSTEM_PROMPT}, + {"role": "user", "content": prompt}, + ] + response = await main_agent.ainvoke({"messages": messages_payload}) + return response.get("messages", [])[-1].content + + async def _schedule_next_occurrence(msg: ScheduledMessage): """Create the next pending occurrence for a recurring message.""" delta = RECURRENCE_DELTAS.get(msg.recurrence) @@ -35,6 +48,7 @@ async def _schedule_next_occurrence(msg: ScheduledMessage): subject=msg.subject, scheduled_at=next_at, recurrence=msg.recurrence, + use_agent=msg.use_agent, created_by_id=msg.created_by_id, ) logger.info( @@ -56,11 +70,16 @@ async def scheduled_messages_loop(): for msg in due: try: + send_content = msg.content + + if msg.use_agent: + send_content = await _run_agent(msg.content) + if msg.channel == MessageChannel.IMESSAGE: from blueprints.imessage import send_imessage from utils.strip_markdown import strip_markdown - await send_imessage(msg.recipient, strip_markdown(msg.content)) + await send_imessage(msg.recipient, strip_markdown(send_content)) elif msg.channel == MessageChannel.EMAIL: from blueprints.email import send_email_reply @@ -68,7 +87,7 @@ async def scheduled_messages_loop(): await send_email_reply( to=msg.recipient, subject=msg.subject or "(no subject)", - body=msg.content, + body=send_content, ) msg.status = MessageStatus.SENT diff --git a/raggr-frontend/src/api/scheduledMessageService.ts b/raggr-frontend/src/api/scheduledMessageService.ts index 45173da..e015cc4 100644 --- a/raggr-frontend/src/api/scheduledMessageService.ts +++ b/raggr-frontend/src/api/scheduledMessageService.ts @@ -9,6 +9,7 @@ export interface ScheduledMessage { scheduled_at: string; status: "pending" | "sent" | "failed" | "cancelled"; recurrence: "none" | "daily" | "weekly" | "monthly"; + use_agent: boolean; error_message: string | null; created_at: string; updated_at: string; @@ -21,6 +22,7 @@ export interface CreateScheduledMessage { subject?: string; scheduled_at: string; recurrence?: "none" | "daily" | "weekly" | "monthly"; + use_agent?: boolean; } class ScheduledMessageService { diff --git a/raggr-frontend/src/components/ScheduledMessagesPanel.tsx b/raggr-frontend/src/components/ScheduledMessagesPanel.tsx index 3fad381..2396d8f 100644 --- a/raggr-frontend/src/components/ScheduledMessagesPanel.tsx +++ b/raggr-frontend/src/components/ScheduledMessagesPanel.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { X, Clock, Send, Trash2, XCircle, RotateCcw, Repeat } from "lucide-react"; +import { X, Clock, Send, Trash2, XCircle, RotateCcw, Repeat, Bot } from "lucide-react"; import { cn } from "../lib/utils"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; @@ -37,6 +37,7 @@ export const ScheduledMessagesPanel = ({ onClose }: Props) => { const [content, setContent] = useState(""); const [scheduledAt, setScheduledAt] = useState(""); const [recurrence, setRecurrence] = useState<"none" | "daily" | "weekly" | "monthly">("none"); + const [useAgent, setUseAgent] = useState(false); const [error, setError] = useState(""); const [submitting, setSubmitting] = useState(false); @@ -59,6 +60,7 @@ export const ScheduledMessagesPanel = ({ onClose }: Props) => { content, scheduled_at: new Date(scheduledAt).toISOString(), recurrence, + use_agent: useAgent, }; if (channel === "email") data.subject = subject; await scheduledMessageService.create(data); @@ -67,6 +69,7 @@ export const ScheduledMessagesPanel = ({ onClose }: Props) => { setContent(""); setScheduledAt(""); setRecurrence("none"); + setUseAgent(false); refresh(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to schedule message"); @@ -189,6 +192,24 @@ export const ScheduledMessagesPanel = ({ onClose }: Props) => { ))} +
+ + + {useAgent ? "Content is a prompt — Simba's response will be sent" : "Content sent as-is"} + +
+ {channel === "email" && ( {