mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-11-24 21:36:57 +01:00
Openai demo app vertical reorg (#234)
* organize demo ai app vertically * Update main.wasp.diff * Update guided-tour.md * Leftover vertical org changes (#233) * newsletter dir & leftovers * update app_diff * Update guided-tour.md * Update guided-tour.md
This commit is contained in:
@@ -1,219 +0,0 @@
|
||||
import { type User, type Task } from 'wasp/entities';
|
||||
import { HttpError } from 'wasp/server';
|
||||
import {
|
||||
type GenerateGptResponse,
|
||||
type CreateTask,
|
||||
type DeleteTask,
|
||||
type UpdateTask,
|
||||
} from 'wasp/server/operations';
|
||||
import { GeneratedSchedule } from '../gpt/schedule';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
const openai = setupOpenAI();
|
||||
function setupOpenAI() {
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
return new HttpError(500, 'OpenAI API key is not set');
|
||||
}
|
||||
return new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||
}
|
||||
|
||||
type GptPayload = {
|
||||
hours: string;
|
||||
};
|
||||
|
||||
export const generateGptResponse: GenerateGptResponse<GptPayload, GeneratedSchedule> = async ({ hours }, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
|
||||
const tasks = await context.entities.Task.findMany({
|
||||
where: {
|
||||
user: {
|
||||
id: context.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const parsedTasks = tasks.map(({ description, time }) => ({
|
||||
description,
|
||||
time,
|
||||
}));
|
||||
|
||||
try {
|
||||
// check if openai is initialized correctly with the API key
|
||||
if (openai instanceof Error) {
|
||||
throw openai;
|
||||
}
|
||||
|
||||
if (!context.user.credits && (!context.user.subscriptionStatus || context.user.subscriptionStatus === 'deleted' || context.user.subscriptionStatus === 'past_due')) {
|
||||
throw new HttpError(402, 'User has not paid or is out of credits');
|
||||
} else if (context.user.credits && !context.user.subscriptionStatus) {
|
||||
console.log('decrementing credits');
|
||||
await context.entities.User.update({
|
||||
where: { id: context.user.id },
|
||||
data: {
|
||||
credits: {
|
||||
decrement: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: 'gpt-3.5-turbo', // you can use any model here, e.g. 'gpt-3.5-turbo', 'gpt-4', etc.
|
||||
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',
|
||||
},
|
||||
},
|
||||
temperature: 1,
|
||||
});
|
||||
|
||||
const gptArgs = completion?.choices[0]?.message?.tool_calls?.[0]?.function.arguments;
|
||||
|
||||
if (!gptArgs) {
|
||||
throw new HttpError(500, 'Bad response from OpenAI');
|
||||
}
|
||||
|
||||
console.log('gpt function call arguments: ', gptArgs);
|
||||
|
||||
await context.entities.GptResponse.create({
|
||||
data: {
|
||||
user: { connect: { id: context.user.id } },
|
||||
content: JSON.stringify(gptArgs),
|
||||
},
|
||||
});
|
||||
|
||||
return JSON.parse(gptArgs);
|
||||
} catch (error: any) {
|
||||
if (!context.user.subscriptionStatus && error?.statusCode != 402) {
|
||||
await context.entities.User.update({
|
||||
where: { id: context.user.id },
|
||||
data: {
|
||||
credits: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
console.error(error);
|
||||
const statusCode = error.statusCode || 500;
|
||||
const errorMessage = error.message || 'Internal server error';
|
||||
throw new HttpError(statusCode, errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
export const createTask: CreateTask<Pick<Task, 'description'>, Task> = async ({ description }, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
|
||||
const task = await context.entities.Task.create({
|
||||
data: {
|
||||
description,
|
||||
user: { connect: { id: context.user.id } },
|
||||
},
|
||||
});
|
||||
|
||||
return task;
|
||||
};
|
||||
|
||||
export const updateTask: UpdateTask<Partial<Task>, Task> = async ({ id, isDone, time }, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
|
||||
const task = await context.entities.Task.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
isDone,
|
||||
time,
|
||||
},
|
||||
});
|
||||
|
||||
return task;
|
||||
};
|
||||
|
||||
export const deleteTask: DeleteTask<Pick<Task, 'id'>, Task> = async ({ id }, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
|
||||
const task = await context.entities.Task.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
return task;
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { type GptResponse, type Task } from 'wasp/entities';
|
||||
import { HttpError } from 'wasp/server';
|
||||
import { type GetGptResponses, type GetAllTasksByUser } from 'wasp/server/operations';
|
||||
|
||||
export const getGptResponses: GetGptResponses<void, GptResponse[]> = async (args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
return context.entities.GptResponse.findMany({
|
||||
where: {
|
||||
user: {
|
||||
id: context.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getAllTasksByUser: GetAllTasksByUser<void, Task[]> = async (_args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
return context.entities.Task.findMany({
|
||||
where: {
|
||||
user: {
|
||||
id: context.user.id,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -35,7 +35,7 @@ function generateMockUserData(): MockUserData {
|
||||
createdAt,
|
||||
lastActiveTimestamp,
|
||||
isAdmin: false,
|
||||
sendEmail: false,
|
||||
sendNewsletter: false,
|
||||
credits,
|
||||
subscriptionStatus,
|
||||
stripeId: hasUserPaidOnStripe ? `cus_test_${faker.string.uuid()}` : null,
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { type EmailChecker } from 'wasp/server/jobs';
|
||||
|
||||
import { type User } from 'wasp/entities';
|
||||
import { emailSender } from 'wasp/server/email';
|
||||
import { type Email } from 'wasp/server/email/core/types'; // TODO fix after it gets fixed in wasp :)
|
||||
|
||||
const emailToSend: Email = {
|
||||
to: '',
|
||||
subject: 'The SaaS App Newsletter',
|
||||
text: 'Hey There! \n\nThis is just a newsletter that sends automatically via cron jobs',
|
||||
html: `<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SaaS App Newsletter</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hey There!</p>
|
||||
|
||||
<p>This is just a newsletter that sends automatically via cron jobs</p>
|
||||
</body>
|
||||
</html>`,
|
||||
};
|
||||
|
||||
// you could use this function to send newsletters, expiration notices, etc.
|
||||
export const checkAndQueueEmails: EmailChecker<never, void> = async (_args, context) => {
|
||||
// e.g. you could send an offer email 2 weeks before their subscription expires
|
||||
const currentDate = new Date();
|
||||
const twoWeeksFromNow = new Date(currentDate.getTime() + 14 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const users = (await context.entities.User.findMany({
|
||||
where: {
|
||||
datePaid: {
|
||||
equals: twoWeeksFromNow,
|
||||
},
|
||||
sendEmail: true,
|
||||
},
|
||||
})) as User[];
|
||||
|
||||
if (users.length === 0) {
|
||||
return;
|
||||
}
|
||||
await Promise.allSettled(
|
||||
users.map(async (user) => {
|
||||
if (user.email) {
|
||||
try {
|
||||
emailToSend.to = user.email;
|
||||
await emailSender.send(emailToSend);
|
||||
} catch (error) {
|
||||
console.error('Error sending notice to user: ', user.id, error);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user