Skip to content

Commit e2b9d6d

Browse files
authored
Port aap_chatbot changes to ansible_ai_connect_chatbot (#1602)
* Port aap_chatbot changes to ansible_ai_connect_chatbot * Fix an eslint error
1 parent 3538386 commit e2b9d6d

File tree

5 files changed

+195
-30
lines changed

5 files changed

+195
-30
lines changed

ansible_ai_connect_chatbot/src/AnsibleChatbot/AnsibleChatbot.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
DropdownList,
77
DropdownItem,
88
DropdownGroup,
9+
ExpandableSection,
910
} from "@patternfly/react-core";
1011

1112
import ChatbotContent from "@patternfly/chatbot/dist/dynamic/ChatbotContent";
@@ -44,6 +45,7 @@ import {
4445
ChatbotAlert,
4546
ChatbotConversationHistoryNav,
4647
ChatbotDisplayMode,
48+
ChatbotFootnoteProps,
4749
ChatbotHeaderMain,
4850
ChatbotHeaderMenu,
4951
ChatbotHeaderSelectorDropdown,
@@ -58,7 +60,7 @@ import {
5860
} from "../Constants";
5961
import { SystemPromptModal } from "../SystemPromptModal/SystemPromptModal";
6062

61-
const footnoteProps = {
63+
const footnoteProps: ChatbotFootnoteProps = {
6264
label: FOOTNOTE_LABEL,
6365
popover: {
6466
title: FOOTNOTE_TITLE,
@@ -346,6 +348,7 @@ export const AnsibleChatbot: React.FunctionComponent = () => {
346348
{
347349
referenced_documents,
348350
scrollToHere,
351+
collapse,
349352
...message
350353
}: ExtendedMessage,
351354
index,
@@ -357,14 +360,27 @@ export const AnsibleChatbot: React.FunctionComponent = () => {
357360
ref={messagesEndRef}
358361
/>
359362
)}
360-
<div key={`m_div_${index}`}>
361-
<Message key={`m_msg_${index}`} {...message} />
362-
<ReferencedDocuments
363-
key={`m_docs_${index}`}
364-
caption="Refer to the following for more information:"
365-
referenced_documents={referenced_documents}
366-
/>
367-
</div>
363+
{collapse ? (
364+
<div key={`m_div_${index}`}>
365+
<ExpandableSection toggleText="Show more">
366+
<Message key={`m_msg_${index}`} {...message} />
367+
<ReferencedDocuments
368+
key={`m_docs_${index}`}
369+
caption="Refer to the following for more information:"
370+
referenced_documents={referenced_documents}
371+
/>
372+
</ExpandableSection>
373+
</div>
374+
) : (
375+
<div key={`m_div_${index}`}>
376+
<Message key={`m_msg_${index}`} {...message} />
377+
<ReferencedDocuments
378+
key={`m_docs_${index}`}
379+
caption="Refer to the following for more information:"
380+
referenced_documents={referenced_documents}
381+
/>
382+
</div>
383+
)}
368384
</div>
369385
),
370386
)}

ansible_ai_connect_chatbot/src/App.test.tsx

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,22 +197,71 @@ function mockFetchEventSource() {
197197
},
198198
];
199199

200+
const streamAgentNormalData: object[] = [
201+
{
202+
event: "start",
203+
data: { conversation_id: "6e33a46e-2b15-4128-8e5c-c6f637ebfbb4" },
204+
},
205+
{
206+
event: "token",
207+
data: { id: 0, token: "Let me search for " },
208+
},
209+
{
210+
event: "token",
211+
data: { id: 1, token: "information about EDA." },
212+
},
213+
{
214+
event: "tool_call",
215+
data: { id: 2, token: '{ "key":"value"}' },
216+
},
217+
{
218+
event: "step_details",
219+
data: { id: 3, token: '{ "key":"value"}' },
220+
},
221+
{
222+
event: "token",
223+
data: { id: 4, token: "EDA stands for " },
224+
},
225+
{
226+
event: "token",
227+
data: { id: 5, token: "Event Driven Ansible." },
228+
},
229+
{
230+
event: "step_complete",
231+
data: { id: 6, token: '{ "key":"value"}' },
232+
},
233+
{
234+
event: "turn_complete",
235+
data: { id: 0, token: "Turn complete." },
236+
},
237+
];
238+
200239
return vi.fn(async (_, init) => {
201240
let status = 200;
202241
let errorCase = false;
242+
let agent = false;
203243
const o = JSON.parse(init.body);
204244
if (o.query.startsWith("status=")) {
205245
status = parseInt(o.query.substring(7));
206246
} else if (o.query.startsWith("error in stream")) {
207247
errorCase = true;
248+
} else if (o.query.startsWith("agent")) {
249+
agent = true;
208250
}
209251
console.log(`status ${status}`);
210252

211253
const ok = status === 200;
212254
await init.onopen({ status, ok });
255+
213256
if (status === 200) {
214-
const streamData = errorCase ? streamErrorData : streamNormalData;
215-
for (const data of streamData) {
257+
const streamData = () => {
258+
if (agent) {
259+
return streamAgentNormalData;
260+
} else {
261+
return errorCase ? streamErrorData : streamNormalData;
262+
}
263+
};
264+
for (const data of streamData()) {
216265
init.onmessage({ data: JSON.stringify(data) });
217266
}
218267
}
@@ -293,15 +342,15 @@ test("Basic chatbot interaction", async () => {
293342
.not.toBeInTheDocument();
294343

295344
const footNoteLink = page.getByText(
296-
"Lightspeed uses AI. Check for mistakes.",
345+
"Always review AI-generated content prior to use.",
297346
);
298347
await footNoteLink.click();
299348
await expect
300349
.element(page.getByText("While Lightspeed strives for accuracy,"))
301350
.toBeVisible();
302351
await page.getByText("Got it").click();
303352
await expect
304-
.element(view.getByText("While Lightspeed strives for accuracy,"))
353+
.element(page.getByText("While Lightspeed strives for accuracy,"))
305354
.not.toBeVisible();
306355

307356
await textArea.fill("Tell me about Ansible.");
@@ -608,6 +657,12 @@ test("Test system prompt override", async () => {
608657
});
609658

610659
test("Chat streaming test", async () => {
660+
let ghIssueLinkSpy = 0;
661+
vi.stubGlobal("open", () => {
662+
ghIssueLinkSpy++;
663+
});
664+
mockAxios(200);
665+
611666
const view = await renderApp(false, true);
612667
const textArea = page.getByLabelText("Send a message...");
613668
await textArea.fill("Hello");
@@ -621,6 +676,53 @@ test("Chat streaming test", async () => {
621676
),
622677
)
623678
.toBeVisible();
679+
680+
const thumbsDownIcon = await screen.findByRole("button", {
681+
name: "Bad response",
682+
});
683+
await thumbsDownIcon.click();
684+
685+
const sureButton = await screen.findByText("Sure!");
686+
await expect.element(sureButton).toBeVisible();
687+
await sureButton.click();
688+
689+
expect(ghIssueLinkSpy).toEqual(1);
690+
});
691+
692+
test("Agent chat streaming test", async () => {
693+
let ghIssueLinkSpy = 0;
694+
vi.stubGlobal("open", () => {
695+
ghIssueLinkSpy++;
696+
});
697+
mockAxios(200, false, false, referencedDocumentExample);
698+
699+
const view = await renderApp(false, true);
700+
const textArea = page.getByLabelText("Send a message...");
701+
await textArea.fill("agent test");
702+
703+
await userEvent.keyboard("{Enter}");
704+
705+
await expect.element(view.getByText("Turn complete")).toBeVisible();
706+
707+
const thumbsDownIcon = await screen.findByRole("button", {
708+
name: "Bad response",
709+
});
710+
await thumbsDownIcon.click();
711+
712+
const sureButton = await screen.findByText("Sure!");
713+
await expect.element(sureButton).toBeVisible();
714+
await sureButton.click();
715+
716+
expect(ghIssueLinkSpy).toEqual(1);
717+
718+
await expect
719+
.element(view.getByText("EDA stands for Event Driven Ansible."))
720+
.not.toBeVisible();
721+
const showMoreLink = await screen.findByRole("button", { name: "Show more" });
722+
await showMoreLink.click();
723+
await expect
724+
.element(view.getByText("EDA stands for Event Driven Ansible."))
725+
.toBeVisible();
624726
});
625727

626728
test("Chat streaming error at API call", async () => {

ansible_ai_connect_chatbot/src/Constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export const TOO_MANY_REQUESTS_MSG =
1313
"_Chatbot service is busy with too many requests. Please try again later._";
1414

1515
/* Footnote label */
16-
export const FOOTNOTE_LABEL = "Lightspeed uses AI. Check for mistakes.";
16+
export const FOOTNOTE_LABEL =
17+
"Always review AI-generated content prior to use.";
1718

1819
/* Footnote title */
1920
export const FOOTNOTE_TITLE = "Verify accuracy";

ansible_ai_connect_chatbot/src/types/Message.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type ReferencedDocumentsProp = {
3737
export type ExtendedMessage = MessageProps & {
3838
referenced_documents: ReferencedDocument[];
3939
scrollToHere?: boolean;
40+
collapse?: boolean;
4041
};
4142

4243
export type ChatFeedback = {

ansible_ai_connect_chatbot/src/useChatbot/useChatbot.ts

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,11 @@ export const useChatbot = () => {
172172
});
173173
};
174174

175-
const appendMessageChunk = (chunk: string) => {
175+
const appendMessageChunk = (chunk: string, query: string = "") => {
176176
setMessages((msgs: ExtendedMessage[]) => {
177177
const lastMessage = msgs[msgs.length - 1];
178178
if (!lastMessage || lastMessage.role === "user") {
179-
const newMessage: ExtendedMessage = botMessage(chunk);
179+
const newMessage: ExtendedMessage = botMessage(chunk, query);
180180
chunk = "";
181181
return [...msgs, newMessage];
182182
} else {
@@ -222,16 +222,34 @@ export const useChatbot = () => {
222222
timestamp: getTimestamp(),
223223
referenced_documents: [],
224224
};
225-
const sendFeedback = async (sentiment: Sentiment) => {
226-
if (typeof response === "object") {
225+
const sendFeedback = async (
226+
sentiment: Sentiment,
227+
content: string = "",
228+
referenced_documents: ReferencedDocument[] = [],
229+
) => {
230+
if (typeof response === "string") {
231+
const resp = {
232+
conversation_id: conversationId
233+
? conversationId
234+
: "00000000-0000-0000-0000-000000000000",
235+
response: content,
236+
referenced_documents,
237+
truncated: false,
238+
};
239+
handleFeedback({ query, response: resp, sentiment, message });
240+
} else {
227241
handleFeedback({ query, response, sentiment, message });
228242
}
229243
};
230244

231245
message.actions = {
232246
positive: {
233247
onClick: () => {
234-
sendFeedback(Sentiment.THUMBS_UP);
248+
sendFeedback(
249+
Sentiment.THUMBS_UP,
250+
message.content,
251+
message.referenced_documents,
252+
);
235253
if (message.actions) {
236254
message.actions.positive.isDisabled = true;
237255
message.actions.negative.isDisabled = true;
@@ -241,7 +259,11 @@ export const useChatbot = () => {
241259
},
242260
negative: {
243261
onClick: () => {
244-
sendFeedback(Sentiment.THUMBS_DOWN);
262+
sendFeedback(
263+
Sentiment.THUMBS_DOWN,
264+
message.content,
265+
message.referenced_documents,
266+
);
245267
if (message.actions) {
246268
message.actions.positive.isDisabled = true;
247269
message.actions.negative.isDisabled = true;
@@ -323,10 +345,10 @@ export const useChatbot = () => {
323345
setAbortController(new AbortController());
324346
};
325347

326-
const handleSend = async (message: string | number) => {
348+
const handleSend = async (query: string | number) => {
327349
const userMessage: ExtendedMessage = {
328350
role: "user",
329-
content: message.toString(),
351+
content: query.toString(),
330352
name: userName,
331353
avatar: userLogo,
332354
timestamp: getTimestamp(),
@@ -336,7 +358,7 @@ export const useChatbot = () => {
336358

337359
const chatRequest: ChatRequest = {
338360
conversation_id: conversationId,
339-
query: message.toString(),
361+
query: query.toString(),
340362
};
341363

342364
if (systemPrompt !== QUERY_SYSTEM_INSTRUCTION) {
@@ -385,8 +407,13 @@ export const useChatbot = () => {
385407
});
386408
}
387409
},
388-
onmessage(event: any) {
389-
const message = JSON.parse(event.data);
410+
onmessage(msg: any) {
411+
let message = msg;
412+
if (!msg.event) {
413+
message = JSON.parse(msg.data);
414+
} else {
415+
message.data = JSON.parse(msg.data);
416+
}
390417
if (message.event === "start") {
391418
if (!conversationId) {
392419
setConversationId(message.data.conversation_id);
@@ -395,7 +422,7 @@ export const useChatbot = () => {
395422
if (message.data.token !== "") {
396423
setIsLoading(false);
397424
}
398-
appendMessageChunk(message.data.token);
425+
appendMessageChunk(message.data.token, query.toString());
399426
} else if (message.event === "end") {
400427
if (message.data.referenced_documents.length > 0) {
401428
addReferencedDocuments(message.data.referenced_documents);
@@ -409,6 +436,27 @@ export const useChatbot = () => {
409436
`cause="${data.cause}"`,
410437
variant: "danger",
411438
});
439+
} else if (
440+
message.event === "tool_call" ||
441+
message.event === "step_complete"
442+
) {
443+
console.log(
444+
`!![${message.event}] ${JSON.stringify(message.data)}`,
445+
);
446+
appendMessageChunk(
447+
"\n\n`[" +
448+
message.event +
449+
"]`\n```json\n" +
450+
message.data.token +
451+
"\n```\n",
452+
);
453+
} else if (message.event === "turn_complete") {
454+
setMessages((msgs: ExtendedMessage[]) => {
455+
const lastMessage = msgs[msgs.length - 1];
456+
lastMessage.collapse = true;
457+
msgs.push(botMessage(message.data.token, query.toString()));
458+
return msgs;
459+
});
412460
}
413461
},
414462
onclose() {
@@ -440,10 +488,7 @@ export const useChatbot = () => {
440488
if (!conversationId) {
441489
setConversationId(chatResponse.conversation_id);
442490
}
443-
const newBotMessage: any = botMessage(
444-
chatResponse,
445-
message.toString(),
446-
);
491+
const newBotMessage: any = botMessage(chatResponse, query.toString());
447492
newBotMessage.referenced_documents = referenced_documents;
448493
addMessage(newBotMessage);
449494
} else {

0 commit comments

Comments
 (0)