diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 37c2507..feab771 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -43,6 +43,7 @@ model User { messages Message[] blogReaction BlogReaction[] blogComment BlogComment[] + aiChat AIChat[] } model Session { @@ -60,49 +61,67 @@ model PasswordResetToken { } model Message { - id String @id @default(cuid()) - mentionedToId String? - message String - createdAt DateTime @default(now()) @map("created_at") - isShow Boolean? @default(true) @map("is_show") - user User @relation(fields: [userId], references: [id]) - userId String + id String @id @default(cuid()) + mentionedToId String? + message String + createdAt DateTime @default(now()) @map("created_at") + isShow Boolean? @default(true) @map("is_show") + user User @relation(fields: [userId], references: [id]) + userId String - mentionedTo Message? @relation("MessageMentions", fields: [mentionedToId], references: [id]) - mentionedBy Message[] @relation("MessageMentions") + mentionedTo Message? @relation("MessageMentions", fields: [mentionedToId], references: [id]) + mentionedBy Message[] @relation("MessageMentions") +} + +model AIChat { + id String @id @default(cuid()) + userId String + chatTitle String + messages AIChatMessage[] + createdAt DateTime @default(now()) @map("created_at") + user User @relation(fields: [userId], references: [id]) +} + +model AIChatMessage { + id String @id @default(cuid()) + msg String + role String + createdAt DateTime @default(now()) @map("created_at") + AIChat AIChat? @relation(fields: [aIChatId], references: [id]) + aIChatId String? } model Blog { - id Int @id @default(autoincrement()) - img String - title String - slug String @unique - description String @db.Text - content String @db.Text - tags String? @db.Text - viewCount Int @default(0) - createdAt DateTime @default(now()) - comments BlogComment[] - reactions BlogReaction[] + id Int @id @default(autoincrement()) + img String + title String + slug String @unique + description String @db.Text + content String @db.Text + tags String? @db.Text + viewCount Int @default(0) + createdAt DateTime @default(now()) + comments BlogComment[] + reactions BlogReaction[] } model BlogComment { - id Int @id @default(autoincrement()) - userId String - blogId Int - content String - createdAt DateTime @default(now()) - user User @relation(fields: [userId], references: [id]) - blog Blog @relation(fields: [blogId], references: [id]) + id Int @id @default(autoincrement()) + userId String + blogId Int + content String + createdAt DateTime @default(now()) + user User @relation(fields: [userId], references: [id]) + blog Blog @relation(fields: [blogId], references: [id]) } model BlogReaction { - id Int @id @default(autoincrement()) - userId String - blogId Int - type BlogReactionType - blog Blog @relation(fields: [blogId], references: [id]) - user User @relation(fields: [userId], references: [id]) + id Int @id @default(autoincrement()) + userId String + blogId Int + type BlogReactionType + blog Blog @relation(fields: [blogId], references: [id]) + user User @relation(fields: [userId], references: [id]) } enum BlogReactionType { @@ -115,46 +134,46 @@ enum BlogReactionType { } model Portfolio { - id Int @id @default(autoincrement()) - img String - title String - description String? @db.Text - content String? @db.Text - href String? - projectLink String? - isFeatured Boolean @default(false) - stacks PortfolioStack[] - createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + img String + title String + description String? @db.Text + content String? @db.Text + href String? + projectLink String? + isFeatured Boolean @default(false) + stacks PortfolioStack[] + createdAt DateTime @default(now()) } model PortfolioStack { - id Int @id @default(autoincrement()) - description String - stackId Int - portfolio Portfolio @relation(fields: [stackId], references: [id], onDelete: Cascade) + id Int @id @default(autoincrement()) + description String + stackId Int + portfolio Portfolio @relation(fields: [stackId], references: [id], onDelete: Cascade) } model Education { - id Int @id @default(autoincrement()) - instance String - address String - date String + id Int @id @default(autoincrement()) + instance String + address String + date String } model Work { - id Int @id @default(autoincrement()) - logo String - jobTitle String - instance String - instanceLink String - address String - date String + id Int @id @default(autoincrement()) + logo String + jobTitle String + instance String + instanceLink String + address String + date String responsibilities WorkResponsibility[] } model WorkResponsibility { - id Int @id @default(autoincrement()) - description String - workId Int - work Work @relation(fields: [workId], references: [id], onDelete: Cascade) + id Int @id @default(autoincrement()) + description String + workId Int + work Work @relation(fields: [workId], references: [id], onDelete: Cascade) } diff --git a/src/api/controller/ai.controller.ts b/src/api/controller/ai.controller.ts new file mode 100644 index 0000000..eb8153b --- /dev/null +++ b/src/api/controller/ai.controller.ts @@ -0,0 +1,164 @@ +import { t } from "elysia"; + +import { createElysia } from "@libs/elysia"; +import { authGuard } from "@libs/authGuard"; +import { ChatCompletionResponse, Message } from "@t/ai.types"; +import { prismaClient } from "@libs/prismaDatabase"; + +export const AIController = createElysia() + .model({ + "ai.req.model": t.Object({ + msg: t.String(), + aiChatId: t.Optional(t.String()), + }), + }) + .use(authGuard) + .get( + "/", + async ({ user }) => { + const aiChats = await prismaClient.aIChat.findMany({ + where: { + userId: user.id, + }, + select: { + userId: false, + chatTitle: true, + id: true, + createdAt: true, + }, + }); + + return { + status: 200, + data: aiChats, + }; + }, + { + detail: { + tags: ["AI"], + }, + } + ) + .get( + "/:id", + async ({ params: { id }, user }) => { + const aiChat = await prismaClient.aIChat.findUnique({ + where: { + id, + userId: user.id, + }, + select: { + messages: true, + }, + }); + + return { + status: 200, + data: aiChat, + }; + }, + { + detail: { + tags: ["AI"], + }, + } + ) + .post( + "/", + async ({ body, user, env }) => { + const { msg, aiChatId } = body; + let previousMsg: Message[] = []; + let responseAIChatId = aiChatId ?? ""; + + if (aiChatId) { + const chat = await prismaClient.aIChat.findUnique({ + where: { + id: aiChatId, + userId: user.id, + }, + include: { + messages: true, + }, + }); + if (chat) { + previousMsg = chat.messages.map((message) => ({ + role: message.role, + content: message.msg, + })); + } + } + + const ai: Promise = fetch(env.AZURE_AI_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + "api-key": env.AZURE_AI_API_KEY, + }, + body: JSON.stringify({ + messages: [ + ...previousMsg, + { + role: "user", + content: msg, + }, + ], + max_tokens: 100, + }), + }).then(async (response) => await response.json()); + + const { choices } = await ai; + + if (!aiChatId) { + const createAIChat = await prismaClient.aIChat.create({ + data: { + chatTitle: msg, + userId: user.id, + messages: { + createMany: { + data: [ + { + msg, + role: "user", + }, + { + msg: choices[0].message.content, + role: "assistant", + }, + ], + }, + }, + }, + }); + responseAIChatId = createAIChat.id; + } else { + await prismaClient.aIChatMessage.createMany({ + data: [ + { + aIChatId: aiChatId, + msg, + role: "user", + }, + { + aIChatId: aiChatId, + msg: choices[0].message.content, + role: "assistant", + }, + ], + }); + } + + return { + status: 200, + data: { + id: responseAIChatId, + result: choices[0].message.content, + }, + }; + }, + { + body: "ai.req.model", + detail: { + tags: ["AI"], + }, + } + ); diff --git a/src/api/index.ts b/src/api/index.ts index 4bbdec3..5826238 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,11 +1,13 @@ import { createElysia } from "@libs/elysia"; +import { me } from "./controller/user.controller"; +import { auth } from "./controller/auth"; + import { PortfolioController } from "./controller/portfolio.controller"; import { WorkController } from "./controller/work.controller"; import { MessageController } from "./controller/message.controller"; -import { auth } from "./controller/auth"; -import { me } from "./controller/user.controller"; import { BlogController } from "./controller/blog.controller"; import { CloudinaryController } from "./controller/cloudinary.controller"; +import { AIController } from "./controller/ai.controller"; const apiRoutes = createElysia({ prefix: "v2/" }) .group("auth", (api) => api.use(auth)) @@ -14,6 +16,7 @@ const apiRoutes = createElysia({ prefix: "v2/" }) .group("work", (api) => api.use(WorkController)) .group("portfolio", (api) => api.use(PortfolioController)) .group("blog", (api) => api.use(BlogController)) - .group("assets", (api) => api.use(CloudinaryController)); + .group("assets", (api) => api.use(CloudinaryController)) + .group("ai", (api) => api.use(AIController)); export default apiRoutes; diff --git a/src/libs/env.ts b/src/libs/env.ts index d7b4a45..48c27a2 100644 --- a/src/libs/env.ts +++ b/src/libs/env.ts @@ -16,6 +16,8 @@ const envValidateScheme = z.object({ CLOUDINARY_CLOUD_NAME: z.string(), CLOUDINARY_API_KEY: z.string(), CLOUDINARY_API_SECRET: z.string(), + AZURE_AI_URL: z.string(), + AZURE_AI_API_KEY: z.string(), }); const env = () => { diff --git a/src/types/ai.types.ts b/src/types/ai.types.ts new file mode 100644 index 0000000..b446c07 --- /dev/null +++ b/src/types/ai.types.ts @@ -0,0 +1,57 @@ +type ChatCompletionResponse = { + id: string; + object: string; + created: number; + model: string; + choices: Choice[]; + usage: Usage; + prompt_filter_results: PromptFilterResult[]; + system_fingerprint: string; +}; + +type Choice = { + index: number; + message: Message; + finish_reason: string; + logprobs: null; + content_filter_results: ContentFilterResults; +}; + +type Message = { + role: string; + content: string; +}; + +type ContentFilterResults = { + hate: FilterResult; + self_harm: FilterResult; + sexual: FilterResult; + violence: FilterResult; +}; + +type FilterResult = { + filtered: boolean; + severity: "safe" | "low" | "medium" | "high"; +}; + +type PromptFilterResult = { + prompt_index: number; + content_filter_results: ContentFilterResultsWithJailbreak; +}; + +type ContentFilterResultsWithJailbreak = ContentFilterResults & { + jailbreak: JailbreakResult; +}; + +type JailbreakResult = { + filtered: boolean; + detected: boolean; +}; + +type Usage = { + completion_tokens: number; + prompt_tokens: number; + total_tokens: number; +}; + +export { Message, ChatCompletionResponse };