diff --git a/package-lock.json b/package-lock.json index 3c8b41ea87..dc42dcdc0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "alkemio-server", - "version": "0.82.8", + "version": "0.82.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "alkemio-server", - "version": "0.82.8", + "version": "0.82.9", "license": "EUPL-1.2", "dependencies": { "@alkemio/matrix-adapter-lib": "^0.3.6", diff --git a/package.json b/package.json index 384eeb45c5..ac2a2dda9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alkemio-server", - "version": "0.82.8", + "version": "0.82.9", "description": "Alkemio server, responsible for managing the shared Alkemio platform", "author": "Alkemio Foundation", "private": false, diff --git a/src/domain/community/ai-persona/ai.persona.service.ts b/src/domain/community/ai-persona/ai.persona.service.ts index ceb18c216b..535cb07d63 100644 --- a/src/domain/community/ai-persona/ai.persona.service.ts +++ b/src/domain/community/ai-persona/ai.persona.service.ts @@ -16,7 +16,6 @@ import { AiServerAdapter } from '@services/adapters/ai-server-adapter/ai.server. import { AiServerAdapterAskQuestionInput } from '@services/adapters/ai-server-adapter/dto/ai.server.adapter.dto.ask.question'; import { AiPersonaDataAccessMode } from '@common/enums/ai.persona.data.access.mode'; import { AiPersonaInteractionMode } from '@common/enums/ai.persona.interaction.mode'; -import { SpaceIngestionPurpose } from '@services/infrastructure/event-bus/commands'; import { IMessageAnswerToQuestion } from '@domain/communication/message.answer.to.question/message.answer.to.question.interface'; @Injectable() @@ -45,10 +44,7 @@ export class AiPersonaService { aiPersonaData.aiPersonaServiceID ); - this.aiServerAdapter.ensurePersonaIsUsable( - personaService.id, - SpaceIngestionPurpose.KNOWLEDGE - ); + this.aiServerAdapter.refreshBodyOfKnowlege(personaService.id); aiPersona.aiPersonaServiceID = personaService.id; } else if (aiPersonaData.aiPersonaService) { const aiPersonaService = diff --git a/src/domain/community/community/community.module.ts b/src/domain/community/community/community.module.ts index bb1986d8c4..4fb143fb89 100644 --- a/src/domain/community/community/community.module.ts +++ b/src/domain/community/community/community.module.ts @@ -31,6 +31,7 @@ import { VirtualContributorModule } from '../virtual-contributor/virtual.contrib import { LicenseEngineModule } from '@core/license-engine/license.engine.module'; import { ContributorModule } from '../contributor/contributor.module'; import { PlatformInvitationModule } from '@platform/invitation/platform.invitation.module'; +import { AiServerAdapterModule } from '@services/adapters/ai-server-adapter/ai.server.adapter.module'; @Module({ imports: [ @@ -59,6 +60,7 @@ import { PlatformInvitationModule } from '@platform/invitation/platform.invitati TypeOrmModule.forFeature([Community]), TrustRegistryAdapterModule, ContributionReporterModule, + AiServerAdapterModule, // TODO REMOVE ], providers: [ CommunityService, diff --git a/src/domain/community/community/community.service.ts b/src/domain/community/community/community.service.ts index fa12038734..6f99d04fbe 100644 --- a/src/domain/community/community/community.service.ts +++ b/src/domain/community/community/community.service.ts @@ -63,6 +63,7 @@ import { IContributor } from '../contributor/contributor.interface'; import { PlatformInvitationService } from '@platform/invitation/platform.invitation.service'; import { IPlatformInvitation } from '@platform/invitation'; import { CreatePlatformInvitationInput } from '@platform/invitation/dto/platform.invitation.dto.create'; +import { AiServerAdapter } from '@services/adapters/ai-server-adapter/ai.server.adapter'; @Injectable() export class CommunityService { @@ -84,6 +85,7 @@ export class CommunityService { private formService: FormService, private communityPolicyService: CommunityPolicyService, private storageAggregatorResolverService: StorageAggregatorResolverService, + private aiServerAdapter: AiServerAdapter, //TODO: remove this asap @InjectRepository(Community) private communityRepository: Repository, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService @@ -719,6 +721,12 @@ export class CommunityService { agentInfo, triggerNewMemberEvents ); + // TO: THIS BREAKS THE DECOUPLING + const space = + await this.communityResolverService.getSpaceForCommunityOrFail( + community.id + ); + this.aiServerAdapter.ensureContextIsLoaded(space.id); return virtualContributor; } 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 91b6665718..ee68001e20 100644 --- a/src/services/adapters/ai-server-adapter/ai.server.adapter.ts +++ b/src/services/adapters/ai-server-adapter/ai.server.adapter.ts @@ -5,7 +5,6 @@ import { AiServerService } from '@services/ai-server/ai-server/ai.server.service import { CreateAiPersonaServiceInput } from '@services/ai-server/ai-persona-service/dto'; import { IAiPersonaService } from '@services/ai-server/ai-persona-service'; import { AgentInfo } from '@core/authentication.agent.info/agent.info'; -import { SpaceIngestionPurpose } from '@services/infrastructure/event-bus/commands'; import { IMessageAnswerToQuestion } from '@domain/communication/message.answer.to.question/message.answer.to.question.interface'; import { AiPersonaBodyOfKnowledgeType } from '@common/enums/ai.persona.body.of.knowledge.type'; @@ -17,25 +16,12 @@ export class AiServerAdapter { private readonly logger: LoggerService ) {} - async ensureSpaceIsUsable( - spaceID: string, - purpose: SpaceIngestionPurpose - ): Promise { - return this.aiServer.ensureSpaceIsUsable(spaceID, purpose); - } - - async ensurePersonaIsUsable( - personaServiceId: string, - purpose: SpaceIngestionPurpose - ): Promise { - return this.aiServer.ensurePersonaIsUsable(personaServiceId, purpose); + async refreshBodyOfKnowlege(personaServiceId: string): Promise { + return this.aiServer.ensurePersonaIsUsable(personaServiceId); } - async refreshBodyOfKnowlege(personaServiceId: string): Promise { - return this.aiServer.ensurePersonaIsUsable( - personaServiceId, - SpaceIngestionPurpose.KNOWLEDGE - ); + async ensureContextIsLoaded(spaceID: string): Promise { + await this.aiServer.ensureContextIsIngested(spaceID); } async getPersonaServiceBodyOfKnowledgeType( diff --git a/src/services/ai-server/ai-server/ai.server.resolver.mutations.ts b/src/services/ai-server/ai-server/ai.server.resolver.mutations.ts index 9959224d30..8e2f7dc5a2 100644 --- a/src/services/ai-server/ai-server/ai.server.resolver.mutations.ts +++ b/src/services/ai-server/ai-server/ai.server.resolver.mutations.ts @@ -14,7 +14,6 @@ import { CreateAiPersonaServiceInput } from '../ai-persona-service/dto/ai.person import { IAiPersonaService } from '../ai-persona-service/ai.persona.service.interface'; import { ConfigurationTypes } from '@common/enums'; import { Space } from '@domain/space/space/space.entity'; -import { SpaceIngestionPurpose } from '@services/infrastructure/event-bus/commands'; import { ChromaClient } from 'chromadb'; import { ConfigService } from '@nestjs/config'; import { InjectEntityManager } from '@nestjs/typeorm'; @@ -109,67 +108,6 @@ export class AiServerResolverMutations { return { success: true }; } - @UseGuards(GraphqlGuard) - @Mutation(() => IMigrateEmbeddingsResponse, { - description: 'Copies collections nameID-... into UUID-...', - }) - @Profiling.api - async copyCollections( - @CurrentUser() agentInfo: AgentInfo - ): Promise { - const platformAuthorization = - await this.platformAuthorizationService.getPlatformAuthorizationPolicy(); - this.authorizationService.grantAccessOrFail( - agentInfo, - platformAuthorization, - AuthorizationPrivilege.PLATFORM_ADMIN, - 'User not authenticated to migrate embeddings' - ); - - const vectorDb = this.config.get(ConfigurationTypes.PLATFORM).vector_db; - - const chroma = new ChromaClient({ - path: `http://${vectorDb.host}:${vectorDb.port}`, - }); - // get all chroma collections - const collections = await chroma.listCollections(); - - for (const collection of collections) { - // extract collection identifier and purpose - const [, nameID, purpose] = - collection.name.match(/(.*)-(knowledge|context)/) || []; - - // if the identifier is of UUID format, skip it - if ( - /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/.test( - nameID - ) - ) { - continue; - } - // get the space by nameID - const space = await this.entityManager.findOne(Space, { - where: { nameID: nameID }, - }); - - // if the space doesn't exit skip the colletion - if (!space) { - this.logger.warn( - `Space with nameID ${nameID} does't exist but ${collection.name} is still in Chroma` - ); - continue; - } - - // ask the AI server to ingest the space again; - // the ingest space service uses the UUID as collection identifier now - this.aiServerService.ensureSpaceIsUsable( - space.id, - purpose as SpaceIngestionPurpose - ); - } - - return { success: true }; - } @UseGuards(GraphqlGuard) @Mutation(() => IAiServer, { 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 fbd7111362..c6e91344b1 100644 --- a/src/services/ai-server/ai-server/ai.server.service.ts +++ b/src/services/ai-server/ai-server/ai.server.service.ts @@ -7,15 +7,11 @@ import { FindOneOptions, FindOptionsRelations, Repository } from 'typeorm'; import { AiServer } from './ai.server.entity'; import { IAiServer } from './ai.server.interface'; import { IAuthorizationPolicy } from '@domain/common/authorization-policy'; -import { ForbiddenException } from '@common/exceptions/forbidden.exception'; -import { AuthorizationCredential } from '@common/enums/authorization.credential'; -import { ICredentialDefinition } from '@domain/agent/credential/credential.definition.interface'; import { AiPersonaService, IAiPersonaService, } from '@services/ai-server/ai-persona-service'; import { AiPersonaServiceService } from '../ai-persona-service/ai.persona.service.service'; -import { AiServerRole } from '@common/enums/ai.server.role'; import { AiPersonaEngineAdapter } from '../ai-persona-engine-adapter/ai.persona.engine.adapter'; import { AiServerIngestAiPersonaServiceInput } from './dto/ai.server.dto.ingest.ai.persona.service'; import { AiPersonaEngineAdapterInputBase } from '../ai-persona-engine-adapter/dto/ai.persona.engine.adapter.dto.base'; @@ -27,14 +23,16 @@ import { SpaceIngestionPurpose, } from '@services/infrastructure/event-bus/commands'; import { EventBus } from '@nestjs/cqrs'; +import { ConfigService } from '@nestjs/config'; +import { ChromaClient } from 'chromadb'; +import { ConfigurationTypes } from '@common/enums/configuration.type'; @Injectable() export class AiServerService { constructor( - // private userService: UserService, - // private agentService: AgentService, private aiPersonaServiceService: AiPersonaServiceService, private aiPersonaEngineAdapter: AiPersonaEngineAdapter, + private config: ConfigService, @InjectRepository(AiServer) private aiServerRepository: Repository, @Inject(WINSTON_MODULE_NEST_PROVIDER) @@ -42,34 +40,59 @@ export class AiServerService { private eventBus: EventBus ) {} - async ensurePersonaIsUsable( - personaServiceId: string, - purpose: SpaceIngestionPurpose - ): Promise { + async ensurePersonaIsUsable(personaServiceId: string): Promise { const aiPersonaService = await this.aiPersonaServiceService.getAiPersonaServiceOrFail( personaServiceId ); - await this.ensureSpaceIsUsable(aiPersonaService.bodyOfKnowledgeID, purpose); + await this.ensureSpaceBoNIsIngested(aiPersonaService.bodyOfKnowledgeID); return true; } - async ensureSpaceIsUsable( - spaceID: string, - purpose: SpaceIngestionPurpose - ): Promise { - this.eventBus.publish(new IngestSpace(spaceID, purpose)); + public async ensureSpaceBoNIsIngested(spaceID: string): Promise { + this.eventBus.publish( + new IngestSpace(spaceID, SpaceIngestionPurpose.KNOWLEDGE) + ); + } + + public async ensureContextIsIngested(spaceID: string): Promise { + this.eventBus.publish( + new IngestSpace(spaceID, SpaceIngestionPurpose.CONTEXT) + ); } - async askQuestion( + public async askQuestion( questionInput: AiPersonaServiceQuestionInput, agentInfo: AgentInfo, - contextSapceNameID: string + contextID: string ) { + if (!(await this.isContextLoaded(contextID))) { + this.eventBus.publish( + new IngestSpace(contextID, SpaceIngestionPurpose.CONTEXT) + ); + } return this.aiPersonaServiceService.askQuestion( questionInput, agentInfo, - contextSapceNameID + contextID + ); + } + + private getContextCollectionID(contextID: string): string { + return `${contextID}-${SpaceIngestionPurpose.CONTEXT}`; + } + + private async isContextLoaded(contextID: string): Promise { + const { host, port } = this.config.get( + ConfigurationTypes.PLATFORM + ).vector_db; + const chroma = new ChromaClient({ path: `http://${host}:${port}` }); + + const collections = await chroma.listCollections(); + const collectionSearchedFor = this.getContextCollectionID(contextID); + + return collections.some(entry => + entry.name.includes(collectionSearchedFor) ); } @@ -171,43 +194,6 @@ export class AiServerService { return authorization; } - // public async assignAiServerRoleToUser( - // assignData: AssignAiServerRoleToUserInput - // ): Promise { - // const agent = await this.userService.getAgent(assignData.userID); - - // const credential = this.getCredentialForRole(assignData.role); - - // // assign the credential - // await this.agentService.grantCredential({ - // agentID: agent.id, - // ...credential, - // }); - - // return await this.userService.getUserWithAgent(assignData.userID); - // } - - // public async removeAiServerRoleFromUser( - // removeData: RemoveAiServerRoleFromUserInput - // ): Promise { - // const agent = await this.userService.getAgent(removeData.userID); - - // // Validation logic - // if (removeData.role === AiServerRole.GLOBAL_ADMIN) { - // // Check not the last global admin - // await this.removeValidationSingleGlobalAdmin(); - // } - - // const credential = this.getCredentialForRole(removeData.role); - - // await this.agentService.revokeCredential({ - // agentID: agent.id, - // ...credential, - // }); - - // return await this.userService.getUserWithAgent(removeData.userID); - // } - public async ingestAiPersonaService( ingestData: AiServerIngestAiPersonaServiceInput ): Promise { @@ -224,39 +210,4 @@ export class AiServerService { ); return result; } - - // private async removeValidationSingleGlobalAdmin(): Promise { - // // Check more than one - // const globalAdmins = await this.userService.usersWithCredentials({ - // type: AuthorizationCredential.GLOBAL_ADMIN, - // }); - // if (globalAdmins.length < 2) - // throw new ForbiddenException( - // `Not allowed to remove ${AuthorizationCredential.GLOBAL_ADMIN}: last AI Server global-admin`, - // LogContext.AUTH - // ); - - // return true; - // } - - private getCredentialForRole(role: AiServerRole): ICredentialDefinition { - const result: ICredentialDefinition = { - type: '', - resourceID: '', - }; - switch (role) { - case AiServerRole.GLOBAL_ADMIN: - result.type = AuthorizationCredential.GLOBAL_ADMIN; - break; - case AiServerRole.SUPPORT: - result.type = AuthorizationCredential.GLOBAL_SUPPORT; - break; - default: - throw new ForbiddenException( - `Role not supported: ${role}`, - LogContext.AI_SERVER - ); - } - return result; - } }