core/apps/webapp/app/services/conversation.server.ts
Harshith Mullapudi 54e535d57d
Feat: v2 (#12)
* Feat: v2

* feat: add chat functionality

* First cut: integrations

* Feat: add conversation API

* Enhance conversation handling and memory management

* Feat: added conversation

---------

Co-authored-by: Manoj K <saimanoj58@gmail.com>
2025-07-08 22:41:00 +05:30

362 lines
8.1 KiB
TypeScript

import { UserTypeEnum } from "@core/types";
import { auth, runs, tasks } from "@trigger.dev/sdk/v3";
import { prisma } from "~/db.server";
import { createConversationTitle } from "~/trigger/conversation/create-conversation-title";
import { z } from "zod";
import { type ConversationHistory } from "@prisma/client";
export const CreateConversationSchema = z.object({
message: z.string(),
title: z.string().optional(),
conversationId: z.string().optional(),
userType: z.nativeEnum(UserTypeEnum).optional(),
});
export type CreateConversationDto = z.infer<typeof CreateConversationSchema>;
// Create a new conversation
export async function createConversation(
workspaceId: string,
userId: string,
conversationData: CreateConversationDto,
) {
const { title, conversationId, ...otherData } = conversationData;
if (conversationId) {
// Add a new message to an existing conversation
const conversationHistory = await prisma.conversationHistory.create({
data: {
...otherData,
userType: otherData.userType || UserTypeEnum.User,
...(userId && {
user: {
connect: { id: userId },
},
}),
conversation: {
connect: { id: conversationId },
},
},
include: {
conversation: true,
},
});
const context = await getConversationContext(conversationHistory.id);
const handler = await tasks.trigger(
"chat",
{
conversationHistoryId: conversationHistory.id,
conversationId: conversationHistory.conversation.id,
context,
},
{ tags: [conversationHistory.id, workspaceId, conversationId] },
);
return {
id: handler.id,
token: handler.publicAccessToken,
conversationId: conversationHistory.conversation.id,
conversationHistoryId: conversationHistory.id,
};
}
// Create a new conversation and its first message
const conversation = await prisma.conversation.create({
data: {
workspaceId,
userId,
title:
title?.substring(0, 100) ?? conversationData.message.substring(0, 100),
ConversationHistory: {
create: {
userId,
userType: otherData.userType || UserTypeEnum.User,
...otherData,
},
},
},
include: {
ConversationHistory: true,
},
});
const conversationHistory = conversation.ConversationHistory[0];
const context = await getConversationContext(conversationHistory.id);
// Trigger conversation title task
await tasks.trigger<typeof createConversationTitle>(
createConversationTitle.id,
{
conversationId: conversation.id,
message: conversationData.message,
},
{ tags: [conversation.id, workspaceId] },
);
const handler = await tasks.trigger(
"chat",
{
conversationHistoryId: conversationHistory.id,
conversationId: conversation.id,
context,
},
{ tags: [conversationHistory.id, workspaceId, conversation.id] },
);
return {
id: handler.id,
token: handler.publicAccessToken,
conversationId: conversation.id,
conversationHistoryId: conversationHistory.id,
};
}
// Get a conversation by ID
export async function getConversation(conversationId: string) {
return prisma.conversation.findUnique({
where: { id: conversationId },
});
}
// Delete a conversation (soft delete)
export async function deleteConversation(conversationId: string) {
return prisma.conversation.update({
where: { id: conversationId },
data: {
deleted: new Date().toISOString(),
},
});
}
// Mark a conversation as read
export async function readConversation(conversationId: string) {
return prisma.conversation.update({
where: { id: conversationId },
data: { unread: false },
});
}
export async function getCurrentConversationRun(
conversationId: string,
workspaceId: string,
) {
const conversationHistory = await prisma.conversationHistory.findFirst({
where: {
conversationId,
conversation: {
workspaceId,
},
},
orderBy: {
updatedAt: "desc",
},
});
if (!conversationHistory) {
throw new Error("No run found");
}
const response = await runs.list({
tag: [conversationId, conversationHistory.id],
status: ["QUEUED", "EXECUTING"],
limit: 1,
});
const run = response.data[0];
if (!run) {
return undefined;
}
const publicToken = await auth.createPublicToken({
scopes: {
read: {
runs: [run.id],
},
},
});
return {
id: run.id,
token: publicToken,
conversationId,
conversationHistoryId: conversationHistory.id,
};
}
export async function stopConversation(
conversationId: string,
workspaceId: string,
) {
const conversationHistory = await prisma.conversationHistory.findFirst({
where: {
conversationId,
conversation: {
workspaceId,
},
},
orderBy: {
updatedAt: "desc",
},
});
if (!conversationHistory) {
throw new Error("No run found");
}
const response = await runs.list({
tag: [conversationId, conversationHistory.id],
status: ["QUEUED", "EXECUTING"],
limit: 1,
});
const run = response.data[0];
if (!run) {
await prisma.conversation.update({
where: {
id: conversationId,
},
data: {
status: "failed",
},
});
return undefined;
}
return await runs.cancel(run.id);
}
export async function getConversationContext(
conversationHistoryId: string,
): Promise<{
previousHistory: ConversationHistory[];
}> {
const conversationHistory = await prisma.conversationHistory.findUnique({
where: { id: conversationHistoryId },
include: { conversation: true },
});
if (!conversationHistory) {
return {
previousHistory: [],
};
}
// Get previous conversation history message and response
let previousHistory: ConversationHistory[] = [];
if (conversationHistory.conversationId) {
previousHistory = await prisma.conversationHistory.findMany({
where: {
conversationId: conversationHistory.conversationId,
id: {
not: conversationHistoryId,
},
deleted: null,
},
orderBy: {
createdAt: "asc",
},
});
}
return {
previousHistory,
};
}
export const getConversationAndHistory = async (
conversationId: string,
userId: string,
) => {
const conversation = await prisma.conversation.findFirst({
where: {
id: conversationId,
},
include: {
ConversationHistory: true,
},
});
return conversation;
};
export const GetConversationsListSchema = z.object({
page: z.string().optional().default("1"),
limit: z.string().optional().default("20"),
search: z.string().optional(),
});
export type GetConversationsListDto = z.infer<typeof GetConversationsListSchema>;
export async function getConversationsList(
workspaceId: string,
userId: string,
params: GetConversationsListDto,
) {
const page = parseInt(params.page);
const limit = parseInt(params.limit);
const skip = (page - 1) * limit;
const where = {
workspaceId,
userId,
deleted: null,
...(params.search && {
OR: [
{
title: {
contains: params.search,
mode: "insensitive" as const,
},
},
{
ConversationHistory: {
some: {
message: {
contains: params.search,
mode: "insensitive" as const,
},
},
},
},
],
}),
};
const [conversations, total] = await Promise.all([
prisma.conversation.findMany({
where,
include: {
ConversationHistory: {
take: 1,
orderBy: {
createdAt: "desc",
},
},
},
orderBy: {
updatedAt: "desc",
},
skip,
take: limit,
}),
prisma.conversation.count({ where }),
]);
return {
conversations,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
hasNext: page < Math.ceil(total / limit),
hasPrev: page > 1,
},
};
}