Skip to content

Commit 13f7681

Browse files
authored
Merge pull request #147 from NeurProjects/feature/dynamic-action-scheduling
Dynamic action scheduling
2 parents 4eb65b9 + 823a3e8 commit 13f7681

File tree

8 files changed

+82
-38
lines changed

8 files changed

+82
-38
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- AlterTable
2+
ALTER TABLE "actions" ADD COLUMN "startTime" TIMESTAMP(3);
3+
ALTER TABLE "actions" ADD COLUMN "name" VARCHAR(255);
4+
UPDATE "actions" SET name = description;

prisma/schema.prisma

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ model Action {
105105
stoppedBy Int[] // Array of rule IDs required to stop this action
106106
frequency Int? // Frequency in seconds (e.g., 3600 for 1 hour, 86400 for 1 day)
107107
maxExecutions Int? // Times to execute before stopping
108+
name String? @db.VarChar(255) // User defined name for the action
108109
description String @db.VarChar(255) // Human readable description of the action, or message to send to AI
109110
actionType String @db.VarChar(255) // Type of action (e.g., "call_function", "invoke_api")
110111
params Json? // JSON object for action parameters (e.g., inputs for the function)
@@ -118,6 +119,7 @@ model Action {
118119
priority Int @default(0) // Priority level for execution, higher numbers execute first
119120
createdAt DateTime @default(now())
120121
updatedAt DateTime @updatedAt
122+
startTime DateTime? // Time to start executing the action
121123
122124
user User @relation(fields: [userId], references: [id])
123125
conversation Conversation @relation(fields: [conversationId], references: [id])

src/ai/generic/action.tsx

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { ActionEmitter } from '@/components/action-emitter';
44
import { Card } from '@/components/ui/card';
55
import { verifyUser } from '@/server/actions/user';
66
import { dbCreateAction } from '@/server/db/queries';
7+
import { NO_CONFIRMATION_MESSAGE } from '@/lib/constants';
78

89
interface CreateActionResultProps {
910
id: string;
1011
description: string;
1112
frequency: number;
1213
maxExecutions: number | null;
14+
startTime: number | null;
1315
}
1416

1517
function getFrequencyLabel(frequency: number): string {
@@ -29,37 +31,31 @@ function getFrequencyLabel(frequency: number): string {
2931
}
3032
}
3133

32-
const NO_CONFIRMATION_MESSAGE = ' (Does not require confirmation)';
33-
function getNextExecutionTime(frequency: number): string {
34-
// TODO: improve this - currently server executes actions every 15 minutes
35-
const now = new Date();
34+
function getNextExecutionTime(startTime: number | null): string {
35+
if (startTime) {
36+
return new Date(startTime).toLocaleString();
37+
}
3638

37-
// Set to next 15 minute interval
38-
const next15 = new Date(now);
39-
next15.setMilliseconds(0);
40-
next15.setSeconds(0);
39+
// Set to the next minute interval
40+
const nextMinute = new Date();
41+
nextMinute.setMilliseconds(0); // Reset milliseconds
42+
nextMinute.setSeconds(0); // Reset seconds
4143

42-
const minutes = next15.getMinutes();
43-
const remainder = minutes % 15;
44+
const currentMinutes = nextMinute.getMinutes();
45+
nextMinute.setMinutes(currentMinutes + 1); // Move to the next minute
4446

45-
if (remainder === 0) {
46-
// Already on a quarter-hour mark; move to the next one
47-
next15.setMinutes(minutes + 15);
48-
} else {
49-
// Round up to the next quarter-hour
50-
next15.setMinutes(minutes + (15 - remainder));
51-
}
52-
return next15.toLocaleString();
47+
return nextMinute.toLocaleString();
5348
}
5449

5550
function CreateActionResult({
5651
id,
5752
description,
5853
frequency,
5954
maxExecutions,
55+
startTime
6056
}: CreateActionResultProps) {
6157
const frequencyLabel = getFrequencyLabel(frequency);
62-
const nextExecution = getNextExecutionTime(frequency);
58+
const nextExecution = getNextExecutionTime(startTime);
6359

6460
return (
6561
<Card className="bg-card p-6">
@@ -114,6 +110,11 @@ const createActionTool = {
114110
conversationId: z
115111
.string()
116112
.describe('Conversation that the action belongs to'),
113+
name: z
114+
.string()
115+
.describe(
116+
'Shorthand human readable name to classify the action.',
117+
),
117118
description: z
118119
.string()
119120
.describe(
@@ -128,6 +129,10 @@ const createActionTool = {
128129
.number()
129130
.optional()
130131
.describe('Max number of times the action can be executed'),
132+
startTimeOffset: z
133+
.number()
134+
.optional()
135+
.describe('Offset in milliseconds for how long to wait before starting the action. Useful for scheduling actions in the future, e.g. 1 hour from now = 3600000'),
131136
}),
132137
execute: async function (
133138
params: z.infer<typeof this.parameters>,
@@ -140,9 +145,13 @@ const createActionTool = {
140145
return { success: false, error: 'Unauthorized' };
141146
}
142147

148+
console.log('action params');
149+
console.dir(params);
150+
143151
const action = await dbCreateAction({
144152
userId,
145153
conversationId: params.conversationId,
154+
name: params.name,
146155
description: `${params.description}${NO_CONFIRMATION_MESSAGE}`,
147156
actionType: 'default',
148157
frequency: params.frequency,
@@ -160,6 +169,7 @@ const createActionTool = {
160169
lastExecutedAt: null,
161170
lastFailureAt: null,
162171
lastSuccessAt: null,
172+
startTime: params.startTimeOffset ? new Date(Date.now() + params.startTimeOffset) : null,
163173
});
164174

165175
if (!action) {
@@ -197,11 +207,12 @@ const createActionTool = {
197207
);
198208
}
199209

200-
const { id, description, frequency, maxExecutions } = typedResult.data as {
210+
const { id, description, frequency, maxExecutions, startTime } = typedResult.data as {
201211
id: string;
202212
description: string;
203213
frequency: number;
204214
maxExecutions: number | null;
215+
startTime: number | null;
205216
};
206217

207218
return (
@@ -212,6 +223,7 @@ const createActionTool = {
212223
description={description}
213224
frequency={frequency}
214225
maxExecutions={maxExecutions}
226+
startTime={startTime}
215227
/>
216228
</>
217229
);

src/app/api/cron/15-min/route.ts renamed to src/app/api/cron/minute/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export async function GET(request: Request) {
1212
});
1313
}
1414

15-
// Quarter-hour cron job
15+
// Minute cron job
1616
// Get all Actions that are not completed or paused
1717
const actions = await dbGetActions({
1818
triggered: true,
@@ -22,7 +22,7 @@ export async function GET(request: Request) {
2222

2323
console.log(`[cron/action] Fetched ${actions.length} actions`);
2424

25-
// This job runs every 15 minutes, but we only need to process actions that are ready to be processed, based on their frequency
25+
// This job runs every minute minute, but we only need to process actions that are ready to be processed, based on their frequency
2626
// Filter the actions to only include those that are ready to be processed based on their lastExecutedAt and frequency
2727
const now = new Date();
2828
const actionsToProcess = actions.filter((action) => {

src/components/dashboard/app-sidebar-automations.tsx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { useActions } from '@/hooks/use-actions';
4848
import { useUser } from '@/hooks/use-user';
4949
import { EVENTS } from '@/lib/events';
5050
import { cn } from '@/lib/utils';
51+
import { NO_CONFIRMATION_MESSAGE } from '@/lib/constants';
5152

5253
interface ActionMenuItemProps {
5354
action: Action;
@@ -62,10 +63,12 @@ const ActionMenuItem = ({ action, onDelete, onEdit }: ActionMenuItemProps) => {
6263
const router = useRouter();
6364
const [isEditing, setIsEditing] = useState(false);
6465
const [isLoading, setIsLoading] = useState(false);
66+
6567
const [formData, setFormData] = useState({
66-
description: action.description,
67-
frequency: action.frequency || 0,
68-
maxExecutions: action.maxExecutions || 0,
68+
name: action.name || action.description,
69+
description: action.description.replace(NO_CONFIRMATION_MESSAGE, ''),
70+
frequency: action.frequency ?? null,
71+
maxExecutions: action.maxExecutions ?? null,
6972
});
7073

7174
const handleDelete = async () => {
@@ -93,9 +96,10 @@ const ActionMenuItem = ({ action, onDelete, onEdit }: ActionMenuItemProps) => {
9396

9497
try {
9598
const result = await onEdit(action.id, {
96-
description: formData.description,
97-
frequency: formData.frequency || null,
98-
maxExecutions: formData.maxExecutions || null,
99+
name: formData.name,
100+
description: formData.description + NO_CONFIRMATION_MESSAGE,
101+
frequency: formData.frequency,
102+
maxExecutions: formData.maxExecutions,
99103
});
100104

101105
if (result?.success) {
@@ -118,7 +122,7 @@ const ActionMenuItem = ({ action, onDelete, onEdit }: ActionMenuItemProps) => {
118122
<SidebarMenuItem>
119123
<SidebarMenuButton asChild>
120124
<Link href={`/chat/${action.conversationId}`}>
121-
<span>{action.description}</span>
125+
<span>{action.name}</span>
122126
</Link>
123127
</SidebarMenuButton>
124128
<DropdownMenu>
@@ -153,6 +157,21 @@ const ActionMenuItem = ({ action, onDelete, onEdit }: ActionMenuItemProps) => {
153157
<DialogTitle>Edit Automation</DialogTitle>
154158
</DialogHeader>
155159
<div className="space-y-4 py-4">
160+
<div className="space-y-2">
161+
<Label htmlFor="name">Name</Label>
162+
<Input
163+
id="name"
164+
value={formData.name}
165+
onChange={(e) =>
166+
setFormData((prev) => ({
167+
...prev,
168+
name: e.target.value,
169+
}))
170+
}
171+
placeholder="Enter action name"
172+
disabled={isLoading}
173+
/>
174+
</div>
156175
<div className="space-y-2">
157176
<Label htmlFor="description">Message</Label>
158177
<Input
@@ -173,11 +192,11 @@ const ActionMenuItem = ({ action, onDelete, onEdit }: ActionMenuItemProps) => {
173192
<Input
174193
id="frequency"
175194
type="number"
176-
value={formData.frequency}
195+
value={formData.frequency ?? ''}
177196
onChange={(e) =>
178197
setFormData((prev) => ({
179198
...prev,
180-
frequency: parseInt(e.target.value),
199+
frequency: e.target.value ? parseInt(e.target.value, 10) : null, // Parse or null
181200
}))
182201
}
183202
placeholder="Enter frequency in seconds"
@@ -189,11 +208,11 @@ const ActionMenuItem = ({ action, onDelete, onEdit }: ActionMenuItemProps) => {
189208
<Input
190209
id="maxExecutions"
191210
type="number"
192-
value={formData.maxExecutions}
211+
value={formData.maxExecutions ?? ''}
193212
onChange={(e) =>
194213
setFormData((prev) => ({
195214
...prev,
196-
maxExecutions: parseInt(e.target.value),
215+
maxExecutions: e.target.value ? parseInt(e.target.value, 10) : null, // Parse or null
197216
}))
198217
}
199218
placeholder="Enter max executions"

src/lib/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ export const RPC_URL =
88
'https://api.mainnet-beta.solana.com';
99

1010
export const MAX_TOKEN_MESSAGES = 10;
11+
12+
export const NO_CONFIRMATION_MESSAGE = ' (Does not require confirmation)';

src/server/db/queries.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,9 @@ export async function dbGetConversations({ userId }: { userId: string }) {
221221
* @returns {Promise<Action[]>} Array of actions
222222
*/
223223
export async function dbGetActions({
224-
triggered,
225-
paused,
226-
completed,
224+
triggered = true,
225+
paused = false,
226+
completed = false,
227227
}: {
228228
triggered: boolean;
229229
paused: boolean;
@@ -235,6 +235,10 @@ export async function dbGetActions({
235235
triggered,
236236
paused,
237237
completed,
238+
OR: [
239+
{ startTime: { lte: new Date() } },
240+
{ startTime: null }
241+
]
238242
},
239243
orderBy: { createdAt: 'desc' },
240244
include: { user: { include: { wallets: true } } },
@@ -401,6 +405,7 @@ export async function dbUpdateAction({
401405
try {
402406
// Validate and clean the data before update
403407
const validData = {
408+
name: data.name,
404409
description: data.description,
405410
frequency: data.frequency === 0 ? null : data.frequency,
406411
maxExecutions: data.maxExecutions === 0 ? null : data.maxExecutions,

vercel.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"crons": [
33
{
4-
"path": "/api/cron/15-min",
5-
"schedule": "*/15 * * * *"
4+
"path": "/api/cron/minute",
5+
"schedule": "* * * * *"
66
}
77
]
88
}

0 commit comments

Comments
 (0)