diff --git a/ansible_ai_connect_chatbot/package-lock.json b/ansible_ai_connect_chatbot/package-lock.json index ba96504b4..e1bc47efe 100644 --- a/ansible_ai_connect_chatbot/package-lock.json +++ b/ansible_ai_connect_chatbot/package-lock.json @@ -7629,16 +7629,15 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, diff --git a/ansible_ai_connect_chatbot/src/App.test.tsx b/ansible_ai_connect_chatbot/src/App.test.tsx index d18e96394..027c35dca 100644 --- a/ansible_ai_connect_chatbot/src/App.test.tsx +++ b/ansible_ai_connect_chatbot/src/App.test.tsx @@ -55,7 +55,16 @@ describe("App tests", () => { return error; }; - const mockAxios = (status: number, reject = false, timeout = false) => { + const referencedDocumentExample = [ + "https://docs.redhat.com/en/documentation/red_hat_ansible_automation_platform/2.5/html-single/getting_started_with_playbooks/index#ref-create-variables", + ]; + + const mockAxios = ( + status: number, + reject = false, + timeout = false, + refDocs: string[] = referencedDocumentExample, + ) => { const spy = vi.spyOn(axios, "post"); if (reject) { if (timeout) { @@ -77,14 +86,10 @@ describe("App tests", () => { spy.mockResolvedValue({ data: { conversation_id: "123e4567-e89b-12d3-a456-426614174000", - referenced_documents: [ - { - docs_url: - "https://docs.redhat.com/en/documentation/red_hat_ansible_automation_platform/2.5/html-single/" + - "getting_started_with_playbooks/index#ref-create-variables", - title: "Create variables", - }, - ], + referenced_documents: refDocs.map((d, index) => ({ + docs_url: d, + title: "Create variables" + (index > 0 ? index : ""), + })), response: "In Ansible, the precedence of variables is determined by the order...", truncated: false, @@ -157,6 +162,7 @@ describe("App tests", () => { }); it("ThumbsDown icon test", async () => { + const ghIssueLinkSpy = vi.spyOn(global, "open"); mockAxios(200); renderApp(); const textArea = screen.getByLabelText("Send a message..."); @@ -175,6 +181,50 @@ describe("App tests", () => { const sureButton = screen.getByText("Sure!"); await act(async () => fireEvent.click(sureButton)); + + expect(ghIssueLinkSpy.mock.calls.length).toEqual(1); + expect(ghIssueLinkSpy.mock.calls[0][0]).toContain( + encodeURIComponent(referencedDocumentExample[0]), + ); + }); + + const REF_DOCUMENT_EXAMPLE_REGEXP = new RegExp( + encodeURIComponent(referencedDocumentExample[0]), + "g", + ); + + it("Too many reference documents for the GU issue creation query param.", async () => { + const ghIssueLinkSpy = vi.spyOn(global, "open"); + // Initialize 35 reference documents for this test case, in order to verify that the url + // will not contain more than the max allowed documents (30). + const lotsOfRefDocs = []; + for (let i = 0; i < 35; i++) { + lotsOfRefDocs.push(referencedDocumentExample[0]); + } + mockAxios(200, false, false, lotsOfRefDocs); + renderApp(); + const textArea = screen.getByLabelText("Send a message..."); + await act(async () => userEvent.type(textArea, "Hello")); + const sendButton = screen.getByLabelText("Send button"); + await act(async () => fireEvent.click(sendButton)); + expect( + screen.getByText( + "In Ansible, the precedence of variables is determined by the order...", + ), + ).toBeInTheDocument(); + expect(screen.getByText("Create variables")).toBeInTheDocument(); + + const thumbsDownIcon = screen.getByRole("button", { name: "Bad response" }); + await act(async () => fireEvent.click(thumbsDownIcon)); + + const sureButton = screen.getByText("Sure!"); + await act(async () => fireEvent.click(sureButton)); + + expect(ghIssueLinkSpy.mock.calls.length).toEqual(1); + // Assert the size of the resulting documents in the query parameter is 30, + // as the max defined, instead of the 35 being present. + const url: string | undefined = ghIssueLinkSpy.mock.calls[0][0]?.toString(); + expect((url?.match(REF_DOCUMENT_EXAMPLE_REGEXP) || []).length).toEqual(30); }); it("Chat service returns 500", async () => { diff --git a/ansible_ai_connect_chatbot/src/Constants.ts b/ansible_ai_connect_chatbot/src/Constants.ts index 9ae95c6b7..0b1dd3d4d 100644 --- a/ansible_ai_connect_chatbot/src/Constants.ts +++ b/ansible_ai_connect_chatbot/src/Constants.ts @@ -26,7 +26,5 @@ export enum Sentiment { THUMBS_DOWN = 1, } -export const GITHUB_NEW_ISSUE_URL = - "https://github.com/ansible/ansible-lightspeed-va-feedback/issues/new" + - "?assignees=korenaren&labels=bug%2Ctriage&projects=&template=chatbot_feedback.yml" + - "&title=Chatbot+response+can+be+improved"; +export const GITHUB_NEW_ISSUE_BASE_URL = + "https://github.com/ansible/ansible-lightspeed-va-feedback/issues/new"; diff --git a/ansible_ai_connect_chatbot/src/useChatbot/useChatbot.ts b/ansible_ai_connect_chatbot/src/useChatbot/useChatbot.ts index 7dabeae34..1a9a8f3a7 100644 --- a/ansible_ai_connect_chatbot/src/useChatbot/useChatbot.ts +++ b/ansible_ai_connect_chatbot/src/useChatbot/useChatbot.ts @@ -12,7 +12,7 @@ import logo from "../assets/lightspeed.svg"; import userLogo from "../assets/user_logo.png"; import { API_TIMEOUT, - GITHUB_NEW_ISSUE_URL, + GITHUB_NEW_ISSUE_BASE_URL, Sentiment, TIMEOUT_MSG, TOO_MANY_REQUESTS_MSG, @@ -82,12 +82,7 @@ export const feedbackMessage = (f: ChatFeedback): MessageProps => ({ content: "Sure!", id: "response", onClick: () => - window - .open( - `${GITHUB_NEW_ISSUE_URL}&conversation_id=${f.response.conversation_id}&prompt=${f.query}&response=${f.response.response}`, - "_blank", - ) - ?.focus(), + window.open(createGitHubIssueURL(f), "_blank")?.focus(), }, ], }); @@ -114,6 +109,32 @@ const INITIAL_NOTICE: AlertMessage = { variant: "info", }; +const createGitHubIssueURL = (f: ChatFeedback): string => { + const searchParams: URLSearchParams = new URLSearchParams(); + searchParams.append("assignees", "korenaren"); + searchParams.append("labels", "bug,triage"); + searchParams.append("projects", ""); + searchParams.append("template", "chatbot_feedback.yml"); + searchParams.append("conversation_id", f.response.conversation_id); + searchParams.append("prompt", f.query); + searchParams.append("response", f.response.response); + // Referenced documents may increase as more source documents being ingested, + // so let's be try not to generate long length for query parameter "ref_docs", + // otherwise GH returns 414 URI Too Long error page. Assuming max of 30 docs. + // See https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/creating-an-issue#creating-an-issue-from-a-url-query. + searchParams.append( + "ref_docs", + f.response.referenced_documents + ?.slice(0, 30) + .map((doc) => doc.docs_url) + .join("\n"), + ); + + const url = new URL(GITHUB_NEW_ISSUE_BASE_URL); + url.search = searchParams.toString(); + return url.toString(); +}; + export const useChatbot = () => { const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -185,7 +206,9 @@ export const useChatbot = () => { try { const csrfToken = readCookie("csrftoken"); const resp = await axios.post( - "/api/v0/ai/feedback/", + import.meta.env.PROD + ? "/api/v0/ai/chat/" + : "http://localhost:8080/v1/feedback/", { chatFeedback: feedbackRequest, },