Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/agents/_data_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,12 @@ def append_messages(self, messages):
if not (isinstance(final_message, AIMessage) and final_message.tool_calls):
self._append_messages([final_message])

error = CancelError("Execution canceled.")
self.append_error_to_frontend(error)
raise error
if not (
isinstance(final_message, AIMessage) and not final_message.tool_calls
):
error = CancelError("Execution canceled.")
self.append_error_to_frontend(error)
raise error
else:
self._append_messages(messages)

Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/agents/chat_app/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<link rel="icon" href="./favicon.png" type="image/png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Chat Assistant</title>
<script type="module" crossorigin src="./assets/index-LVlEMt5c.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DLP8jyRf.css">
<script type="module" crossorigin src="./assets/index-CcVbEd9v.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-B9GfoYlZ.css">
</head>
<body>
<div id="app"></div>
Expand Down
2 changes: 1 addition & 1 deletion src/agents/chat_app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@
"npm": "please-use-pnpm"
},
"packageManager": "pnpm@10.7.0"
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { flushPromises, mount } from "@vue/test-utils";
import { createTestingPinia } from "@pinia/testing";
import { setActivePinia } from "pinia";

import { useChatStore } from "@/stores/chat";
import type { ViewMessage } from "@/types";
import MessageBox from "../chat/message/MessageBox.vue";
import NodeViewMessage from "../chat/message/NodeViewMessage.vue";
import { useChatStore } from "@/stores/chat";
import { version } from "os";

const mockCallKnimeUiApi = vi.fn(() =>
Promise.resolve({
Expand Down
13 changes: 10 additions & 3 deletions src/agents/chat_app/src/components/chat/ChatInterface.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script setup lang="ts">
import { ref } from "vue";
import { computed, ref } from "vue";

import { SkeletonItem } from "@knime/components";

import { useScrollToBottom } from "@/composables/useScrollToBottom";
import { useChatStore } from "@/stores/chat";

import MessageInput from "./MessageInput.vue";
import ToolUseIndicator from "./ToolUseIndicator.vue";
import StatusIndicator from "./StatusIndicator.vue";
import AiMessage from "./message/AiMessage.vue";
import ErrorMessage from "./message/ErrorMessage.vue";
import HumanMessage from "./message/HumanMessage.vue";
Expand All @@ -20,6 +20,10 @@ const chatStore = useChatStore();
const scrollableContainer = ref<HTMLElement | null>(null);
const messagesList = ref<HTMLElement | null>(null);

const statusIndicatorLabel = computed(() =>
chatStore.isInterrupted ? "Cancelling" : "Using tools",
);

const chatItemComponents = {
ai: AiMessage,
view: NodeViewMessage,
Expand All @@ -39,7 +43,10 @@ useScrollToBottom(scrollableContainer, messagesList);
<component :is="chatItemComponents[item.type]" v-bind="item" />
</template>

<ToolUseIndicator v-if="chatStore.shouldShowToolUseIndicator" />
<StatusIndicator
v-if="chatStore.shouldShowStatusIndicator"
:label="statusIndicatorLabel"
/>

<MessageBox v-if="chatStore.shouldShowGenericLoadingIndicator">
<SkeletonItem height="24px" width="200px" />
Expand Down
26 changes: 14 additions & 12 deletions src/agents/chat_app/src/components/chat/MessageInput.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { computed } from "vue";
import { useTextareaAutosize } from "@vueuse/core";

import { FunctionButton } from "@knime/components";
import SendIcon from "@knime/styles/img/icons/paper-flier.svg";
import AbortIcon from "@knime/styles/img/icons/close.svg";
import SendIcon from "@knime/styles/img/icons/paper-flier.svg";

import { useChatStore } from "@/stores/chat";

Expand All @@ -14,8 +14,10 @@ const { textarea, input } = useTextareaAutosize();
const characterLimit = 5000;

const isInputValid = computed(() => input.value?.trim().length > 0);
const isDisabled = computed(() => chatStore.isInterrupted || (!isInputValid.value && !chatStore.isLoading));

const isDisabled = computed(
() =>
chatStore.isInterrupted || (!isInputValid.value && !chatStore.isLoading),
);

const handleClick = (event: MouseEvent) => {
if (event.target === event.currentTarget) {
Expand All @@ -34,7 +36,12 @@ const handleSubmit = () => {

const handleKeyDown = (event: KeyboardEvent) => {
// enter: send message
if (event.key === "Enter" && !chatStore.isLoading && !event.shiftKey && !isDisabled.value) {
if (
event.key === "Enter" &&
!chatStore.isLoading &&
!event.shiftKey &&
!isDisabled.value
) {
event.preventDefault();
handleSubmit();
}
Expand Down Expand Up @@ -72,12 +79,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
aria-hidden="true"
focusable="false"
/>
<SendIcon
v-else
class="send-icon"
aria-hidden="true"
focusable="false"
/>
<SendIcon v-else class="send-icon" aria-hidden="true" focusable="false" />
</FunctionButton>
</div>
</template>
Expand Down Expand Up @@ -119,7 +121,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
& svg {
stroke: var(--knime-dove-gray);

& .send-icon,
& .send-icon,
& .abort-icon {
margin-left: -1px;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@
import WrenchIcon from "@knime/styles/img/icons/wrench.svg";

import AnimatedEllipsis from "./AnimatedEllipsis.vue";

defineProps<{
label: string;
}>();
</script>

<template>
<div class="tool-use-indicator-container">
<div class="status-indicator-container">
<div class="icon">
<WrenchIcon />
</div>
<span>Using tools<AnimatedEllipsis /></span>
<span>{{ label }}<AnimatedEllipsis /></span>
</div>
</template>

<style lang="postcss" scoped>
@import url("@knime/styles/css/mixins");

.tool-use-indicator-container {
.status-indicator-container {
font-size: 12px;
margin-bottom: var(--space-8);
align-self: flex-start;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,20 @@ import { mount } from "@vue/test-utils";
import { createTestingPinia } from "@pinia/testing";
import { setActivePinia } from "pinia";

import { useChatStore } from "@/stores/chat";
import {
createAiMessage,
createToolMessage,
createUserMessage,
} from "@/test/factories/messages";
import type {
AiMessage,
ErrorMessage,
HumanMessage,
Timeline,
ToolMessage,
} from "@/types";
import { useChatStore } from "@/stores/chat";
import ChatInterface from "../ChatInterface.vue";
import {
createAiMessage,
createToolMessage,
createUserMessage,
} from "@/test/factories/messages";
import { nextTick } from "process";

vi.mock("@/composables/useScrollToBottom", () => ({
useScrollToBottom: vi.fn(),
Expand Down Expand Up @@ -50,8 +49,9 @@ describe("ChatInterface", () => {
ExpandableTimeline: {
template: "<div class='timeline'>Timeline</div>",
},
ToolUseIndicator: {
template: "<div class='tool-indicator'>Using tools...</div>",
StatusIndicator: {
props: ["label"],
template: "<div class='status-indicator'>{{ label }}</div>",
},
MessageInput: {
template: "<div class='message-input'>Message Input</div>",
Expand Down Expand Up @@ -118,7 +118,7 @@ describe("ChatInterface", () => {
expect(wrapper.find(".timeline").exists()).toBe(true);
});

it("shows tool use indicator when store indicates tools are being used", async () => {
it("shows 'Using tools' indicator when store indicates tools are being used", async () => {
const wrapper = createWrapper();
const chatStore = useChatStore();

Expand All @@ -136,17 +136,42 @@ describe("ChatInterface", () => {
};
await wrapper.vm.$nextTick();

expect(wrapper.find(".tool-indicator").exists()).toBe(true);
const statusIndicator = wrapper.find(".status-indicator");
expect(statusIndicator.exists()).toBe(true);
expect(statusIndicator.text()).toBe("Using tools");
});

it("shows 'Cancelling' in status indicator when interrupted", async () => {
const wrapper = createWrapper();
const chatStore = useChatStore();

chatStore.isLoading = true;
chatStore.lastMessage = {
type: "tool",
content: "",
toolCallId: "123",
id: "tool1",
};
chatStore.config = {
show_tool_calls_and_results: false,
reexecution_trigger: "NONE",
};
chatStore.isInterrupted = true;
await wrapper.vm.$nextTick();

const statusIndicator = wrapper.find(".status-indicator");
expect(statusIndicator.exists()).toBe(true);
expect(statusIndicator.text()).toBe("Cancelling");
});

it("does not show tool use indicator when store indicates tools are not being used", () => {
it("does not show status indicator when store indicates tools are not being used", () => {
const wrapper = createWrapper();
const chatStore = useChatStore();

chatStore.isLoading = false;
chatStore.lastMessage = createAiMessage("AI response");

expect(wrapper.find(".tool-indicator").exists()).toBe(false);
expect(wrapper.find(".status-indicator").exists()).toBe(false);
});

it("shows generic loading indicator when store indicates generic loading", async () => {
Expand Down Expand Up @@ -184,7 +209,7 @@ describe("ChatInterface", () => {
// Should only contain MessageInput, no messages or indicators
expect(wrapper.find(".ai-message").exists()).toBe(false);
expect(wrapper.find(".human-message").exists()).toBe(false);
expect(wrapper.find(".tool-indicator").exists()).toBe(false);
expect(wrapper.find(".status-indicator").exists()).toBe(false);
expect(wrapper.find(".skeleton-item").exists()).toBe(false);
});

Expand Down Expand Up @@ -212,9 +237,9 @@ describe("ChatInterface", () => {
};
await wrapper.vm.$nextTick();

// Should show both the message and the tool indicator
// Should show both the message and the status indicator
expect(wrapper.find(".human-message").exists()).toBe(true);
expect(wrapper.find(".tool-indicator").exists()).toBe(true);
expect(wrapper.find(".status-indicator").exists()).toBe(true);
expect(wrapper.find(".skeleton-item").exists()).toBe(false);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ describe("MessageInput", () => {
it("calls store sendUserMessage and clears input on send button click", async () => {
const wrapper = createWrapper();
const chatStore = useChatStore();
const sendUserMessageSpy = vi.spyOn(chatStore, "sendUserMessage").mockResolvedValue();
const sendUserMessageSpy = vi
.spyOn(chatStore, "sendUserMessage")
.mockResolvedValue();
const textarea = wrapper.find("textarea");

await textarea.setValue("Test message");
Expand All @@ -84,7 +86,9 @@ describe("MessageInput", () => {
it("calls store cancelAgent when send button is clicked while loading", async () => {
const wrapper = createWrapper();
const chatStore = useChatStore();
const cancelAgentSpy = vi.spyOn(chatStore, "cancelAgent").mockResolvedValue();
const cancelAgentSpy = vi
.spyOn(chatStore, "cancelAgent")
.mockResolvedValue();

chatStore.isLoading = true;
await wrapper.vm.$nextTick();
Expand All @@ -97,8 +101,12 @@ describe("MessageInput", () => {
it("does not call handleSubmit on Enter while loading", async () => {
const wrapper = createWrapper();
const chatStore = useChatStore();
const sendUserMessageSpy = vi.spyOn(chatStore, "sendUserMessage").mockResolvedValue();
const cancelAgentSpy = vi.spyOn(chatStore, "cancelAgent").mockResolvedValue();
const sendUserMessageSpy = vi
.spyOn(chatStore, "sendUserMessage")
.mockResolvedValue();
const cancelAgentSpy = vi
.spyOn(chatStore, "cancelAgent")
.mockResolvedValue();

chatStore.isLoading = true;
await wrapper.vm.$nextTick();
Expand All @@ -115,7 +123,9 @@ describe("MessageInput", () => {
it("calls store sendUserMessage and clears input on Enter key press without Shift", async () => {
const wrapper = createWrapper();
const chatStore = useChatStore();
const sendUserMessageSpy = vi.spyOn(chatStore, "sendUserMessage").mockResolvedValue();
const sendUserMessageSpy = vi
.spyOn(chatStore, "sendUserMessage")
.mockResolvedValue();
const textarea = wrapper.find("textarea");

await textarea.setValue("Enter message");
Expand All @@ -128,7 +138,9 @@ describe("MessageInput", () => {
it("does not send message on Enter if shift key is pressed", async () => {
const wrapper = createWrapper();
const chatStore = useChatStore();
const sendUserMessageSpy = vi.spyOn(chatStore, "sendUserMessage").mockResolvedValue();
const sendUserMessageSpy = vi
.spyOn(chatStore, "sendUserMessage")
.mockResolvedValue();
const textarea = wrapper.find("textarea");

await textarea.setValue("Shift+Enter message");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { mount } from "@vue/test-utils";

import StatusIndicator from "../StatusIndicator.vue";

describe("StatusIndicator", () => {
it("renders the label prop correctly", () => {
const wrapper = mount(StatusIndicator, {
props: {
label: "Test Label",
},
global: {
stubs: {
WrenchIcon: true,
AnimatedEllipsis: true,
},
},
});

expect(wrapper.text()).toContain("Test Label");
});
});
Loading