diff --git a/alkemio.yml b/alkemio.yml index 40aad2c095..a51c1b018d 100644 --- a/alkemio.yml +++ b/alkemio.yml @@ -77,6 +77,8 @@ security: # Cors allowed headers. allowed_headers: ${CORS_ALLOWED_HEADERS}:Origin,X-Requested-With,Content-Type,Accept,Authorization + encryption_key: ${ENCRYPTION_KEY}:ktO2wPinKwidG8cgjhfKTHGqU6D5lxP0NkCVAJglnfw= + innovation_hub: header: ${INNOVATION_HUB_HEADER}:origin diff --git a/package-lock.json b/package-lock.json index fcccb28bfa..812b199923 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@apollo/server": "^4.10.4", "@elastic/elasticsearch": "^8.12.2", "@golevelup/nestjs-rabbitmq": "^5.3.0", + "@hedger/nestjs-encryption": "^0.1.5", "@nestjs/apollo": "^12.2.0", "@nestjs/axios": "^3.0.2", "@nestjs/cache-manager": "^2.2.2", @@ -2293,6 +2294,20 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@hedger/nestjs-encryption": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@hedger/nestjs-encryption/-/nestjs-encryption-0.1.5.tgz", + "integrity": "sha512-TdXV0WvikRqGAS6lDd12EBhuAAQlYQW3PgQivgbNCpCG+3TrdaMPSp9Jq+LiIWts+7s/TePozZs6OdH+jAKELA==", + "bin": { + "nestjs-encryption-keygen": "dist/cli/keygen.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/nhedger" + }, + "peerDependencies": { + "@nestjs/common": "^10 || ^9" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -16080,6 +16095,12 @@ "version": "3.2.0", "requires": {} }, + "@hedger/nestjs-encryption": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@hedger/nestjs-encryption/-/nestjs-encryption-0.1.5.tgz", + "integrity": "sha512-TdXV0WvikRqGAS6lDd12EBhuAAQlYQW3PgQivgbNCpCG+3TrdaMPSp9Jq+LiIWts+7s/TePozZs6OdH+jAKELA==", + "requires": {} + }, "@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", diff --git a/package.json b/package.json index 47359db592..d8d8aeba3a 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@apollo/server": "^4.10.4", "@elastic/elasticsearch": "^8.12.2", "@golevelup/nestjs-rabbitmq": "^5.3.0", + "@hedger/nestjs-encryption": "^0.1.5", "@nestjs/apollo": "^12.2.0", "@nestjs/axios": "^3.0.2", "@nestjs/cache-manager": "^2.2.2", diff --git a/quickstart-services-ai.yml b/quickstart-services-ai.yml index 0617f83fc4..07428249ac 100644 --- a/quickstart-services-ai.yml +++ b/quickstart-services-ai.yml @@ -477,3 +477,52 @@ services: - AUTH_ORY_KRATOS_PUBLIC_BASE_URL=http://localhost:3000/ory/kratos/public - CHUNK_SIZE=1000 - CHUNK_OVERLAP=100 + + virtual_contributor_engine_generic: + container_name: alkemio_dev_virtual_contributor_engine_generic + hostname: virtual-contributor-engine-generic + image: alkemio/virtual-contributor-engine-generic:v0.1.1 + platform: linux/x86_64 + restart: always + volumes: + - /dev/shm:/dev/shm + networks: + - alkemio_dev_net + depends_on: + rabbitmq: + condition: "service_healthy" + environment: + - RABBITMQ_HOST + - RABBITMQ_USER + - RABBITMQ_PASSWORD + - RABBITMQ_PORT + - RABBITMQ_QUEUE=virtual-contributor-engine-generic + - LOCAL_PATH=./ + - LOG_LEVEL=DEBUG + - LANGCHAIN_TRACING_V2 + - LANGCHAIN_ENDPOINT + - LANGCHAIN_API_KEY + - LANGCHAIN_PROJECT=virtual-contributor-engine-generic + - HISTORY_LENGTH=20 + + virtual_contributor_engine_openai_assistant: + container_name: alkemio_dev_virtual_contributor_engine_openai_assistant + hostname: virtual-contributor-engine-openai-assistant + image: alkemio/virtual-contributor-engine-openai-assistant:v0.1.0 + platform: linux/x86_64 + restart: always + volumes: + - /dev/shm:/dev/shm + networks: + - alkemio_dev_net + depends_on: + rabbitmq: + condition: "service_healthy" + environment: + - RABBITMQ_HOST + - RABBITMQ_USER + - RABBITMQ_PASSWORD + - RABBITMQ_PORT + - RABBITMQ_QUEUE=virtual-contributor-engine-openai-assistant + - LOCAL_PATH=./ + - LOG_LEVEL=DEBUG diff --git a/src/app.module.ts b/src/app.module.ts index 0e4c648345..ac1d1ad8bf 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -82,9 +82,24 @@ import { LookupByNameModule } from '@services/api/lookup-by-name'; import { PlatformHubModule } from '@platform/platfrom.hub/platform.hub.module'; import { AdminContributorsModule } from '@platform/admin/avatars/admin.avatar.module'; import { InputCreatorModule } from '@services/api/input-creator/input.creator.module'; +import { Cipher, EncryptionModule } from '@hedger/nestjs-encryption'; @Module({ imports: [ + EncryptionModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => { + const key = configService.get('security.encryption_key', { + infer: true, + }); + + return { + key, + cipher: Cipher.AES_256_CBC, + }; + }, + }), ConfigModule.forRoot({ envFilePath: ['.env'], isGlobal: true, diff --git a/src/common/constants/providers.ts b/src/common/constants/providers.ts index ebcfcc10e5..5dcd4ef274 100644 --- a/src/common/constants/providers.ts +++ b/src/common/constants/providers.ts @@ -17,6 +17,10 @@ export const VIRTUAL_CONTRIBUTOR_ENGINE_COMMUNITY_MANAGER = 'virtual-contributor-engine-community-manager'; export const VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT = 'virtual-contributor-engine-expert'; +export const VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC = + 'virtual-contributor-engine-generic'; +export const VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT = + 'virtual-contributor-engine-openai-assistant'; export const NOTIFICATIONS_SERVICE = 'alkemio-notifications'; export const WALLET_MANAGEMENT_SERVICE = 'alkemio-wallet-manager'; export const REDIS_LOCK_SERVICE = 'redis-lock-service'; diff --git a/src/common/enums/ai.persona.engine.ts b/src/common/enums/ai.persona.engine.ts index a94bd19a7d..f990afd375 100644 --- a/src/common/enums/ai.persona.engine.ts +++ b/src/common/enums/ai.persona.engine.ts @@ -3,6 +3,8 @@ import { registerEnumType } from '@nestjs/graphql'; export enum AiPersonaEngine { GUIDANCE = 'guidance', EXPERT = 'expert', + GENERIC_OPENAI = 'generic-openai', + OPENAI_ASSISTANT = 'openai-assistant', COMMUNITY_MANAGER = 'community-manager', } diff --git a/src/common/enums/logging.context.ts b/src/common/enums/logging.context.ts index 9663b5c048..c19ef34456 100644 --- a/src/common/enums/logging.context.ts +++ b/src/common/enums/logging.context.ts @@ -65,6 +65,10 @@ export enum LogContext { INNOVATION_FLOW = 'innovation-flow', FILE_INTEGRATION = 'file-integration', VIRTUAL_CONTRIBUTOR = 'virtual-contributor', + VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC = 'virtual-contributor-engine-generic', + VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT = 'virtual-contributor-openai-assistant', + VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT = 'virtual-contributor-engine-expert', + VIRTUAL_CONTRIBUTOR_ENGINE_COMMUNITY_MANAGER = 'virtual-contributor-engine-community-manager', AI_SERVER = 'ai-server', AI_SERVER_ADAPTER = 'ai-server-adapter', AI_PERSONA_SERVICE = 'ai-persona-service', diff --git a/src/common/enums/messaging.queue.ts b/src/common/enums/messaging.queue.ts index e3da7018a3..c7936e463c 100644 --- a/src/common/enums/messaging.queue.ts +++ b/src/common/enums/messaging.queue.ts @@ -2,6 +2,8 @@ export enum MessagingQueue { VIRTUAL_CONTRIBUTOR_ENGINE_GUIDANCE = 'virtual-contributor-engine-guidance', VIRTUAL_CONTRIBUTOR_ENGINE_COMMUNITY_MANAGER = 'virtual-contributor-engine-community-manager', VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT = 'virtual-contributor-engine-expert', + VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC = 'virtual-contributor-engine-generic', + VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT = 'virtual-contributor-engine-openai-assistant', // WALLET_MANAGER = 'alkemio-wallet-manager', NOTIFICATIONS = 'alkemio-notifications', diff --git a/src/config/configuration.ts b/src/config/configuration.ts index e31abc5cae..efb7353bb5 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -14,7 +14,7 @@ export default () => { const envConfig = process.env; YAML.visit(doc, { - Scalar(key, node) { + Scalar(_key, node) { if (node.type === 'PLAIN') { node.value = buildYamlNodeValue(node.value, envConfig); } diff --git a/src/core/microservices/auth.reset.service.factory.ts b/src/core/microservices/auth.reset.service.factory.ts deleted file mode 100644 index e7a680bee0..0000000000 --- a/src/core/microservices/auth.reset.service.factory.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { LogContext } from '@common/enums'; -import { MessagingQueue } from '@common/enums/messaging.queue'; -import { LoggerService } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { ClientProxyFactory, Transport } from '@nestjs/microservices'; -import { AlkemioConfig } from '@src/types'; - -export async function authResetServiceFactory( - logger: LoggerService, - configService: ConfigService -): Promise { - const rabbitMqOptions = configService?.get('microservices.rabbitmq', { - infer: true, - }); - const connectionOptions = rabbitMqOptions.connection; - const connectionString = `amqp://${connectionOptions.user}:${connectionOptions.password}@${connectionOptions.host}:${connectionOptions.port}?heartbeat=30`; - try { - const options = { - urls: [connectionString], - queue: MessagingQueue.AUTH_RESET, - queueOptions: { - // the queue will survive a broker restart - durable: true, - }, - noAck: true, - }; - return ClientProxyFactory.create({ transport: Transport.RMQ, options }); - } catch (err) { - logger.error( - `Could not connect to RabbitMQ: ${err}, logging in...`, - LogContext.AUTH - ); - return undefined; - } -} diff --git a/src/core/microservices/client.proxy.factory.ts b/src/core/microservices/client.proxy.factory.ts new file mode 100644 index 0000000000..aaec24cc62 --- /dev/null +++ b/src/core/microservices/client.proxy.factory.ts @@ -0,0 +1,45 @@ +import { LogContext, MessagingQueue } from '@common/enums'; +import { LoggerService } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ClientProxyFactory, Transport } from '@nestjs/microservices'; +import { AlkemioConfig } from '@src/types'; + +const QUEUE_CONTEXT_MAP: { [key in MessagingQueue]?: LogContext } = { + [MessagingQueue.AUTH_RESET]: LogContext.AUTH, + [MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT]: + LogContext.VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT, + [MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC]: + LogContext.VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC, + [MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT]: + LogContext.VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT, +}; + +export const clientProxyFactory = (queue: MessagingQueue, durable = true) => { + const context = QUEUE_CONTEXT_MAP[queue]; + return async ( + logger: LoggerService, + configService: ConfigService + ) => { + try { + const rabbitMqOptions = configService.get('microservices.rabbitmq', { + infer: true, + }); + const { user, password, host, port } = rabbitMqOptions.connection; + const connectionString = `amqp://${user}:${password}@${host}:${port}?heartbeat=30`; + + const options = { + urls: [connectionString], + queue, + queueOptions: { + // the queue will survive a broker restart + durable, + }, + noAck: true, + }; + return ClientProxyFactory.create({ transport: Transport.RMQ, options }); + } catch (err) { + logger.error(`Could not connect to RabbitMQ: ${err}`, context); + return undefined; + } + }; +}; diff --git a/src/core/microservices/matrix.adapter.service.factory.ts b/src/core/microservices/matrix.adapter.service.factory.ts deleted file mode 100644 index be88635851..0000000000 --- a/src/core/microservices/matrix.adapter.service.factory.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { LogContext } from '@common/enums'; -import { MessagingQueue } from '@common/enums/messaging.queue'; -import { LoggerService } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { ClientProxyFactory, Transport } from '@nestjs/microservices'; -import { AlkemioConfig } from '@src/types'; - -export async function matrixAdapterServiceFactory( - logger: LoggerService, - configService: ConfigService -): Promise { - const rabbitMqOptions = configService.get('microservices.rabbitmq', { - infer: true, - }); - const connectionOptions = rabbitMqOptions.connection; - const connectionString = `amqp://${connectionOptions.user}:${connectionOptions.password}@${connectionOptions.host}:${connectionOptions.port}?heartbeat=30`; - try { - const options = { - urls: [connectionString], - queue: MessagingQueue.MATRIX_ADAPTER, - queueOptions: { - // the queue will survive a broker restart - durable: true, - }, - noAck: true, - }; - return ClientProxyFactory.create({ transport: Transport.RMQ, options }); - } catch (err) { - logger.error( - `Could not connect to RabbitMQ: ${err}, logging in...`, - LogContext.COMMUNICATION - ); - return undefined; - } -} diff --git a/src/core/microservices/microservices.module.ts b/src/core/microservices/microservices.module.ts index 0f2c0ab389..826a31df49 100644 --- a/src/core/microservices/microservices.module.ts +++ b/src/core/microservices/microservices.module.ts @@ -17,6 +17,8 @@ import { VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT, VIRTUAL_CONTRIBUTOR_ENGINE_GUIDANCE, SUBSCRIPTION_VIRTUAL_CONTRIBUTOR_UPDATED, + VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC, + VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT, } from '@common/constants/providers'; import { MessagingQueue } from '@common/enums/messaging.queue'; import { @@ -24,13 +26,8 @@ import { RABBITMQ_EXCHANGE_NAME_DIRECT, } from '@src/common/constants'; import { subscriptionFactoryProvider } from './subscription.factory.provider'; -import { notificationsServiceFactory } from './notifications.service.factory'; -import { walletManagerServiceFactory } from './wallet-manager.service.factory'; -import { matrixAdapterServiceFactory } from './matrix.adapter.service.factory'; -import { authResetServiceFactory } from './auth.reset.service.factory'; -import { virtualContributorEngineGuidanceServiceFactory } from './virtual.contributor.engine.guidance.service.factory'; -import { virtualContributorEngineCommunityManagerServiceFactory } from './virtual.contributor.engine.community.manager.service.factory'; -import { virtualContributorEngineExpertServiceFactory } from './virtual.contributor.engine.expert.service.factory'; + +import { clientProxyFactory } from './client.proxy.factory'; const subscriptionConfig: { provide: string; queueName: MessagingQueue }[] = [ { @@ -84,37 +81,61 @@ const excalidrawPubSubFactoryProvider = subscriptionFactoryProvider( ...subscriptionFactoryProviders, { provide: NOTIFICATIONS_SERVICE, - useFactory: notificationsServiceFactory, + useFactory: clientProxyFactory(MessagingQueue.NOTIFICATIONS), inject: [WINSTON_MODULE_NEST_PROVIDER, ConfigService], }, { provide: MATRIX_ADAPTER_SERVICE, - useFactory: matrixAdapterServiceFactory, + useFactory: clientProxyFactory(MessagingQueue.MATRIX_ADAPTER), + inject: [WINSTON_MODULE_NEST_PROVIDER, ConfigService], }, { provide: WALLET_MANAGEMENT_SERVICE, - useFactory: walletManagerServiceFactory, + useFactory: clientProxyFactory(MessagingQueue.WALLET_MANAGER), inject: [WINSTON_MODULE_NEST_PROVIDER, ConfigService], }, { provide: VIRTUAL_CONTRIBUTOR_ENGINE_GUIDANCE, - useFactory: virtualContributorEngineGuidanceServiceFactory, + useFactory: clientProxyFactory( + MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_GUIDANCE, + false + ), + inject: [WINSTON_MODULE_NEST_PROVIDER, ConfigService], }, { provide: VIRTUAL_CONTRIBUTOR_ENGINE_COMMUNITY_MANAGER, - useFactory: virtualContributorEngineCommunityManagerServiceFactory, + useFactory: clientProxyFactory( + MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_COMMUNITY_MANAGER, + false + ), inject: [WINSTON_MODULE_NEST_PROVIDER, ConfigService], }, { provide: VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT, - useFactory: virtualContributorEngineExpertServiceFactory, + useFactory: clientProxyFactory( + MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT + ), + inject: [WINSTON_MODULE_NEST_PROVIDER, ConfigService], + }, + { + provide: VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC, + useFactory: clientProxyFactory( + MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC + ), + inject: [WINSTON_MODULE_NEST_PROVIDER, ConfigService], + }, + { + provide: VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT, + useFactory: clientProxyFactory( + MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT + ), inject: [WINSTON_MODULE_NEST_PROVIDER, ConfigService], }, { provide: AUTH_RESET_SERVICE, - useFactory: authResetServiceFactory, + useFactory: clientProxyFactory(MessagingQueue.AUTH_RESET), inject: [WINSTON_MODULE_NEST_PROVIDER, ConfigService], }, excalidrawPubSubFactoryProvider, @@ -126,6 +147,8 @@ const excalidrawPubSubFactoryProvider = subscriptionFactoryProvider( MATRIX_ADAPTER_SERVICE, VIRTUAL_CONTRIBUTOR_ENGINE_COMMUNITY_MANAGER, VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT, + VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC, + VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT, VIRTUAL_CONTRIBUTOR_ENGINE_GUIDANCE, AUTH_RESET_SERVICE, EXCALIDRAW_PUBSUB_PROVIDER, diff --git a/src/core/microservices/notifications.service.factory.ts b/src/core/microservices/notifications.service.factory.ts deleted file mode 100644 index 0319fb0b9c..0000000000 --- a/src/core/microservices/notifications.service.factory.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { LogContext } from '@common/enums'; -import { MessagingQueue } from '@common/enums/messaging.queue'; -import { LoggerService } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { ClientProxyFactory, Transport } from '@nestjs/microservices'; -import { AlkemioConfig } from '@src/types'; - -export async function notificationsServiceFactory( - logger: LoggerService, - configService: ConfigService -): Promise { - const rabbitMqOptions = configService?.get('microservices.rabbitmq', { - infer: true, - }); - const connectionOptions = rabbitMqOptions.connection; - const connectionString = `amqp://${connectionOptions.user}:${connectionOptions.password}@${connectionOptions.host}:${connectionOptions.port}?heartbeat=30`; - try { - const options = { - urls: [connectionString], - queue: MessagingQueue.NOTIFICATIONS, - queueOptions: { - // the queue will survive a broker restart - durable: true, - }, - // https://github.com/nestjs/nest/issues/11966#issuecomment-1619622486 - // In v9 there was a bug causing noAck: false to always be ignored & set to true (since we used || instead of ??). - // If you want your app to keep working as it was in v9, - // set noAck to true (or just remove this option whatsoever as true is the default). - noAck: true, - }; - return ClientProxyFactory.create({ transport: Transport.RMQ, options }); - } catch (err) { - logger.error( - `Could not connect to RabbitMQ: ${err}, logging in...`, - LogContext.NOTIFICATIONS - ); - return undefined; - } -} diff --git a/src/core/microservices/virtual.contributor.engine.community.manager.service.factory.ts b/src/core/microservices/virtual.contributor.engine.community.manager.service.factory.ts deleted file mode 100644 index 4c2f4cedd0..0000000000 --- a/src/core/microservices/virtual.contributor.engine.community.manager.service.factory.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { LogContext } from '@common/enums'; -import { MessagingQueue } from '@common/enums/messaging.queue'; -import { LoggerService } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { ClientProxyFactory, Transport } from '@nestjs/microservices'; -import { AlkemioConfig } from '@src/types'; - -export async function virtualContributorEngineCommunityManagerServiceFactory( - logger: LoggerService, - configService: ConfigService -): Promise { - const connectionOptions = configService.get( - 'microservices.rabbitmq.connection', - { infer: true } - ); - const connectionString = `amqp://${connectionOptions.user}:${connectionOptions.password}@${connectionOptions.host}:${connectionOptions.port}?heartbeat=30`; - try { - const options = { - urls: [connectionString], - queue: MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_COMMUNITY_MANAGER, - queueOptions: { - // the queue will survive a broker restart - durable: false, - }, - noAck: true, - }; - return ClientProxyFactory.create({ transport: Transport.RMQ, options }); - } catch (err) { - logger.error( - `Could not connect to RabbitMQ: ${err}, logging in...`, - LogContext.CHAT_GUIDANCE - ); - return undefined; - } -} diff --git a/src/core/microservices/virtual.contributor.engine.expert.service.factory.ts b/src/core/microservices/virtual.contributor.engine.expert.service.factory.ts deleted file mode 100644 index 7ab03b8943..0000000000 --- a/src/core/microservices/virtual.contributor.engine.expert.service.factory.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { LogContext } from '@common/enums'; -import { MessagingQueue } from '@common/enums/messaging.queue'; -import { LoggerService } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { ClientProxyFactory, Transport } from '@nestjs/microservices'; -import { AlkemioConfig } from '@src/types'; - -export async function virtualContributorEngineExpertServiceFactory( - logger: LoggerService, - configService: ConfigService -): Promise { - const rabbitMqOptions = configService.get('microservices.rabbitmq', { - infer: true, - }); - const connectionOptions = rabbitMqOptions.connection; - const connectionString = `amqp://${connectionOptions.user}:${connectionOptions.password}@${connectionOptions.host}:${connectionOptions.port}?heartbeat=30`; - try { - const options = { - urls: [connectionString], - queue: MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT, - queueOptions: { - // the queue will survive a broker restart - durable: false, - }, - noAck: true, - }; - return ClientProxyFactory.create({ transport: Transport.RMQ, options }); - } catch (err) { - logger.error( - `Could not connect to RabbitMQ: ${err}, logging in...`, - LogContext.CHAT_GUIDANCE - ); - return undefined; - } -} diff --git a/src/core/microservices/virtual.contributor.engine.guidance.service.factory.ts b/src/core/microservices/virtual.contributor.engine.guidance.service.factory.ts deleted file mode 100644 index e07c590a6b..0000000000 --- a/src/core/microservices/virtual.contributor.engine.guidance.service.factory.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { LogContext } from '@common/enums'; -import { MessagingQueue } from '@common/enums/messaging.queue'; -import { LoggerService } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { ClientProxyFactory, Transport } from '@nestjs/microservices'; -import { AlkemioConfig } from '@src/types'; - -export async function virtualContributorEngineGuidanceServiceFactory( - logger: LoggerService, - configService: ConfigService -): Promise { - const rabbitMqOptions = configService.get('microservices.rabbitmq', { - infer: true, - }); - const connectionOptions = rabbitMqOptions.connection; - const connectionString = `amqp://${connectionOptions.user}:${connectionOptions.password}@${connectionOptions.host}:${connectionOptions.port}?heartbeat=30`; - try { - const options = { - urls: [connectionString], - queue: MessagingQueue.VIRTUAL_CONTRIBUTOR_ENGINE_GUIDANCE, - queueOptions: { - // the queue will survive a broker restart - durable: false, - }, - noAck: true, - }; - return ClientProxyFactory.create({ transport: Transport.RMQ, options }); - } catch (err) { - logger.error( - `Could not connect to RabbitMQ: ${err}, logging in...`, - LogContext.CHAT_GUIDANCE - ); - return undefined; - } -} diff --git a/src/core/microservices/wallet-manager.service.factory.ts b/src/core/microservices/wallet-manager.service.factory.ts deleted file mode 100644 index 15d943b935..0000000000 --- a/src/core/microservices/wallet-manager.service.factory.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { LogContext } from '@common/enums'; -import { MessagingQueue } from '@common/enums/messaging.queue'; -import { LoggerService } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { ClientProxyFactory, Transport } from '@nestjs/microservices'; -import { AlkemioConfig } from '@src/types'; - -export async function walletManagerServiceFactory( - logger: LoggerService, - configService: ConfigService -): Promise { - const rabbitMqOptions = configService.get('microservices.rabbitmq', { - infer: true, - }); - const connectionOptions = rabbitMqOptions.connection; - const connectionString = `amqp://${connectionOptions.user}:${connectionOptions.password}@${connectionOptions.host}:${connectionOptions.port}?heartbeat=30`; - try { - const options = { - urls: [connectionString], - queue: MessagingQueue.WALLET_MANAGER, - queueOptions: { - // the queue will survive a broker restart - durable: true, - }, - }; - return ClientProxyFactory.create({ transport: Transport.RMQ, options }); - } catch (err) { - logger.error( - `Could not connect to RabbitMQ: ${err}, logging in...`, - LogContext.NOTIFICATIONS - ); - return undefined; - } -} diff --git a/src/domain/communication/message.answer.to.question/message.answer.to.question.interface.ts b/src/domain/communication/message.answer.to.question/message.answer.to.question.interface.ts index 371037def6..d0c1a5ba06 100644 --- a/src/domain/communication/message.answer.to.question/message.answer.to.question.interface.ts +++ b/src/domain/communication/message.answer.to.question/message.answer.to.question.interface.ts @@ -28,4 +28,6 @@ export class IMessageAnswerToQuestion { description: 'The answer to the question', }) answer!: string; + + threadId?: string; } diff --git a/src/domain/communication/room/room.service.ts b/src/domain/communication/room/room.service.ts index 06fb649eaa..28ec9cecfa 100644 --- a/src/domain/communication/room/room.service.ts +++ b/src/domain/communication/room/room.service.ts @@ -122,7 +122,7 @@ export class RoomService { } const interaction = - this.vcInteractionService.createVcInteraction(interactionData); + this.vcInteractionService.buildVcInteraction(interactionData); room.vcInteractions.push(interaction); await this.save(room); return interaction; diff --git a/src/domain/communication/vc-interaction/vc.interaction.entity.ts b/src/domain/communication/vc-interaction/vc.interaction.entity.ts index 9fafaf8b53..3838809f64 100644 --- a/src/domain/communication/vc-interaction/vc.interaction.entity.ts +++ b/src/domain/communication/vc-interaction/vc.interaction.entity.ts @@ -4,6 +4,10 @@ import { IVcInteraction } from './vc.interaction.interface'; import { Room } from '../room/room.entity'; import { MESSAGEID_LENGTH, UUID_LENGTH } from '@common/constants'; +export type ExternalMetadata = { + threadId?: string; +}; + @Entity() export class VcInteraction extends BaseAlkemioEntity implements IVcInteraction { @ManyToOne(() => Room, room => room.vcInteractions, { @@ -18,4 +22,7 @@ export class VcInteraction extends BaseAlkemioEntity implements IVcInteraction { @Column('char', { length: UUID_LENGTH, nullable: false }) virtualContributorID!: string; + + @Column('simple-json') + externalMetadata: ExternalMetadata = {}; } diff --git a/src/domain/communication/vc-interaction/vc.interaction.interface.ts b/src/domain/communication/vc-interaction/vc.interaction.interface.ts index 1a8ec280c8..4ef9df71e9 100644 --- a/src/domain/communication/vc-interaction/vc.interaction.interface.ts +++ b/src/domain/communication/vc-interaction/vc.interaction.interface.ts @@ -2,6 +2,7 @@ import { IBaseAlkemio } from '@domain/common/entity/base-entity'; import { Field, ObjectType } from '@nestjs/graphql'; import { IRoom } from '../room/room.interface'; import { UUID } from '@domain/common/scalars'; +import { ExternalMetadata } from './vc.interaction.entity'; @ObjectType('VcInteraction') export abstract class IVcInteraction extends IBaseAlkemio { @@ -13,4 +14,6 @@ export abstract class IVcInteraction extends IBaseAlkemio { @Field(() => UUID) virtualContributorID!: string; + + externalMetadata: ExternalMetadata = {}; } diff --git a/src/domain/communication/vc-interaction/vc.interaction.service.ts b/src/domain/communication/vc-interaction/vc.interaction.service.ts index 95c43b45e0..4cf064268e 100644 --- a/src/domain/communication/vc-interaction/vc.interaction.service.ts +++ b/src/domain/communication/vc-interaction/vc.interaction.service.ts @@ -14,7 +14,7 @@ export class VcInteractionService { private interactionRepository: Repository ) {} - public createVcInteraction( + public buildVcInteraction( interactionData: CreateVcInteractionInput ): VcInteraction { const interaction = new VcInteraction(); @@ -41,6 +41,10 @@ export class VcInteractionService { return interaction; } + async save(vcInteraction: IVcInteraction): Promise { + return this.interactionRepository.save(vcInteraction); + } + async getVcInteraction(interactionID: string): Promise { const VcInteraction = await this.interactionRepository.findOneBy({ id: interactionID, diff --git a/src/domain/community/ai-persona/ai.persona.service.ts b/src/domain/community/ai-persona/ai.persona.service.ts index 1875dc7b2e..9823b5d586 100644 --- a/src/domain/community/ai-persona/ai.persona.service.ts +++ b/src/domain/community/ai-persona/ai.persona.service.ts @@ -47,7 +47,7 @@ export class AiPersonaService { aiPersonaData.aiPersonaServiceID ); - this.aiServerAdapter.refreshBodyOfKnowlege(personaService.id); + this.aiServerAdapter.refreshBodyOfKnowledge(personaService.id); aiPersona.aiPersonaServiceID = personaService.id; } else if (aiPersonaData.aiPersonaService) { const aiPersonaService = diff --git a/src/domain/community/organization/organization.resolver.fields.ts b/src/domain/community/organization/organization.resolver.fields.ts index 2e57d69f79..88f3f76cb2 100644 --- a/src/domain/community/organization/organization.resolver.fields.ts +++ b/src/domain/community/organization/organization.resolver.fields.ts @@ -58,14 +58,14 @@ export class OrganizationResolverFields { parent.id ); - await this.authorizationService.grantAccessOrFail( + this.authorizationService.grantAccessOrFail( agentInfo, organization.authorization, AuthorizationPrivilege.READ, `read user groups on org: ${organization.id}` ); - return await this.organizationService.getUserGroups(organization); + return this.organizationService.getUserGroups(organization); } @UseGuards(GraphqlGuard) @@ -84,7 +84,7 @@ export class OrganizationResolverFields { parent.id ); - await this.authorizationService.grantAccessOrFail( + this.authorizationService.grantAccessOrFail( agentInfo, organization.authorization, AuthorizationPrivilege.READ, diff --git a/src/domain/community/virtual-contributor/virtual.contributor.module.ts b/src/domain/community/virtual-contributor/virtual.contributor.module.ts index 10d5ea7618..1eee9e4343 100644 --- a/src/domain/community/virtual-contributor/virtual.contributor.module.ts +++ b/src/domain/community/virtual-contributor/virtual.contributor.module.ts @@ -19,6 +19,7 @@ import { AccountHostModule } from '@domain/space/account.host/account.host.modul import { ContributorModule } from '../contributor/contributor.module'; import { VirtualContributorResolverSubscriptions } from './virtual.contributor.resolver.subscriptions'; import { SubscriptionServiceModule } from '@services/subscriptions/subscription-service'; +import { VcInteractionModule } from '@domain/communication/vc-interaction/vc.interaction.module'; @Module({ imports: [ @@ -35,6 +36,7 @@ import { SubscriptionServiceModule } from '@services/subscriptions/subscription- TypeOrmModule.forFeature([VirtualContributor]), PlatformAuthorizationPolicyModule, SubscriptionServiceModule, + VcInteractionModule, ], providers: [ VirtualContributorService, diff --git a/src/domain/community/virtual-contributor/virtual.contributor.resolver.mutations.ts b/src/domain/community/virtual-contributor/virtual.contributor.resolver.mutations.ts index 22a39fe326..81a568a5b0 100644 --- a/src/domain/community/virtual-contributor/virtual.contributor.resolver.mutations.ts +++ b/src/domain/community/virtual-contributor/virtual.contributor.resolver.mutations.ts @@ -108,7 +108,7 @@ export class VirtualContributorResolverMutations { AuthorizationPrivilege.UPDATE, `refresh body of knowledge: ${virtual.id}` ); - return await this.virtualContributorService.refershBodyOfKnowledge( + return await this.virtualContributorService.refreshBodyOfKnowledge( virtual, agentInfo ); diff --git a/src/domain/community/virtual-contributor/virtual.contributor.service.ts b/src/domain/community/virtual-contributor/virtual.contributor.service.ts index 45a3a8ca87..978679f7c5 100644 --- a/src/domain/community/virtual-contributor/virtual.contributor.service.ts +++ b/src/domain/community/virtual-contributor/virtual.contributor.service.ts @@ -44,6 +44,7 @@ import { ContributorService } from '../contributor/contributor.service'; import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; import { Invitation } from '@domain/access/invitation/invitation.entity'; import { IStorageBucket } from '@domain/storage/storage-bucket/storage.bucket.interface'; +import { VcInteractionService } from '@domain/communication/vc-interaction/vc.interaction.service'; @Injectable() export class VirtualContributorService { @@ -57,6 +58,7 @@ export class VirtualContributorService { private aiPersonaService: AiPersonaService, private aiServerAdapter: AiServerAdapter, private accountHostService: AccountHostService, + private vcInteractionService: VcInteractionService, @InjectEntityManager('default') private entityManager: EntityManager, @InjectRepository(VirtualContributor) @@ -347,7 +349,7 @@ export class VirtualContributorService { return storageBucket; } - public async refershBodyOfKnowledge( + public async refreshBodyOfKnowledge( virtualContributor: IVirtualContributor, agentInfo: AgentInfo ): Promise { @@ -364,7 +366,7 @@ export class VirtualContributorService { const aiPersona = virtualContributor.aiPersona; - return await this.aiServerAdapter.refreshBodyOfKnowlege( + return await this.aiServerAdapter.refreshBodyOfKnowledge( aiPersona.aiPersonaServiceID ); } @@ -393,18 +395,34 @@ export class VirtualContributorService { `still need to use the context ${vcQuestionInput.contextSpaceID}, ${vcQuestionInput.userID}`, LogContext.AI_PERSONA_SERVICE_ENGINE ); + + const vcInteraction = + await this.vcInteractionService.getVcInteractionOrFail( + vcQuestionInput.vcInteractionID! + ); + const aiServerAdapterQuestionInput: AiServerAdapterAskQuestionInput = { aiPersonaServiceID: virtualContributor.aiPersona.aiPersonaServiceID, question: vcQuestionInput.question, contextID: vcQuestionInput.contextSpaceID, userID: vcQuestionInput.userID, threadID: vcQuestionInput.threadID, - vcInteractionID: vcQuestionInput.vcInteractionID, + vcInteractionID: vcInteraction.id, + externalMetadata: vcInteraction.externalMetadata, description: virtualContributor.profile.description, displayName: virtualContributor.profile.displayName, }; - return await this.aiServerAdapter.askQuestion(aiServerAdapterQuestionInput); + const response = await this.aiServerAdapter.askQuestion( + aiServerAdapterQuestionInput + ); + + if (!vcInteraction.externalMetadata.threadId && response.threadId) { + vcInteraction.externalMetadata.threadId = response.threadId; + await this.vcInteractionService.save(vcInteraction); + } + + return response; } // TODO: move to store @@ -440,7 +458,7 @@ export class VirtualContributorService { async save( virtualContributor: IVirtualContributor ): Promise { - return await this.virtualContributorRepository.save(virtualContributor); + return this.virtualContributorRepository.save(virtualContributor); } public async getAgent( diff --git a/src/migrations/1727787748227-extendAiPersonaServiceAndVcInteractionForGenericEngines.ts b/src/migrations/1727787748227-extendAiPersonaServiceAndVcInteractionForGenericEngines.ts new file mode 100644 index 0000000000..93f9dc12d3 --- /dev/null +++ b/src/migrations/1727787748227-extendAiPersonaServiceAndVcInteractionForGenericEngines.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ExtendAiPersonaServiceAndVcInteractionForGenericEngines1727787748227 + implements MigrationInterface +{ + name = 'ExtendAiPersonaServiceAndVcInteractionForGenericEngines1727787748227'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`vc_interaction\` ADD \`externalMetadata\` text NOT NULL` + ); + + await queryRunner.query( + `ALTER TABLE \`ai_persona_service\` ADD \`externalConfig\` text NULL` + ); + + await queryRunner.query( + `UPDATE \`ai_persona_service\` SET \`prompt\` = IF(prompt = '', JSON_ARRAY(), JSON_ARRAY(prompt))` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`ai_persona_service\` DROP COLUMN \`externalConfig\`` + ); + await queryRunner.query( + `ALTER TABLE \`vc_interaction\` DROP COLUMN \`externalMetadata\`` + ); + } +} diff --git a/src/services/adapters/ai-server-adapter/ai.server.adapter.ts b/src/services/adapters/ai-server-adapter/ai.server.adapter.ts index 7b58448753..8a8ce73e3b 100644 --- a/src/services/adapters/ai-server-adapter/ai.server.adapter.ts +++ b/src/services/adapters/ai-server-adapter/ai.server.adapter.ts @@ -22,7 +22,7 @@ export class AiServerAdapter { return this.aiServer.getBodyOfKnowledgeLastUpdated(personaServiceId); } - async refreshBodyOfKnowlege(personaServiceId: string): Promise { + async refreshBodyOfKnowledge(personaServiceId: string): Promise { this.logger.verbose?.( `Refresh body of knowledge mutation invoked for AI Persona service ${personaServiceId}`, LogContext.AI_SERVER_ADAPTER @@ -68,6 +68,7 @@ export class AiServerAdapter { const vcInteractionID = questionInput.vcInteractionID; return this.aiServer.askQuestion({ ...questionInput, + externalMetadata: questionInput.externalMetadata || {}, interactionID: vcInteractionID, }); } diff --git a/src/services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.ask.question.ts b/src/services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.ask.question.ts index 73e340ade3..4cb3061f4e 100644 --- a/src/services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.ask.question.ts +++ b/src/services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.ask.question.ts @@ -1,3 +1,5 @@ +import { ExternalMetadata } from '@domain/communication/vc-interaction/vc.interaction.entity'; + export class AiServerAdapterAskQuestionInput { question!: string; aiPersonaServiceID!: string; @@ -7,4 +9,5 @@ export class AiServerAdapterAskQuestionInput { vcInteractionID?: string; description?: string; displayName!: string; + externalMetadata?: ExternalMetadata = {}; } diff --git a/src/services/ai-server/ai-persona-engine-adapter/ai.persona.engine.adapter.ts b/src/services/ai-server/ai-persona-engine-adapter/ai.persona.engine.adapter.ts index 41f62802a5..fa940fce5e 100644 --- a/src/services/ai-server/ai-persona-engine-adapter/ai.persona.engine.adapter.ts +++ b/src/services/ai-server/ai-persona-engine-adapter/ai.persona.engine.adapter.ts @@ -6,6 +6,8 @@ import { VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT, VIRTUAL_CONTRIBUTOR_ENGINE_GUIDANCE, VIRTUAL_CONTRIBUTOR_ENGINE_COMMUNITY_MANAGER, + VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC, + VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT, } from '@common/constants'; import { Source } from '../../adapters/chat-guidance-adapter/source.type'; import { AiPersonaEngineAdapterQueryInput } from './dto/ai.persona.engine.adapter.dto.question.input'; @@ -34,6 +36,10 @@ export class AiPersonaEngineAdapter { private virtualContributorEngineCommunityManager: ClientProxy, @Inject(VIRTUAL_CONTRIBUTOR_ENGINE_EXPERT) private virtualContributorEngineExpert: ClientProxy, + @Inject(VIRTUAL_CONTRIBUTOR_ENGINE_GENERIC) + private virtualContributorEngineGeneric: ClientProxy, + @Inject(VIRTUAL_CONTRIBUTOR_ENGINE_OPENAI_ASSISTANT) + private virtualContributorEngineOpenaiAssistant: ClientProxy, @Inject(VIRTUAL_CONTRIBUTOR_ENGINE_GUIDANCE) private virtualContributorEngineGuidance: ClientProxy, @Inject(WINSTON_MODULE_NEST_PROVIDER) @@ -44,7 +50,6 @@ export class AiPersonaEngineAdapter { eventData: AiPersonaEngineAdapterQueryInput ): Promise { let responseData: AiPersonaEngineAdapterQueryResponse | undefined; - try { switch (eventData.engine) { case AiPersonaEngine.COMMUNITY_MANAGER: @@ -60,6 +65,21 @@ export class AiPersonaEngineAdapter { >({ cmd: AiPersonaEngineEventType.QUERY }, eventData); responseData = await firstValueFrom(responseCommunityManager); break; + case AiPersonaEngine.GENERIC_OPENAI: + const responseGeneric = this.virtualContributorEngineGeneric.send< + AiPersonaEngineAdapterQueryResponse, + AiPersonaEngineAdapterQueryInput + >({ cmd: AiPersonaEngineEventType.QUERY }, eventData); + responseData = await firstValueFrom(responseGeneric); + break; + case AiPersonaEngine.OPENAI_ASSISTANT: + const responseOpenaiAssistant = + this.virtualContributorEngineOpenaiAssistant.send< + AiPersonaEngineAdapterQueryResponse, + AiPersonaEngineAdapterQueryInput + >({ cmd: AiPersonaEngineEventType.QUERY }, eventData); + responseData = await firstValueFrom(responseOpenaiAssistant); + break; case AiPersonaEngine.EXPERT: if (!eventData.contextID || !eventData.bodyOfKnowledgeID) throw new ValidationException( diff --git a/src/services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.question.input.ts b/src/services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.question.input.ts index 88c02f2058..69dce2bf0c 100644 --- a/src/services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.question.input.ts +++ b/src/services/ai-server/ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.question.input.ts @@ -1,14 +1,18 @@ +import { ExternalMetadata } from '@domain/communication/vc-interaction/vc.interaction.entity'; import { AiPersonaEngineAdapterInputBase } from './ai.persona.engine.adapter.dto.base'; import { InteractionMessage } from '@services/ai-server/ai-persona-service/dto/interaction.message'; +import { IExternalConfig } from '@services/ai-server/ai-persona-service/dto/external.config'; export interface AiPersonaEngineAdapterQueryInput extends AiPersonaEngineAdapterInputBase { question: string; - prompt?: string; + prompt?: string[]; contextID?: string; bodyOfKnowledgeID: string; interactionID?: string; history?: InteractionMessage[]; description?: string; displayName: string; + externalConfig: IExternalConfig; + externalMetadata: ExternalMetadata; } diff --git a/src/services/ai-server/ai-persona-engine-adapter/index.ts b/src/services/ai-server/ai-persona-engine-adapter/index.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/services/ai-server/ai-persona-service/ai.persona.service.entity.ts b/src/services/ai-server/ai-persona-service/ai.persona.service.entity.ts index 81a8bcf254..7387039c19 100644 --- a/src/services/ai-server/ai-persona-service/ai.persona.service.entity.ts +++ b/src/services/ai-server/ai-persona-service/ai.persona.service.entity.ts @@ -6,6 +6,7 @@ import { AiPersonaDataAccessMode } from '@common/enums/ai.persona.data.access.mo import { AiPersonaBodyOfKnowledgeType } from '@common/enums/ai.persona.body.of.knowledge.type'; import { AiPersonaEngine } from '@common/enums/ai.persona.engine'; import { ENUM_LENGTH, SMALL_TEXT_LENGTH } from '@common/constants'; +import { IExternalConfig } from './dto/external.config'; @Entity() export class AiPersonaService @@ -24,8 +25,8 @@ export class AiPersonaService @Column('varchar', { length: ENUM_LENGTH, nullable: false }) dataAccessMode!: AiPersonaDataAccessMode; - @Column('text', { nullable: false }) - prompt!: string; + @Column('simple-json', { nullable: false }) + prompt!: string[]; @Column('varchar', { length: ENUM_LENGTH, nullable: false }) bodyOfKnowledgeType!: AiPersonaBodyOfKnowledgeType; @@ -35,4 +36,10 @@ export class AiPersonaService @Column({ type: 'datetime', nullable: true }) bodyOfKnowledgeLastUpdated: Date | null = null; + + @Column({ + type: 'simple-json', + nullable: true, + }) + externalConfig?: IExternalConfig = {}; } diff --git a/src/services/ai-server/ai-persona-service/ai.persona.service.interface.ts b/src/services/ai-server/ai-persona-service/ai.persona.service.interface.ts index a959b0a565..04fe2808a2 100644 --- a/src/services/ai-server/ai-persona-service/ai.persona.service.interface.ts +++ b/src/services/ai-server/ai-persona-service/ai.persona.service.interface.ts @@ -5,6 +5,7 @@ import { AiPersonaDataAccessMode } from '@common/enums/ai.persona.data.access.mo import { IAiServer } from '../ai-server/ai.server.interface'; import { AiPersonaBodyOfKnowledgeType } from '@common/enums/ai.persona.body.of.knowledge.type'; import { AiPersonaEngine } from '@common/enums/ai.persona.engine'; +import { IExternalConfig } from './dto/external.config'; @ObjectType('AiPersonaService') export class IAiPersonaService extends IAuthorizable { @@ -16,11 +17,11 @@ export class IAiPersonaService extends IAuthorizable { }) engine!: AiPersonaEngine; - @Field(() => String, { + @Field(() => [String], { nullable: false, description: 'The prompt used by this Virtual Persona', }) - prompt!: string; + prompt!: string[]; @Field(() => AiPersonaDataAccessMode, { nullable: false, @@ -45,4 +46,6 @@ export class IAiPersonaService extends IAuthorizable { description: 'When wat the body of knowledge of the VC last updated.', }) bodyOfKnowledgeLastUpdated!: Date | null; + + externalConfig?: IExternalConfig; } diff --git a/src/services/ai-server/ai-persona-service/ai.persona.service.service.ts b/src/services/ai-server/ai-persona-service/ai.persona.service.service.ts index 7d616a7fd6..2deb20a0ce 100644 --- a/src/services/ai-server/ai-persona-service/ai.persona.service.service.ts +++ b/src/services/ai-server/ai-persona-service/ai.persona.service.service.ts @@ -23,6 +23,8 @@ import { IngestSpace, SpaceIngestionPurpose, } from '@services/infrastructure/event-bus/messages/ingest.space.command'; +import { IExternalConfig } from './dto/external.config'; +import { EncryptionService } from '@hedger/nestjs-encryption'; @Injectable() export class AiPersonaServiceService { @@ -32,7 +34,9 @@ export class AiPersonaServiceService { private eventBus: EventBus, @InjectRepository(AiPersonaService) private aiPersonaServiceRepository: Repository, - @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService + private readonly crypto: EncryptionService, + @Inject(WINSTON_MODULE_NEST_PROVIDER) + private readonly logger: LoggerService ) {} async createAiPersonaService( @@ -51,6 +55,10 @@ export class AiPersonaServiceService { aiPersonaService.prompt = aiPersonaServiceData.prompt; aiPersonaService.dataAccessMode = aiPersonaServiceData.dataAccessMode; + aiPersonaService.externalConfig = this.encryptExternalConfig( + aiPersonaServiceData.externalConfig + ); + const savedAiPersonaService = await this.aiPersonaServiceRepository.save(aiPersonaService); this.logger.verbose?.( @@ -78,13 +86,17 @@ export class AiPersonaServiceService { aiPersonaServiceData.ID ); - if (aiPersonaServiceData.prompt !== undefined) { + if (aiPersonaServiceData.prompt) { aiPersonaService.prompt = aiPersonaServiceData.prompt; } - if (aiPersonaServiceData.engine !== undefined) { + if (aiPersonaServiceData.engine) { aiPersonaService.engine = aiPersonaServiceData.engine; } + aiPersonaService.externalConfig = this.encryptExternalConfig({ + ...this.decryptExternalConfig(aiPersonaService.externalConfig || {}), + ...(aiPersonaServiceData.externalConfig || {}), + }); return await this.aiPersonaServiceRepository.save(aiPersonaService); } @@ -161,8 +173,12 @@ export class AiPersonaServiceService { contextID: personaQuestionInput.contextID, history, interactionID: personaQuestionInput.interactionID, + externalMetadata: personaQuestionInput.externalMetadata, displayName: personaQuestionInput.displayName, description: personaQuestionInput.description, + externalConfig: this.decryptExternalConfig( + aiPersonaService.externalConfig + ), }; return this.aiPersonaEngineAdapter.sendQuery(input); @@ -175,4 +191,36 @@ export class AiPersonaServiceService { userID: aiPersonaService.id, // TODO: clearly wrong, just getting code to compile }); } + + private encryptExternalConfig( + config: IExternalConfig | undefined + ): IExternalConfig { + if (!config) { + return {}; + } + const result: IExternalConfig = {}; + if (config.apiKey) { + result.apiKey = this.crypto.encrypt(config.apiKey); + } + if (config.assistantId) { + result.assistantId = this.crypto.encrypt(config.assistantId); + } + return result; + } + + private decryptExternalConfig( + config: IExternalConfig | undefined + ): IExternalConfig { + if (!config) { + return {}; + } + const result: IExternalConfig = {}; + if (config.apiKey) { + result.apiKey = this.crypto.decrypt(config.apiKey); + } + if (config.assistantId) { + result.assistantId = this.crypto.decrypt(config.assistantId); + } + return result; + } } diff --git a/src/services/ai-server/ai-persona-service/dto/ai.persona.service.dto.create.ts b/src/services/ai-server/ai-persona-service/dto/ai.persona.service.dto.create.ts index 824ca46499..b7711b7c0e 100644 --- a/src/services/ai-server/ai-persona-service/dto/ai.persona.service.dto.create.ts +++ b/src/services/ai-server/ai-persona-service/dto/ai.persona.service.dto.create.ts @@ -1,11 +1,11 @@ import { Field, InputType } from '@nestjs/graphql'; -import { MaxLength } from 'class-validator'; +import { ArrayMaxSize, IsString, MaxLength } from 'class-validator'; import { LONG_TEXT_LENGTH, SMALL_TEXT_LENGTH } from '@src/common/constants'; -import JSON from 'graphql-type-json'; import { AiPersonaEngine } from '@common/enums/ai.persona.engine'; import { UUID } from '@domain/common/scalars'; import { AiPersonaBodyOfKnowledgeType } from '@common/enums/ai.persona.body.of.knowledge.type'; import { AiPersonaDataAccessMode } from '@common/enums/ai.persona.data.access.mode'; +import { IExternalConfig } from './external.config'; @InputType() export class CreateAiPersonaServiceInput { @@ -16,9 +16,11 @@ export class CreateAiPersonaServiceInput { @MaxLength(SMALL_TEXT_LENGTH) engine!: AiPersonaEngine; - @Field(() => JSON, { nullable: true, defaultValue: '' }) - @MaxLength(LONG_TEXT_LENGTH) - prompt!: string; + @Field(() => [String], { nullable: true, defaultValue: [] }) + @ArrayMaxSize(10) + @IsString({ each: true }) + @MaxLength(LONG_TEXT_LENGTH, { each: true }) + prompt!: string[]; @Field(() => AiPersonaDataAccessMode, { nullable: true, @@ -37,4 +39,7 @@ export class CreateAiPersonaServiceInput { @Field(() => UUID, { nullable: true }) @MaxLength(SMALL_TEXT_LENGTH) bodyOfKnowledgeID: string = ''; // cannot default to a valid UUID + + @Field(() => IExternalConfig, { nullable: true }) + externalConfig?: IExternalConfig; } diff --git a/src/services/ai-server/ai-persona-service/dto/ai.persona.service.dto.update.ts b/src/services/ai-server/ai-persona-service/dto/ai.persona.service.dto.update.ts index 28a1953d05..d9a4df890e 100644 --- a/src/services/ai-server/ai-persona-service/dto/ai.persona.service.dto.update.ts +++ b/src/services/ai-server/ai-persona-service/dto/ai.persona.service.dto.update.ts @@ -1,17 +1,16 @@ import { Field, InputType } from '@nestjs/graphql'; -import { MaxLength } from 'class-validator'; -import { LONG_TEXT_LENGTH, SMALL_TEXT_LENGTH } from '@src/common/constants'; -import JSON from 'graphql-type-json'; import { AiPersonaEngine } from '@common/enums/ai.persona.engine'; import { UpdateBaseAlkemioInput } from '@domain/common/entity/base-entity'; +import { IExternalConfig } from './external.config'; @InputType() export class UpdateAiPersonaServiceInput extends UpdateBaseAlkemioInput { - @Field(() => AiPersonaEngine, { nullable: false }) - @MaxLength(SMALL_TEXT_LENGTH) - engine!: AiPersonaEngine; + @Field(() => AiPersonaEngine, { nullable: true }) + engine?: AiPersonaEngine; - @Field(() => JSON, { nullable: true }) - @MaxLength(LONG_TEXT_LENGTH) - prompt!: string; + @Field(() => [String], { nullable: true }) + prompt?: string[]; + + @Field(() => IExternalConfig, { nullable: true }) + externalConfig?: IExternalConfig; } diff --git a/src/services/ai-server/ai-persona-service/dto/ai.persona.service.question.dto.input.ts b/src/services/ai-server/ai-persona-service/dto/ai.persona.service.question.dto.input.ts index bcb63dd591..c7418b8b2e 100644 --- a/src/services/ai-server/ai-persona-service/dto/ai.persona.service.question.dto.input.ts +++ b/src/services/ai-server/ai-persona-service/dto/ai.persona.service.question.dto.input.ts @@ -1,4 +1,5 @@ import { UUID } from '@domain/common/scalars'; +import { ExternalMetadata } from '@domain/communication/vc-interaction/vc.interaction.entity'; import { Field, InputType } from '@nestjs/graphql'; @InputType() @@ -53,4 +54,8 @@ export class AiPersonaServiceQuestionInput { description: 'The Virtual Contributor displayName.', }) displayName!: string; + + // intentially skippuing the Field decorator as we are not sure we want to expose this data + // through the API + externalMetadata: ExternalMetadata = {}; } diff --git a/src/services/ai-server/ai-persona-service/dto/external.config.ts b/src/services/ai-server/ai-persona-service/dto/external.config.ts new file mode 100644 index 0000000000..845cd3ed85 --- /dev/null +++ b/src/services/ai-server/ai-persona-service/dto/external.config.ts @@ -0,0 +1,17 @@ +import { Field, InputType } from '@nestjs/graphql'; + +@InputType('ExternalConfig') +export class IExternalConfig { + @Field(() => String, { + nullable: true, + description: 'The API key for the external LLM provider.', + }) + apiKey?: string; + + @Field(() => String, { + nullable: true, + description: + 'The assistent ID backing the service in OpenAI`s assistant API', + }) + assistantId?: string; +} diff --git a/src/services/ai-server/ai-server/ai.server.service.ts b/src/services/ai-server/ai-server/ai.server.service.ts index d51a9f0fd8..06bad9bdef 100644 --- a/src/services/ai-server/ai-server/ai.server.service.ts +++ b/src/services/ai-server/ai-server/ai.server.service.ts @@ -36,6 +36,7 @@ import { AiPersonaServiceAuthorizationService } from '@services/ai-server/ai-per import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; import { SubscriptionPublishService } from '@services/subscriptions/subscription-service'; import { VirtualContributor } from '@domain/community/virtual-contributor/virtual.contributor.entity'; +import { AiPersonaEngine } from '@common/enums/ai.persona.engine'; @Injectable() export class AiServerService { @@ -157,24 +158,44 @@ export class AiServerService { public async askQuestion( questionInput: AiPersonaServiceQuestionInput ): Promise { - if ( - questionInput.contextID && - !(await this.isContextLoaded(questionInput.contextID)) - ) { - this.eventBus.publish( - new IngestSpace(questionInput.contextID, SpaceIngestionPurpose.CONTEXT) + // the context is currently not used so no point in keeping this + // commenting it out for now to save some work + // if ( + // questionInput.contextID && + // !(await this.isContextLoaded(questionInput.contextID)) + // ) { + // this.eventBus.publish( + // new IngestSpace(questionInput.contextID, SpaceIngestionPurpose.CONTEXT) + // ); + // } + + const personaService = + await this.aiPersonaServiceService.getAiPersonaServiceOrFail( + questionInput.aiPersonaServiceID ); - } - const historyLimit = parseInt( - this.config.get('platform.virtual_contributors.history_length', { - infer: true, - }) - ); - const history = await this.getLastNInteractionMessages( - questionInput.interactionID, - historyLimit - ); + const HISTORY_ENABLED_ENGINES = new Set([ + AiPersonaEngine.EXPERT, + ]); + const loadHistory = HISTORY_ENABLED_ENGINES.has(personaService.engine); + + // history should be loaded trough the GQL API of the collaboration server + let history: InteractionMessage[] = []; + if (loadHistory) { + const historyLimit = parseInt( + this.config.get( + 'platform.virtual_contributors.history_length', + { + infer: true, + } + ) + ); + + history = await this.getLastNInteractionMessages( + questionInput.interactionID, + historyLimit + ); + } return await this.aiPersonaServiceService.askQuestion( questionInput, @@ -260,7 +281,11 @@ export class AiServerService { // try to get the collection and return true if it is there await chroma.getCollection({ name }); return true; - } catch { + } catch (err) { + this.logger.error( + `Error checking if context is loaded for contextID ${contextID}: ${err}`, + LogContext.AI_SERVER + ); return false; } } diff --git a/src/types/alkemio.config.ts b/src/types/alkemio.config.ts index 8027aa03b1..0ef222ece2 100644 --- a/src/types/alkemio.config.ts +++ b/src/types/alkemio.config.ts @@ -26,6 +26,7 @@ export type AlkemioConfig = { methods: string; allowed_headers: string; }; + encryption_key: string; }; innovation_hub: { header: string;