Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- CreateTable
CREATE TABLE "AiConversation" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"projectUserId" UUID NOT NULL,
"projectId" TEXT NOT NULL REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE,
"title" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "AiConversation_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "AiMessage" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"conversationId" UUID NOT NULL REFERENCES "AiConversation"("id") ON DELETE CASCADE ON UPDATE CASCADE,
"position" INTEGER NOT NULL,
"role" TEXT NOT NULL,
"content" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "AiMessage_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "AiConversation_projectUserId_projectId_updatedAt_idx" ON "AiConversation"("projectUserId", "projectId", "updatedAt" DESC);

-- CreateIndex
CREATE INDEX "AiMessage_conversationId_position_idx" ON "AiMessage"("conversationId", "position" ASC);
26 changes: 26 additions & 0 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ model Project {
branchConfigOverrides BranchConfigOverride[]
environmentConfigOverrides EnvironmentConfigOverride[]
localEmulatorProject LocalEmulatorProject?
aiConversations AiConversation[]

@@index([ownerTeamId], map: "Project_ownerTeamId_idx")
}
Expand Down Expand Up @@ -1090,6 +1091,31 @@ model ThreadMessage {
@@id([tenancyId, id])
}

model AiConversation {
id String @id @default(uuid()) @db.Uuid
projectUserId String @db.Uuid
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
title String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
messages AiMessage[]

@@index([projectUserId, projectId, updatedAt(sort: Desc)])
}

model AiMessage {
id String @id @default(uuid()) @db.Uuid
conversationId String @db.Uuid
position Int
role String
content Json
createdAt DateTime @default(now())
conversation AiConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)

@@index([conversationId, position])
}

enum CustomerType {
USER
TEAM
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { globalPrismaClient, retryTransaction } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, yupArray, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { getOwnedConversation } from "../../utils";

export const PUT = createSmartRouteHandler({
metadata: {
summary: "Replace conversation messages",
description: "Replace all messages in a conversation",
},
request: yupObject({
auth: yupObject({
type: adaptSchema,
user: adaptSchema.defined(),
project: yupObject({
id: yupString().oneOf(["internal"]).defined(),
}).defined(),
}).defined(),
params: yupObject({
conversationId: yupString().defined(),
}),
body: yupObject({
messages: yupArray(
yupObject({
role: yupString().oneOf(["user", "assistant"]).defined(),
content: yupMixed().defined(),
})
).defined(),
}),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({}).defined(),
}),
handler: async ({ auth, params, body }) => {
await getOwnedConversation(params.conversationId, auth.user.id);

await retryTransaction(globalPrismaClient, async (tx) => {
await tx.aiMessage.deleteMany({
where: { conversationId: params.conversationId },
});

if (body.messages.length > 0) {
await tx.aiMessage.createMany({
data: body.messages.map((msg, index) => ({
conversationId: params.conversationId,
position: index,
role: msg.role,
content: msg.content as object,
})),
});
}

await tx.aiConversation.update({
where: { id: params.conversationId },
data: { updatedAt: new Date() },
});
});
Comment thread
N2D4 marked this conversation as resolved.
Outdated

return {
statusCode: 200 as const,
bodyType: "json" as const,
body: {},
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { globalPrismaClient } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, yupArray, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { getOwnedConversation } from "../utils";

export const GET = createSmartRouteHandler({
metadata: {
summary: "Get AI conversation",
description: "Fetch a single AI conversation with all its messages",
},
request: yupObject({
auth: yupObject({
type: adaptSchema,
user: adaptSchema.defined(),
project: yupObject({
id: yupString().oneOf(["internal"]).defined(),
}).defined(),
}).defined(),
params: yupObject({
conversationId: yupString().defined(),
}),
method: yupString().oneOf(["GET"]).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
id: yupString().defined(),
title: yupString().defined(),
projectId: yupString().defined(),
messages: yupArray(yupObject({
id: yupString().defined(),
role: yupString().defined(),
content: yupMixed().defined(),
}).noUnknown(false)).defined(),
}).defined(),
}),
handler: async ({ auth, params }) => {
const conversation = await getOwnedConversation(params.conversationId, auth.user.id);

const messages = await globalPrismaClient.aiMessage.findMany({
where: { conversationId: conversation.id },
orderBy: { position: "asc" },
select: {
id: true,
role: true,
content: true,
},
});

return {
statusCode: 200 as const,
bodyType: "json" as const,
body: {
id: conversation.id,
title: conversation.title,
projectId: conversation.projectId,
messages: messages.map(m => ({ ...m, content: m.content as object })),
},
};
},
});

export const PATCH = createSmartRouteHandler({
metadata: {
summary: "Update AI conversation",
description: "Update the title of an AI conversation",
},
request: yupObject({
auth: yupObject({
type: adaptSchema,
user: adaptSchema.defined(),
project: yupObject({
id: yupString().oneOf(["internal"]).defined(),
}).defined(),
}).defined(),
params: yupObject({
conversationId: yupString().defined(),
}),
body: yupObject({
title: yupString().defined(),
}),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({}).defined(),
}),
handler: async ({ auth, params, body }) => {
await getOwnedConversation(params.conversationId, auth.user.id);

await globalPrismaClient.aiConversation.update({
where: { id: params.conversationId },
data: { title: body.title },
});

return {
statusCode: 200 as const,
bodyType: "json" as const,
body: {},
};
},
});

export const DELETE = createSmartRouteHandler({
metadata: {
summary: "Delete AI conversation",
description: "Delete an AI conversation and all its messages",
},
request: yupObject({
auth: yupObject({
type: adaptSchema,
user: adaptSchema.defined(),
project: yupObject({
id: yupString().oneOf(["internal"]).defined(),
}).defined(),
}).defined(),
params: yupObject({
conversationId: yupString().defined(),
}),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({}).defined(),
}),
handler: async ({ auth, params }) => {
await getOwnedConversation(params.conversationId, auth.user.id);

await globalPrismaClient.aiConversation.delete({
where: { id: params.conversationId },
});

return {
statusCode: 200 as const,
bodyType: "json" as const,
body: {},
};
},
});
Loading
Loading