diff --git a/backend/interfaces/neo4j-connection-instance.ts b/backend/interfaces/neo4j-connection-instance.ts new file mode 100644 index 0000000..041006d --- /dev/null +++ b/backend/interfaces/neo4j-connection-instance.ts @@ -0,0 +1,13 @@ +import { Config } from "neo4j-driver"; + +export type Neo4jScheme = 'neo4j' | 'neo4j+s' | 'neo4j+scc' | 'bolt' | 'bolt+s' | 'bolt+scc' + +export interface Neo4jConnection { + scheme: Neo4jScheme; + host: string; + port: number | string; + username: string; + password: string; + database?: string; + config?: Config; +} \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 4fb3a15..113bfa7 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,6 +12,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "neo4j-driver": "^5.14.0", "nodemon": "^3.0.1" }, "devDependencies": { @@ -266,6 +267,25 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -317,6 +337,29 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -764,6 +807,25 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -955,6 +1017,31 @@ "node": ">= 0.6" } }, + "node_modules/neo4j-driver": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/neo4j-driver/-/neo4j-driver-5.14.0.tgz", + "integrity": "sha512-K8x3UtEHXJQtfHLdqc+DU+OJsujNen2I9schZWs3mtnIYXcP+dFvWLusaXxRDQO1MBnplM3nvR54eYUYMqhfnw==", + "dependencies": { + "neo4j-driver-bolt-connection": "5.14.0", + "neo4j-driver-core": "5.14.0", + "rxjs": "^7.8.1" + } + }, + "node_modules/neo4j-driver-bolt-connection": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-5.14.0.tgz", + "integrity": "sha512-JocVRGGSTLtqcLwSsbodU6ZWVf4OPN5X1ozmy2OfRhmRR7CfhHaImjT9N0MBZ9NcahH9V5XqLroRK7DZdqKJEg==", + "dependencies": { + "buffer": "^6.0.3", + "neo4j-driver-core": "5.14.0", + "string_decoder": "^1.3.0" + } + }, + "node_modules/neo4j-driver-core": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/neo4j-driver-core/-/neo4j-driver-core-5.14.0.tgz", + "integrity": "sha512-IpO2BOazTF2QLGE7kYjwbicBxGdsu/JkDhdSmsziBcqEiRb9TBXRILhvlxiC00p1QyqMi7iVmJtIsbROqi5e/w==" + }, "node_modules/nodemon": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", @@ -1185,6 +1272,14 @@ "rimraf": "bin.js" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1335,6 +1430,14 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -1504,6 +1607,11 @@ "strip-json-comments": "^2.0.0" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/backend/package.json b/backend/package.json index be4cd59..569e2c6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,6 +15,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "neo4j-driver": "^5.14.0", "nodemon": "^3.0.1" }, "devDependencies": { diff --git a/backend/src/controllers/index.ts b/backend/src/controllers/index.ts index c9cc1c4..761a220 100644 --- a/backend/src/controllers/index.ts +++ b/backend/src/controllers/index.ts @@ -1,16 +1,362 @@ -import express, { Express, Request, Response , Application } from 'express'; -import dotenv from 'dotenv'; +import express, { Express, Request, Response, Application, request } from "express"; +import dotenv from "dotenv"; +import neo4j, { Driver, Session } from "neo4j-driver"; +import { + conversationResponse, + conversation, + createdResponse, + docs, +} from "../types/conversation"; -//For env File dotenv.config(); const app: Application = express(); +app.use(express.json()); const port = process.env.PORT || 8000; -app.get('/', (req: Request, res: Response) => { - res.send('Welcome to Express & TypeScript Server'); +export const createDriver = async () => { + const driver = neo4j.driver( + String(process.env.NEO4J_URI), + neo4j.auth.basic( + String(process.env.NEO4J_USER), + String(process.env.NEO4J_PASSWORD) + ) + ); + + return driver; +}; + +app.get("/", async (req: Request, res: Response) => { + const driver = await createDriver(); + + const session = driver.session(); + + const { records } = await session.run("MATCH (p:User) return p"); + + const result = records.map((record) => record.get("p")); + closeConnection(driver, session); + res.json(result); +}); + +app.get("/login", async (req: Request, res: Response) => { + const { username, password } = req.query; + const driver = await createDriver(); + + const session = driver.session(); + + const { records } = await session.run( + "MATCH (p:User {surname: $propertyValue, name: $password }) RETURN ID(p) AS userId, p", + { propertyValue: username, password: password } + ); + if (records.length <= 0) { + res.status(401).json({ + response: "Login Failed!", + }); + closeConnection(driver, session); + return; + } + closeConnection(driver, session); + res.status(200).json({ + response: "Successfully logged in!", + userID: records[0].get("userId").toInt(), + }); +}); + +app.delete("/history", async (req: Request, res: Response) => { + const { historyID, userID } = req.query; + const driver = await createDriver(); + + const session = driver.session(); + const result = await session.run( + "MATCH (h:History WHERE ID(h) = $history)-[:HAS_CONVERSATION]->(c:Conversation) detach delete h, c", + { history: Number(historyID) } + ); + closeConnection(driver, session); + res.status(200).json(await getHistoryByUser(Number(userID))); +}); + +app.post("/search", async (req: Request, res: Response) => { + const { userID, historyID } = req.query; + const data = req.body; + + const convo = req.body; + const prompt = "Prompt from MML"; + const results: docs[] = []; + + const convoObj: conversation = { + prompt: prompt, + request: req.body, + docs: results, + }; + + const driver = await createDriver(); + + const session = driver.session(); + + const createdUserHistoryRel = await session.run( + "MATCH (u:User where ID(u) = $userID) CREATE (h:History {createdAt: localdatetime()}) SET h += $history CREATE (u)-[:HAS_HISTORY]->(h)", + { history: data, userID: Number(userID) } + ); + + if (!historyID) { + const returnedVal = createHistory(Number(userID), data) + + const response = Number((await returnedVal).response.statusCode) + + if (response == 200) { + const hID = Number((await returnedVal).hID) + console.log(hID) + + const createdHistoryConversationRel = await session.run( + "MATCH (h:History) WHERE ID(h) = $hID CREATE (c:Conversation {createdAt: localdatetime()}) SET c += $conversation CREATE (h)-[:HAS_CONVERSATION]->(c)", + { conversation: data, hID: Number(hID) } + ); + return res.json(createdHistoryConversationRel) + } + } else { + const appendHistoryConversationRel = await session.run( + "MATCH (h:History) WHERE ID(h) = $history CREATE (c:Conversation {createdAt: localdatetime()}) SET c += $conversation CREATE (h)-[:HAS_CONVERSATION]->(c)", + { conversation: data, history: Number(historyID) } + ); + return res.json(appendHistoryConversationRel) + } + +}); + +async function createHistory(userID: number, retrievedData: string) { + const data = retrievedData; + + const driver = await createDriver(); + + const session = driver.session(); + + try { + const createdUserHistoryRel = await session.run( + "MATCH (u:User) WHERE ID(u) = $uID CREATE (h:History {createdAt: localdatetime()}) SET h += $history CREATE (u)-[:HAS_HISTORY]->(h) return ID(h) as nodeId", + { history: data, uID: userID } + ); + const historyID: number = createdUserHistoryRel.records[0].get("nodeId"); + const response: createdResponse = { + statusCode: 200, + response: { + response: "Successfully created conversation", + } + } + const returnData = { "response": response, "hID": historyID } + return returnData + } catch (error) { + const response: createdResponse = { + statusCode: 500, + response: { + response: "Successfully created conversation", + } + } + const returnData = { "response": response, "hID": null} + return returnData + } + + + + + +} + +// app.post("/history", async (req: Request, res: Response) => { +// const { userID } = req.query; +// const data = req.body; + +// const driver = await createDriver(); + +// const session = driver.session(); + +// const createdUserHistoryRel = await session.run( +// "MATCH (u:User {surname : 'Doe'}) CREATE (h:History {createdAt: localdatetime()}) SET h += $history CREATE (u)-[:HAS_HISTORY]->(h)", +// { history: data } +// ); + +// return res.json(createdUserHistoryRel) +// }); + +app.get("/history", async (req: Request, res: Response) => { + const { userID } = req.query; + /* const driver = await createDriver(); + const session = driver.session(); + + const { records } = await session.run( + "MATCH (p:User where ID(p) = $userIDDB)-[r:HAS_HISTORY]->(h:History) return h, ID(h) as historyID", + { userIDDB: Number(userID) } + ); + + if (records.length <= 0) { + return res.status(200).json([]); + } + + const attributes = records.map((record) => { + const data = record.get("h"); + + return { + historyID: record.get("historyID").toInt(), + title: data.properties.title, + language: data.properties.language, + }; + }); */ + + res.status(200).json(await getHistoryByUser(Number(userID))); +}); + +app.get("/conversation", async (req: Request, res: Response) => { + const { historyID } = req.query; + if (!historyID) { + return res.status(404).json({ + response: "historyID is required!", + }); + } + const driver = await createDriver(); + const session = driver.session(); + const { records } = await session.run( + "MATCH (h:History where ID(h) = $historyID)-[r:HAS_CONVERSATION]->(c:Conversation) return c, h ORDER BY c.createdAt ASC", + { historyID: Number(historyID) } + ); + closeConnection(driver, session); + if (records.length <= 0) { + return res.status(404).json({ + response: "No Conversation found!", + }); + } + + const historyRecord = records[0].get("h"); + const historyTitle = historyRecord.properties.title; + + const allConversations: conversation[] = records.map((record) => { + const singleRecord = record.get("c"); + const createdAt = singleRecord.properties.createdAt; + return { + prompt: singleRecord.properties.prompt, + request: singleRecord.properties.request, + docs: singleRecord.properties.docs, + createdAt: new Date( + createdAt.year.low, + createdAt.month.low - 1, + createdAt.day.low, + createdAt.hour.low, + createdAt.minute.low, + createdAt.second.low, + createdAt.nanosecond.low / 1000000 + ), + }; + }); + + const conversationResponse: conversationResponse = { + historyID: Number(historyID), + title: historyTitle, + conversations: allConversations, + }; + + return res.status(200).json(conversationResponse); +}); + +app.post("/conversation", async (req: Request, res: Response) => { + const { historyID } = req.query; + //Search search from frontend + const convo = req.body; + const prompt = "Prompt from MML"; + const results: docs[] = []; + + const convoObj: conversation = { + prompt: prompt, + request: req.body, + docs: results, + }; + + const addConvoResponse: createdResponse = await addConversation( + convo, + Number(historyID) + ); + + return res + .status(addConvoResponse.statusCode) + .json(addConvoResponse.response); }); +const getLLMData = async (request: string) => { + const requestURL: string = (process.env.LLM_URI as string) + "/search"; + const response = await fetch(requestURL, { + method: "GET", + mode: "cors", + cache: "no-cache", + headers: { + "Content-Type": "application/json", + }, + }); + if (!response.ok) { + //TODO Error Handling + } + + return await response.json(); +}; + +const addConversation = async ( + conversationData: conversation, + historyID: number +) => { + const driver = await createDriver(); + const session = driver.session(); + try { + const createdConvo = await session.run( + "MATCH (h:History where ID(h) = $historyID) CREATE (c:Conversation {createdAt: localdatetime()}) SET c += $convo CREATE (h)-[:HAS_CONVERSATION]->(c)", + { historyID: Number(historyID), convo: conversationData } + ); + closeConnection(driver, session); + const response: createdResponse = { + statusCode: 200, + response: { + response: "Successfully created conversation", + }, + }; + return response; + } catch (error) { + closeConnection(driver, session); + const response: createdResponse = { + statusCode: 500, + response: { + response: "Failed to create conversation!", + }, + }; + return response; + } +}; + +const getHistoryByUser = async (userID: Number) => { + const driver = await createDriver(); + const session = driver.session(); + + const { records } = await session.run( + "MATCH (p:User where ID(p) = $userIDDB)-[r:HAS_HISTORY]->(h:History) return h, ID(h) as historyID", + { userIDDB: Number(userID) } + ); + closeConnection(driver, session); + + if (records.length <= 0) { + return []; + } + + const attributes = records.map((record) => { + const data = record.get("h"); + + return { + historyID: record.get("historyID").toInt(), + title: data.properties.title, + language: data.properties.language, + }; + }); + return attributes; +}; + +const closeConnection = (driver: Driver, session: Session) => { + session.close(); + driver.close(); +}; + app.listen(port, () => { - console.log(`Server is Fire at http://localhost:${port}`); -}); \ No newline at end of file + console.log(`Server is running at http://localhost:${port}`); +}); diff --git a/backend/src/types/LLMData.ts b/backend/src/types/LLMData.ts new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/types/conversation.ts b/backend/src/types/conversation.ts new file mode 100644 index 0000000..433c56c --- /dev/null +++ b/backend/src/types/conversation.ts @@ -0,0 +1,21 @@ +import { DateTime } from "neo4j-driver"; + +export type docs = { + KBView_URL: string; +}; + +export type conversation = { + prompt: string; + request: string; + createdAt?: Date; + docs: docs[]; +}; +export type conversationResponse = { + historyID: number; + title: string; + conversations: conversation[]; +}; +export type createdResponse = { + statusCode: number; + response: {}; +};