Skip to content

Commit ed9d04f

Browse files
authored
Merge branch 'dev' into fix-session-replays-access-token-fetch
2 parents e616f50 + 8aa80ce commit ed9d04f

11 files changed

Lines changed: 1784 additions & 537 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-- CreateTable
2+
CREATE TABLE "AiConversation" (
3+
"id" UUID NOT NULL,
4+
"projectUserId" UUID NOT NULL,
5+
"projectId" TEXT NOT NULL REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE,
6+
"title" TEXT NOT NULL,
7+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
8+
"updatedAt" TIMESTAMP(3) NOT NULL,
9+
10+
CONSTRAINT "AiConversation_pkey" PRIMARY KEY ("id")
11+
);
12+
13+
-- CreateTable
14+
CREATE TABLE "AiMessage" (
15+
"id" UUID NOT NULL,
16+
"conversationId" UUID NOT NULL REFERENCES "AiConversation"("id") ON DELETE CASCADE ON UPDATE CASCADE,
17+
"position" INTEGER NOT NULL,
18+
"role" TEXT NOT NULL,
19+
"content" JSONB NOT NULL,
20+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
21+
22+
CONSTRAINT "AiMessage_pkey" PRIMARY KEY ("id")
23+
);
24+
25+
-- CreateIndex
26+
CREATE INDEX "AiConversation_projectUserId_projectId_updatedAt_idx" ON "AiConversation"("projectUserId", "projectId", "updatedAt" DESC);
27+
28+
-- CreateIndex
29+
CREATE INDEX "AiMessage_conversationId_position_idx" ON "AiMessage"("conversationId", "position" ASC);

apps/backend/prisma/schema.prisma

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ model Project {
4343
branchConfigOverrides BranchConfigOverride[]
4444
environmentConfigOverrides EnvironmentConfigOverride[]
4545
localEmulatorProject LocalEmulatorProject?
46+
aiConversations AiConversation[]
4647
4748
@@index([ownerTeamId], map: "Project_ownerTeamId_idx")
4849
}
@@ -1143,6 +1144,31 @@ model ThreadMessage {
11431144
@@id([tenancyId, id])
11441145
}
11451146

1147+
model AiConversation {
1148+
id String @id @default(uuid()) @db.Uuid
1149+
projectUserId String @db.Uuid
1150+
projectId String
1151+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
1152+
title String
1153+
createdAt DateTime @default(now())
1154+
updatedAt DateTime @updatedAt
1155+
messages AiMessage[]
1156+
1157+
@@index([projectUserId, projectId, updatedAt(sort: Desc)])
1158+
}
1159+
1160+
model AiMessage {
1161+
id String @id @default(uuid()) @db.Uuid
1162+
conversationId String @db.Uuid
1163+
position Int
1164+
role String
1165+
content Json
1166+
createdAt DateTime @default(now())
1167+
conversation AiConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
1168+
1169+
@@index([conversationId, position])
1170+
}
1171+
11461172
enum CustomerType {
11471173
USER
11481174
TEAM
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { globalPrismaClient } from "@/prisma-client";
2+
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
3+
import { adaptSchema, yupArray, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
4+
import { getOwnedConversation } from "../../utils";
5+
6+
export const PUT = createSmartRouteHandler({
7+
metadata: {
8+
summary: "Replace conversation messages",
9+
description: "Replace all messages in a conversation",
10+
},
11+
request: yupObject({
12+
auth: yupObject({
13+
type: adaptSchema,
14+
user: adaptSchema.defined(),
15+
project: yupObject({
16+
id: yupString().oneOf(["internal"]).defined(),
17+
}).defined(),
18+
}).defined(),
19+
params: yupObject({
20+
conversationId: yupString().defined(),
21+
}),
22+
body: yupObject({
23+
messages: yupArray(
24+
yupObject({
25+
role: yupString().oneOf(["user", "assistant"]).defined(),
26+
content: yupMixed().defined(),
27+
})
28+
).defined(),
29+
}),
30+
}),
31+
response: yupObject({
32+
statusCode: yupNumber().oneOf([200]).defined(),
33+
bodyType: yupString().oneOf(["json"]).defined(),
34+
body: yupObject({}).defined(),
35+
}),
36+
handler: async ({ auth, params, body }) => {
37+
await getOwnedConversation(params.conversationId, auth.user.id);
38+
39+
await globalPrismaClient.$executeRaw`
40+
WITH input AS (
41+
SELECT
42+
(ord - 1)::int AS position,
43+
(elem->>'role')::text AS role,
44+
elem->'content' AS content
45+
FROM jsonb_array_elements(${JSON.stringify(body.messages)}::jsonb)
46+
WITH ORDINALITY AS t(elem, ord)
47+
),
48+
deleted AS (
49+
DELETE FROM "AiMessage" WHERE "conversationId" = ${params.conversationId}::uuid
50+
),
51+
inserted AS (
52+
INSERT INTO "AiMessage" ("id", "conversationId", "position", "role", "content")
53+
SELECT gen_random_uuid(), ${params.conversationId}::uuid, position, role, content
54+
FROM input
55+
)
56+
UPDATE "AiConversation"
57+
SET "updatedAt" = NOW()
58+
WHERE "id" = ${params.conversationId}::uuid
59+
`;
60+
61+
return {
62+
statusCode: 200 as const,
63+
bodyType: "json" as const,
64+
body: {},
65+
};
66+
},
67+
});
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { globalPrismaClient } from "@/prisma-client";
2+
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
3+
import { adaptSchema, yupArray, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
4+
import { getOwnedConversation } from "../utils";
5+
6+
export const GET = createSmartRouteHandler({
7+
metadata: {
8+
summary: "Get AI conversation",
9+
description: "Fetch a single AI conversation with all its messages",
10+
},
11+
request: yupObject({
12+
auth: yupObject({
13+
type: adaptSchema,
14+
user: adaptSchema.defined(),
15+
project: yupObject({
16+
id: yupString().oneOf(["internal"]).defined(),
17+
}).defined(),
18+
}).defined(),
19+
params: yupObject({
20+
conversationId: yupString().defined(),
21+
}),
22+
method: yupString().oneOf(["GET"]).defined(),
23+
}),
24+
response: yupObject({
25+
statusCode: yupNumber().oneOf([200]).defined(),
26+
bodyType: yupString().oneOf(["json"]).defined(),
27+
body: yupObject({
28+
id: yupString().defined(),
29+
title: yupString().defined(),
30+
projectId: yupString().defined(),
31+
messages: yupArray(yupObject({
32+
id: yupString().defined(),
33+
role: yupString().defined(),
34+
content: yupMixed().defined(),
35+
}).noUnknown(false)).defined(),
36+
}).defined(),
37+
}),
38+
handler: async ({ auth, params }) => {
39+
const conversation = await getOwnedConversation(params.conversationId, auth.user.id);
40+
41+
const messages = await globalPrismaClient.aiMessage.findMany({
42+
where: { conversationId: conversation.id },
43+
orderBy: { position: "asc" },
44+
select: {
45+
id: true,
46+
role: true,
47+
content: true,
48+
},
49+
});
50+
51+
return {
52+
statusCode: 200 as const,
53+
bodyType: "json" as const,
54+
body: {
55+
id: conversation.id,
56+
title: conversation.title,
57+
projectId: conversation.projectId,
58+
messages: messages.map(m => ({ ...m, content: m.content as object })),
59+
},
60+
};
61+
},
62+
});
63+
64+
export const PATCH = createSmartRouteHandler({
65+
metadata: {
66+
summary: "Update AI conversation",
67+
description: "Update the title of an AI conversation",
68+
},
69+
request: yupObject({
70+
auth: yupObject({
71+
type: adaptSchema,
72+
user: adaptSchema.defined(),
73+
project: yupObject({
74+
id: yupString().oneOf(["internal"]).defined(),
75+
}).defined(),
76+
}).defined(),
77+
params: yupObject({
78+
conversationId: yupString().defined(),
79+
}),
80+
body: yupObject({
81+
title: yupString().defined(),
82+
}),
83+
}),
84+
response: yupObject({
85+
statusCode: yupNumber().oneOf([200]).defined(),
86+
bodyType: yupString().oneOf(["json"]).defined(),
87+
body: yupObject({}).defined(),
88+
}),
89+
handler: async ({ auth, params, body }) => {
90+
await getOwnedConversation(params.conversationId, auth.user.id);
91+
92+
await globalPrismaClient.aiConversation.update({
93+
where: { id: params.conversationId },
94+
data: { title: body.title },
95+
});
96+
97+
return {
98+
statusCode: 200 as const,
99+
bodyType: "json" as const,
100+
body: {},
101+
};
102+
},
103+
});
104+
105+
export const DELETE = createSmartRouteHandler({
106+
metadata: {
107+
summary: "Delete AI conversation",
108+
description: "Delete an AI conversation and all its messages",
109+
},
110+
request: yupObject({
111+
auth: yupObject({
112+
type: adaptSchema,
113+
user: adaptSchema.defined(),
114+
project: yupObject({
115+
id: yupString().oneOf(["internal"]).defined(),
116+
}).defined(),
117+
}).defined(),
118+
params: yupObject({
119+
conversationId: yupString().defined(),
120+
}),
121+
}),
122+
response: yupObject({
123+
statusCode: yupNumber().oneOf([200]).defined(),
124+
bodyType: yupString().oneOf(["json"]).defined(),
125+
body: yupObject({}).defined(),
126+
}),
127+
handler: async ({ auth, params }) => {
128+
await getOwnedConversation(params.conversationId, auth.user.id);
129+
130+
await globalPrismaClient.aiConversation.delete({
131+
where: { id: params.conversationId },
132+
});
133+
134+
return {
135+
statusCode: 200 as const,
136+
bodyType: "json" as const,
137+
body: {},
138+
};
139+
},
140+
});

0 commit comments

Comments
 (0)