Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Integrate ChatGPT fully. #48

Merged
merged 16 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ Welcome to our Quayside MVP. The tech stack for this is the MERN framework (Mong
## Setup
You need to install npm (you can do this by installing [Node.js](https://nodejs.org/en/download)). Once that is done, run `npm install` in this directory to install all the requirements.

For accessing the mongo database locally, you will need the following generated database Atlas creds in an `.env.local` file (fyi, these creds are different than your creds to login to Mongo Atlas). In the same file you will also ned your oath creds(for Google and GitHub as examples). Here is the forma:

For accessing the mongo database locally, you will need the following generated database Atlas creds in an `.env.local` file (fyi, these creds are different than your creds to login to Mongo Atlas). In the same file you will also ned your oath creds(for google and github as examples). You will also need your chatGPT key. Here is the forma:
For accessing the mongo database locally, you will need the following generated database Atlas creds in an `.env.local` file (fyi, these creds are different than your creds to login to Mongo Atlas). In the same file you will also ned your oath creds(for google and github as examples). You will also need your chatGPT key. Here is the format:

```bash
MONGO_USERNAME=<your username>
Expand Down
11 changes: 8 additions & 3 deletions src/app/[...projectIds]/page.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import TreeGraph from '../../components/Graph'
import Button from '../../components/Button'

/**
* Renders a page component that displays the tree graph with a dynamic route
Expand All @@ -11,10 +12,14 @@ import TreeGraph from '../../components/Graph'
*/
export default function page ({ params }) {
return (
<div>
<div className='p-4 text-xl flex w-full flex-wrap'>

Project: {params.projectIds}
<TreeGraph projectID={params.projectIds} />
<div className='flex w-full'>
<div className='flex w-11/12'> Project: {params.projectIds} </div>
<div className='flex w-1/12 justify-end'><Button label='Delete Project' /></div>
</div>

<TreeGraph projectID={params.projectIds} className='flex w-full' />
</div>
)
}
21 changes: 6 additions & 15 deletions src/app/api/auth/[...nextauth]/options.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import GitHubProvider from 'next-auth/providers/github'
import GoogleProvider from 'next-auth/providers/google'
import mongoose from 'mongoose'
import { URI } from '../../mongoDB/mongoData.js'
import { User } from '../../mongoDB/mongoModels.js'
import { getUsers } from '../../mongoDB/getUsers/getUsers'
import { createUser } from '../../mongoDB/createUser/createUser'

export const options = {
// configure one or more authentication providers
Expand All @@ -20,8 +19,7 @@ export const options = {
],
callbacks: {
async signIn ({ user, account, profile }) {
if (mongoose.connection.readyState !== 1) await mongoose.connect(URI)
const existingUser = await User.findOne({ email: user.email })
const existingUser = await getUsers(null, user.email)

if (existingUser) {
return true
Expand All @@ -32,13 +30,7 @@ export const options = {
const lastName = nameParts.length > 1 ? nameParts.slice(1).join(' ') : ''

// Create User
const newUser = await User.create({
firstName,
lastName,
email: user.email,
username: '',
teamIDs: []
})
const newUser = await createUser(user.email, firstName, lastName)
return !!newUser // Return true if creation is successful
}
},
Expand All @@ -52,9 +44,8 @@ export const options = {
async jwt ({ token, user, account, profile, isNewUser }) {
// This callback is called whenever a JWT is created. So session.userId is the mongo User _id
if (user) {
if (mongoose.connection.readyState !== 1) await mongoose.connect(URI)
const mongoUser = await User.findOne({ email: user.email }) // TODO maybe store this so don't have to do more queries
token.sub = mongoUser._id
const mongoUsers = await getUsers(null, user.email)
token.sub = mongoUsers[0]._id
}
return token
}
Expand Down
114 changes: 114 additions & 0 deletions src/app/api/chat-gpt/generateTasks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import OpenAI from 'openai'

/**
* Asynchronously generates a list of tasks and subtasks based on a user prompt using OpenAI's GPT-3.5 model.
* The function connects to OpenAI using an API key and sends a formatted request to receive a structured
* breakdown of tasks and subtasks. It processes the response to create a hierarchical list of tasks.
*
* This function can be used server side.Alternatively the POST request in the route.js should
* be used on client side (which uses this logic).
*
* @param {string} userPrompt - A prompt describing the project or task for which subtasks need to be generated.
* @returns {Promise<Array.<Object>>} - A promise that resolves to an array of task objects, each with a structure
* containing the task's id, name, parent, and an array of subtasks.
* @throws {Error} - Throws an error if the API key is missing, if there's an issue with the OpenAI request, or if
* the response processing encounters an error.
*
* @example
* // Example of using generateTasks function
* generateTasks('Plan a company retreat for 100 employees')
* .then(tasks => console.log('Generated tasks:', tasks))
* .catch(error => console.error('Error in generating tasks:', error));
*
* // Expected output structure of each task object:
* // {
* // id: '1',
* // name: 'Task name',
* // parent: 'root',
* // subtasks: [{ id: '1.1', name: 'Subtask name', parent: '1' }, ...]
* // }
*/
export async function generateTasks (userPrompt) {
const userAPIKey = process.env.QUAYSIDE_API_KEY
if (!userAPIKey) {
throw new Error('API key is required')
}

const openai = new OpenAI({
apiKey: userAPIKey
})

const response = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [
{
// set the tone of the response you get back
role: 'system',
content: 'You are given as input a project or task that a single person or a team wants to take on.' +
'Divide the task into less than 5 subtasks and list them hierarchically in the format where task 1 has subtasks 1.1, 1.2,...' +
'and task 2 has subtasks 2.1, 2.2, 2.3,... and so forth' +
'Make sure that every task is on one line after the number, NEVER create new paragraphs within a task or subtask.'
},
{
// here is where the user prompt gets used
role: 'user',
content: userPrompt
}
],
temperature: 0,
max_tokens: 1024,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0
})

const responseString = response.choices[0].message.content
console.log(responseString)
const newTasks = []

// Split the input string into an array of lines. ATTENTION! THIS REQUIRES THE CHATGPT RESPONSE TO PRINT ONE LINE PER TASK/SUBTASK
const lines = responseString.split('\n')

let currentTaskId = null
// Loop through the lines and extract tasks
for (let i = 0; i < lines.length; i++) {
// for every line,
const line = lines[i]
const primaryMatch = line.match(/^(\d+)\.\s(.+)/) // this checks for this structure: 1. <tasktext>
const subtaskMatch = line.match(/^\s+(\d+\.\d+\.?)\s(.+)/) // this checks for this structure: 1.2 <subtasktext> or 1.2. <subtasktext>

if (primaryMatch) {
const taskNumber = primaryMatch[1]
const taskText = primaryMatch[2]
currentTaskId = taskNumber

// making the 1st layer deep of tasks into a dictionary
newTasks.push({
id: taskNumber,
name: taskText,
parent: 'root', // this should be replaced by the user prompt
subtasks: []

})
console.log('Parsed task', taskNumber, ':', taskText)
} else if (subtaskMatch) {
const subtaskNumber = subtaskMatch[1]// .replace('.', '_'); // Convert 1.1 to 1_1
const subtaskText = subtaskMatch[2]

// Find the parent task
const parentTask = newTasks.find(task => task.id === currentTaskId)

// If the parent task is found, push the subtask into its subtasks array
if (parentTask) {
parentTask.subtasks.push({
id: subtaskNumber,
name: subtaskText,
parent: currentTaskId
})
console.log('Parsed subtask', subtaskNumber, ':', subtaskText)
}
}
}
console.log('Full dictionary:', newTasks)
return newTasks
}
92 changes: 50 additions & 42 deletions src/app/api/chat-gpt/route.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,59 @@
import { NextResponse } from 'next/server'
import { options } from '../auth/[...nextauth]/options'
import { getServerSession } from 'next-auth/next'
import { generateTasks } from './generateTasks'

import OpenAI from 'openai'

/**
* Handles a POST request to generate tasks based on a user prompt using the ChatGPT model. This endpoint first
* authenticates the user session. If authenticated, it extracts the user prompt from the request and calls
* the `generateTasks` function to generate a structured list of tasks and subtasks. It returns these tasks
* in the response.
*
* @param {Object} request - The incoming request object containing the user prompt in JSON format.
* @returns {Object} - A response object with a status code and either the generated tasks or an error message.
*
* @throws {Error} - Throws an error if there is an issue with authentication, generating tasks, or any other
* internal errors.
*
* @example
* // Example of a POST request to this endpoint
* fetch(`/api/generateTasks`, {
* method: 'POST',
* headers: { 'Content-Type': 'application/json' },
* body: JSON.stringify({ prompt: 'Organize a team-building event' }),
* }).then(async (response) => {
* const body = await response.json();
* if (!response.ok) {
* console.error('Error:', body.message);
* } else {
* console.log('Generated tasks:', body.newTasks);
* }
* }).catch(error => console.error('Error in POST request:', error));
*
* // Expected output structure of each task object:
* // {
* // id: '1',
* // name: 'Task name',
* // parent: 'root',
* // subtasks: [{ id: '1.1', name: 'Subtask name', parent: '1' }, ...]
* // }
*
* @property {string} request.body.prompt - The user prompt used to generate tasks.
*/
export async function POST (request) {
// Authenticates user
const session = await getServerSession(options)
if (!session) {
return NextResponse.json({ success: false, message: 'authentication failed' }, { status: 401 })
}
console.log(process.env.QUAYSIDE_API_KEY) // remove later

// magically getting the user form data from NewProjectModal form
const params = await request.json()
const userAPIKey = params.apiKey
const userPrompt = params.prompt
try {
const session = await getServerSession(options)
if (!session) {
return NextResponse.json({ success: false, message: 'authentication failed' }, { status: 401 })
}

if (!userAPIKey) {
return NextResponse.json({ message: 'API key is required' }, { status: 400 })
}

const openai = new OpenAI({
apiKey: userAPIKey
})
const params = await request.json()
const userPrompt = params.prompt

const response = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [
{
// set the tone of the response you get back
role: 'system',
content: 'Given the user prompt, list its subtasks and subtask\'s subtasks, etc... in a tree structure starting from 1, and going deeper with 1.1, 1.1.1, 1.2, 1.2.2, etc.' +
'Ensure every task and subtask has 5 or less children tasks. Keep the subtasks directly related to the user input task.'
},
{
// here is where the user prompt gets used
role: 'user',
content: userPrompt
}
],
temperature: 0,
max_tokens: 1024,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0
})
const newTasks = generateTasks(userPrompt)

return NextResponse.json(response)
return NextResponse.json({ newTasks }, { status: 500 })
} catch (error) {
return NextResponse.json({ message: 'Error calling ChatGPT:' + error }, { status: 500 })
}
}
37 changes: 30 additions & 7 deletions src/app/api/mongoDB/createProject/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import { getServerSession } from 'next-auth/next'
import { NextResponse } from 'next/server'
import { Project, User } from '../mongoModels'
import { URI } from '../mongoData.js'
import { generateTasks } from '../../chat-gpt/generateTasks'
import { createTask } from '../createTask/createTask'

/**
* Handles a POST request to create and store a new project in the database.
* Handles a POST request to create and store a new project, along with the tasks generated by chatGPT, in the database.
*
*
* @param {Object} request - The request object containing the project details.
* @returns {Object} - A response object with a status code and a message.
*
* @throws Will throw an error if any of the required fields are missing, if there's an issue connecting to the database,
* or if not authenticated.
*
* @example
* fetch(`/api/mongoDB/createProject`, {
* method: 'POST',
Expand Down Expand Up @@ -83,7 +83,7 @@ export async function POST (request) {
}
}

await Project.create({
const projectDocument = await Project.create({
name: params.name, // Required
userIDs: params.userIDs, // Required

Expand Down Expand Up @@ -114,8 +114,31 @@ export async function POST (request) {
teams: params.teams || []
})

return NextResponse.json({ message: 'Project stored successfully' }, { status: 200 })
// Parse and create projects
const chatGptResponse = await generateTasks(params.name)
const projectId = projectDocument._id
async function parseTask (task, parentId, projectId) {
const taskDocument = await createTask(task.name, projectId, parentId)
if (task.subtasks) {
for (const subtask of task.subtasks) {
parseTask(subtask, taskDocument._id, projectId)
}
}
}

let rootId = null
// If there is not already a parent task, create one
if (chatGptResponse.length !== 1) {
const rootTaskDocument = await createTask(params.name, projectId)
rootId = rootTaskDocument._id
}

for (const task of chatGptResponse) {
parseTask(task, rootId, projectId)
}

return NextResponse.json({ message: 'Project and tasks created successfully' }, { status: 200 })
} catch (error) {
return NextResponse.json({ message: 'Error storing data ' + error }, { status: 500 })
return NextResponse.json({ message: 'Error creating projects and tasks:' + error }, { status: 500 })
}
}
Loading