diff --git a/apps/server/src/controllers/agent/index.ts b/apps/server/src/controllers/agent/index.ts index 1326c5c..f54f33a 100644 --- a/apps/server/src/controllers/agent/index.ts +++ b/apps/server/src/controllers/agent/index.ts @@ -1,20 +1,17 @@ -import { AgentType } from '@prisma/client'; -import z from 'zod'; - import { HTTPSTATUS } from '../../configs/http'; import { asyncHandler } from '../../middleware/async'; -import { - createAgent, - deleteAgentById, - getAllAgents, - updateAgentById, -} from '../../services/agent'; +import { AgentService } from '../../services/agent'; import AppResponse from '../../utils/appResponse'; +import { + agentIdSchema, + createNewAgentValidator, + updateAgentSchema, +} from '../../validator/agent'; const createNewAgent = asyncHandler(async (req, res) => { const data = createNewAgentValidator.parse(req.body); - await createAgent(data); + await AgentService.createAgent(data); new AppResponse({ res, @@ -25,7 +22,7 @@ const createNewAgent = asyncHandler(async (req, res) => { }); const agentLists = asyncHandler(async (_, res) => { - const agents = await getAllAgents(); + const agents = await AgentService.getAllAgents(); new AppResponse({ res, @@ -39,7 +36,7 @@ const agentLists = asyncHandler(async (_, res) => { const updateAgent = asyncHandler(async (req, res) => { const params = agentIdSchema.parse(req.params); const data = updateAgentSchema.parse(req.body); - await updateAgentById(params.agentId, data); + await AgentService.updateAgentById(params.agentId, data); new AppResponse({ res, @@ -51,7 +48,7 @@ const updateAgent = asyncHandler(async (req, res) => { const deleteAgent = asyncHandler(async (req, res) => { const data = agentIdSchema.parse(req.params); - await deleteAgentById(data.agentId); + await AgentService.deleteAgentById(data.agentId); new AppResponse({ res, @@ -61,49 +58,6 @@ const deleteAgent = asyncHandler(async (req, res) => { }); }); -const createNewAgentValidator = z.object({ - datasourceIds: z.array(z.string().min(1)).optional(), - name: z.string().min(1), - description: z.string().optional(), - type: z.enum([AgentType.AI, AgentType.HUMAN]), - model: z.string().min(1), - system_prompt: z - .string({ - message: 'System prompt is required', - }) - .optional(), - user_prompt: z - .string({ - message: 'System prompt is required', - }) - .optional(), - prompt_variables: z.record(z.string()).optional(), -}); - -const updateAgentSchema = z.object({ - datasourceIds: z.array(z.string().min(1)).optional(), - name: z.string().min(1), - description: z.string().optional(), - type: z.enum([AgentType.AI, AgentType.HUMAN]), - model: z.string().min(1), - system_prompt: z - .string({ - message: 'System prompt is required', - }) - .optional(), - user_prompt: z - .string({ - message: 'System prompt is required', - }) - .optional(), - prompt_variables: z.record(z.string()).optional(), - lastActive: z.string().optional(), -}); - -const agentIdSchema = z.object({ - agentId: z.string().min(1), -}); - export const agentController = { createNewAgent, agentLists, diff --git a/apps/server/src/controllers/chat/index.ts b/apps/server/src/controllers/chat/index.ts index 1578109..3f66b2f 100644 --- a/apps/server/src/controllers/chat/index.ts +++ b/apps/server/src/controllers/chat/index.ts @@ -7,14 +7,9 @@ import z from 'zod'; import type { CreateMessage } from '../../types/interface/message'; import type { Agent, Prisma } from '@prisma/client'; import { asyncHandler } from '../../middleware/async'; -import { getAgentById, updateAgentById } from '../../services/agent'; -import { - createConversation, - getConversationByChatId, - getConversationById, - updateConversationById, -} from '../../services/conversation'; -import { createMessage } from '../../services/messages'; +import { AgentService } from '../../services/agent'; +import { ConversationService } from '../../services/conversation'; +import { messageService } from '../../services/messages'; import { UpdateAgent } from '../../types/interface/agent'; import { AppError } from '../../utils/appError'; import { graph } from '../../utils/mygraph/cs_graph'; @@ -22,6 +17,7 @@ import { QUERY_SYSTEM_PROMPT_TEMPLATE, RESPONSE_SYSTEM_PROMPT_TEMPLATE, } from '../../utils/mygraph/cs_graph/prompt'; +import { createNewMessageValidator } from '../../validator/chat'; type MessageEvent = { eventData: Record; @@ -29,19 +25,9 @@ type MessageEvent = { res: Response; }; -const createNewMessageValidator = z.object({ - messages: z.array( - z.object({ role: z.enum(['human', 'ai']), content: z.string().min(1) }) - ), - userId: z.string().min(1), - agentId: z.string().min(1), - chatId: z.number().min(1).optional().describe('Telegram chat id'), - conversationId: z.string().min(1).optional(), -}); - const createNewMessage = asyncHandler(async (req, res) => { const data = createNewMessageValidator.parse(req.body); - const agent = await getAgentById(data.agentId); + const agent = await AgentService.getAgentById(data.agentId); if (!agent) { throw AppError.notFound('Agent not found'); } @@ -166,7 +152,7 @@ function createMessagesToStore( async function storeMessages(messages: CreateMessage[]) { for (const message of messages) { - await createMessage(message); + await messageService.createMessage(message); } } @@ -175,14 +161,18 @@ const getOrCreateConversation = async ( agent: Agent ) => { if (data.conversationId) { - const conversation = await getConversationById(data.conversationId); + const conversation = await ConversationService.getConversationById( + data.conversationId + ); if (conversation) return conversation; } else if (data.chatId) { - const conversation = await getConversationByChatId(data.chatId); + const conversation = await ConversationService.getConversationByChatId( + data.chatId + ); if (conversation) return conversation; } - return await createConversation({ + return await ConversationService.createConversation({ agentId: agent.id, userId: data.userId, chatId: data.chatId, @@ -196,13 +186,13 @@ const updateConversation = async ( conversationId: string, filteredData: Record ) => { - return await updateConversationById(conversationId, { + return await ConversationService.updateConversationById(conversationId, { priority: filteredData.priority as 'low' | 'medium' | 'high', }); }; const updateAgent: UpdateAgent = async (agentId, data) => { - return await updateAgentById(agentId, data); + return await AgentService.updateAgentById(agentId, data); }; function handleMessageEvent({ diff --git a/apps/server/src/controllers/conversation/index.ts b/apps/server/src/controllers/conversation/index.ts index 2a9ff5a..beace8f 100644 --- a/apps/server/src/controllers/conversation/index.ts +++ b/apps/server/src/controllers/conversation/index.ts @@ -1,18 +1,14 @@ -import z from 'zod'; - import { HTTPSTATUS } from '../../configs/http'; import { asyncHandler } from '../../middleware/async'; -import { - deleteConversationById, - getAllConversations, - getConversationById, -} from '../../services/conversation'; +import { ConversationService } from '../../services/conversation'; import { AppError } from '../../utils/appError'; import AppResponse from '../../utils/appResponse'; +import { conversationIdSchema } from '../../validator/chat'; const getConversationId = asyncHandler(async (req, res) => { const { conversationId } = conversationIdSchema.parse(req.params); - const conversation = await getConversationById(conversationId); + const conversation = + await ConversationService.getConversationById(conversationId); if (!conversation) { throw AppError.notFound('Conversation not found'); @@ -28,7 +24,7 @@ const getConversationId = asyncHandler(async (req, res) => { }); const conversationLists = asyncHandler(async (_req, res) => { - const conversations = await getAllConversations(); + const conversations = await ConversationService.getAllConversations(); new AppResponse({ res, @@ -42,7 +38,7 @@ const conversationLists = asyncHandler(async (_req, res) => { const deleteConversation = asyncHandler(async (req, res) => { const { conversationId } = conversationIdSchema.parse(req.params); - await deleteConversationById(conversationId); + await ConversationService.deleteConversationById(conversationId); new AppResponse({ res, @@ -52,8 +48,6 @@ const deleteConversation = asyncHandler(async (req, res) => { }); }); -const conversationIdSchema = z.object({ conversationId: z.string().min(1) }); - export const conversationController = { getConversationId, conversationLists, diff --git a/apps/server/src/controllers/datasource/index.ts b/apps/server/src/controllers/datasource/index.ts index b67c2f8..c087002 100644 --- a/apps/server/src/controllers/datasource/index.ts +++ b/apps/server/src/controllers/datasource/index.ts @@ -1,24 +1,23 @@ -import { $Enums, Datasource } from '@prisma/client'; -import z from 'zod'; +import { Datasource } from '@prisma/client'; import { HTTPSTATUS } from '../../configs/http'; import { asyncHandler } from '../../middleware/async'; -import { - createDatasource, - deleteDatasourceById, - getAllDatasource, - updateDatasourceById, -} from '../../services/datasource'; +import { DatasourceService } from '../../services/datasource'; import { storeDocument } from '../../services/document'; import { AppError } from '../../utils/appError'; import AppResponse from '../../utils/appResponse'; +import { + createNewDatasouceSchema, + datasourceIdSchema, + updateDatasouceSchema, +} from '../../validator/datasource'; const createNewDatasource = asyncHandler(async (req, res) => { const data = createNewDatasouceSchema.parse(req.body); let datasource: Datasource = {} as Datasource; if (data.type === 'DOCUMENT' && data.fileUrl) { - datasource = await createDatasource(data); + datasource = await DatasourceService.createDatasource(data); await storeDocument({ fileUrl: data.fileUrl, @@ -27,7 +26,7 @@ const createNewDatasource = asyncHandler(async (req, res) => { } if (data.type === 'TEXT' && data.content) { - datasource = await createDatasource(data); + datasource = await DatasourceService.createDatasource(data); await storeDocument({ content: data.content, @@ -36,7 +35,7 @@ const createNewDatasource = asyncHandler(async (req, res) => { } if (data.type === 'WEB' && data.url) { - datasource = await createDatasource(data); + datasource = await DatasourceService.createDatasource(data); await storeDocument({ url: data.url, @@ -61,7 +60,7 @@ const updateDatasource = asyncHandler(async (req, res) => { const { datasourceId } = datasourceIdSchema.parse(req.params); const data = updateDatasouceSchema.parse(req.body); - await updateDatasourceById(datasourceId, data); + await DatasourceService.updateDatasourceById(datasourceId, data); new AppResponse({ res, @@ -72,7 +71,7 @@ const updateDatasource = asyncHandler(async (req, res) => { }); const datasourceLists = asyncHandler(async (_, res) => { - const datasources = await getAllDatasource(); + const datasources = await DatasourceService.getAllDatasource(); new AppResponse({ res, @@ -86,7 +85,7 @@ const datasourceLists = asyncHandler(async (_, res) => { const deteleDatasource = asyncHandler(async (req, res) => { const { datasourceId } = datasourceIdSchema.parse(req.params); - await deleteDatasourceById(datasourceId); + await DatasourceService.deleteDatasourceById(datasourceId); new AppResponse({ res, @@ -96,69 +95,6 @@ const deteleDatasource = asyncHandler(async (req, res) => { }); }); -const createNewDatasouceSchema = z - .object({ - name: z.string().min(1), - description: z.string().optional(), - agentIds: z.array(z.string().min(1)).optional(), - fileUrl: z.string().url().optional(), - type: z.nativeEnum($Enums.DatasourceType), - category: z.string().optional(), - content: z.string().optional(), - url: z.string().optional(), - size: z.number().optional(), - }) - .refine( - (data) => - data.type !== 'DOCUMENT' || (data.type === 'DOCUMENT' && data.fileUrl), - { message: 'fileUrl is required when type is DOCUMENT', path: ['fileUrl'] } - ) - .refine( - (data) => data.type !== 'TEXT' || (data.type === 'TEXT' && data.content), - { message: 'content is required when type is TEXT', path: ['content'] } - ) - .refine((data) => data.type !== 'WEB' || (data.type === 'WEB' && data.url), { - message: 'url is required when type is WEBSITE', - path: ['url'], - }); -// .refine( -// (data) => -// data.type !== 'DATABASE' || (data.type === 'DATABASE' && data.size), -// { message: 'size is required when type is DATABASE', path: ['size'] } -// ); - -const datasourceIdSchema = z.object({ - datasourceId: z.string().min(1), -}); - -const updateDatasouceSchema = z - .object({ - name: z.string().min(1, { - message: 'Source name cannot be empty', - }), - description: z.string().optional(), - agentIds: z.array(z.string().min(1)).optional(), - fileUrl: z.string().url().optional(), - type: z.enum(['DOCUMENT', 'TEXT', 'WEB', 'DATABASE']), - category: z.string().optional(), - content: z.string().optional(), - url: z.string().optional(), - size: z.number().optional(), - }) - .refine( - (data) => - data.type !== 'DOCUMENT' || (data.type === 'DOCUMENT' && data.fileUrl), - { message: 'fileUrl is required when type is DOCUMENT', path: ['fileUrl'] } - ) - .refine( - (data) => data.type !== 'TEXT' || (data.type === 'TEXT' && data.content), - { message: 'content is required when type is TEXT', path: ['content'] } - ) - .refine((data) => data.type !== 'WEB' || (data.type === 'WEB' && data.url), { - message: 'url is required when type is WEBSITE', - path: ['url'], - }); - export const dataSourceController = { createNewDatasource, updateDatasource, diff --git a/apps/server/src/controllers/model/index.ts b/apps/server/src/controllers/model/index.ts index d189e09..0d9c33f 100644 --- a/apps/server/src/controllers/model/index.ts +++ b/apps/server/src/controllers/model/index.ts @@ -1,9 +1,9 @@ import { asyncHandler } from '../../middleware/async'; -import { getAllGroqModels } from '../../services/model'; +import { ModelService } from '../../services/model'; import AppResponse from '../../utils/appResponse'; const groqModelLists = asyncHandler(async (_req, res) => { - const models = await getAllGroqModels(); + const models = await ModelService.getAllModels(); new AppResponse({ res, diff --git a/apps/server/src/services/agent/index.ts b/apps/server/src/services/agent/index.ts index c1d4457..a713044 100644 --- a/apps/server/src/services/agent/index.ts +++ b/apps/server/src/services/agent/index.ts @@ -8,189 +8,199 @@ import { } from '../../types/interface/agent'; import { AppError } from '../../utils/appError'; import { SYSTEM_PROMPT, USER_PROMPT } from '../../utils/prompt'; -import { getDatasourceById } from '../datasource'; - -const validateDatasourceIds = async (datasourceIds: string[]) => { - await Promise.all( - datasourceIds.map(async (datasourceId) => { - const isDatasourceExists = await getDatasourceById(datasourceId); - if (!isDatasourceExists) { - throw AppError.notFound(`Datasource with ID ${datasourceId} not found`); - } - }) - ); -}; - -const manageAgentDatasourceRelations = async ( - agentId: string, - datasourceIds: string[] -) => { - if (datasourceIds.length > 0) { +import { DatasourceService } from '../datasource'; + +export class AgentService { + static async validateDatasourceIds(datasourceIds: string[]) { await Promise.all( datasourceIds.map(async (datasourceId) => { - const agentDatasource = await db.agentOnDatasource.findFirst({ - where: { - agentId, - datasourceId, - }, - }); - if (!agentDatasource) { - await db.agentOnDatasource.create({ - data: { - agentId, - datasourceId, - }, - }); + const isDatasourceExists = + await DatasourceService.getDatasourceById(datasourceId); + if (!isDatasourceExists) { + throw AppError.notFound( + `Datasource with ID ${datasourceId} not found` + ); } }) ); - } else { - await db.agentOnDatasource.deleteMany({ - where: { - agentId, - }, - }); } -}; -export const createAgent: CreateNewAgent = async (data) => { - try { - if (Array.isArray(data.datasourceIds) && data.datasourceIds.length > 0) { - await validateDatasourceIds(data.datasourceIds); + static async manageAgentDatasourceRelations( + agentId: string, + datasourceIds: string[] + ) { + if (datasourceIds.length > 0) { + await Promise.all( + datasourceIds.map(async (datasourceId) => { + const agentDatasource = await db.agentOnDatasource.findFirst({ + where: { + agentId, + datasourceId, + }, + }); + if (!agentDatasource) { + await db.agentOnDatasource.create({ + data: { + agentId, + datasourceId, + }, + }); + } + }) + ); + } else { + await db.agentOnDatasource.deleteMany({ + where: { + agentId, + }, + }); } + } + + static createAgent: CreateNewAgent = async (data) => { + try { + if (Array.isArray(data.datasourceIds) && data.datasourceIds.length > 0) { + await this.validateDatasourceIds(data.datasourceIds); + } - const { datasourceIds, ...agentData } = data; - - const newAgent = await db.agent.create({ - data: { - ...agentData, - model: agentData.model ?? 'llama-3.3-70b-versatile', - successRate: 0, - active: true, - lastActive: new Date().toISOString(), - system_prompt: agentData.system_prompt ?? SYSTEM_PROMPT, - user_prompt: agentData.user_prompt ?? USER_PROMPT, - prompt_variables: agentData.prompt_variables ?? {}, - }, - }); - - await manageAgentDatasourceRelations(newAgent.id, datasourceIds ?? []); - - return newAgent; - } catch (error) { - console.error(error); - if (error instanceof AppError) { - throw error; + const { datasourceIds, ...agentData } = data; + + const newAgent = await db.agent.create({ + data: { + ...agentData, + model: agentData.model ?? 'llama-3.3-70b-versatile', + successRate: 0, + active: true, + lastActive: new Date().toISOString(), + system_prompt: agentData.system_prompt ?? SYSTEM_PROMPT, + user_prompt: agentData.user_prompt ?? USER_PROMPT, + prompt_variables: agentData.prompt_variables ?? {}, + }, + }); + + await this.manageAgentDatasourceRelations( + newAgent.id, + datasourceIds ?? [] + ); + + return newAgent; + } catch (error) { + console.error(error); + if (error instanceof AppError) { + throw error; + } + throw new Error('Failed to create agent'); } - throw new Error('Failed to create agent'); - } -}; - -export const getAgentById: GetAgent = async (agentId) => { - try { - const agent = await db.agent.findFirst({ - where: { - id: agentId, - }, - include: { - datasources: { - select: { - datasource: true, - datasourceId: true, + }; + + static getAgentById: GetAgent = async (agentId) => { + try { + const agent = await db.agent.findFirst({ + where: { + id: agentId, + }, + include: { + datasources: { + select: { + datasource: true, + datasourceId: true, + }, }, }, - }, - }); - - return agent; - } catch (error) { - console.error(error); - throw new Error('Failed to get agent'); - } -}; + }); -export const updateAgentById: UpdateAgent = async (agentId, data) => { - try { - if (Array.isArray(data.datasourceIds) && data.datasourceIds.length > 0) { - await validateDatasourceIds(data.datasourceIds); + return agent; + } catch (error) { + console.error(error); + throw new Error('Failed to get agent'); } + }; - const isAgentExists = await getAgentById(agentId); - if (!isAgentExists) { - throw AppError.notFound(`Agent with ID ${agentId} not found`); - } + static updateAgentById: UpdateAgent = async (agentId, data) => { + try { + if (Array.isArray(data.datasourceIds) && data.datasourceIds.length > 0) { + await this.validateDatasourceIds(data.datasourceIds); + } - const { datasourceIds, ...agentData } = data; + const isAgentExists = await this.getAgentById(agentId); + if (!isAgentExists) { + throw AppError.notFound(`Agent with ID ${agentId} not found`); + } - const updatedAgent = await db.agent.update({ - where: { - id: agentId, - }, - data: { - ...agentData, - }, - }); + const { datasourceIds, ...agentData } = data; - await manageAgentDatasourceRelations(agentId, datasourceIds || []); + const updatedAgent = await db.agent.update({ + where: { + id: agentId, + }, + data: { + ...agentData, + }, + }); - return updatedAgent; - } catch (error) { - console.error(error); - if (error instanceof AppError) { - throw error; + await this.manageAgentDatasourceRelations(agentId, datasourceIds || []); + + return updatedAgent; + } catch (error) { + console.error(error); + if (error instanceof AppError) { + throw error; + } + throw new Error('Failed to update agent'); } - throw new Error('Failed to update agent'); - } -}; - -export const getAllAgents: GetAllAgents = async () => { - try { - const agents = await db.agent.findMany({ - include: { - conversations: true, - datasources: { - select: { - datasource: true, - datasourceId: true, + }; + + static getAllAgents: GetAllAgents = async () => { + try { + const agents = await db.agent.findMany({ + include: { + conversations: true, + datasources: { + select: { + datasource: true, + datasourceId: true, + }, }, }, - }, - }); - - const remappedAgents = agents.map((agent) => ({ - ...agent, - datasourceIds: agent.datasources.map( - (datasource) => datasource.datasourceId - ), - datasources: agent.datasources.map((datasource) => datasource.datasource), - })); - - return remappedAgents; - } catch (error) { - console.error(error); - throw new Error('Failed to get agents'); - } -}; - -export const deleteAgentById: DeleteAgent = async (agentId) => { - try { - const isAgentExists = await getAgentById(agentId); - if (!isAgentExists) { - throw AppError.notFound('Agent not found'); + }); + + const remappedAgents = agents.map((agent) => ({ + ...agent, + datasourceIds: agent.datasources.map( + (datasource) => datasource.datasourceId + ), + datasources: agent.datasources.map( + (datasource) => datasource.datasource + ), + })); + + return remappedAgents; + } catch (error) { + console.error(error); + throw new Error('Failed to get agents'); } + }; + + static deleteAgentById: DeleteAgent = async (agentId) => { + try { + const isAgentExists = await this.getAgentById(agentId); + if (!isAgentExists) { + throw AppError.notFound('Agent not found'); + } + + const deletedAgent = await db.agent.delete({ + where: { + id: agentId, + }, + }); - const deletedAgent = await db.agent.delete({ - where: { - id: agentId, - }, - }); - - return deletedAgent; - } catch (error) { - console.error(error); - if (error instanceof AppError) { - throw error; + return deletedAgent; + } catch (error) { + console.error(error); + if (error instanceof AppError) { + throw error; + } + throw new Error('Failed to delete agent'); } - throw new Error('Failed to delete agent'); - } -}; + }; +} diff --git a/apps/server/src/services/conversation/index.ts b/apps/server/src/services/conversation/index.ts index d500495..a51f851 100644 --- a/apps/server/src/services/conversation/index.ts +++ b/apps/server/src/services/conversation/index.ts @@ -5,118 +5,121 @@ import { } from '../../types/interface/conversation'; import { AppError } from '../../utils/appError'; -export const createConversation = async (data: CreateConversation) => { - try { - const newConversation = await db.conversation.create({ - data, - include: { - messages: true, - user: true, - }, - }); +export class ConversationService { + public static async createConversation(data: CreateConversation) { + try { + const newConversation = await db.conversation.create({ + data, + include: { + messages: true, + user: true, + }, + }); - return newConversation; - } catch (error) { - console.error(error); - throw error; + return newConversation; + } catch (error) { + console.error(error); + throw error; + } } -}; -export const updateConversationById = async ( - conversationId: string, - data: UpdateConversation -) => { - try { - const updatedConversation = await db.conversation.update({ - where: { - id: conversationId, - }, - data, - }); + public static async updateConversationById( + conversationId: string, + data: UpdateConversation + ) { + try { + const updatedConversation = await db.conversation.update({ + where: { + id: conversationId, + }, + data, + }); - return updatedConversation; - } catch (error) { - console.error(error); - throw error; + return updatedConversation; + } catch (error) { + console.error(error); + throw error; + } } -}; -export const getConversationById = async (conversationId?: string) => { - try { - const conversation = await db.conversation.findFirst({ - where: { - id: conversationId, - }, - include: { - messages: true, - user: true, - }, - }); + public static async getConversationById(conversationId?: string) { + try { + const conversation = await db.conversation.findFirst({ + where: { + id: conversationId, + }, + include: { + messages: true, + user: true, + }, + }); - return conversation; - } catch (error) { - console.error(error); - throw error; + return conversation; + } catch (error) { + console.error(error); + throw error; + } } -}; -export const getConversationByChatId = async (chatId: number) => { - try { - const conversation = await db.conversation.findFirst({ - where: { - chatId, - }, - include: { - messages: true, - user: true, - }, - }); + public static async getConversationByChatId(chatId: number) { + try { + const conversation = await db.conversation.findFirst({ + where: { + chatId, + }, + include: { + messages: true, + user: true, + }, + }); - return conversation; - } catch (error) { - console.error(error); - throw error; + return conversation; + } catch (error) { + console.error(error); + throw error; + } } -}; -export const getAllConversations = async () => { - try { - const conversations = await db.conversation.findMany({ - include: { - messages: { - take: 1, + public static async getAllConversations() { + try { + const conversations = await db.conversation.findMany({ + include: { + messages: { + take: 1, + }, + user: true, }, - user: true, - }, - }); + }); - return conversations; - } catch (error) { - console.error(error); - throw error; + return conversations; + } catch (error) { + console.error(error); + throw error; + } } -}; -export const deleteConversationById = async (conversationId: string) => { - try { - const isConversationExits = await getConversationById(conversationId); + public static async deleteConversationById(conversationId: string) { + try { + const isConversationExits = + await this.getConversationById(conversationId); - if (!isConversationExits) { - throw AppError.notFound('Conversation not found'); - } + if (!isConversationExits) { + throw AppError.notFound('Conversation not found'); + } - const deletedConversation = await db.conversation.delete({ - where: { - id: conversationId, - }, - }); + const deletedConversation = await db.conversation.delete({ + where: { + id: conversationId, + }, + }); - return deletedConversation; - } catch (error) { - console.error(error); - if (error instanceof AppError) { - throw error; + return deletedConversation; + } catch (error) { + console.error(error); + if (error instanceof AppError) { + throw error; + } + throw new Error('Failed to delete conversation'); } - throw new Error('Failed to delete conversation'); } -}; +} diff --git a/apps/server/src/services/datasource/index.ts b/apps/server/src/services/datasource/index.ts index fa7c783..c9269bf 100644 --- a/apps/server/src/services/datasource/index.ts +++ b/apps/server/src/services/datasource/index.ts @@ -1,256 +1,262 @@ -import { PineconeStore } from '@langchain/pinecone'; - import { db } from '../../configs/database'; import { CreateNewDatasource, UpdateDataSource, } from '../../types/interface/datasource'; import { AppError } from '../../utils/appError'; -import { getEmbeddings } from '../../utils/embeddings'; -import { getPineconeIndex } from '../../utils/pinecone'; -import { getAgentById } from '../agent'; -import { storeDocument } from '../document'; - -const validateAgentIds = async (agentIds: string[]) => { - await Promise.all( - agentIds.map(async (agentId) => { - const isAgentExists = await getAgentById(agentId); - if (!isAgentExists) { - throw AppError.notFound(`Agent with ID ${agentId} not found`); - } - }) - ); -}; +import { AgentService } from '../agent'; -const manageAgentDatasourceRelations = async ( - agentIds: string[], - datasourceId: string -) => { - if (agentIds.length > 0) { +export class DatasourceService { + static async validateAgentIds(agentIds: string[]) { await Promise.all( agentIds.map(async (agentId) => { - const agentDatasource = await db.agentOnDatasource.findFirst({ - where: { - agentId, - datasourceId, - }, - }); - if (!agentDatasource) { - await db.agentOnDatasource.create({ - data: { - agentId, - datasourceId, - }, - }); + const isAgentExists = await AgentService.getAgentById(agentId); + if (!isAgentExists) { + throw AppError.notFound(`Agent with ID ${agentId} not found`); } }) ); - } else { - await db.agentOnDatasource.deleteMany({ - where: { - datasourceId, - }, - }); } -}; -export const createDatasource: CreateNewDatasource = async (data) => { - try { - if (Array.isArray(data.agentIds) && data.agentIds.length > 0) { - await validateAgentIds(data.agentIds); + static async manageAgentDatasourceRelations( + agentIds: string[], + datasourceId: string + ) { + if (agentIds.length > 0) { + await Promise.all( + agentIds.map(async (agentId) => { + const agentDatasource = await db.agentOnDatasource.findFirst({ + where: { + agentId, + datasourceId, + }, + }); + if (!agentDatasource) { + await db.agentOnDatasource.create({ + data: { + agentId, + datasourceId, + }, + }); + } + }) + ); + } else { + await db.agentOnDatasource.deleteMany({ + where: { + datasourceId, + }, + }); } + } - const { agentIds, ...datasourceData } = data; + static createDatasource: CreateNewDatasource = async (data) => { + try { + if (Array.isArray(data.agentIds) && data.agentIds.length > 0) { + await this.validateAgentIds(data.agentIds); + } - const newDatasource = await db.datasource.create({ - data: { - ...datasourceData, - }, - }); + const { agentIds, ...datasourceData } = data; - console.log(agentIds); - await manageAgentDatasourceRelations(agentIds || [], newDatasource.id); + const newDatasource = await db.datasource.create({ + data: { + ...datasourceData, + }, + }); - return newDatasource; - } catch (error) { - console.error(error); - if (error instanceof AppError) { - throw error; - } - throw new Error('Failed to create datasource'); - } -}; + await this.manageAgentDatasourceRelations( + agentIds || [], + newDatasource.id + ); -export const updateDatasourceById: UpdateDataSource = async ( - datasourceId, - data -) => { - try { - if (Array.isArray(data.agentIds) && data.agentIds.length > 0) { - await validateAgentIds(data.agentIds); + return newDatasource; + } catch (error) { + console.error(error); + if (error instanceof AppError) { + throw error; + } + throw new Error('Failed to create datasource'); } + }; - const isDatasourceExists = await getDatasourceById(datasourceId); - if (!isDatasourceExists) { - throw AppError.notFound(`Datasource with ID ${datasourceId} not found`); - } + static updateDatasourceById: UpdateDataSource = async ( + datasourceId, + data + ) => { + try { + if (Array.isArray(data.agentIds) && data.agentIds.length > 0) { + await this.validateAgentIds(data.agentIds); + } - const vectorStore = await PineconeStore.fromExistingIndex(getEmbeddings(), { - pineconeIndex: getPineconeIndex(), - maxConcurrency: 5, - }); + const isDatasourceExists = await this.getDatasourceById(datasourceId); + if (!isDatasourceExists) { + throw AppError.notFound(`Datasource with ID ${datasourceId} not found`); + } - if ( - data.type === 'DOCUMENT' && - data.fileUrl && - data.fileUrl !== isDatasourceExists.fileUrl - ) { - await vectorStore.delete({ - ids: [isDatasourceExists.id], - }); + /** (not used maybe deleted in the future) + const vectorStore = await PineconeStore.fromExistingIndex( + getEmbeddings(), + { + pineconeIndex: getPineconeIndex(), + maxConcurrency: 5, + } + ); - await storeDocument({ - fileUrl: data.fileUrl, - datasourceId: datasourceId, - }); - } + if ( + data.type === 'DOCUMENT' && + data.fileUrl && + data.fileUrl !== isDatasourceExists.fileUrl + ) { + await vectorStore.delete({ + ids: [isDatasourceExists.id], + }); - if ( - data.type === 'TEXT' && - data.content && - data.content !== isDatasourceExists.content - ) { - await vectorStore.delete({ - filter: { - datasourceId, - }, - }); + await storeDocument({ + fileUrl: data.fileUrl, + datasourceId: datasourceId, + }); + } - await storeDocument({ - content: data.content, - datasourceId: datasourceId, - }); - } + if ( + data.type === 'TEXT' && + data.content && + data.content !== isDatasourceExists.content + ) { + await vectorStore.delete({ + filter: { + datasourceId, + }, + }); - if ( - data.type === 'WEB' && - data.url && - data.url !== isDatasourceExists.url - ) { - await vectorStore.delete({ - filter: { - datasourceId, - }, - }); + await storeDocument({ + content: data.content, + datasourceId: datasourceId, + }); + } - await storeDocument({ - url: data.url, - datasourceId: datasourceId, - }); - } + if ( + data.type === 'WEB' && + data.url && + data.url !== isDatasourceExists.url + ) { + await vectorStore.delete({ + filter: { + datasourceId, + }, + }); + + await storeDocument({ + url: data.url, + datasourceId: datasourceId, + }); + } + */ - const { agentIds, ...datasourceData } = data; + const { agentIds, ...datasourceData } = data; - const updatedDatasource = await db.datasource.update({ - where: { - id: datasourceId, - }, - data: { - ...datasourceData, - }, - }); + const updatedDatasource = await db.datasource.update({ + where: { + id: datasourceId, + }, + data: { + ...datasourceData, + }, + }); - await manageAgentDatasourceRelations(agentIds || [], datasourceId); + await this.manageAgentDatasourceRelations(agentIds || [], datasourceId); - return updatedDatasource; - } catch (error) { - console.error(error); - if (error instanceof AppError) { - throw error; + return updatedDatasource; + } catch (error) { + console.error(error); + if (error instanceof AppError) { + throw error; + } + throw new Error('Failed to update datasource'); } - throw new Error('Failed to update datasource'); - } -}; + }; -export const getDatasourceById = async (datasourceId: string) => { - try { - const datasource = await db.datasource.findFirst({ - where: { - id: datasourceId, - }, - include: { - agents: { - select: { - agent: true, - agentId: true, + static async getDatasourceById(datasourceId: string) { + try { + const datasource = await db.datasource.findFirst({ + where: { + id: datasourceId, + }, + include: { + agents: { + select: { + agent: true, + agentId: true, + }, }, }, - }, - }); - - return datasource; - } catch (error) { - console.error(error); - throw new Error('Failed to get datasource'); - } -}; + }); -export const deleteDatasourceById = async (datasourceId: string) => { - try { - const isDatasourceExists = await getDatasourceById(datasourceId); - if (!isDatasourceExists) { - throw AppError.notFound(`Datasource with ID ${datasourceId} not found`); + return datasource; + } catch (error) { + console.error(error); + throw new Error('Failed to get datasource'); } + } - const vectorStore = await PineconeStore.fromExistingIndex(getEmbeddings(), { - pineconeIndex: getPineconeIndex(), - maxConcurrency: 5, - }); + static async deleteDatasourceById(datasourceId: string) { + try { + const isDatasourceExists = await this.getDatasourceById(datasourceId); + if (!isDatasourceExists) { + throw AppError.notFound(`Datasource with ID ${datasourceId} not found`); + } - await vectorStore.delete({ - filter: { - datasourceId, - }, - }); + /** (not used maybe deleted in the future) + const vectorStore = await PineconeStore.fromExistingIndex(getEmbeddings(), { + pineconeIndex: getPineconeIndex(), + maxConcurrency: 5, + }); + + await vectorStore.delete({ + filter: { + datasourceId, + }, + }); + */ - const deletedDatasource = await db.datasource.delete({ - where: { - id: datasourceId, - }, - }); + const deletedDatasource = await db.datasource.delete({ + where: { + id: datasourceId, + }, + }); - return deletedDatasource; - } catch (error) { - console.error(error); - if (error instanceof AppError) { - throw error; + return deletedDatasource; + } catch (error) { + console.error(error); + if (error instanceof AppError) { + throw error; + } + throw new Error('Failed to delete datasource'); } - throw new Error('Failed to delete datasource'); } -}; -export const getAllDatasource = async () => { - try { - const datasources = await db.datasource.findMany({ - include: { - agents: { - select: { - agent: true, - agentId: true, + static async getAllDatasource() { + try { + const datasources = await db.datasource.findMany({ + include: { + agents: { + select: { + agent: true, + agentId: true, + }, }, }, - }, - }); + }); - const remappedDatasources = datasources.map((datasource) => ({ - ...datasource, - agentIds: datasource.agents.map((agent) => agent.agentId), - agents: datasource.agents.map((agent) => agent.agent), - })); + const remappedDatasources = datasources.map((datasource) => ({ + ...datasource, + agentIds: datasource.agents.map((agent) => agent.agentId), + agents: datasource.agents.map((agent) => agent.agent), + })); - return remappedDatasources; - } catch (error) { - console.error(error); - throw new Error('Failed to get datasource'); + return remappedDatasources; + } catch (error) { + console.error(error); + throw new Error('Failed to get datasource'); + } } -}; +} diff --git a/apps/server/src/services/document/index.ts b/apps/server/src/services/document/index.ts index bf7ce2c..0fcc7ae 100644 --- a/apps/server/src/services/document/index.ts +++ b/apps/server/src/services/document/index.ts @@ -8,105 +8,127 @@ import { loadUrlDocument } from '../../utils/loadDocument'; import { getPineconeIndex } from '../../utils/pinecone'; import { textSplitter } from '../../utils/textSplitter'; import { loadWebDocument } from '../../utils/webLoader'; -import { updateDatasourceById } from '../datasource'; +import { DatasourceService } from '../datasource'; -export const storeDocument: StoreDocument = async (data) => { - try { - let docs: Document[]; - const result: string[] = []; +class DocumentService { + private vectorStore: PineconeStore; + + constructor() { + this.vectorStore = null!; + } - const vectorStore = await PineconeStore.fromExistingIndex(getEmbeddings(), { + async initializeVectorStore() { + this.vectorStore = await PineconeStore.fromExistingIndex(getEmbeddings(), { pineconeIndex: getPineconeIndex(), maxConcurrency: 5, }); + } + + private async processAndStoreDocuments( + docs: Document[], + datasourceId: string, + chunkSize: number = 10 + ): Promise { + const splitDocs = await textSplitter(docs); + const mapSplitDocs = splitDocs.map((doc) => ({ + pageContent: doc.pageContent, + metadata: { + ...doc.metadata, + datasourceId, + }, + })); + + const result: string[] = []; + for (let i = 0; i < mapSplitDocs.length; i += chunkSize) { + const chunk = mapSplitDocs.slice(i, i + chunkSize); + const chunkIds = chunk.map(() => uuid()); + await this.vectorStore.addDocuments(chunk, { ids: chunkIds }); + result.push(...chunkIds); + } + return result; + } + + async storeFileUrl(fileUrl: string, datasourceId: string): Promise { + const docs = await loadUrlDocument(fileUrl); + return this.processAndStoreDocuments(docs, datasourceId); + } + + async storeContent(content: string, datasourceId: string): Promise { + const docs = [ + new Document({ + pageContent: content, + metadata: { author: 'user', datasourceId }, + }), + ]; + return this.processAndStoreDocuments(docs, datasourceId); + } + + async storeUrl( + url: string, + datasourceId: string, + agentIds?: string[] + ): Promise { + const docs = await loadWebDocument(url); + const splitDocs = await textSplitter(docs); + const mapSplitDocs = splitDocs.map((doc) => ({ + pageContent: doc.pageContent, + metadata: { + ...doc.metadata, + datasourceId, + }, + })); + + const size = mapSplitDocs + .map((doc) => doc.pageContent.length) + .reduce((a, b) => a + b, 0); + + await DatasourceService.updateDatasourceById(datasourceId, { + agentIds, + size, + }); + + const result: string[] = []; + const chunkSize = 10; + for (let i = 0; i < mapSplitDocs.length; i += chunkSize) { + const chunk = mapSplitDocs.slice(i, i + chunkSize); + const chunkIds = chunk.map(() => uuid()); + await this.vectorStore.addDocuments(chunk, { ids: chunkIds }); + result.push(...chunkIds); + } + return result; + } +} + +export const storeDocument: StoreDocument = async (data) => { + try { + const documentService = new DocumentService(); + await documentService.initializeVectorStore(); + + const result: string[] = []; if (data.fileUrl) { - docs = await loadUrlDocument(data.fileUrl); - const splitDocs = await textSplitter(docs); - const mapSplitDocs = splitDocs.map((doc) => ({ - pageContent: doc.pageContent, - metadata: { - ...doc.metadata, - datasourceId: data.datasourceId, - }, - })); - // Split the documents into chunks to avoid exceeding the token limit - const chunkSize = 10; - for (let i = 0; i < mapSplitDocs.length; i += chunkSize) { - const chunk = mapSplitDocs.slice(i, i + chunkSize); - const chunkIds = chunk.map(() => uuid()); - await vectorStore.addDocuments(chunk, { - ids: chunkIds, - }); - - result.push(...chunkIds); - } + const fileResult = await documentService.storeFileUrl( + data.fileUrl, + data.datasourceId + ); + result.push(...fileResult); } if (data.content) { - // Split the content into smaller chunks - const splitDocs = await textSplitter([ - new Document({ - pageContent: data.content, - metadata: { - author: 'user', - datasourceId: data.datasourceId, - }, - }), - ]); - - const mapSplitDocs = splitDocs.map((doc) => ({ - pageContent: doc.pageContent, - metadata: { - ...doc.metadata, - datasourceId: data.datasourceId, - }, - })); - // Split the documents into chunks to avoid exceeding the token limit - const chunkSize = 10; - for (let i = 0; i < mapSplitDocs.length; i += chunkSize) { - const chunk = mapSplitDocs.slice(i, i + chunkSize); - const chunkIds = chunk.map(() => uuid()); - await vectorStore.addDocuments(chunk, { - ids: chunkIds, - }); - - result.push(...chunkIds); - } + const contentResult = await documentService.storeContent( + data.content, + data.datasourceId + ); + result.push(...contentResult); } if (data.url) { - docs = await loadWebDocument(data.url); - - const splitDocs = await textSplitter(docs); - const mapSplitDocs = splitDocs.map((doc) => ({ - pageContent: doc.pageContent, - metadata: { - ...doc.metadata, - datasourceId: data.datasourceId, - }, - })); - - const size = mapSplitDocs - .map((doc) => doc.pageContent.length) - .reduce((a, b) => a + b, 0); - - await updateDatasourceById(data.datasourceId, { - agentIds: data.agentIds, - size, - }); - - // Split the documents into chunks to avoid exceeding the token limit - const chunkSize = 10; - for (let i = 0; i < mapSplitDocs.length; i += chunkSize) { - const chunk = mapSplitDocs.slice(i, i + chunkSize); - const chunkIds = chunk.map(() => uuid()); - await vectorStore.addDocuments(chunk, { - ids: chunkIds, - }); - - result.push(...chunkIds); - } + const urlResult = await documentService.storeUrl( + data.url, + data.datasourceId, + data.agentIds + ); + result.push(...urlResult); } return result; diff --git a/apps/server/src/services/messages/index.ts b/apps/server/src/services/messages/index.ts index 620e088..270e445 100644 --- a/apps/server/src/services/messages/index.ts +++ b/apps/server/src/services/messages/index.ts @@ -1,15 +1,19 @@ import { db } from '../../configs/database'; import { CreateMessage } from '../../types/interface/message'; -export const createMessage = async (data: CreateMessage) => { - try { - const newMessage = await db.message.create({ - data, - }); +class MessageService { + async createMessage(data: CreateMessage) { + try { + const newMessage = await db.message.create({ + data, + }); - return newMessage; - } catch (error) { - console.error(error); - throw error; + return newMessage; + } catch (error) { + console.error('Error creating message:', error); + throw error; + } } -}; +} + +export const messageService = new MessageService(); diff --git a/apps/server/src/services/model/index.ts b/apps/server/src/services/model/index.ts index 969c648..9848cd8 100644 --- a/apps/server/src/services/model/index.ts +++ b/apps/server/src/services/model/index.ts @@ -1,12 +1,13 @@ import { groq } from '../../utils/groq'; -export const getAllGroqModels = async () => { - try { - const models = await groq.models.list(); - - return models; - } catch (error) { - console.error(error); - throw error; +export class ModelService { + static async getAllModels() { + try { + const models = await groq.models.list(); + return models; + } catch (error) { + console.error('Error fetching models:', error); + throw error; + } } -}; +} diff --git a/apps/server/src/utils/integrations/telegram/index.ts b/apps/server/src/utils/integrations/telegram/index.ts index c7be6b8..a639b3f 100644 --- a/apps/server/src/utils/integrations/telegram/index.ts +++ b/apps/server/src/utils/integrations/telegram/index.ts @@ -2,7 +2,7 @@ import { Prisma } from '@prisma/client'; import { Telegraf } from 'telegraf'; import { message } from 'telegraf/filters'; -import { getConversationByChatId } from '../../../services/conversation'; +import { ConversationService } from '../../../services/conversation'; import { handleTelegramStream } from '../handleStream'; export class TelegramBotInstance { @@ -38,7 +38,8 @@ export class TelegramBotInstance { const userMessage = ctx.message.text; const chatId = ctx.chat.id; - this.conversation = await getConversationByChatId(chatId); + this.conversation = + await ConversationService.getConversationByChatId(chatId); const lastMessages = this.conversation?.messages.slice(-10) || []; diff --git a/apps/server/src/validator/agent.ts b/apps/server/src/validator/agent.ts new file mode 100644 index 0000000..5e4bcef --- /dev/null +++ b/apps/server/src/validator/agent.ts @@ -0,0 +1,47 @@ +import { AgentType } from '@prisma/client'; +import z from 'zod'; + +const createNewAgentValidator = z.object({ + datasourceIds: z.array(z.string().min(1)).optional(), + name: z.string().min(1), + description: z.string().optional(), + type: z.enum([AgentType.AI, AgentType.HUMAN]), + model: z.string().min(1), + system_prompt: z + .string({ + message: 'System prompt is required', + }) + .optional(), + user_prompt: z + .string({ + message: 'System prompt is required', + }) + .optional(), + prompt_variables: z.record(z.string()).optional(), +}); + +const updateAgentSchema = z.object({ + datasourceIds: z.array(z.string().min(1)).optional(), + name: z.string().min(1), + description: z.string().optional(), + type: z.enum([AgentType.AI, AgentType.HUMAN]), + model: z.string().min(1), + system_prompt: z + .string({ + message: 'System prompt is required', + }) + .optional(), + user_prompt: z + .string({ + message: 'System prompt is required', + }) + .optional(), + prompt_variables: z.record(z.string()).optional(), + lastActive: z.string().optional(), +}); + +const agentIdSchema = z.object({ + agentId: z.string().min(1), +}); + +export { createNewAgentValidator, updateAgentSchema, agentIdSchema }; diff --git a/apps/server/src/validator/chat.ts b/apps/server/src/validator/chat.ts new file mode 100644 index 0000000..75cfda0 --- /dev/null +++ b/apps/server/src/validator/chat.ts @@ -0,0 +1,15 @@ +import z from 'zod'; + +const createNewMessageValidator = z.object({ + messages: z.array( + z.object({ role: z.enum(['human', 'ai']), content: z.string().min(1) }) + ), + userId: z.string().min(1), + agentId: z.string().min(1), + chatId: z.number().min(1).optional().describe('Telegram chat id'), + conversationId: z.string().min(1).optional(), +}); + +const conversationIdSchema = z.object({ conversationId: z.string().min(1) }); + +export { createNewMessageValidator, conversationIdSchema }; diff --git a/apps/server/src/validator/datasource.ts b/apps/server/src/validator/datasource.ts new file mode 100644 index 0000000..e2a09fc --- /dev/null +++ b/apps/server/src/validator/datasource.ts @@ -0,0 +1,67 @@ +import { $Enums } from '@prisma/client'; +import z from 'zod'; + +const createNewDatasouceSchema = z + .object({ + name: z.string().min(1), + description: z.string().optional(), + agentIds: z.array(z.string().min(1)).optional(), + fileUrl: z.string().url().optional(), + type: z.nativeEnum($Enums.DatasourceType), + category: z.string().optional(), + content: z.string().optional(), + url: z.string().optional(), + size: z.number().optional(), + }) + .refine( + (data) => + data.type !== 'DOCUMENT' || (data.type === 'DOCUMENT' && data.fileUrl), + { message: 'fileUrl is required when type is DOCUMENT', path: ['fileUrl'] } + ) + .refine( + (data) => data.type !== 'TEXT' || (data.type === 'TEXT' && data.content), + { message: 'content is required when type is TEXT', path: ['content'] } + ) + .refine((data) => data.type !== 'WEB' || (data.type === 'WEB' && data.url), { + message: 'url is required when type is WEBSITE', + path: ['url'], + }); +// .refine( +// (data) => +// data.type !== 'DATABASE' || (data.type === 'DATABASE' && data.size), +// { message: 'size is required when type is DATABASE', path: ['size'] } +// ); + +const datasourceIdSchema = z.object({ + datasourceId: z.string().min(1), +}); + +const updateDatasouceSchema = z + .object({ + name: z.string().min(1, { + message: 'Source name cannot be empty', + }), + description: z.string().optional(), + agentIds: z.array(z.string().min(1)).optional(), + fileUrl: z.string().url().optional(), + type: z.enum(['DOCUMENT', 'TEXT', 'WEB', 'DATABASE']), + category: z.string().optional(), + content: z.string().optional(), + url: z.string().optional(), + size: z.number().optional(), + }) + .refine( + (data) => + data.type !== 'DOCUMENT' || (data.type === 'DOCUMENT' && data.fileUrl), + { message: 'fileUrl is required when type is DOCUMENT', path: ['fileUrl'] } + ) + .refine( + (data) => data.type !== 'TEXT' || (data.type === 'TEXT' && data.content), + { message: 'content is required when type is TEXT', path: ['content'] } + ) + .refine((data) => data.type !== 'WEB' || (data.type === 'WEB' && data.url), { + message: 'url is required when type is WEBSITE', + path: ['url'], + }); + +export { createNewDatasouceSchema, datasourceIdSchema, updateDatasouceSchema };