diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 56dcdba1..cd01485e 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -1,24 +1,33 @@ -import { streamText, ModelMessage, stepCountIs } from 'ai' +import { streamText, generateText, ModelMessage, stepCountIs, UserModelMessage } from 'ai' import { AgentParams } from './types' -import { system } from './prompts' -import { getMemoryTools } from './tools' +import { system, schedule as schedulePrompt } from './prompts' +import { getMemoryTools, getScheduleTools } from './tools' import { createChatGateway } from '@memohome/ai-gateway' +import { Schedule } from '@memohome/shared' export const createAgent = (params: AgentParams) => { const messages: ModelMessage[] = [] const gateway = createChatGateway(params.model) + const maxContextLoadTime = params.maxContextLoadTime ?? 60 + const language = params.language ?? 'Same as user input' + const getTools = async () => { return { ...getMemoryTools({ searchMemory: params.onSearchMemory ?? (() => Promise.resolve([])) }), + ...getScheduleTools({ + onGetSchedules: params.onGetSchedules ?? (() => Promise.resolve([])), + onRemoveSchedule: params.onRemoveSchedule ?? (() => Promise.resolve()), + onSchedule: params.onSchedule ?? (() => Promise.resolve()), + }), } } const loadContext = async () => { - const from = new Date(Date.now() - params.maxContextLoadTime * 60 * 1000) + const from = new Date(Date.now() - maxContextLoadTime * 60 * 1000) const to = new Date() const memory = await params.onReadMemory?.(from, to) ?? [] const context = memory.flatMap(m => m.messages) @@ -28,18 +37,45 @@ export const createAgent = (params: AgentParams) => { const getSystemPrompt = () => { return system({ date: new Date(), - language: params.language ?? 'Same as user input', + language, locale: params.locale, - maxContextLoadTime: params.maxContextLoadTime, + maxContextLoadTime, }) } + const getSchedulePrompt = (schedule: Schedule) => { + return schedulePrompt({ + schedule, + locale: params.locale, + date: new Date(), + }) + } + + async function askDirectly(input: string) { + await loadContext() + const user = { + role: 'user', + content: input, + } as UserModelMessage + messages.push(user) + const { response } = await generateText({ + model: gateway, + system: getSystemPrompt(), + messages, + tools: await getTools(), + }) + await params.onFinish?.([ + user as ModelMessage, + ...response.messages, + ]) + } + async function* ask(input: string) { await loadContext() const user = { role: 'user', content: input, - } + } as UserModelMessage messages.push(user) const { fullStream, response } = streamText({ model: gateway, @@ -63,9 +99,17 @@ export const createAgent = (params: AgentParams) => { ]) } + const triggerSchedule = async (schedule: Schedule) => { + const prompt = getSchedulePrompt(schedule) + await askDirectly(prompt) + } + return { ask, + askDirectly, loadContext, getSystemPrompt, + getSchedulePrompt, + triggerSchedule, } } \ No newline at end of file diff --git a/packages/agent/src/prompts/index.ts b/packages/agent/src/prompts/index.ts index 44c0319e..a8707725 100644 --- a/packages/agent/src/prompts/index.ts +++ b/packages/agent/src/prompts/index.ts @@ -1 +1,4 @@ -export * from './system' \ No newline at end of file +export * from './system' +export * from './schedule' +export * from './shared' +export * from './utils' \ No newline at end of file diff --git a/packages/agent/src/prompts/schedule.ts b/packages/agent/src/prompts/schedule.ts new file mode 100644 index 00000000..68050240 --- /dev/null +++ b/packages/agent/src/prompts/schedule.ts @@ -0,0 +1,26 @@ +import { Schedule } from '@memohome/shared' +import { time } from './shared' + +export interface ScheduleParams { + schedule: Schedule + locale?: Intl.LocalesArgument + date: Date +} + +export const schedule = (params: ScheduleParams) => { + return ` +--- +notice: **This is a scheduled task automatically send to you by the system, not the user input** +${time({ date: params.date, locale: params.locale })} +schedule-name: ${params.schedule.name} +schedule-description: ${params.schedule.description} +schedule-id: ${params.schedule.id} +max-calls: ${params.schedule.maxCalls ?? 'Unlimited'} +cron-pattern: ${params.schedule.pattern} +--- + +**COMMAND** + +${params.schedule.command} + `.trim() +} \ No newline at end of file diff --git a/packages/agent/src/prompts/shared.ts b/packages/agent/src/prompts/shared.ts new file mode 100644 index 00000000..c8a537ea --- /dev/null +++ b/packages/agent/src/prompts/shared.ts @@ -0,0 +1,9 @@ +export const time = (params: { + date: Date + locale?: Intl.LocalesArgument +}) => { + return ` +date: ${params.date.toLocaleDateString(params.locale)} +time: ${params.date.toLocaleTimeString(params.locale)} + `.trim() +} \ No newline at end of file diff --git a/packages/agent/src/prompts/system.ts b/packages/agent/src/prompts/system.ts index 37ce9286..6423245f 100644 --- a/packages/agent/src/prompts/system.ts +++ b/packages/agent/src/prompts/system.ts @@ -1,3 +1,4 @@ +import { time } from './shared' import { quote } from './utils' export interface SystemParams { @@ -9,20 +10,28 @@ export interface SystemParams { export const system = ({ date, locale, language, maxContextLoadTime }: SystemParams) => { return ` - --- - date: ${date.toLocaleDateString(locale)} - time: ${date.toLocaleTimeString(locale)} - language: ${language} - --- - You are a personal housekeeper assistant, which able to manage the master's daily affairs. +--- +${time({ date, locale })} +language: ${language} +--- +You are a personal housekeeper assistant, which able to manage the master's daily affairs. - Your abilities: - - Long memory: You possess long-term memory; conversations from the last 24 hours will be directly loaded into your context. Additionally, you can use tools to search for past memories. - - Scheduled tasks: You can create scheduled tasks to automatically remind you to do something. - - Messaging: You may allowed to use message software to send messages to the master. +Your abilities: +- Long memory: You possess long-term memory; conversations from the last ${maxContextLoadTime} minutes will be directly loaded into your context. Additionally, you can use tools to search for past memories. +- Scheduled tasks: You can create scheduled tasks to automatically remind you to do something. +- Messaging: You may allowed to use message software to send messages to the master. - **Memory** - - Your context has been loaded from the last ${maxContextLoadTime} minutes. - - You can use ${quote('search-memory')} to search for past memories with natural language. +**Memory** +- Your context has been loaded from the last ${maxContextLoadTime} minutes. +- You can use ${quote('search-memory')} to search for past memories with natural language. + +**Schedule** +- We use **Cron Syntax** to schedule tasks. +- You can use ${quote('get-schedules')} to get the list of schedules. +- You can use ${quote('remove-schedule')} to remove a schedule by id. +- You can use ${quote('schedule')} to schedule a task. + + The ${quote('pattern')} is the pattern of the schedule with **Cron Syntax**. + + The ${quote('command')} is the natural language command to execute, will send to you when the schedule is triggered, which means the command will be executed by presence of you. + + The ${quote('maxCalls')} is the maximum number of calls to the schedule, If you want to run the task only once, set it to 1. `.trim() } \ No newline at end of file diff --git a/packages/agent/src/tools/index.ts b/packages/agent/src/tools/index.ts index 3c69b644..f758e659 100644 --- a/packages/agent/src/tools/index.ts +++ b/packages/agent/src/tools/index.ts @@ -1 +1,2 @@ -export * from './memory' \ No newline at end of file +export * from './memory' +export * from './schedule' \ No newline at end of file diff --git a/packages/agent/src/tools/schedule.ts b/packages/agent/src/tools/schedule.ts new file mode 100644 index 00000000..cba5d0e4 --- /dev/null +++ b/packages/agent/src/tools/schedule.ts @@ -0,0 +1,53 @@ +import { Schedule } from '@memohome/shared' +import { tool } from 'ai' +import z from 'zod' + +export interface GetScheduleToolParams { + onGetSchedules: () => Promise + onRemoveSchedule: (id: string) => Promise + onSchedule: (schedule: Schedule) => Promise +} + +export const getScheduleTools = ({ onGetSchedules, onRemoveSchedule, onSchedule }: GetScheduleToolParams) => { + const getSchedulesTool = tool({ + description: 'Get the list of schedules', + inputSchema: z.object(), + execute: async () => { + const schedules = await onGetSchedules() + return { + success: true, + schedules, + } + }, + }) + + const removeScheduleTool = tool({ + description: 'Remove a schedule', + inputSchema: z.object({ + id: z.string().describe('The id of the schedule'), + }), + execute: async ({ id }) => { + await onRemoveSchedule(id) + }, + }) + + const scheduleTool = tool({ + description: 'Schedule a command', + inputSchema: z.object({ + pattern: z.string().describe('The pattern of the schedule with **Cron Syntax**'), + command: z.string().describe('The natural language command to execute, will send to you when the schedule is triggered'), + name: z.string().describe('The name of the schedule'), + description: z.string().describe('The description of the schedule'), + maxCalls: z.number().describe('The maximum number of calls to the schedule').optional(), + }), + execute: async ({ pattern, command, name, description, maxCalls }) => { + await onSchedule({ pattern, command, name, description, maxCalls }) + }, + }) + + return { + 'get-schedules': getSchedulesTool, + 'remove-schedule': removeScheduleTool, + 'schedule': scheduleTool, + } +} \ No newline at end of file diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index addd4e18..bf81177e 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -1,5 +1,5 @@ import type { MemoryUnit } from '@memohome/memory' -import { ChatModel } from '@memohome/shared' +import { ChatModel, Schedule } from '@memohome/shared' import { ModelMessage } from 'ai' export interface AgentParams { @@ -8,7 +8,7 @@ export interface AgentParams { /** * Unit: minutes */ - maxContextLoadTime: number + maxContextLoadTime?: number locale?: Intl.LocalesArgument @@ -22,6 +22,12 @@ export interface AgentParams { onSearchMemory?: (query: string) => Promise + onSchedule?: (schedule: Schedule) => Promise + + onGetSchedules?: () => Promise + + onRemoveSchedule?: (id: string) => Promise + onFinish?: (messages: ModelMessage[]) => Promise onError?: (error: Error) => Promise diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 8c57dfcf..075dff55 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,6 +1,6 @@ import { Elysia } from 'elysia' import { corsMiddleware, errorMiddleware } from './middlewares' -import { agentModule, authModule, modelModule, settingsModule, userModule } from './modules' +import { agentModule, authModule, modelModule, scheduleModule, settingsModule, userModule } from './modules' import { memoryModule } from './modules/memory' import openapi from '@elysiajs/openapi' @@ -14,6 +14,7 @@ export const app = new Elysia() .use(agentModule) .use(memoryModule) .use(modelModule) + .use(scheduleModule) .use(settingsModule) .use(userModule) .listen(port) diff --git a/packages/api/src/modules/agent/index.ts b/packages/api/src/modules/agent/index.ts index 74e657a5..621d477e 100644 --- a/packages/api/src/modules/agent/index.ts +++ b/packages/api/src/modules/agent/index.ts @@ -1,7 +1,7 @@ import Elysia from 'elysia' import { authMiddleware } from '../../middlewares/auth' import { AgentStreamModel } from './model' -import { createAgentStream } from './service' +import { createAgent } from './service' import { getChatModel, getEmbeddingModel, getSummaryModel } from '../model/service' import { getSettings } from '../settings/service' import { ChatModel, EmbeddingModel } from '@memohome/shared' @@ -38,7 +38,7 @@ export const agentModule = new Elysia({ ?? 'Same as user input' // Create agent - const agent = await createAgentStream({ + const agent = await createAgent({ userId: user.userId, chatModel: chatModel.model as ChatModel, embeddingModel: embeddingModel.model as EmbeddingModel, diff --git a/packages/api/src/modules/agent/service.ts b/packages/api/src/modules/agent/service.ts index 9f65e572..7b1d4f36 100644 --- a/packages/api/src/modules/agent/service.ts +++ b/packages/api/src/modules/agent/service.ts @@ -1,6 +1,7 @@ -import { createAgent } from '@memohome/agent' +import { createAgent as createAgentService } from '@memohome/agent' import { createMemory, filterByTimestamp, MemoryUnit } from '@memohome/memory' -import { ChatModel, EmbeddingModel } from '@memohome/shared' +import { ChatModel, EmbeddingModel, Schedule } from '@memohome/shared' +import { createSchedule, deleteSchedule, getActiveSchedules } from '../schedule/service' // Type for messages passed to onFinish callback type MessageType = Record @@ -10,12 +11,12 @@ export interface CreateAgentStreamParams { chatModel: ChatModel embeddingModel: EmbeddingModel summaryModel: ChatModel - maxContextLoadTime: number + maxContextLoadTime?: number language?: string onFinish?: (messages: MessageType[]) => Promise } -export async function createAgentStream(params: CreateAgentStreamParams) { +export async function createAgent(params: CreateAgentStreamParams) { const { userId, chatModel, @@ -33,7 +34,7 @@ export async function createAgentStream(params: CreateAgentStreamParams) { }) // Create agent - const agent = createAgent({ + const agent = createAgentService({ model: chatModel, maxContextLoadTime, language: language || 'Same as user input', @@ -56,6 +57,29 @@ export async function createAgentStream(params: CreateAgentStreamParams) { // Call custom onFinish handler if provided await onFinish?.(messages) }, + onGetSchedules: async () => { + const schedules = await getActiveSchedules(userId) + return schedules.map(schedule => ({ + id: schedule.id!, + pattern: schedule.pattern, + name: schedule.name, + description: schedule.description, + command: schedule.command, + maxCalls: schedule.maxCalls || undefined, + })) + }, + onRemoveSchedule: async (id: string) => { + await deleteSchedule(id, userId) + }, + onSchedule: async (schedule: Schedule) => { + await createSchedule(userId, { + name: schedule.name, + description: schedule.description, + command: schedule.command, + pattern: schedule.pattern, + maxCalls: schedule.maxCalls || undefined, + }) + }, }) return agent diff --git a/packages/api/src/modules/index.ts b/packages/api/src/modules/index.ts index 0290106d..bd5d0877 100644 --- a/packages/api/src/modules/index.ts +++ b/packages/api/src/modules/index.ts @@ -1,5 +1,6 @@ export * from './agent' export * from './auth' export * from './model' +export * from './schedule' export * from './settings' export * from './user' \ No newline at end of file diff --git a/packages/api/src/modules/schedule/index.ts b/packages/api/src/modules/schedule/index.ts new file mode 100644 index 00000000..e20404ce --- /dev/null +++ b/packages/api/src/modules/schedule/index.ts @@ -0,0 +1,168 @@ +import Elysia from 'elysia' +import { authMiddleware } from '../../middlewares/auth' +import { + CreateScheduleModel, + UpdateScheduleModel, + GetScheduleByIdModel, + DeleteScheduleModel, + GetSchedulesModel, +} from './model' +import { + getSchedules, + getSchedule, + createSchedule, + updateSchedule, + deleteSchedule, + createScheduler, +} from './service' + +export const { scheduleTask, resume } = createScheduler() + +export const scheduleModule = new Elysia({ prefix: '/schedule' }) + .use(authMiddleware) + // Get all schedules for current user + .onStart(async () => { + await resume() + }) + .get('/', async ({ user, query }) => { + try { + const page = parseInt(query.page as string) || 1 + const limit = parseInt(query.limit as string) || 10 + const sortOrder = (query.sortOrder as string) || 'desc' + + const result = await getSchedules(user.userId, { + page, + limit, + sortOrder: sortOrder as 'asc' | 'desc', + }) + + return { + success: true, + ...result, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch schedules', + } + } + }, GetSchedulesModel) + // Get schedule by ID + .get('/:id', async ({ user, params, set }) => { + try { + const schedule = await getSchedule(params.id) + + if (!schedule) { + set.status = 404 + return { + success: false, + error: 'Schedule not found', + } + } + + if (schedule.user !== user.userId) { + set.status = 403 + return { + success: false, + error: 'Forbidden: You do not have permission to access this schedule', + } + } + + return { + success: true, + data: schedule, + } + } catch (error) { + set.status = 500 + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch schedule', + } + } + }, GetScheduleByIdModel) + // Create new schedule + .post('/', async ({ user, body, set }) => { + try { + const newSchedule = await createSchedule(user.userId, body) + + // 启动定时任务 + scheduleTask(user.userId, { + id: newSchedule.id!, + pattern: newSchedule.pattern, + name: newSchedule.name, + description: newSchedule.description, + command: newSchedule.command, + maxCalls: newSchedule.maxCalls || undefined, + }) + + set.status = 201 + return { + success: true, + data: newSchedule, + } + } catch (error) { + set.status = 500 + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create schedule', + } + } + }, CreateScheduleModel) + // Update schedule + .put('/:id', async ({ user, params, body, set }) => { + try { + const updatedSchedule = await updateSchedule(params.id, user.userId, body) + + if (!updatedSchedule) { + set.status = 404 + return { + success: false, + error: 'Schedule not found', + } + } + + return { + success: true, + data: updatedSchedule, + } + } catch (error) { + if (error instanceof Error && error.message.includes('Forbidden')) { + set.status = 403 + } else { + set.status = 500 + } + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to update schedule', + } + } + }, UpdateScheduleModel) + // Delete schedule + .delete('/:id', async ({ user, params, set }) => { + try { + const deletedSchedule = await deleteSchedule(params.id, user.userId) + + if (!deletedSchedule) { + set.status = 404 + return { + success: false, + error: 'Schedule not found', + } + } + + return { + success: true, + data: deletedSchedule, + } + } catch (error) { + if (error instanceof Error && error.message.includes('Forbidden')) { + set.status = 403 + } else { + set.status = 500 + } + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to delete schedule', + } + } + }, DeleteScheduleModel) diff --git a/packages/api/src/modules/schedule/model.ts b/packages/api/src/modules/schedule/model.ts new file mode 100644 index 00000000..a284da40 --- /dev/null +++ b/packages/api/src/modules/schedule/model.ts @@ -0,0 +1,59 @@ +import { z } from 'zod' + +// 创建 Schedule 的 Schema +const CreateScheduleSchema = z.object({ + name: z.string().min(1, 'Name is required').max(100), + description: z.string().min(1, 'Description is required'), + command: z.string().min(1, 'Command is required'), + pattern: z.string().min(1, 'Cron pattern is required'), + maxCalls: z.number().int().positive().optional(), +}) + +// 更新 Schedule 的 Schema +const UpdateScheduleSchema = z.object({ + name: z.string().min(1).max(100).optional(), + description: z.string().optional(), + command: z.string().optional(), + pattern: z.string().optional(), + maxCalls: z.number().int().positive().optional(), + active: z.boolean().optional(), +}) + +// 查询参数 Schema +const GetSchedulesQuerySchema = z.object({ + page: z.string().optional(), + limit: z.string().optional(), + sortOrder: z.enum(['asc', 'desc']).optional(), +}) + +export type CreateScheduleInput = z.infer +export type UpdateScheduleInput = z.infer +export type GetSchedulesQuery = z.infer + +export const CreateScheduleModel = { + body: CreateScheduleSchema, +} + +export const UpdateScheduleModel = { + params: z.object({ + id: z.string().uuid('Invalid schedule ID format'), + }), + body: UpdateScheduleSchema, +} + +export const GetScheduleByIdModel = { + params: z.object({ + id: z.string().uuid('Invalid schedule ID format'), + }), +} + +export const DeleteScheduleModel = { + params: z.object({ + id: z.string().uuid('Invalid schedule ID format'), + }), +} + +export const GetSchedulesModel = { + query: GetSchedulesQuerySchema, +} + diff --git a/packages/api/src/modules/schedule/service.ts b/packages/api/src/modules/schedule/service.ts new file mode 100644 index 00000000..8bec4a99 --- /dev/null +++ b/packages/api/src/modules/schedule/service.ts @@ -0,0 +1,244 @@ +import { db } from '@memohome/db' +import { schedule } from '@memohome/db/schema' +import { ChatModel, EmbeddingModel, Schedule } from '@memohome/shared' +import { eq, desc, asc, and, sql } from 'drizzle-orm' +import cron from 'node-cron' +import { createAgent } from '../agent/service' +import { getChatModel, getEmbeddingModel, getSummaryModel } from '../model/service' +import { getSettings } from '../settings/service' +import { calculateOffset, createPaginatedResult, type PaginatedResult } from '../../utils/pagination' +import type { CreateScheduleInput, UpdateScheduleInput } from './model' + +/** + * Schedule 列表返回类型 + */ +type ScheduleListItem = { + id: string + name: string + description: string + command: string + pattern: string + maxCalls: number | null + user: string + createdAt: Date + updatedAt: Date + active: boolean +} + + +/** + * 获取用户的所有 schedules(支持分页) + */ +export const getSchedules = async ( + userId: string, + params?: { + limit?: number + page?: number + sortOrder?: 'asc' | 'desc' + } +): Promise> => { + const limit = params?.limit || 10 + const page = params?.page || 1 + const sortOrder = params?.sortOrder || 'desc' + const offset = calculateOffset(page, limit) + + // 获取总数 + const [{ count }] = await db + .select({ count: sql`count(*)` }) + .from(schedule) + .where(eq(schedule.user, userId)) + + // 获取分页数据 + const orderFn = sortOrder === 'desc' ? desc : asc + const schedules = await db + .select() + .from(schedule) + .where(eq(schedule.user, userId)) + .orderBy(orderFn(schedule.createdAt)) + .limit(limit) + .offset(offset) + + return createPaginatedResult(schedules, Number(count), page, limit) +} + +export const getActiveSchedules = async ( + userId?: string +) => { + const schedules = await db + .select().from(schedule) + .where(and(...[ + userId ? eq(schedule.user, userId) : undefined, + eq(schedule.active, true), + ])) + .orderBy(desc(schedule.createdAt)) + return schedules +} + +/** + * 根据 ID 获取单个 schedule + */ +export const getSchedule = async ( + scheduleId: string +) => { + const [result] = await db + .select() + .from(schedule) + .where(eq(schedule.id, scheduleId)) + return result +} + +/** + * 创建新的 schedule + */ +export const createSchedule = async ( + userId: string, + data: CreateScheduleInput +) => { + const [newSchedule] = await db + .insert(schedule) + .values({ + user: userId, + name: data.name, + description: data.description, + command: data.command, + pattern: data.pattern, + maxCalls: data.maxCalls || null, + active: true, + }) + .returning() + + return newSchedule +} + +/** + * 更新 schedule + */ +export const updateSchedule = async ( + scheduleId: string, + userId: string, + data: UpdateScheduleInput +) => { + // 检查 schedule 是否存在且属于该用户 + const existingSchedule = await getSchedule(scheduleId) + if (!existingSchedule) { + return null + } + + if (existingSchedule.user !== userId) { + throw new Error('Forbidden: You do not have permission to update this schedule') + } + + const [updatedSchedule] = await db + .update(schedule) + .set({ + ...data, + updatedAt: new Date(), + }) + .where(eq(schedule.id, scheduleId)) + .returning() + + return updatedSchedule +} + +/** + * 删除 schedule + */ +export const deleteSchedule = async ( + scheduleId: string, + userId: string +) => { + // 检查 schedule 是否存在且属于该用户 + const existingSchedule = await getSchedule(scheduleId) + if (!existingSchedule) { + return null + } + + if (existingSchedule.user !== userId) { + throw new Error('Forbidden: You do not have permission to delete this schedule') + } + + const [deletedSchedule] = await db + .delete(schedule) + .where(eq(schedule.id, scheduleId)) + .returning() + + return deletedSchedule +} + +export const setMaxCalls = async ( + scheduleId: string, + maxCalls: number +) => { + await db + .update(schedule) + .set({ maxCalls }) + .where(eq(schedule.id, scheduleId)) +} + +export const setActive = async ( + scheduleId: string, + active: boolean +) => { + await db + .update(schedule) + .set({ active }) + .where(eq(schedule.id, scheduleId)) +} + +export const createScheduler = () => { + const scheduleTask = (userId: string, schedule: Schedule) => { + const task = cron.schedule(schedule.pattern, async () => { + const [chatModel, embeddingModel, summaryModel, userSettings] = await Promise.all([ + getChatModel(userId), + getEmbeddingModel(userId), + getSummaryModel(userId), + getSettings(userId), + ]) + if (!chatModel || !embeddingModel || !summaryModel) { + throw new Error('Model configuration not found. Please configure your models in settings.') + } + const agent = await createAgent({ + userId, + chatModel: chatModel.model as ChatModel, + embeddingModel: embeddingModel.model as EmbeddingModel, + summaryModel: summaryModel.model as ChatModel, + maxContextLoadTime: userSettings?.maxContextLoadTime || undefined, + language: userSettings?.language || undefined, + }) + await agent.triggerSchedule(schedule) + }, { + maxExecutions: schedule.maxCalls || undefined, + }) + task.on('execution:finished', async () => { + const { maxCalls } = await getSchedule(schedule.id!) + if (maxCalls) { + setMaxCalls(schedule.id!, maxCalls - 1) + if (maxCalls - 1 === 0) { + await setActive(schedule.id!, false) + } + } + }) + task.on('execution:maxReached', async () => { + await setActive(schedule.id!, false) + }) + } + + const resume = async () => { + const schedules = await getActiveSchedules() + for (const schedule of schedules) { + scheduleTask(schedule.user, { + id: schedule.id!, + pattern: schedule.pattern, + name: schedule.name, + description: schedule.description, + command: schedule.command, + maxCalls: schedule.maxCalls || undefined, + }) + } + } + + return { + scheduleTask, + resume, + } +} diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 00000000..7d322a7c --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1 @@ +# @memohome/shared diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 00000000..32e52c90 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,16 @@ +{ + "name": "@memohome/cli", + "version": "1.0.0", + "description": "", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "start": "bun run src/index.ts" + }, + "dependencies": { + "@memohome/api": "workspace:*", + "@memohome/shared": "workspace:*" + }, + "packageManager": "pnpm@10.27.0" +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/db/src/schedule.ts b/packages/db/src/schedule.ts new file mode 100644 index 00000000..83776147 --- /dev/null +++ b/packages/db/src/schedule.ts @@ -0,0 +1,15 @@ +import { boolean, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' +import { users } from './users' + +export const schedule = pgTable('schedule', { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name').notNull(), + description: text('description').notNull(), + command: text('command').notNull(), + pattern: text('pattern').notNull(), + maxCalls: integer('max_calls'), + user: uuid('user').notNull().references(() => users.id), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + active: boolean('active').notNull().default(true), +}) \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 9ab6ed47..a85ed99d 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -1,4 +1,5 @@ export * from './history' export * from './model' export * from './settings' +export * from './schedule' export * from './users' \ No newline at end of file diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index bc4e2a78..15b9157a 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1 +1,2 @@ -export * from './model' \ No newline at end of file +export * from './model' +export * from './schedule' \ No newline at end of file diff --git a/packages/shared/src/schedule.ts b/packages/shared/src/schedule.ts new file mode 100644 index 00000000..bec96523 --- /dev/null +++ b/packages/shared/src/schedule.ts @@ -0,0 +1,8 @@ +export interface Schedule { + id?: string + pattern: string + name: string + description: string + command: string + maxCalls?: number +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e09be31..ca55c7d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -189,6 +189,18 @@ importers: specifier: ^0.4.1 version: 0.4.1(zod-to-json-schema@3.25.1(zod@4.3.5))(zod@4.3.5) + packages/schedule: + dependencies: + '@memohome/db': + specifier: workspace:* + version: link:../db + '@memohome/shared': + specifier: workspace:* + version: link:../shared + drizzle-orm: + specifier: ^0.45.1 + version: 0.45.1(@cloudflare/workers-types@4.20260109.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.5)(pg@8.16.3)(sqlite3@5.1.7) + packages/shared: {} packages/ui: