Skip to main content
Glama
ai.controller.ts19.3 kB
import { type AIConfig, type AIOptions, type ChatCompletionRequestMessage, getAIConfig, } from '@intlayer/ai'; import type { KeyPath, Locale } from '@intlayer/types'; import type { ResponseWithSession } from '@middlewares/sessionAuth.middleware'; import { getDictionariesByTags } from '@services/dictionary.service'; import * as tagService from '@services/tag.service'; import { getTagsByKeys } from '@services/tag.service'; import * as askDocQuestionUtil from '@utils/AI/askDocQuestion/askDocQuestion'; import * as auditContentDeclarationUtil from '@utils/AI/auditDictionary'; import * as auditContentDeclarationFieldUtil from '@utils/AI/auditDictionaryField'; import * as auditContentDeclarationMetadataUtil from '@utils/AI/auditDictionaryMetadata'; import * as auditTagUtil from '@utils/AI/auditTag'; import * as autocompleteUtil from '@utils/AI/autocomplete'; import * as customQueryUtil from '@utils/AI/customQuery'; import * as translateJSONUtil from '@utils/AI/translateJSON'; import { type AppError, ErrorHandler } from '@utils/errors'; import { type DiscussionFiltersParams, getDiscussionFiltersAndPagination, } from '@utils/filtersAndPagination/getDiscussionFiltersAndPagination'; import { formatPaginatedResponse, formatResponse, type PaginatedResponse, type ResponseData, } from '@utils/responseData'; import type { NextFunction, Request } from 'express'; import { DiscussionModel } from '@/models/discussion.model'; import type { Dictionary } from '@/types/dictionary.types'; import type { DiscussionAPI } from '@/types/discussion.types'; import type { Tag, TagAPI } from '@/types/tag.types'; type ReplaceAIConfigByOptions<T> = Omit<T, 'aiConfig'> & { aiOptions?: AIOptions; }; export type CustomQueryBody = ReplaceAIConfigByOptions<customQueryUtil.CustomQueryOptions> & { tagsKeys?: string[]; applicationContext?: string; }; export type CustomQueryResult = ResponseData<customQueryUtil.CustomQueryResultData>; export const customQuery = async ( req: Request<CustomQueryBody>, res: ResponseWithSession<CustomQueryResult>, _next: NextFunction ): Promise<void> => { // biome-ignore lint/correctness/noUnusedVariables: Just filter out tagsKeys const { aiOptions, tagsKeys, ...rest } = req.body; let aiConfig: AIConfig; try { aiConfig = await getAIConfig( { userOptions: aiOptions, defaultOptions: customQueryUtil.aiDefaultOptions, accessType: ['registered_user', 'apiKey'], }, !!res.locals.user ); } catch (_error) { ErrorHandler.handleGenericErrorResponse(res, 'AI_ACCESS_DENIED'); return; } try { const auditResponse = await customQueryUtil.customQuery({ ...rest, aiConfig, applicationContext: aiOptions?.applicationContext, }); if (!auditResponse) { ErrorHandler.handleGenericErrorResponse(res, 'QUERY_FAILED'); return; } const responseData = formatResponse<customQueryUtil.CustomQueryResultData>({ data: auditResponse, }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type TranslateJSONBody = Omit< ReplaceAIConfigByOptions<translateJSONUtil.TranslateJSONOptions<JSON>>, 'tags' > & { tagsKeys?: string[]; }; export type TranslateJSONResult = ResponseData< translateJSONUtil.TranslateJSONResultData<JSON> >; export const translateJSON = async ( req: Request<TranslateJSONBody>, res: ResponseWithSession<TranslateJSONResult>, _next: NextFunction ): Promise<void> => { const { project } = res.locals; const { aiOptions, tagsKeys, ...rest } = req.body; let aiConfig: AIConfig; try { aiConfig = await getAIConfig( { userOptions: aiOptions, defaultOptions: translateJSONUtil.aiDefaultOptions, accessType: ['registered_user', 'apiKey'], }, !!res.locals.user ); } catch (_error) { ErrorHandler.handleGenericErrorResponse(res, 'AI_ACCESS_DENIED'); return; } try { let tags: Tag[] = []; if (project?.organizationId && tagsKeys) { tags = await getTagsByKeys(tagsKeys, project.organizationId); } const auditResponse = await translateJSONUtil.translateJSON<any>({ ...rest, aiConfig, applicationContext: aiOptions?.applicationContext, tags, }); if (!auditResponse) { ErrorHandler.handleGenericErrorResponse(res, 'AUDIT_FAILED'); return; } const responseData = formatResponse< translateJSONUtil.TranslateJSONResultData<any> >({ data: auditResponse, }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type AuditContentDeclarationBody = { aiOptions?: AIOptions; locales: Locale[]; defaultLocale: Locale; fileContent: string; filePath?: string; tagsKeys?: string[]; }; export type AuditContentDeclarationResult = ResponseData<auditContentDeclarationUtil.AuditFileResultData>; /** * Retrieves a list of dictionaries based on filters and pagination. */ export const auditContentDeclaration = async ( req: Request<AuditContentDeclarationBody>, res: ResponseWithSession<AuditContentDeclarationResult>, _next: NextFunction ): Promise<void> => { const { project } = res.locals; const { fileContent, filePath, aiOptions, locales, defaultLocale, tagsKeys } = req.body; let aiConfig: AIConfig; try { aiConfig = await getAIConfig( { userOptions: aiOptions, defaultOptions: auditContentDeclarationUtil.aiDefaultOptions, accessType: ['registered_user', 'apiKey'], }, !!res.locals.user ); } catch (_error) { ErrorHandler.handleGenericErrorResponse(res, 'AI_ACCESS_DENIED'); return; } try { let tags: Tag[] = []; if (project?.organizationId) { tags = await getTagsByKeys(tagsKeys, project.organizationId); } const auditResponse = await auditContentDeclarationUtil.auditDictionary({ fileContent, filePath, aiConfig, applicationContext: aiOptions?.applicationContext, locales, defaultLocale, tags, }); if (!auditResponse) { ErrorHandler.handleGenericErrorResponse(res, 'AUDIT_FAILED'); return; } const responseData = formatResponse<auditContentDeclarationUtil.AuditFileResultData>({ data: auditResponse, }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type AuditContentDeclarationFieldBody = { aiOptions?: AIOptions; locales: Locale[]; fileContent: string; filePath?: string; tagsKeys?: string[]; keyPath: KeyPath[]; }; export type AuditContentDeclarationFieldResult = ResponseData<auditContentDeclarationFieldUtil.AuditDictionaryFieldResultData>; /** * Retrieves a list of dictionaries based on filters and pagination. */ export const auditContentDeclarationField = async ( req: Request<AuditContentDeclarationFieldBody>, res: ResponseWithSession<AuditContentDeclarationFieldResult>, _next: NextFunction ): Promise<void> => { const { project } = res.locals; const { fileContent, aiOptions, locales, tagsKeys, keyPath } = req.body; let aiConfig: AIConfig; try { aiConfig = await getAIConfig( { userOptions: aiOptions, defaultOptions: auditContentDeclarationFieldUtil.aiDefaultOptions, accessType: ['registered_user', 'apiKey'], }, !!res.locals.user ); } catch (_error) { ErrorHandler.handleGenericErrorResponse(res, 'AI_ACCESS_DENIED'); return; } try { let tags: Tag[] = []; if (project?.organizationId) { tags = await getTagsByKeys(tagsKeys, project.organizationId); } const auditResponse = await auditContentDeclarationFieldUtil.auditDictionaryField({ fileContent, aiConfig, applicationContext: aiOptions?.applicationContext, locales, tags, keyPath, }); if (!auditResponse) { ErrorHandler.handleGenericErrorResponse(res, 'AUDIT_FAILED'); return; } const responseData = formatResponse<auditContentDeclarationFieldUtil.AuditDictionaryFieldResultData>( { data: auditResponse, } ); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type AuditContentDeclarationMetadataBody = { aiOptions?: AIOptions; fileContent: string; }; export type AuditContentDeclarationMetadataResult = ResponseData<auditContentDeclarationMetadataUtil.AuditFileResultData>; /** * Retrieves a list of dictionaries based on filters and pagination. */ export const auditContentDeclarationMetadata = async ( req: Request<AuditContentDeclarationMetadataBody>, res: ResponseWithSession<AuditContentDeclarationMetadataResult>, _next: NextFunction ): Promise<void> => { const { organization } = res.locals; const { fileContent, aiOptions } = req.body; let aiConfig: AIConfig; try { aiConfig = await getAIConfig( { userOptions: aiOptions, defaultOptions: auditContentDeclarationMetadataUtil.aiDefaultOptions, accessType: ['registered_user', 'apiKey'], }, !!res.locals.user ); } catch (_error) { ErrorHandler.handleGenericErrorResponse(res, 'AI_ACCESS_DENIED'); return; } try { const tags: Tag[] = await tagService.findTags( { organizationId: organization?.id, }, 0, 1000 ); const auditResponse = await auditContentDeclarationMetadataUtil.auditDictionaryMetadata({ fileContent, aiConfig, applicationContext: aiOptions?.applicationContext, tags, }); if (!auditResponse) { ErrorHandler.handleGenericErrorResponse(res, 'AUDIT_FAILED'); return; } const responseData = formatResponse<auditContentDeclarationMetadataUtil.AuditFileResultData>({ data: auditResponse, }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type AuditTagBody = { aiOptions?: AIOptions; tag: TagAPI; }; export type AuditTagResult = ResponseData<auditTagUtil.TranslateJSONResultData>; /** * Retrieves a list of dictionaries based on filters and pagination. */ export const auditTag = async ( req: Request<undefined, undefined, AuditTagBody>, res: ResponseWithSession<AuditTagResult>, _next: NextFunction ): Promise<void> => { const { project } = res.locals; const { aiOptions, tag } = req.body; let aiConfig: AIConfig; try { aiConfig = await getAIConfig( { userOptions: aiOptions, defaultOptions: auditTagUtil.aiDefaultOptions, accessType: ['registered_user', 'apiKey'], }, !!res.locals.user ); } catch (_error) { ErrorHandler.handleGenericErrorResponse(res, 'AI_ACCESS_DENIED'); return; } try { let dictionaries: Dictionary[] = []; if (project?.organizationId) { dictionaries = await getDictionariesByTags([tag.key], project.id); } const auditResponse = await auditTagUtil.auditTag({ aiConfig, dictionaries, tag, applicationContext: aiOptions?.applicationContext, }); if (!auditResponse) { ErrorHandler.handleGenericErrorResponse(res, 'AUDIT_FAILED'); return; } const responseData = formatResponse<auditTagUtil.TranslateJSONResultData>({ data: auditResponse, }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type AskDocQuestionBody = { messages: ChatCompletionRequestMessage[]; discussionId: string; }; export type AskDocQuestionResult = ResponseData<askDocQuestionUtil.AskDocQuestionResult>; export const askDocQuestion = async ( req: Request<undefined, undefined, AskDocQuestionBody>, res: ResponseWithSession<AskDocQuestionResult>, _next: NextFunction ): Promise<void> => { const { messages = [], discussionId } = req.body; const { user, project, organization } = res.locals; let aiConfig: AIConfig; try { aiConfig = await getAIConfig( { userOptions: {}, accessType: ['public'], }, !!res.locals.user ); } catch (_error) { ErrorHandler.handleGenericErrorResponse(res, 'AI_ACCESS_DENIED'); return; } // 1. Prepare SSE headers and flush them NOW res.setHeader('Content-Type', 'text/event-stream; charset=utf-8'); res.setHeader('Cache-Control', 'no-cache, no-transform'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); // disable nginx buffering res.flushHeaders?.(); res.write(': connected\n\n'); // initial comment keeps some browsers happy res.flush?.(); // 2. Kick off the upstream stream WITHOUT awaiting it askDocQuestionUtil .askDocQuestion(messages, aiConfig, { onMessage: (chunk) => { res.write(`data: ${JSON.stringify({ chunk })}\n\n`); res.flush?.(); }, }) .then(async (fullResponse) => { const lastUserMessageContent = messages.findLast( (message) => message.role === 'user' )?.content; const lastUserMessageNbWords = lastUserMessageContent ? lastUserMessageContent.split(' ').length : 0; if (lastUserMessageNbWords > 2) { // If the last user message is less than 3 words, don't persist the discussion // Example: "Hello", "Hi", "Hey", "test", etc. // 3. Persist discussion while the client already has all chunks await DiscussionModel.findOneAndUpdate( { discussionId }, { $set: { discussionId, userId: user?.id, projectId: project?.id, organizationId: organization?.id, messages: [ ...messages.map((msg) => ({ role: msg.role, content: msg.content, timestamp: msg.timestamp, })), { role: 'assistant', content: fullResponse.response, relatedFiles: fullResponse.relatedFiles, timestamp: new Date(), }, ], }, }, { upsert: true, new: true } ); } // 4. Tell the client we're done and close the stream res.write( `data: ${JSON.stringify({ done: true, response: fullResponse })}\n\n` ); res.end(); }) .catch((err) => { // propagate error as an SSE event so the client knows why it closed res.write( `event: error\ndata: ${JSON.stringify({ message: err.message })}\n\n` ); res.end(); }); }; export type AutocompleteBody = { text: string; aiOptions?: AIOptions; contextBefore?: string; currentLine?: string; contextAfter?: string; }; export type AutocompleteResponse = ResponseData<{ autocompletion: string; }>; export const autocomplete = async ( req: Request<AutocompleteBody>, res: ResponseWithSession<AutocompleteResponse>, _next: NextFunction ): Promise<void> => { try { const { text, aiOptions, contextBefore, currentLine, contextAfter } = req.body; let aiConfig: AIConfig; try { aiConfig = await getAIConfig( { userOptions: aiOptions, defaultOptions: autocompleteUtil.aiDefaultOptions, accessType: ['public'], }, !!res.locals.user ); } catch (_error) { ErrorHandler.handleGenericErrorResponse(res, 'AI_ACCESS_DENIED'); return; } const response = (await autocompleteUtil.autocomplete({ text, aiConfig, applicationContext: aiOptions?.applicationContext, contextBefore, currentLine, contextAfter, })) ?? { autocompletion: '', tokenUsed: 0, }; const responseData = formatResponse<autocompleteUtil.AutocompleteFileResultData>({ data: response, }); res.json(responseData); } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type GetDiscussionsParams = | ({ page?: string | number; pageSize?: string | number; includeMessages?: 'true' | 'false'; } & DiscussionFiltersParams) | undefined; export type GetDiscussionsResult = PaginatedResponse<DiscussionAPI>; /** * Retrieves a list of discussions with filters and pagination. * Only the owner or admins can access. By default, users only see their own. */ export const getDiscussions = async ( req: Request<GetDiscussionsParams>, res: ResponseWithSession<GetDiscussionsResult>, _next: NextFunction ): Promise<void> => { const { user, roles } = res.locals; const { filters, sortOptions, pageSize, skip, page, getNumberOfPages } = getDiscussionFiltersAndPagination(req, res); const includeMessagesParam = (req.query as any)?.includeMessages as | 'true' | 'false' | undefined; const includeMessages = includeMessagesParam !== 'false'; if (!user) { ErrorHandler.handleGenericErrorResponse(res, 'USER_NOT_DEFINED'); return; } try { const projection = includeMessages ? {} : { messages: 0 }; const discussions = await DiscussionModel.find(filters, projection) .sort(sortOptions) .skip(skip) .limit(pageSize) .lean(); // Compute number of messages for each discussion const numberOfMessagesById: Record<string, number> = {}; if (!includeMessages && discussions.length > 0) { const ids = discussions.map((d: any) => d._id); const counts = await DiscussionModel.aggregate([ { $match: { _id: { $in: ids } } }, { $project: { numberOfMessages: { $size: { $ifNull: ['$messages', []] } }, }, }, ]); for (const c of counts as any[]) { numberOfMessagesById[String(c._id)] = c.numberOfMessages ?? 0; } } // Permission: allow admin, or the owner for all returned entries const allOwnedByUser = discussions.every( (d) => String(d.userId) === String(user.id) ); const isAllowed = roles.includes('admin') || allOwnedByUser; if (!isAllowed) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } const totalItems = await DiscussionModel.countDocuments(filters); const responseData = formatPaginatedResponse({ data: discussions.map((d: any) => ({ ...d, id: String(d._id ?? d.id), numberOfMessages: includeMessages ? Array.isArray(d.messages) ? d.messages.length : 0 : (numberOfMessagesById[String(d._id ?? d.id)] ?? 0), })), page, pageSize, totalPages: getNumberOfPages(totalItems), totalItems, }); res.json(responseData as any); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server