Add "Ask Simba" option to scheduled messages
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>
This commit is contained in:
@@ -23,6 +23,7 @@ def _serialize(msg: ScheduledMessage) -> dict:
|
|||||||
"scheduled_at": msg.scheduled_at.isoformat(),
|
"scheduled_at": msg.scheduled_at.isoformat(),
|
||||||
"status": msg.status.value,
|
"status": msg.status.value,
|
||||||
"recurrence": msg.recurrence.value,
|
"recurrence": msg.recurrence.value,
|
||||||
|
"use_agent": msg.use_agent,
|
||||||
"error_message": msg.error_message,
|
"error_message": msg.error_message,
|
||||||
"created_at": msg.created_at.isoformat(),
|
"created_at": msg.created_at.isoformat(),
|
||||||
"updated_at": msg.updated_at.isoformat(),
|
"updated_at": msg.updated_at.isoformat(),
|
||||||
@@ -89,6 +90,8 @@ async def create_message():
|
|||||||
|
|
||||||
user_id = get_jwt_identity()
|
user_id = get_jwt_identity()
|
||||||
|
|
||||||
|
use_agent = bool(data.get("use_agent", False))
|
||||||
|
|
||||||
msg = await ScheduledMessage.create(
|
msg = await ScheduledMessage.create(
|
||||||
recipient=recipient,
|
recipient=recipient,
|
||||||
channel=channel_enum,
|
channel=channel_enum,
|
||||||
@@ -96,6 +99,7 @@ async def create_message():
|
|||||||
subject=subject,
|
subject=subject,
|
||||||
scheduled_at=scheduled_at,
|
scheduled_at=scheduled_at,
|
||||||
recurrence=recurrence_enum,
|
recurrence=recurrence_enum,
|
||||||
|
use_agent=use_agent,
|
||||||
created_by_id=user_id,
|
created_by_id=user_id,
|
||||||
)
|
)
|
||||||
return jsonify(_serialize(msg)), 201
|
return jsonify(_serialize(msg)), 201
|
||||||
@@ -131,6 +135,8 @@ async def update_message(msg_id: str):
|
|||||||
msg.recurrence = Recurrence(data["recurrence"])
|
msg.recurrence = Recurrence(data["recurrence"])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return jsonify({"error": f"Invalid recurrence: {data['recurrence']}"}), 400
|
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:
|
if "scheduled_at" in data:
|
||||||
try:
|
try:
|
||||||
scheduled_at = datetime.fromisoformat(data["scheduled_at"])
|
scheduled_at = datetime.fromisoformat(data["scheduled_at"])
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class ScheduledMessage(Model):
|
|||||||
recurrence = fields.CharEnumField(
|
recurrence = fields.CharEnumField(
|
||||||
enum_type=Recurrence, max_length=20, default=Recurrence.NONE
|
enum_type=Recurrence, max_length=20, default=Recurrence.NONE
|
||||||
)
|
)
|
||||||
|
use_agent = fields.BooleanField(default=False)
|
||||||
error_message = fields.TextField(null=True)
|
error_message = fields.TextField(null=True)
|
||||||
created_by = fields.ForeignKeyField(
|
created_by = fields.ForeignKeyField(
|
||||||
"models.User", related_name="scheduled_messages"
|
"models.User", related_name="scheduled_messages"
|
||||||
|
|||||||
@@ -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):
|
async def _schedule_next_occurrence(msg: ScheduledMessage):
|
||||||
"""Create the next pending occurrence for a recurring message."""
|
"""Create the next pending occurrence for a recurring message."""
|
||||||
delta = RECURRENCE_DELTAS.get(msg.recurrence)
|
delta = RECURRENCE_DELTAS.get(msg.recurrence)
|
||||||
@@ -35,6 +48,7 @@ async def _schedule_next_occurrence(msg: ScheduledMessage):
|
|||||||
subject=msg.subject,
|
subject=msg.subject,
|
||||||
scheduled_at=next_at,
|
scheduled_at=next_at,
|
||||||
recurrence=msg.recurrence,
|
recurrence=msg.recurrence,
|
||||||
|
use_agent=msg.use_agent,
|
||||||
created_by_id=msg.created_by_id,
|
created_by_id=msg.created_by_id,
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -56,11 +70,16 @@ async def scheduled_messages_loop():
|
|||||||
|
|
||||||
for msg in due:
|
for msg in due:
|
||||||
try:
|
try:
|
||||||
|
send_content = msg.content
|
||||||
|
|
||||||
|
if msg.use_agent:
|
||||||
|
send_content = await _run_agent(msg.content)
|
||||||
|
|
||||||
if msg.channel == MessageChannel.IMESSAGE:
|
if msg.channel == MessageChannel.IMESSAGE:
|
||||||
from blueprints.imessage import send_imessage
|
from blueprints.imessage import send_imessage
|
||||||
from utils.strip_markdown import strip_markdown
|
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:
|
elif msg.channel == MessageChannel.EMAIL:
|
||||||
from blueprints.email import send_email_reply
|
from blueprints.email import send_email_reply
|
||||||
@@ -68,7 +87,7 @@ async def scheduled_messages_loop():
|
|||||||
await send_email_reply(
|
await send_email_reply(
|
||||||
to=msg.recipient,
|
to=msg.recipient,
|
||||||
subject=msg.subject or "(no subject)",
|
subject=msg.subject or "(no subject)",
|
||||||
body=msg.content,
|
body=send_content,
|
||||||
)
|
)
|
||||||
|
|
||||||
msg.status = MessageStatus.SENT
|
msg.status = MessageStatus.SENT
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface ScheduledMessage {
|
|||||||
scheduled_at: string;
|
scheduled_at: string;
|
||||||
status: "pending" | "sent" | "failed" | "cancelled";
|
status: "pending" | "sent" | "failed" | "cancelled";
|
||||||
recurrence: "none" | "daily" | "weekly" | "monthly";
|
recurrence: "none" | "daily" | "weekly" | "monthly";
|
||||||
|
use_agent: boolean;
|
||||||
error_message: string | null;
|
error_message: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
@@ -21,6 +22,7 @@ export interface CreateScheduledMessage {
|
|||||||
subject?: string;
|
subject?: string;
|
||||||
scheduled_at: string;
|
scheduled_at: string;
|
||||||
recurrence?: "none" | "daily" | "weekly" | "monthly";
|
recurrence?: "none" | "daily" | "weekly" | "monthly";
|
||||||
|
use_agent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScheduledMessageService {
|
class ScheduledMessageService {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
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 { cn } from "../lib/utils";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
@@ -37,6 +37,7 @@ export const ScheduledMessagesPanel = ({ onClose }: Props) => {
|
|||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const [scheduledAt, setScheduledAt] = useState("");
|
const [scheduledAt, setScheduledAt] = useState("");
|
||||||
const [recurrence, setRecurrence] = useState<"none" | "daily" | "weekly" | "monthly">("none");
|
const [recurrence, setRecurrence] = useState<"none" | "daily" | "weekly" | "monthly">("none");
|
||||||
|
const [useAgent, setUseAgent] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ export const ScheduledMessagesPanel = ({ onClose }: Props) => {
|
|||||||
content,
|
content,
|
||||||
scheduled_at: new Date(scheduledAt).toISOString(),
|
scheduled_at: new Date(scheduledAt).toISOString(),
|
||||||
recurrence,
|
recurrence,
|
||||||
|
use_agent: useAgent,
|
||||||
};
|
};
|
||||||
if (channel === "email") data.subject = subject;
|
if (channel === "email") data.subject = subject;
|
||||||
await scheduledMessageService.create(data);
|
await scheduledMessageService.create(data);
|
||||||
@@ -67,6 +69,7 @@ export const ScheduledMessagesPanel = ({ onClose }: Props) => {
|
|||||||
setContent("");
|
setContent("");
|
||||||
setScheduledAt("");
|
setScheduledAt("");
|
||||||
setRecurrence("none");
|
setRecurrence("none");
|
||||||
|
setUseAgent(false);
|
||||||
refresh();
|
refresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to schedule message");
|
setError(err instanceof Error ? err.message : "Failed to schedule message");
|
||||||
@@ -189,6 +192,24 @@ export const ScheduledMessagesPanel = ({ onClose }: Props) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setUseAgent(!useAgent)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors cursor-pointer",
|
||||||
|
useAgent
|
||||||
|
? "bg-leaf-pale text-leaf-dark"
|
||||||
|
: "bg-sand-light/40 text-warm-gray hover:text-charcoal",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Bot size={12} />
|
||||||
|
Ask Simba
|
||||||
|
</button>
|
||||||
|
<span className="text-xs text-warm-gray">
|
||||||
|
{useAgent ? "Content is a prompt — Simba's response will be sent" : "Content sent as-is"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{channel === "email" && (
|
{channel === "email" && (
|
||||||
<Input
|
<Input
|
||||||
value={subject}
|
value={subject}
|
||||||
@@ -200,7 +221,7 @@ export const ScheduledMessagesPanel = ({ onClose }: Props) => {
|
|||||||
<textarea
|
<textarea
|
||||||
value={content}
|
value={content}
|
||||||
onChange={(e) => setContent(e.target.value)}
|
onChange={(e) => setContent(e.target.value)}
|
||||||
placeholder="Message content..."
|
placeholder={useAgent ? "Enter a prompt for Simba..." : "Message content..."}
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full rounded-xl border border-sand bg-cream-light px-3 py-2 text-sm text-charcoal placeholder:text-warm-gray/50 focus:outline-none focus:ring-2 focus:ring-leaf/30 resize-none"
|
className="w-full rounded-xl border border-sand bg-cream-light px-3 py-2 text-sm text-charcoal placeholder:text-warm-gray/50 focus:outline-none focus:ring-2 focus:ring-leaf/30 resize-none"
|
||||||
/>
|
/>
|
||||||
@@ -249,10 +270,15 @@ export const ScheduledMessagesPanel = ({ onClose }: Props) => {
|
|||||||
<TableCell className="text-xs truncate max-w-[140px]" title={msg.recipient}>
|
<TableCell className="text-xs truncate max-w-[140px]" title={msg.recipient}>
|
||||||
{msg.recipient}
|
{msg.recipient}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs truncate max-w-[180px]" title={msg.content}>
|
<TableCell className="text-xs max-w-[180px]" title={msg.content}>
|
||||||
{msg.content.length > 60
|
<div className="flex items-center gap-1">
|
||||||
? msg.content.slice(0, 60) + "..."
|
{msg.use_agent && <Bot size={10} className="text-leaf-dark shrink-0" />}
|
||||||
: msg.content}
|
<span className="truncate">
|
||||||
|
{msg.content.length > 60
|
||||||
|
? msg.content.slice(0, 60) + "..."
|
||||||
|
: msg.content}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs text-warm-gray">
|
<TableCell className="text-xs text-warm-gray">
|
||||||
{new Date(msg.scheduled_at).toLocaleString()}
|
{new Date(msg.scheduled_at).toLocaleString()}
|
||||||
|
|||||||
Reference in New Issue
Block a user