import { DataSource, Repository } from 'typeorm'; import { AiConfigService } from '../ai-config/ai-config.service'; import { AgentToolExecutor } from '../agent-tools/agent-tool.executor'; import { AgentToolContextFactory, type AgentSkillDescriptor, } from '../agent-tools/agent-tool.types'; import { AuthorizationService, CaslAbilityFactory, type AuthenticatedUser } from '../authorization'; import { AiAttachmentService } from './ai-attachment.service'; import { ImportsService } from '../imports/imports.service'; import { AiChartService } from './ai-chart.service'; import { AiExcelReaderService } from './ai-excel-reader.service'; import { AiFormService } from './ai-form.service'; import { AiReviewService } from './ai-review.service'; import { A2uiSubmissionsService } from './ai-a2ui-submissions.service'; import { AiModelStreamService } from './ai-model-stream.service'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; import { AiConversation, AiMessage, AiReview, AiToolRun, type AiReviewSectionType, } from './entities'; import type { AiChatServiceContext, AiSseEmitter, GenerationInput, ModelContentPart, ModelMessage, ModelToolCall, PublicConversation, } from './ai-chat.types'; import { listConversations, createConversation, updateConversation, deleteConversation, deleteAllConversations, getMessages, deleteMessage, } from './ai-chat.conversations'; import { streamMessage, regenerateMessage, editMessage, } from './ai-chat.streaming'; import { resolveFormConversationId, resolveReviewConversationId, submitForm, submitReview, confirmReviewStep, confirmReviewGroup, assertReviewImportPermissions, a2uiSubmitInfo, buildFormSubmitModelContent, markFormSubmittedOnMessage, a2uiReviewSubmitInfo, buildReviewSubmitModelContent, markReviewSubmittedOnMessage, } from './ai-chat.submissions'; import { denyWriteTool, executeTool } from './ai-chat.tools'; import { executeStartImportWizard } from './ai-chat.tool-actions'; export abstract class AiChatServiceBase implements AiChatServiceContext { readonly activeConversations = new Set(); abstract listSkills(user: AuthenticatedUser): AgentSkillDescriptor[]; abstract serializeMessage(message: AiMessage): Record; abstract redactText(value: string): string; abstract summarize(value: unknown): string | null; abstract safeStructured(value: unknown): unknown; abstract parseToolArguments(value: string): unknown; abstract safeToolName(name: string): string; abstract throwIfAborted(signal: AbortSignal): void; abstract errorCode(error: unknown): string; abstract assertGeneratedLength(reasoning: string, content: string): void; abstract buildContext( conversationId: number, focusUserMessageId: number, focusContent: string | ModelContentPart[], skillKey: string | null, supportsVision: boolean, ): Promise; abstract buildUserContent( text: string, attachments: any[], supportsVision: boolean, ): Promise; abstract truncateText(value: string, max: number): string; abstract metadataSkillKey(metadata: Record | null): string | null; abstract normalizeTitle(title?: string): string; abstract titleFromMessage(message: string): string; abstract assertSkillAvailable(user: AuthenticatedUser, skillKey?: string | null): void; abstract requireOwnedConversation(userId: number, id: number): Promise; abstract acquireConversation(conversationId: number): Promise; abstract executeGeneration(input: GenerationInput): Promise; constructor( readonly conversations: Repository, readonly messages: Repository, readonly toolRuns: Repository, readonly dataSource: DataSource, readonly configService: AiConfigService, readonly toolExecutor: AgentToolExecutor, readonly modelStream: AiModelStreamService, readonly attachmentService: AiAttachmentService, readonly formService: AiFormService, readonly reviewService: AiReviewService, readonly chartService: AiChartService, readonly abilityFactory: CaslAbilityFactory, readonly authorization: AuthorizationService, readonly a2uiSubmissions?: A2uiSubmissionsService, readonly excelReader?: AiExcelReaderService, readonly importsService?: ImportsService, readonly opLog?: OperationLogsService, ) {} a2uiSubmitInfo(metadata: Record | null) { return a2uiSubmitInfo(metadata); } a2uiReviewSubmitInfo(metadata: Record | null) { return a2uiReviewSubmitInfo(metadata); } buildFormSubmitModelContent(submit: { title: string; values: Record; submissionId?: string; fieldErrors?: Array<{ field: string; message: string }>; }): string { return buildFormSubmitModelContent(submit); } buildReviewSubmitModelContent(submit: { reviewId: string; reviewTitle: string; resultMessage: string; submissionId?: string; nextSteps?: Array<{ key: string; label: string }>; }): string { return buildReviewSubmitModelContent(submit); } markFormSubmittedOnMessage(assistantMessageId: number, conversationId: number): Promise { return markFormSubmittedOnMessage(this, assistantMessageId, conversationId); } markReviewSubmittedOnMessage( assistantMessageId: number, conversationId: number, review?: AiReview, ): Promise { return markReviewSubmittedOnMessage(this, assistantMessageId, conversationId, review); } assertReviewImportPermissions( user: AuthenticatedUser, review: AiReview, sectionKey?: string, sectionType?: AiReviewSectionType, ): void { return assertReviewImportPermissions(this, user, review, sectionKey, sectionType); } executeTool( messageId: number, call: ModelToolCall, context: ReturnType, allowedSkillKey: string | null, allowWriteTools: boolean, reviewSubmitted: boolean, userId: number, emit: AiSseEmitter, ): Promise { return executeTool( this, messageId, call, context, allowedSkillKey, allowWriteTools, reviewSubmitted, userId, emit, ); } denyWriteTool( messageId: number, call: ModelToolCall, emit: AiSseEmitter, ): Promise { return denyWriteTool(this, messageId, call, emit); } executeStartImportWizard( messageId: number, call: ModelToolCall, context: ReturnType, emit: AiSseEmitter, ): Promise { return executeStartImportWizard(this, messageId, call, context, emit); } listConversations(userId: number): Promise { return listConversations(this, userId); } createConversation( user: AuthenticatedUser, title?: string, lockedSkillKey?: string | null, ): Promise { return createConversation(this, user, title, lockedSkillKey); } updateConversation( user: AuthenticatedUser, id: number, dto: { title?: string; lockedSkillKey?: string | null }, ): Promise { return updateConversation(this, user, id, dto); } deleteConversation(userId: number, id: number): Promise { return deleteConversation(this, userId, id); } deleteAllConversations(userId: number): Promise { return deleteAllConversations(this, userId); } getMessages(userId: number, conversationId: number, page = 1, limit = 50) { return getMessages(this, userId, conversationId, page, limit); } deleteMessage( userId: number, conversationId: number, messageId: number, ): Promise<{ deletedIds: number[] }> { return deleteMessage(this, userId, conversationId, messageId); } streamMessage( user: AuthenticatedUser, conversationId: number, dto: { message: string; attachmentIds?: number[]; clientRequestId: string; skillKey?: string | null; reasoningEffort?: string | null; }, signal: AbortSignal, emit: AiSseEmitter, onReady: () => void, ): Promise { return streamMessage(this, user, conversationId, dto, signal, emit, onReady); } regenerateMessage( user: AuthenticatedUser, conversationId: number, assistantMessageId: number, clientRequestId: string, reasoningEffort: string | null | undefined, signal: AbortSignal, emit: AiSseEmitter, onReady: () => void, ): Promise { return regenerateMessage( this, user, conversationId, assistantMessageId, clientRequestId, reasoningEffort, signal, emit, onReady, ); } editMessage( user: AuthenticatedUser, conversationId: number, messageId: number, dto: { content: string; clientRequestId: string; reasoningEffort?: string | null }, signal: AbortSignal, emit: AiSseEmitter, onReady: () => void, ): Promise { return editMessage(this, user, conversationId, messageId, dto, signal, emit, onReady); } resolveFormConversationId(userId: number, formId: string): Promise { return resolveFormConversationId(this, userId, formId); } resolveReviewConversationId(userId: number, reviewId: string): Promise { return resolveReviewConversationId(this, userId, reviewId); } submitForm( user: AuthenticatedUser, formId: string, dto: { values: Record; clientRequestId: string; reasoningEffort?: string | null; }, signal: AbortSignal, emit: AiSseEmitter, onReady: () => void, ): Promise { return submitForm(this, user, formId, dto, signal, emit, onReady); } submitReview( user: AuthenticatedUser, reviewId: string, dto: { clientRequestId: string; reasoningEffort?: string | null }, signal: AbortSignal, emit: AiSseEmitter, onReady: () => void, ): Promise { return submitReview(this, user, reviewId, dto, signal, emit, onReady); } confirmReviewStep( user: AuthenticatedUser, reviewId: string, sectionKey: string, ): Promise> { return confirmReviewStep(this, user, reviewId, sectionKey); } confirmReviewGroup( user: AuthenticatedUser, reviewId: string, type: AiReviewSectionType, ): Promise> { return confirmReviewGroup(this, user, reviewId, type); } }