import { Injectable } from '@nestjs/common';
import type { AiProvider } from '../domain/ai-provider';
import type {
AiProviderKey,
AiRequest,
AiResponse,
} from '../domain/ai.types';
import { NoopAiProvider } from '../infrastructure/noop-ai.provider';
import { AuditService } from '../../audit/audit.service';
@Injectable()
export class AiService {
constructor(
private readonly noopProvider: NoopAiProvider,
private readonly audit: AuditService,
) {}
private getProvider(): AiProvider {
const configured = (process.env.AI_PROVIDER ?? 'noop') as AiProviderKey;
switch (configured) {
case 'noop':
return this.noopProvider;
case 'openai':
case 'gemini':
throw new Error(`ai_provider_not_configured:${configured}`);
default:
throw new Error(`unsupported_ai_provider:${configured}`);
}
}
async generate(request: AiRequest): Promise<AiResponse> {
const startedAt = Date.now();
const configuredProvider = process.env.AI_PROVIDER ?? 'noop';
try {
const provider = this.getProvider();
const response = await provider.generate(request);
await this.audit.record({
organizationId: request.organizationId,
actorUserId: request.userId,
eventType: 'AiGenerationSucceeded',
entityType: 'AiGeneration',
metadata: {
provider: response.provider,
model: response.model,
purpose: request.purpose,
locationId: request.locationId ?? null,
locale: request.locale,
durationMs: Date.now() - startedAt,
requestId: response.requestId ?? null,
usage: response.usage ?? null,
},
});
return response;
} catch (error) {
await this.audit.record({
organizationId: request.organizationId,
actorUserId: request.userId,
eventType: 'AiGenerationFailed',
entityType: 'AiGeneration',
metadata: {
provider: configuredProvider,
purpose: request.purpose,
locationId: request.locationId ?? null,
locale: request.locale,
durationMs: Date.now() - startedAt,
errorName: error instanceof Error ? error.name : 'UnknownError',
errorCode:
error instanceof Error
? error.message.split(':')[0]
: 'unknown_error',
},
});
throw error;
}
}
}
