diff --git a/lib/content-services/src/lib/agent/services/agent.service.spec.ts b/lib/content-services/src/lib/agent/services/agent.service.spec.ts new file mode 100644 index 0000000000..58b6f2389b --- /dev/null +++ b/lib/content-services/src/lib/agent/services/agent.service.spec.ts @@ -0,0 +1,63 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AgentService } from './agent.service'; +import { TestBed } from '@angular/core/testing'; +import { AgentPaging } from '@alfresco/js-api'; +import { ContentTestingModule } from '../../testing/content.testing.module'; + +describe('AgentService', () => { + let service: AgentService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ContentTestingModule] + }); + service = TestBed.inject(AgentService); + service.mocked = false; + }); + + describe('getAgents', () => { + it('should load agents', (done) => { + const paging: AgentPaging = { + list: { + entries: [ + { + entry: { + id: '1', + name: 'HR Agent' + } + }, + { + entry: { + id: '2', + name: 'Policy Agent' + } + } + ] + } + }; + spyOn(service.agentsApi, 'getAgents').and.returnValue(Promise.resolve(paging)); + + service.getAgents().subscribe((pagingResponse) => { + expect(pagingResponse).toBe(paging); + expect(service.agentsApi.getAgents).toHaveBeenCalled(); + done(); + }); + }); + }); +}); diff --git a/lib/content-services/src/lib/search-ai/services/search-ai.service.spec.ts b/lib/content-services/src/lib/search-ai/services/search-ai.service.spec.ts new file mode 100644 index 0000000000..df9dca325d --- /dev/null +++ b/lib/content-services/src/lib/search-ai/services/search-ai.service.spec.ts @@ -0,0 +1,255 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; +import { AiAnswerPaging, Node, QuestionModel, QuestionRequest } from '@alfresco/js-api'; +import { ContentTestingModule } from '../../testing/content.testing.module'; +import { SearchAiService } from './search-ai.service'; +import { SearchAiInputState } from '../models/search-ai-input-state'; +import { TranslateService } from '@ngx-translate/core'; + +describe('SearchAiService', () => { + let service: SearchAiService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ContentTestingModule] + }); + service = TestBed.inject(SearchAiService); + service.mocked = false; + }); + + describe('ask', () => { + it('should load information about question', (done) => { + const question: QuestionModel = { + question: 'some question', + questionId: 'some id', + restrictionQuery: 'node id1,node id 2' + }; + spyOn(service.searchAiApi, 'ask').and.returnValue(Promise.resolve([question])); + const questionRequest: QuestionRequest = { + question: 'some question', + nodeIds: ['node id1', 'node id 2'] + }; + + service.ask(questionRequest).subscribe((questionResponse) => { + expect(questionResponse).toBe(question); + expect(service.searchAiApi.ask).toHaveBeenCalledWith([questionRequest]); + done(); + }); + }); + }); + + describe('getAnswer', () => { + it('should load information about question', (done) => { + const questionId = 'some id'; + const answer: AiAnswerPaging = { + list: { + pagination: { + count: 2, + hasMoreItems: false, + skipCount: 0, + maxItems: 100 + }, + entries: [ + { + entry: { + answer: 'Some answer 1', + questionId, + references: [ + { + referenceId: 'some reference id 1', + referenceText: 'some reference text 1' + } + ] + } + }, + { + entry: { + answer: 'Some answer 2', + questionId, + references: [ + { + referenceId: 'some reference id 2', + referenceText: 'some reference text 2' + } + ] + } + } + ] + } + }; + spyOn(service.searchAiApi, 'getAnswer').and.returnValue(Promise.resolve(answer)); + + service.getAnswer(questionId).subscribe((answerResponse) => { + expect(answerResponse).toBe(answer); + expect(service.searchAiApi.getAnswer).toHaveBeenCalledWith(questionId); + done(); + }); + }); + }); + + describe('updateSearchAiInputState', () => { + it('should trigger toggleSearchAiInput$', () => { + const state: SearchAiInputState = { + active: true, + selectedAgentId: 'some id' + }; + service.updateSearchAiInputState(state); + + service.toggleSearchAiInput$.subscribe((receivedState) => { + expect(receivedState).toBe(state); + }); + }); + }); + + describe('checkSearchAvailability', () => { + let translateService: TranslateService; + + const noFilesSelectedError = 'Please select some file.'; + const tooManyFilesSelectedError = 'Please select no more than 100 files.'; + const nonTextFileSelectedError = 'Only text related files are compatible with AI Agents.'; + const folderSelectedError = 'Folders are not compatible with AI Agents.'; + + beforeEach(() => { + translateService = TestBed.inject(TranslateService); + spyOn(translateService, 'instant').and.callFake((key) => { + switch (key) { + case 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.NO_FILES_SELECTED': + return noFilesSelectedError; + case 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.TOO_MANY_FILES_SELECTED': + return tooManyFilesSelectedError; + case 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.NON_TEXT_FILE_SELECTED': + return nonTextFileSelectedError; + case 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.FOLDER_SELECTED': + return folderSelectedError; + default: + return ''; + } + }); + }); + + it('should return error for no selected nodes', () => { + expect( + service.checkSearchAvailability({ + count: 0, + nodes: [], + libraries: [], + isEmpty: true + }) + ).toBe(noFilesSelectedError); + }); + + it('should return error for too many files selected', () => { + expect( + service.checkSearchAvailability({ + count: 101, + nodes: [], + libraries: [], + isEmpty: false + }) + ).toBe(tooManyFilesSelectedError); + expect(translateService.instant).toHaveBeenCalledWith('KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.TOO_MANY_FILES_SELECTED', { + maxFiles: 100, + key: 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.TOO_MANY_FILES_SELECTED' + }); + }); + + it('should return error for non text file selected if non text file is selected and it is not a folder', () => { + expect( + service.checkSearchAvailability({ + count: 1, + nodes: [ + { + entry: { + isFolder: false, + content: { + mimeType: 'image/jpeg', + mimeTypeName: 'image/jpeg', + sizeInBytes: 100 + } + } as Node + } + ], + libraries: [], + isEmpty: false + }) + ).toBe(nonTextFileSelectedError); + }); + + it('should not return error for non text file selected if non text mime type node is selected and it is a folder', () => { + expect( + service.checkSearchAvailability({ + count: 1, + nodes: [ + { + entry: { + isFolder: true, + content: { + mimeType: 'some mime type', + mimeTypeName: 'some mime type', + sizeInBytes: 100 + } + } as Node + } + ], + libraries: [], + isEmpty: false + }) + ).not.toBe(nonTextFileSelectedError); + }); + + it('should return error for folder selected if', () => { + expect( + service.checkSearchAvailability({ + count: 1, + nodes: [ + { + entry: { + isFolder: true + } as Node + } + ], + libraries: [], + isEmpty: false + }) + ).toBe(folderSelectedError); + }); + + it('should return more than one error if more validators detected issues', () => { + expect( + service.checkSearchAvailability({ + count: 101, + nodes: [ + { + entry: { + isFolder: false, + content: { + mimeType: 'image/jpeg', + mimeTypeName: 'image/jpeg', + sizeInBytes: 100 + } + } as Node + } + ], + libraries: [], + isEmpty: false + }) + ).toBe(`${tooManyFilesSelectedError} ${nonTextFileSelectedError}`); + }); + }); +}); diff --git a/lib/js-api/src/api/content-rest-api/api/search-ai.api.ts b/lib/js-api/src/api/content-rest-api/api/search-ai.api.ts index e71e460d62..745b8b6490 100644 --- a/lib/js-api/src/api/content-rest-api/api/search-ai.api.ts +++ b/lib/js-api/src/api/content-rest-api/api/search-ai.api.ts @@ -33,9 +33,9 @@ export class SearchAiApi extends BaseApi { ask(questions: QuestionRequest[]): Promise { return this.get({ path: 'questions', - bodyParam: questions.map((question) => ({ - ...question, - restrictionQuery: question.nodeIds.join(',') + bodyParam: questions.map((questionRequest) => ({ + question: questionRequest.question, + restrictionQuery: questionRequest.nodeIds.join(',') })) }); } diff --git a/lib/js-api/test/content-services/agentsApi.spec.ts b/lib/js-api/test/content-services/agentsApi.spec.ts new file mode 100644 index 0000000000..8b81fffd16 --- /dev/null +++ b/lib/js-api/test/content-services/agentsApi.spec.ts @@ -0,0 +1,71 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AgentMock, EcmAuthMock } from '../mockObjects'; +import { AgentsApi, AlfrescoApi } from '../../src'; +import assert from 'assert'; + +describe('AgentsApi', () => { + let agentMock: AgentMock; + let agentsApi: AgentsApi; + + beforeEach((done) => { + const hostEcm = 'https://127.0.0.1:8080'; + const authResponseMock = new EcmAuthMock(hostEcm); + agentMock = new AgentMock(hostEcm); + authResponseMock.get201Response(); + const alfrescoJsApi = new AlfrescoApi({ + hostEcm + }); + alfrescoJsApi.login('admin', 'admin').then(() => done()); + agentsApi = new AgentsApi(alfrescoJsApi); + }); + + describe('getAgents', () => { + it('should load list of agents', (done) => { + agentMock.mockGetAgents200Response(); + + agentsApi.getAgents().then((paging) => { + assert.deepStrictEqual(paging, { + list: { + pagination: { + count: 2, + hasMoreItems: false, + skipCount: 0, + maxItems: 100 + }, + entries: [ + { + entry: { + id: 'some id 1', + name: 'some name 1' + } + }, + { + entry: { + id: 'some id 2', + name: 'some name 2' + } + } + ] + } + }); + done(); + }); + }); + }); +}); diff --git a/lib/js-api/test/content-services/searchAiApi.spec.ts b/lib/js-api/test/content-services/searchAiApi.spec.ts new file mode 100644 index 0000000000..8cc2e75978 --- /dev/null +++ b/lib/js-api/test/content-services/searchAiApi.spec.ts @@ -0,0 +1,116 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AlfrescoApi, SearchAiApi } from '../../src'; +import { EcmAuthMock, SearchAiMock } from '../mockObjects'; +import assert from 'assert'; + +describe('SearchAiApi', () => { + let searchAiApi: SearchAiApi; + let searchAiMock: SearchAiMock; + + beforeEach((done) => { + const hostEcm = 'https://127.0.0.1:8080'; + const authResponseMock = new EcmAuthMock(hostEcm); + searchAiMock = new SearchAiMock(hostEcm); + authResponseMock.get201Response(); + const alfrescoJsApi = new AlfrescoApi({ + hostEcm + }); + alfrescoJsApi.login('admin', 'admin').then(() => done()); + searchAiApi = new SearchAiApi(alfrescoJsApi); + }); + + describe('ask', () => { + it('should load question information', (done) => { + searchAiMock.mockGetAsk200Response(); + + searchAiApi + .ask([ + { + question: 'some question 1', + nodeIds: ['some node id 1'] + }, + { + question: 'some question 2', + nodeIds: ['some node id 2', 'some node id 3'] + } + ]) + .then((questions) => { + assert.deepStrictEqual(questions, [ + { + questionId: 'some id 1', + question: 'some question 1', + restrictionQuery: 'some node id 1' + }, + { + questionId: 'some id 2', + question: 'some question 2', + restrictionQuery: 'some node id 2,some node id 3' + } + ]); + done(); + }); + }); + }); + + describe('getAnswer', () => { + it('should load question answer', (done) => { + searchAiMock.mockGetAnswer200Response(); + + searchAiApi.getAnswer('id1').then((answer) => { + assert.deepStrictEqual(answer, { + list: { + pagination: { + count: 2, + hasMoreItems: false, + skipCount: 0, + maxItems: 100 + }, + entries: [ + { + entry: { + answer: 'Some answer 1', + questionId: 'some id 1', + references: [ + { + referenceId: 'some reference id 1', + referenceText: 'some reference text 1' + } + ] + } + }, + { + entry: { + answer: 'Some answer 2', + questionId: 'some id 2', + references: [ + { + referenceId: 'some reference id 2', + referenceText: 'some reference text 2' + } + ] + } + } + ] + } + }); + done(); + }); + }); + }); +}); diff --git a/lib/js-api/test/mockObjects/content-services/agent.mock.ts b/lib/js-api/test/mockObjects/content-services/agent.mock.ts new file mode 100644 index 0000000000..fc657e2193 --- /dev/null +++ b/lib/js-api/test/mockObjects/content-services/agent.mock.ts @@ -0,0 +1,50 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BaseMock } from '../base.mock'; +import nock from 'nock'; + +export class AgentMock extends BaseMock { + mockGetAgents200Response(): void { + nock(this.host, { encodedQueryParams: true }) + .get('/alfresco/api/-default-/private/hxi/versions/1/agents') + .reply(200, { + list: { + pagination: { + count: 2, + hasMoreItems: false, + skipCount: 0, + maxItems: 100 + }, + entries: [ + { + entry: { + id: 'some id 1', + name: 'some name 1' + } + }, + { + entry: { + id: 'some id 2', + name: 'some name 2' + } + } + ] + } + }); + } +} diff --git a/lib/js-api/test/mockObjects/content-services/search-ai.mock.ts b/lib/js-api/test/mockObjects/content-services/search-ai.mock.ts new file mode 100644 index 0000000000..876b0d6139 --- /dev/null +++ b/lib/js-api/test/mockObjects/content-services/search-ai.mock.ts @@ -0,0 +1,88 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BaseMock } from '../base.mock'; +import nock from 'nock'; + +export class SearchAiMock extends BaseMock { + mockGetAsk200Response(): void { + nock(this.host, { encodedQueryParams: true }) + .get('/alfresco/api/-default-/private/hxi/versions/1/questions', [ + { + question: 'some question 1', + restrictionQuery: 'some node id 1' + }, + { + question: 'some question 2', + restrictionQuery: 'some node id 2,some node id 3' + } + ]) + .reply(200, [ + { + question: 'some question 1', + questionId: 'some id 1', + restrictionQuery: 'some node id 1' + }, + { + question: 'some question 2', + questionId: 'some id 2', + restrictionQuery: 'some node id 2,some node id 3' + } + ]); + } + + mockGetAnswer200Response(): void { + nock(this.host, { encodedQueryParams: true }) + .get('/alfresco/api/-default-/private/hxi/versions/1/answers?questionId=id1') + .reply(200, { + list: { + pagination: { + count: 2, + hasMoreItems: false, + skipCount: 0, + maxItems: 100 + }, + entries: [ + { + entry: { + answer: 'Some answer 1', + questionId: 'some id 1', + references: [ + { + referenceId: 'some reference id 1', + referenceText: 'some reference text 1' + } + ] + } + }, + { + entry: { + answer: 'Some answer 2', + questionId: 'some id 2', + references: [ + { + referenceId: 'some reference id 2', + referenceText: 'some reference text 2' + } + ] + } + } + ] + } + }); + } +} diff --git a/lib/js-api/test/mockObjects/index.ts b/lib/js-api/test/mockObjects/index.ts index c3df053f61..717eb97009 100644 --- a/lib/js-api/test/mockObjects/index.ts +++ b/lib/js-api/test/mockObjects/index.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +export * from './content-services/agent.mock'; export * from './content-services/categories.mock'; export * from './content-services/comment.mock'; export * from './content-services/ecm-auth.mock'; @@ -26,6 +27,7 @@ export * from './content-services/groups.mock'; export * from './content-services/find-nodes.mock'; export * from './content-services/rendition.mock'; export * from './content-services/search.mock'; +export * from './content-services/search-ai.mock'; export * from './content-services/tag.mock'; export * from './content-services/upload.mock'; export * from './content-services/version.mock';