Skip to content

Commit ed6add7

Browse files
committed
Refactor generateGptResponse to include a transaction
1 parent 8055995 commit ed6add7

File tree

2 files changed

+155
-146
lines changed

2 files changed

+155
-146
lines changed
Lines changed: 155 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as z from 'zod';
2-
import type { Task, GptResponse } from 'wasp/entities';
2+
import type { Task, GptResponse, User } from 'wasp/entities';
33
import type {
44
GenerateGptResponse,
55
CreateTask,
@@ -8,18 +8,18 @@ import type {
88
GetGptResponses,
99
GetAllTasksByUser,
1010
} from 'wasp/server/operations';
11-
import { HttpError } from 'wasp/server';
11+
import { HttpError, prisma } from 'wasp/server';
1212
import { GeneratedSchedule } from './schedule';
1313
import OpenAI from 'openai';
1414
import { SubscriptionStatus } from '../payment/plans';
1515
import { ensureArgsSchemaOrThrowHttpError } from '../server/validation';
1616

17-
const openai = setupOpenAI();
18-
function setupOpenAI() {
19-
if (!process.env.OPENAI_API_KEY) {
20-
return new HttpError(500, 'OpenAI API key is not set');
17+
function getOpenAI(): OpenAI | null {
18+
if (process.env.OPENAI_API_KEY) {
19+
return new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
20+
} else {
21+
return null;
2122
}
22-
return new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
2323
}
2424

2525
//#region Actions
@@ -35,11 +35,15 @@ export const generateGptResponse: GenerateGptResponse<GenerateGptResponseInput,
3535
context
3636
) => {
3737
if (!context.user) {
38-
throw new HttpError(401);
38+
throw new HttpError(401, 'Only authenticated users are allowed to perform this operation');
3939
}
4040

4141
const { hours } = ensureArgsSchemaOrThrowHttpError(generateGptResponseInputSchema, rawArgs);
4242

43+
if (!isEligibleForResponse(context.user)) {
44+
throw new HttpError(402, 'User has not paid or is out of credits');
45+
}
46+
4347
const tasks = await context.entities.Task.findMany({
4448
where: {
4549
user: {
@@ -48,150 +52,61 @@ export const generateGptResponse: GenerateGptResponse<GenerateGptResponseInput,
4852
},
4953
});
5054

51-
const parsedTasks = tasks.map(({ description, time }) => ({
52-
description,
53-
time,
54-
}));
55+
console.log('Calling open AI api');
56+
const dailyPlanJson = await getDailyPlanFromGpt(tasks, hours);
57+
if (dailyPlanJson === null) {
58+
throw new HttpError(500, 'Bad response from OpenAI');
59+
}
5560

56-
try {
57-
// check if openai is initialized correctly with the API key
58-
if (openai instanceof Error) {
59-
throw openai;
60-
}
61-
62-
const hasCredits = context.user.credits > 0;
63-
const hasValidSubscription =
64-
!!context.user.subscriptionStatus &&
65-
context.user.subscriptionStatus !== SubscriptionStatus.Deleted &&
66-
context.user.subscriptionStatus !== SubscriptionStatus.PastDue;
67-
const canUserContinue = hasCredits || hasValidSubscription;
68-
69-
if (!canUserContinue) {
70-
throw new HttpError(402, 'User has not paid or is out of credits');
71-
} else {
72-
console.log('decrementing credits');
73-
await context.entities.User.update({
74-
where: { id: context.user.id },
75-
data: {
76-
credits: {
77-
decrement: 1,
78-
},
79-
},
80-
});
81-
}
82-
83-
const completion = await openai.chat.completions.create({
84-
model: 'gpt-3.5-turbo', // you can use any model here, e.g. 'gpt-3.5-turbo', 'gpt-4', etc.
85-
messages: [
86-
{
87-
role: 'system',
88-
content:
89-
'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.',
90-
},
91-
{
92-
role: 'user',
93-
content: `I will work ${hours} hours today. Here are the tasks I have to complete: ${JSON.stringify(
94-
parsedTasks
95-
)}. Please help me plan my day by breaking the tasks down into actionable subtasks with time and priority status.`,
96-
},
97-
],
98-
tools: [
99-
{
100-
type: 'function',
101-
function: {
102-
name: 'parseTodaysSchedule',
103-
description: 'parses the days tasks and returns a schedule',
104-
parameters: {
105-
type: 'object',
106-
properties: {
107-
mainTasks: {
108-
type: 'array',
109-
description: 'Name of main tasks provided by user, ordered by priority',
110-
items: {
111-
type: 'object',
112-
properties: {
113-
name: {
114-
type: 'string',
115-
description: 'Name of main task provided by user',
116-
},
117-
priority: {
118-
type: 'string',
119-
enum: ['low', 'medium', 'high'],
120-
description: 'task priority',
121-
},
122-
},
123-
},
124-
},
125-
subtasks: {
126-
type: 'array',
127-
items: {
128-
type: 'object',
129-
properties: {
130-
description: {
131-
type: 'string',
132-
description:
133-
'detailed breakdown and description of sub-task related to main task. e.g., "Prepare your learning session by first reading through the documentation"',
134-
},
135-
time: {
136-
type: 'number',
137-
description: 'time allocated for a given subtask in hours, e.g. 0.5',
138-
},
139-
mainTaskName: {
140-
type: 'string',
141-
description: 'name of main task related to subtask',
142-
},
143-
},
144-
},
145-
},
146-
},
147-
required: ['mainTasks', 'subtasks', 'time', 'priority'],
148-
},
149-
},
150-
},
151-
],
152-
tool_choice: {
153-
type: 'function',
154-
function: {
155-
name: 'parseTodaysSchedule',
156-
},
157-
},
158-
temperature: 1,
159-
});
61+
// TODO: Do I need a try catch now that I'm saving a response and
62+
// decrementing credits in a transaction?
16063

161-
const gptArgs = completion?.choices[0]?.message?.tool_calls?.[0]?.function.arguments;
64+
// NOTE: I changed this up, first I do the request, and then I decrement
65+
// credits. Is that dangerous? Protecting from it could be too complicated
66+
// TODO: Potential issue here, user can lose credits between the point we
67+
// inject it into the context and here
68+
// Seems to me that there should be users in the database with negative credits
69+
const decrementCredit = context.entities.User.update({
70+
where: { id: context.user.id },
71+
data: {
72+
credits: {
73+
decrement: 1,
74+
},
75+
},
76+
});
16277

163-
if (!gptArgs) {
164-
throw new HttpError(500, 'Bad response from OpenAI');
165-
}
78+
const createResponse = context.entities.GptResponse.create({
79+
data: {
80+
user: { connect: { id: context.user.id } },
81+
content: dailyPlanJson,
82+
},
83+
});
16684

167-
console.log('gpt function call arguments: ', gptArgs);
85+
// NOTE: Since these two are now brought together, I put them in a single transaction - so no rollback necessary
86+
// TODO: But what if the server crashes after the transaction and before
87+
// the response? I guess no big deal, since the response is in the db.
88+
console.log('Decrementing credits and saving response');
89+
prisma.$transaction([decrementCredit, createResponse]);
16890

169-
await context.entities.GptResponse.create({
170-
data: {
171-
user: { connect: { id: context.user.id } },
172-
content: JSON.stringify(gptArgs),
173-
},
174-
});
175-
176-
return JSON.parse(gptArgs);
177-
} catch (error: any) {
178-
if (!context.user.subscriptionStatus && error?.statusCode != 402) {
179-
await context.entities.User.update({
180-
where: { id: context.user.id },
181-
data: {
182-
credits: {
183-
increment: 1,
184-
},
185-
},
186-
});
187-
}
188-
console.error(error);
189-
const statusCode = error.statusCode || 500;
190-
const errorMessage = error.message || 'Internal server error';
191-
throw new HttpError(statusCode, errorMessage);
192-
}
91+
// TODO: Can this ever fail?
92+
return JSON.parse(dailyPlanJson);
19393
};
19494

95+
function isEligibleForResponse(user: User) {
96+
// TODO: Why not check for allowed states?
97+
const isUserSubscribed =
98+
user.subscriptionStatus !== SubscriptionStatus.Deleted &&
99+
user.subscriptionStatus !== SubscriptionStatus.PastDue;
100+
const userHasCredits = user.credits > 0;
101+
// TODO: If the user is subscribed (flat rate, use their subscription) -
102+
// shouldn't take priority over credits since it makes no sece spending
103+
// credits when a subscription is active?
104+
// If they aren't subscribed, then it would make sense to spend credits.
105+
// The old code always subtracted credits so I kept that, is this a bug?
106+
// Also, no one checks for subscription plan (10credits or flat rate).
107+
return isUserSubscribed || userHasCredits;
108+
}
109+
195110
const createTaskInputSchema = z.object({
196111
description: z.string().nonempty(),
197112
});
@@ -296,3 +211,98 @@ export const getAllTasksByUser: GetAllTasksByUser<void, Task[]> = async (_args,
296211
});
297212
};
298213
//#endregion
214+
215+
// TODO: Why is hours a string?
216+
async function getDailyPlanFromGpt(tasks: Task[], hours: string): Promise<string | null> {
217+
// TODO: Why was this a singleton
218+
const openai = getOpenAI();
219+
if (openai === null) {
220+
return null;
221+
}
222+
223+
const parsedTasks = tasks.map(({ description, time }) => ({
224+
description,
225+
time,
226+
}));
227+
228+
const completion = await openai.chat.completions.create({
229+
model: 'gpt-3.5-turbo', // you can use any model here, e.g. 'gpt-3.5-turbo', 'gpt-4', etc.
230+
messages: [
231+
{
232+
role: 'system',
233+
content:
234+
'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.',
235+
},
236+
{
237+
role: 'user',
238+
content: `I will work ${hours} hours today. Here are the tasks I have to complete: ${JSON.stringify(
239+
parsedTasks
240+
)}. Please help me plan my day by breaking the tasks down into actionable subtasks with time and priority status.`,
241+
},
242+
],
243+
tools: [
244+
{
245+
type: 'function',
246+
function: {
247+
name: 'parseTodaysSchedule',
248+
description: 'parses the days tasks and returns a schedule',
249+
parameters: {
250+
type: 'object',
251+
properties: {
252+
mainTasks: {
253+
type: 'array',
254+
description: 'Name of main tasks provided by user, ordered by priority',
255+
items: {
256+
type: 'object',
257+
properties: {
258+
name: {
259+
type: 'string',
260+
description: 'Name of main task provided by user',
261+
},
262+
priority: {
263+
type: 'string',
264+
enum: ['low', 'medium', 'high'],
265+
description: 'task priority',
266+
},
267+
},
268+
},
269+
},
270+
subtasks: {
271+
type: 'array',
272+
items: {
273+
type: 'object',
274+
properties: {
275+
description: {
276+
type: 'string',
277+
description:
278+
'detailed breakdown and description of sub-task related to main task. e.g., "Prepare your learning session by first reading through the documentation"',
279+
},
280+
time: {
281+
type: 'number',
282+
description: 'time allocated for a given subtask in hours, e.g. 0.5',
283+
},
284+
mainTaskName: {
285+
type: 'string',
286+
description: 'name of main task related to subtask',
287+
},
288+
},
289+
},
290+
},
291+
},
292+
required: ['mainTasks', 'subtasks', 'time', 'priority'],
293+
},
294+
},
295+
},
296+
],
297+
tool_choice: {
298+
type: 'function',
299+
function: {
300+
name: 'parseTodaysSchedule',
301+
},
302+
},
303+
temperature: 1,
304+
});
305+
306+
const gptResponse = completion?.choices[0]?.message?.tool_calls?.[0]?.function.arguments;
307+
return gptResponse ?? null;
308+
}

template/app/src/payment/plans.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as z from 'zod';
21
import { requireNodeEnvVar } from '../server/utils';
32

43
export enum SubscriptionStatus {

0 commit comments

Comments
 (0)