Skip to content

Commit

Permalink
feat(ai): Initial AI Chat feature with Azure AI Studio
Browse files Browse the repository at this point in the history
  • Loading branch information
xyzuan committed Nov 8, 2024
1 parent 2c67151 commit d14591f
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 65 deletions.
143 changes: 81 additions & 62 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ model User {
messages Message[]
blogReaction BlogReaction[]
blogComment BlogComment[]
aiChat AIChat[]
}

model Session {
Expand All @@ -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 {
Expand All @@ -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)
}
164 changes: 164 additions & 0 deletions src/api/controller/ai.controller.ts
Original file line number Diff line number Diff line change
@@ -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<ChatCompletionResponse> = 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"],
},
}
);
9 changes: 6 additions & 3 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -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))
Expand All @@ -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;
2 changes: 2 additions & 0 deletions src/libs/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
Loading

0 comments on commit d14591f

Please sign in to comment.