diff --git a/app/main.wasp b/app/main.wasp index 8df71e5..ef547fd 100644 --- a/app/main.wasp +++ b/app/main.wasp @@ -82,7 +82,7 @@ app SaaSTemplate { ("headlessui", "^0.0.0"), ("@faker-js/faker", "8.3.1"), ("@google-analytics/data", "4.1.0"), - ("openai", "^4.24.1"), + ("openai", "^4.28.0"), ("prettier", "3.1.1"), ("prettier-plugin-tailwindcss", "0.5.11"), ("zod", "3.22.4"), diff --git a/app/src/client/app/DemoAppPage.tsx b/app/src/client/app/DemoAppPage.tsx index 57ee80b..49a09c7 100644 --- a/app/src/client/app/DemoAppPage.tsx +++ b/app/src/client/app/DemoAppPage.tsx @@ -8,6 +8,8 @@ import getAllTasksByUser from '@wasp/queries/getAllTasksByUser'; import { Task } from '@wasp/entities'; import { CgSpinner } from 'react-icons/cg'; import { TiDelete } from 'react-icons/ti'; +import { type GeneratedSchedule } from '../../shared/types'; +import { MainTask, Subtask } from '@wasp/shared/types'; export default function DemoAppPage() { return ( @@ -15,11 +17,12 @@ export default function DemoAppPage() {

- AI Day Scheduler + AI Day Scheduler

- This example app uses OpenAI's chat completions with function calling to return a structured JSON object. Try it out, enter your day's tasks, and let AI do the rest! + This example app uses OpenAI's chat completions with function calling to return a structured JSON object. Try + it out, enter your day's tasks, and let AI do the rest!

{/* begin AI-powered Todo List */}
@@ -36,9 +39,7 @@ export default function DemoAppPage() { type TodoProps = Pick; function Todo({ id, isDone, description, time }: TodoProps) { - const handleCheckboxChange = async ( - e: React.ChangeEvent - ) => { + const handleCheckboxChange = async (e: React.ChangeEvent) => { await updateTask({ id, isDone: e.currentTarget.checked, @@ -66,13 +67,7 @@ function Todo({ id, isDone, description, time }: TodoProps) { checked={isDone} onChange={handleCheckboxChange} /> - - {description} - + {description}
- - hrs - + hrs
@@ -104,22 +93,75 @@ function Todo({ id, isDone, description, time }: TodoProps) { ); } -function NewTaskForm({ - handleCreateTask, -}: { - handleCreateTask: typeof createTask; -}) { +function NewTaskForm({ handleCreateTask }: { handleCreateTask: typeof createTask }) { const [description, setDescription] = useState(''); const [todaysHours, setTodaysHours] = useState('8'); - const [response, setResponse] = useState(null); + const [response, setResponse] = useState({ + mainTasks: [ + { + name: 'Respond to emails', + priority: 'high', + }, + { + name: 'Learn WASP', + priority: 'low', + }, + { + name: 'Read a book', + priority: 'medium', + }, + ], + subtasks: [ + { + description: 'Read introduction and chapter 1', + time: 0.5, + mainTaskName: 'Read a book', + }, + { + description: 'Read chapter 2 and take notes', + time: 0.3, + mainTaskName: 'Read a book', + }, + { + description: 'Read chapter 3 and summarize key points', + time: 0.2, + mainTaskName: 'Read a book', + }, + { + description: 'Check and respond to important emails', + time: 1, + mainTaskName: 'Respond to emails', + }, + { + description: 'Organize and prioritize remaining emails', + time: 0.5, + mainTaskName: 'Respond to emails', + }, + { + description: 'Draft responses to urgent emails', + time: 0.5, + mainTaskName: 'Respond to emails', + }, + { + description: 'Watch tutorial video on WASP', + time: 0.5, + mainTaskName: 'Learn WASP', + }, + { + description: 'Complete online quiz on the basics of WASP', + time: 1.5, + mainTaskName: 'Learn WASP', + }, + { + description: 'Review quiz answers and clarify doubts', + time: 1, + mainTaskName: 'Learn WASP', + }, + ], + }); const [isPlanGenerating, setIsPlanGenerating] = useState(false); - const { data: tasks, isLoading: isTasksLoading } = - useQuery(getAllTasksByUser); - - useEffect(() => { - console.log('response', response); - }, [response]); + const { data: tasks, isLoading: isTasksLoading } = useQuery(getAllTasksByUser); const handleSubmit = async () => { try { @@ -137,8 +179,7 @@ function NewTaskForm({ hours: todaysHours, }); if (response) { - console.log('response', response); - setResponse(JSON.parse(response)); + setResponse(response); } } catch (err: any) { window.alert('Error: ' + (err.message || 'Something went wrong')); @@ -179,20 +220,11 @@ function NewTaskForm({ {tasks!! && tasks.length > 0 ? (
{tasks.map((task: Task) => ( - + ))}
-
)}
); } -function TaskTable({ schedule }: { schedule: any[] }) { +function TaskTable({ schedule }: { schedule: GeneratedSchedule }) { return (
- {schedule.map((task: any) => ( - - - - - - - - {task.subtasks.map((subtask: { description: any; time: any }) => ( - - - - ))} +
- {task.name} - - {' '} - {task.priority} priority - -
- -
+ {!!schedule.mainTasks ? ( + schedule.mainTasks + .map((mainTask) => ) + .sort((a, b) => { + const priorityOrder = ['low', 'medium', 'high']; + if (a.props.mainTask.priority && b.props.mainTask.priority) { + return ( + priorityOrder.indexOf(b.props.mainTask.priority) - priorityOrder.indexOf(a.props.mainTask.priority) + ); + } else { + return 0; + } + }) + ) : ( +
OpenAI didn't return any Main Tasks. Try again.
+ )} +
- {task.breaks.map((breakItem: { description: any; time: any }) => ( - - - - - - ))} - - - ))} + {/* ))} */}
); } +function MainTask({ mainTask, subtasks }: { mainTask: MainTask; subtasks: Subtask[] }) { + return ( + <> + + + + {mainTask.name} + {mainTask.priority} priority + + + + {!!subtasks ? ( + subtasks.map((subtask) => { + if (subtask.mainTaskName === mainTask.name) { + return ( + + + + + + + + ); + } + }) + ) : ( +
OpenAI didn't return any Subtasks. Try again.
+ )} + + ); +} + function Subtask({ description, time }: { description: string; time: number }) { const [isDone, setIsDone] = useState(false); @@ -309,9 +354,7 @@ function Subtask({ description, time }: { description: string; time: number }) { if (time === 0) return 0; const hours = Math.floor(time); const minutes = Math.round((time - hours) * 60); - return `${hours > 0 ? hours + 'hr' : ''} ${ - minutes > 0 ? minutes + 'min' : '' - }`; + return `${hours > 0 ? hours + 'hr' : ''} ${minutes > 0 ? minutes + 'min' : ''}`; }; const minutes = useMemo(() => convertHrsToMinutes(time), [time]); @@ -325,17 +368,13 @@ function Subtask({ description, time }: { description: string; time: number }) { onChange={(e) => setIsDone(e.currentTarget.checked)} /> {description} - + {minutes} diff --git a/app/src/server/actions.ts b/app/src/server/actions.ts index ed1e6c4..56f13d3 100644 --- a/app/src/server/actions.ts +++ b/app/src/server/actions.ts @@ -1,8 +1,7 @@ import Stripe from 'stripe'; -import fetch from 'node-fetch'; import HttpError from '@wasp/core/HttpError.js'; import type { User, Task, File } from '@wasp/entities'; -import type { StripePaymentResult } from './types'; +import type { StripePaymentResult, GeneratedSchedule } from '../shared/types'; import { GenerateGptResponse, StripePayment, @@ -16,6 +15,9 @@ import { import { fetchStripeCustomer, createStripeCheckoutSession } from './payments/stripeUtils.js'; import { TierIds } from '@wasp/shared/constants.js'; import { getUploadFileSignedURLFromS3 } from './file-upload/s3Utils.js'; +import OpenAI from 'openai'; + +const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! }); export const stripePayment: StripePayment = async (tier, context) => { if (!context.user || !context.user.email) { @@ -63,7 +65,7 @@ type GptPayload = { hours: string; }; -export const generateGptResponse: GenerateGptResponse = async ({ hours }, context) => { +export const generateGptResponse: GenerateGptResponse = async ({ hours }, context) => { if (!context.user) { throw new HttpError(401); } @@ -76,98 +78,11 @@ export const generateGptResponse: GenerateGptResponse = asyn }, }); - // use map to extract the description and time from each task const parsedTasks = tasks.map(({ description, time }) => ({ description, time, })); - const payload = { - model: 'gpt-3.5-turbo', // e.g. 'gpt-3.5-turbo', 'gpt-4', 'gpt-4-0613', gpt-4-1106-preview - messages: [ - { - role: 'system', - content: - 'you are an expert daily planner and scheduling assistant. you will be given a list of main tasks and an estimated time to complete each task. You will also receive the total amount of hours to be worked that day. Your job is to return a detailed plan of how to achieve those tasks throughout the day by breaking down the main tasks provided by the user into multiple subtasks. Be specific in your reply and offer advice on the best way to fulfill each task. Please also schedule in standing, water, lunch, and coffee breaks. Plan the higher priority tasks first. Also give the total amount of time to be spent on each (sub)task after considering all of the above. ', - }, - { - role: 'user', - content: `I will work ${hours} today. Here are the tasks I have to complete: ` + JSON.stringify(parsedTasks), - }, - ], - functions: [ - { - name: 'parseTodaysSchedule', - description: 'parses the days tasks and returns a schedule', - parameters: { - type: 'object', - properties: { - schedule: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - description: 'Name of main task provided by user', - }, - subtasks: { - type: 'array', - items: { - type: 'object', - properties: { - description: { - type: 'string', - description: - 'detailed breakdown and description of sub-task related to main task. e.g., "Prepare your learning session by first reading through the documentation"', - }, - time: { - type: 'number', - description: 'time allocated for a given subtask in hours, e.g. 0.5', - }, - }, - }, - }, - breaks: { - type: 'array', - items: { - type: 'object', - properties: { - description: { - type: 'string', - description: - 'detailed breakdown and description of break. e.g., "take a 15 minute standing break and reflect on what you have learned".', - }, - time: { - type: 'number', - description: 'time allocated for a given break in hours, e.g. 0.2', - }, - }, - }, - }, - time: { - type: 'number', - description: 'total time in it takes to complete given main task in hours, e.g. 2.75', - }, - priority: { - type: 'string', - enum: ['low', 'medium', 'high'], - description: 'task priority', - }, - }, - }, - }, - }, - required: ['schedule'], - }, - }, - ], - function_call: { - name: 'parseTodaysSchedule', - }, - temperature: 1, - }; - try { if (!context.user.hasPaid && !context.user.credits) { throw new HttpError(402, 'User has not paid or is out of credits'); @@ -183,22 +98,91 @@ export const generateGptResponse: GenerateGptResponse = asyn }); } - const response = await fetch('https://api.openai.com/v1/chat/completions', { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${process.env.OPENAI_API_KEY!}`, + const completion = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: [ + { + role: 'system', + content: + 'you are an expert daily planner. you will be given a list of main tasks and an estimated time to complete each task. You will also receive the total amount of hours to be worked that day. Your job is to return a detailed plan of how to achieve those tasks by breaking each task down into at least 3 subtasks each. MAKE SURE TO ALWAYS CREATE AT LEAST 3 SUBTASKS FOR EACH MAIN TASK PROVIDED BY THE USER! YOU WILL BE REWARDED IF YOU DO.', + }, + { + role: 'user', + content: `I will work ${hours} hours today. Here are the tasks I have to complete: ${JSON.stringify( + parsedTasks + )}. Please help me plan my day by breaking the tasks down into actionable subtasks with time and priority status.`, + }, + ], + tools: [ + { + type: 'function', + function: { + name: 'parseTodaysSchedule', + description: 'parses the days tasks and returns a schedule', + parameters: { + type: 'object', + properties: { + mainTasks: { + type: 'array', + description: 'Name of main tasks provided by user, ordered by priority', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Name of main task provided by user', + }, + priority: { + type: 'string', + enum: ['low', 'medium', 'high'], + description: 'task priority', + }, + }, + }, + }, + subtasks: { + type: 'array', + items: { + type: 'object', + properties: { + description: { + type: 'string', + description: + 'detailed breakdown and description of sub-task related to main task. e.g., "Prepare your learning session by first reading through the documentation"', + }, + time: { + type: 'number', + description: 'time allocated for a given subtask in hours, e.g. 0.5', + }, + mainTaskName: { + type: 'string', + description: 'name of main task related to subtask', + }, + }, + }, + }, + }, + required: ['mainTasks', 'subtasks', 'time', 'priority'], + }, + }, + }, + ], + tool_choice: { + type: 'function', + function: { + name: 'parseTodaysSchedule', + }, }, - method: 'POST', - body: JSON.stringify(payload), + temperature: 1, }); - if (!response.ok) { + const gptArgs = completion?.choices[0]?.message?.tool_calls?.[0]?.function.arguments; + + if (!gptArgs) { throw new HttpError(500, 'Bad response from OpenAI'); } - let json = (await response.json()) as any; - - const gptArgs = json.choices[0].message.function_call.arguments; + console.log('gpt function call arguments: ', gptArgs); await context.entities.GptResponse.create({ data: { @@ -207,7 +191,7 @@ export const generateGptResponse: GenerateGptResponse = asyn }, }); - return gptArgs; + return JSON.parse(gptArgs); } catch (error: any) { if (!context.user.hasPaid && error?.statusCode != 402) { await context.entities.User.update({ diff --git a/app/src/server/types.ts b/app/src/server/types.ts deleted file mode 100644 index ef6bb0a..0000000 --- a/app/src/server/types.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { User } from '@wasp/entities' -import { Prisma } from '@prisma/client' - -export type Context = { - user: User; - entities: { - User: Prisma.UserDelegate<{}>; - }; -}; - -export type StripePaymentResult = { - sessionUrl: string | null; - sessionId: string; -}; - -export type OpenAIResponse = { - id: string; - object: string; - created: number; - usage: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; - choices: [ - { - index: number; - message: { - role: string; - content: string; - }; - finish_reason: string; - } - ]; -}; \ No newline at end of file diff --git a/app/src/shared/types.ts b/app/src/shared/types.ts new file mode 100644 index 0000000..701bfaa --- /dev/null +++ b/app/src/shared/types.ts @@ -0,0 +1,34 @@ +import { User } from '@wasp/entities'; +import { Prisma } from '@prisma/client'; + +export type Context = { + user: User; + entities: { + User: Prisma.UserDelegate<{}>; + }; +}; + +export type StripePaymentResult = { + sessionUrl: string | null; + sessionId: string; +}; + +export type Subtask = { + description: string; // detailed breakdown and description of sub-task + time: number; // total time it takes to complete given main task in hours, e.g. 2.75 + mainTaskName: string; // name of main task related to subtask +}; + +export type MainTask = { + name: string; + priority: 'low' | 'medium' | 'high'; +}; + +export type GeneratedSchedule = { + mainTasks: MainTask[]; // Main tasks provided by user, ordered by priority + subtasks: Subtask[]; // Array of subtasks +}; + +export type FunctionCallResponse = { + schedule: GeneratedSchedule[]; +};