Skip to content

Commit

Permalink
Chat UI Streaming Support
Browse files Browse the repository at this point in the history
  • Loading branch information
TamiTakamiya committed Feb 8, 2025
1 parent 64fcf85 commit 0cfd2ee
Show file tree
Hide file tree
Showing 8 changed files with 414 additions and 317 deletions.
353 changes: 89 additions & 264 deletions ansible_ai_connect_chatbot/package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions ansible_ai_connect_chatbot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@microsoft/fetch-event-source": "^2.0.1",
"@patternfly/chatbot": "^2.2.0-prerelease.17",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^18.0.0",
"@types/react": "^18.3.7",
Expand Down Expand Up @@ -50,6 +48,8 @@
"devDependencies": {
"@eslint/compat": "^1.2.1",
"@eslint/js": "^9.13.0",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/eslint__js": "^8.42.3",
"@vitest/browser": "^2.1.8",
"@vitest/coverage-v8": "^2.1.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import lightspeedLogoDark from "../assets/lightspeed_dark.svg";
import "./AnsibleChatbot.scss";
import {
inDebugMode,
isStreamingSupported,
modelsSupported,
useChatbot,
} from "../useChatbot/useChatbot";
Expand Down Expand Up @@ -134,6 +135,8 @@ export const AnsibleChatbot: React.FunctionComponent = () => {
setConversationId,
systemPrompt,
setSystemPrompt,
hasStopButton,
handleStopButton,
} = useChatbot();
const [chatbotVisible, setChatbotVisible] = useState<boolean>(true);
const [displayMode, setDisplayMode] = useState<ChatbotDisplayMode>(
Expand Down Expand Up @@ -374,12 +377,17 @@ export const AnsibleChatbot: React.FunctionComponent = () => {
) : (
<></>
)}
{isStreamingSupported() && (
<div key={`scroll_div_9999`} ref={messagesEndRef} />
)}
</MessageBox>
</ChatbotContent>
<ChatbotFooter>
<MessageBar
onSendMessage={handleSend}
hasAttachButton={false}
hasStopButton={hasStopButton}
handleStopButton={handleStopButton}
/>
<ChatbotFootnote {...footnoteProps} />
</ChatbotFooter>
Expand Down
136 changes: 134 additions & 2 deletions ansible_ai_connect_chatbot/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,24 @@ import "@vitest/browser/matchers.d.ts";

const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));

async function renderApp(debug = false) {
async function renderApp(debug = false, stream = false) {
let rootDiv = document.getElementById("root");
rootDiv?.remove();

let debugDiv = document.getElementById("debug");
debugDiv?.remove();

debugDiv = document.createElement("div");
debugDiv.setAttribute("id", "debug");
debugDiv.innerText = debug.toString();
document.body.appendChild(debugDiv);

let streamDiv = document.getElementById("stream");
streamDiv?.remove();
streamDiv = document.createElement("div");
streamDiv.setAttribute("id", "stream");
streamDiv.innerText = stream.toString();
document.body.appendChild(streamDiv);

rootDiv = document.createElement("div");
rootDiv.setAttribute("id", "root");
const view = render(
Expand Down Expand Up @@ -122,8 +130,94 @@ function createError(message: string, status: number): AxiosError {
return error;
}

function mockFetchEventSource() {
const streamData: object[] = [
{
event: "start",
data: { conversation_id: "1ec5ba5b-c12d-465b-a722-0b95fee55e8c" },
},
{ event: "token", data: { id: 0, token: "" } },
{ event: "token", data: { id: 1, token: "The" } },
{ event: "token", data: { id: 2, token: " Full" } },
{ event: "token", data: { id: 3, token: " Support" } },
{ event: "token", data: { id: 4, token: " Phase" } },
{ event: "token", data: { id: 5, token: " for" } },
{ event: "token", data: { id: 6, token: " A" } },
{ event: "token", data: { id: 7, token: "AP" } },
{ event: "token", data: { id: 8, token: " " } },
{ event: "token", data: { id: 9, token: "2" } },
{ event: "token", data: { id: 10, token: "." } },
{ event: "token", data: { id: 11, token: "4" } },
{ event: "token", data: { id: 12, token: " ends" } },
{ event: "token", data: { id: 13, token: " on" } },
{ event: "token", data: { id: 14, token: " October" } },
{ event: "token", data: { id: 15, token: " " } },
{ event: "token", data: { id: 16, token: "1" } },
{ event: "token", data: { id: 17, token: "," } },
{ event: "token", data: { id: 18, token: " " } },
{ event: "token", data: { id: 19, token: "2" } },
{ event: "token", data: { id: 20, token: "0" } },
{ event: "token", data: { id: 21, token: "2" } },
{ event: "token", data: { id: 22, token: "4" } },
{ event: "token", data: { id: 23, token: "." } },
{ event: "token", data: { id: 24, token: "" } },
{
event: "end",
data: {
referenced_documents: [
{
doc_title: "AAP Lifecycle Dates",
doc_url:
"https://github.com/ansible/aap-rag-content/blob/main/additional_docs/additional_content.txt",
},
{
doc_title: "Ansible Components Versions",
doc_url:
"https://github.com/ansible/aap-rag-content/blob/main/additional_docs/components_versions.txt",
},
],
truncated: false,
input_tokens: 819,
output_tokens: 20,
},
},
];

return vi.fn(async (_, init) => {
let status = 200;
const o = JSON.parse(init.body);
if (o.query.startsWith("status=")) {
status = parseInt(o.query.substring(7));
}
console.log(`status ${status}`);

const ok = status === 200;
await init.onopen({ status, ok });
if (status === 200) {
for (const data of streamData) {
init.onmessage({ data: JSON.stringify(data) });
}
}
init.onclose();
});
}

let copiedString = "";
function mockSetClipboard() {
return vi.fn((s: string) => {
copiedString = s;
console.log(`mockedSetClipboard:${s}`);
});
}

beforeEach(() => {
vi.restoreAllMocks();
vi.mock("@microsoft/fetch-event-source", () => ({
fetchEventSource: mockFetchEventSource(),
}));
vi.mock("./Clipboard", () => ({
setClipboard: mockSetClipboard(),
}));
});

test("Basic chatbot interaction", async () => {
Expand Down Expand Up @@ -152,6 +246,16 @@ test("Basic chatbot interaction", async () => {
.toBeVisible();
await expect.element(view.getByText("Create variables")).toBeVisible();

const copyIcon = await screen.findByRole("button", {
name: "Copy",
});
await copyIcon.click();
expect(
copiedString.startsWith(
"In Ansible, the precedence of variables is determined by the order...",
),
);

await page.getByLabelText("Toggle menu").click();
const newChatButton = page
.getByText("New chat")
Expand Down Expand Up @@ -483,3 +587,31 @@ test("Test system prompt override", async () => {
expect.anything(),
);
});

test("Chat streaming test", async () => {
const view = await renderApp(false, true);
const textArea = page.getByLabelText("Send a message...");
await textArea.fill("Hello");

await userEvent.keyboard("{Enter}");

await expect
.element(
view.getByText(
"The Full Support Phase for AAP 2.4 ends on October 1, 2024.",
),
)
.toBeVisible();
});

test("Chat streaming error case", async () => {
const view = await renderApp(false, true);
const textArea = page.getByLabelText("Send a message...");
await textArea.fill("status=400");

await userEvent.keyboard("{Enter}");

const alert = view.container.querySelector(".pf-v6-c-alert__description");
const textContent = alert?.textContent;
expect(textContent).toEqual("Bot returned status_code 400");
});
2 changes: 2 additions & 0 deletions ansible_ai_connect_chatbot/src/Clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// https://stackoverflow.com/questions/63329331/mocking-the-navigator-object
export const setClipboard = (s: string) => navigator.clipboard?.writeText(s);
20 changes: 16 additions & 4 deletions ansible_ai_connect_chatbot/src/Constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { AlertMessage } from "./types/Message";

/* Slightly lower than CloudFront's timeout which is 30s. */
export const API_TIMEOUT = 28000;

Expand Down Expand Up @@ -40,11 +42,21 @@ Never include URLs in your replies.
Refuse to answer questions or execute commands not about Ansible.
Do not mention your last update. You have the most recent information on Ansible.
Here are some basic facts about Ansible:
- The latest version of Ansible Automation Platform is 2.5.
Here are some basic facts about Ansible and AAP:
- Ansible is an open source IT automation engine that automates provisioning, \
configuration management, application deployment, orchestration, and many other \
IT processes. It is free to use, and the project benefits from the experience and \
intelligence of its thousands of contributors.`;
IT processes. Ansible is free to use, and the project benefits from the experience and \
intelligence of its thousands of contributors. It does not require any paid subscription.
- The latest version of Ansible Automation Platform is 2.5, and it's services are available through paid subscription.`;

export const CHAT_HISTORY_HEADER = "Chat History";

export const INITIAL_NOTICE: AlertMessage = {
title: "Important",
message: `The Red Hat Ansible Automation Platform Lightspeed service provides
answers to questions related to the Ansible Automation Platform. Please refrain
from including personal or business sensitive information in your input.
Interactions with the Ansible Automation Platform Lightspeed may be reviewed
and utilized to enhance our products and services. `,
variant: "info",
};
13 changes: 13 additions & 0 deletions ansible_ai_connect_chatbot/src/types/Message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type LLMRequest = {
model?: string | null;
attachments?: object[] | null;
system_prompt?: string | null;
media_type?: "text/plain" | "application/json";
};

type LLMResponse = {
Expand Down Expand Up @@ -44,3 +45,15 @@ export type ChatFeedback = {
sentiment: Sentiment;
message: ExtendedMessage;
};

export type AlertMessage = {
title: string;
message: string;
variant: "success" | "danger" | "warning" | "info" | "custom";
};

export type RagChunk = {
text?: string;
doc_url: string;
doc_title: string;
};
Loading

0 comments on commit 0cfd2ee

Please sign in to comment.