Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add kyma companion #2861

Open
wants to merge 52 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
aba43fa
feat: ai assistant UI panel with tabs
chriskari Mar 19, 2024
92d5f64
feat: added PageInsights Tab components
chriskari Mar 20, 2024
361de38
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Mar 27, 2024
82fc74a
feat: added loading indicator & connected initial suggestions with ch…
chriskari Apr 2, 2024
6d6c992
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Apr 2, 2024
d37e6b7
fix: disable input while waiting for response & hide open-Button when…
chriskari Apr 2, 2024
092089d
feat: better design of open-assistant-popover
chriskari Apr 3, 2024
e4311d2
feat: added error handling to inital-suggestions-popover
chriskari Apr 3, 2024
07ae000
fix: error bug in suggestions popover & disabled buttons
chriskari Apr 4, 2024
41746e0
feat: added some markdown formatting for responses of LLM
chriskari Apr 5, 2024
48375aa
fix: markdown formatting adjustments
chriskari Apr 8, 2024
5dd6300
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Apr 8, 2024
95014c1
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Apr 8, 2024
33062da
feat: initial suggestions now depend on currently opened resource
chriskari Apr 9, 2024
e7bb231
feat: assistant closes when open resource changes
chriskari Apr 9, 2024
23901af
feat: added more error handling & fullscreen-mode
chriskari Apr 10, 2024
82e2d4d
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Apr 11, 2024
bd71c24
fix: small code clean-up
chriskari Apr 11, 2024
9f119b4
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Apr 12, 2024
9d00183
feat: added streaming of responses to UI
chriskari Apr 12, 2024
e07c5bf
feat: added ui elements for streaming of message chunks
chriskari Apr 14, 2024
124abed
fix: adjusted styling in fullscreen-mode
chriskari Apr 15, 2024
72f404f
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Apr 15, 2024
1d66cdc
fix: adjusted init-call format to latest changes
chriskari Apr 15, 2024
2d46a44
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Apr 15, 2024
b1c51f5
fix: added funny follow-up questions and fixed styling issue
chriskari Apr 15, 2024
2ff2aea
feat: creation of sessionID
chriskari Apr 16, 2024
b478607
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Apr 16, 2024
bec9f2a
fix: bug with hashing json objects
chriskari Apr 16, 2024
4f1fe8e
feat: added uuid fingerprint to sessionID
chriskari Apr 16, 2024
64282b5
feat: added follow-up questions to Companion-UI
chriskari Apr 23, 2024
74894d8
feat: layout is now rezisable
chriskari Apr 23, 2024
97b64b1
fix: resizing bugs
chriskari Apr 23, 2024
b888e10
fix: some smaller styling adjustments
chriskari Apr 23, 2024
a250767
added error handling and fixed two-chunks-bug
chriskari Apr 23, 2024
c90ab56
fix: cleared unique-key console-warnings
chriskari Apr 23, 2024
592c78d
fix: assistant remains open on edit-view
chriskari Apr 24, 2024
16cebd5
feat: added markdown formatting of links
chriskari Apr 24, 2024
a280294
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Apr 26, 2024
d54429a
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Apr 30, 2024
6d786bc
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Apr 30, 2024
d00dbb7
feat: switched to secure internal cluster endpoints
chriskari Apr 30, 2024
821806c
fix: get token from right path in code
chriskari May 14, 2024
811bc9c
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari May 14, 2024
e7002b8
fix: mistake from merging with main
chriskari May 14, 2024
c111cc3
fix: mistake from merging with main
chriskari May 14, 2024
9d3ceff
fix: added correct token path also for other api calls
chriskari May 14, 2024
66c6f04
fix: adjusted response parsing to handle nested braces
chriskari May 16, 2024
c41783b
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari May 16, 2024
0158821
fix: improved method name
chriskari May 16, 2024
51ed6ad
fix: small styling issue
chriskari May 16, 2024
087430e
fix: css container query
chriskari May 16, 2024
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
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"cronstrue": "^1.114.0",
"didyoumean": "^1.2.2",
"file-saver": "^2.0.2",
"fingerprintjs2": "^2.1.4",
"graphviz-react": "^1.2.5",
"http-status-codes": "^2.2.0",
"i18next": "^22.0.4",
Expand Down
17 changes: 17 additions & 0 deletions public/i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ horizontal-pod-autoscalers: Horizontal Pod Autoscalers
subscriptions: Subscriptions
apps:
title: Apps
ai-assistant:
name: Joule
opener:
use-ai: AI Companion
suggestions: Suggestions
input-placeholder: Ask about this resource
error-message: Couldn't fetch suggestions. Please try again.
error:
title: Service is interrupted
subtitle: A temporary interruption occured. Please try again.
introduction1: Hello there,
introduction2: How can I help you?
placeholder: Type something
tabs:
chat: Chat
page-insights: Page Insights
cluster-overview:
headers:
cpu: CPU
Expand Down Expand Up @@ -264,6 +280,7 @@ common:
remove-all: Remove all
reset: Reset
restart: Restart
retry: Retry
save: Save
submit: Submit
update: Update
Expand Down
74 changes: 74 additions & 0 deletions src/components/AIassistant/api/getChatResponse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { getClusterConfig } from 'state/utils/getBackendInfo';
import { parseWithNestedBrackets } from '../utils/parseNestedBrackets';

export default async function getChatResponse({
prompt,
handleChatResponse,
handleError,
sessionID,
clusterUrl,
token,
certificateAuthorityData,
}) {
const { backendAddress } = getClusterConfig();
const url = `${backendAddress}/api/v1/namespaces/ai-core/services/http:ai-backend-clusterip:5000/proxy/api/v1/chat`;
const payload = { question: prompt, session_id: sessionID };
const k8sAuthorization = `Bearer ${token}`;

fetch(url, {
headers: {
accept: 'application/json',
'content-type': 'application/json',
'X-Cluster-Certificate-Authority-Data': certificateAuthorityData,
'X-Cluster-Url': clusterUrl,
'X-K8s-Authorization': k8sAuthorization,
'X-User': sessionID,
},
body: JSON.stringify(payload),
method: 'POST',
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
readChunk(reader, decoder, handleChatResponse, handleError, sessionID);
})
.catch(error => {
handleError();
console.error('Error fetching data:', error);
});
}

function readChunk(
reader,
decoder,
handleChatResponse,
handleError,
sessionID,
) {
reader
.read()
.then(({ done, value }) => {
if (done) {
return;
}
// Also handles the rare case of two chunks being sent at once
const receivedString = decoder.decode(value, { stream: true });
const chunks = parseWithNestedBrackets(receivedString).map(chunk => {
return JSON.parse(chunk);
});
chunks.forEach(chunk => {
if ('error' in chunk) {
throw new Error(chunk.error);
}
handleChatResponse(chunk);
});
readChunk(reader, decoder, handleChatResponse, handleError, sessionID);
})
.catch(error => {
handleError();
console.error('Error reading stream:', error);
});
}
32 changes: 32 additions & 0 deletions src/components/AIassistant/api/getFollowUpQuestions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { getClusterConfig } from 'state/utils/getBackendInfo';

export default async function getFollowUpQuestions({
sessionID = '',
handleFollowUpQuestions,
clusterUrl,
token,
certificateAuthorityData,
}) {
try {
const { backendAddress } = getClusterConfig();
const url = `${backendAddress}/api/v1/namespaces/ai-core/services/http:ai-backend-clusterip:5000/proxy/api/v1/llm/followup`;
const payload = JSON.parse(`{"session_id":"${sessionID}"}`);
const k8sAuthorization = `Bearer ${token}`;

let { results } = await fetch(url, {
headers: {
accept: 'application/json',
'content-type': 'application/json',
'X-Cluster-Certificate-Authority-Data': certificateAuthorityData,
'X-Cluster-Url': clusterUrl,
'X-K8s-Authorization': k8sAuthorization,
'X-User': sessionID,
},
body: JSON.stringify(payload),
method: 'POST',
}).then(result => result.json());
handleFollowUpQuestions(results);
} catch (error) {
console.error('Error fetching data:', error);
}
}
42 changes: 42 additions & 0 deletions src/components/AIassistant/api/getPromptSuggestions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { getClusterConfig } from 'state/utils/getBackendInfo';
import { extractApiGroup } from 'resources/Roles/helpers';

export default async function getPromptSuggestions({
namespace = '',
resourceType = '',
groupVersion = '',
resourceName = '',
sessionID = '',
clusterUrl,
token,
certificateAuthorityData,
}) {
try {
const { backendAddress } = getClusterConfig();
const url = `${backendAddress}/api/v1/namespaces/ai-core/services/http:ai-backend-clusterip:5000/proxy/api/v1/llm/init`;
const apiGroup = extractApiGroup(groupVersion);
const payload = JSON.parse(
`{"resource_type":"${resourceType.toLowerCase()}${
apiGroup.length ? `.${apiGroup}` : ''
}","resource_name":"${resourceName}","namespace":"${namespace}","session_id":"${sessionID}"}`,
);
const k8sAuthorization = `Bearer ${token}`;

let { results } = await fetch(url, {
headers: {
accept: 'application/json',
'content-type': 'application/json',
'X-Cluster-Certificate-Authority-Data': certificateAuthorityData,
'X-Cluster-Url': clusterUrl,
'X-K8s-Authorization': k8sAuthorization,
'X-User': sessionID,
},
body: JSON.stringify(payload),
method: 'POST',
}).then(result => result.json());
return results;
} catch (error) {
console.error('Error fetching data:', error);
return false;
}
}
162 changes: 162 additions & 0 deletions src/components/AIassistant/components/AIOpener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import {
Button,
CustomListItem,
FlexBox,
Icon,
Input,
List,
Loader,
Popover,
Text,
Title,
} from '@ui5/webcomponents-react';
import { spacing } from '@ui5/webcomponents-react-base';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom';
import { initialPromptState } from '../state/initalPromptAtom';
import getPromptSuggestions from 'components/AIassistant/api/getPromptSuggestions';
import { createPortal } from 'react-dom';
import { authDataState } from 'state/authDataAtom';
import { sessionIDState } from '../state/sessionIDAtom';
import generateSessionID from '../utils/generateSesssionID';
import './AIOpener.scss';
import { clusterState } from 'state/clusterAtom';

export default function AIOpener({
namespace,
resourceType,
groupVersion,
resourceName,
}) {
const { t } = useTranslation();
const [showAssistant, setShowAssistant] = useRecoilState(
showAIassistantState,
);
const setInitialPrompt = useSetRecoilState(initialPromptState);
const [popoverOpen, setPopoverOpen] = useState(false);
const [suggestions, setSuggestions] = useState([]);
const [inputValue, setInputValue] = useState('');
const [errorOccured, setErrorOccured] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const authData = useRecoilValue(authDataState);
const setSessionID = useSetRecoilState(sessionIDState);
const cluster = useRecoilValue(clusterState);

const fetchSuggestions = async () => {
setErrorOccured(false);
setPopoverOpen(true);
if (!isLoading && suggestions.length === 0) {
setIsLoading(true);
const sessionID = await generateSessionID(authData);
setSessionID(sessionID);
const promptSuggestions = await getPromptSuggestions({
namespace,
resourceType,
groupVersion,
resourceName,
sessionID,
clusterUrl: cluster.currentContext.cluster.cluster.server,
token: authData.token,
certificateAuthorityData:
cluster.currentContext.cluster.cluster['certificate-authority-data'],
});
setIsLoading(false);
if (!promptSuggestions) {
setErrorOccured(true);
} else {
setSuggestions(promptSuggestions);
}
}
};

const sendInitialPrompt = prompt => {
setInitialPrompt(prompt);
setPopoverOpen(false);
setShowAssistant({
show: true,
fullScreen: false,
});
};

const onSubmitInput = () => {
if (inputValue.length === 0) return;
const prompt = inputValue;
setInputValue('');
sendInitialPrompt(prompt);
};

return (
<>
<Button
icon="ai"
className="ai-button"
id="openPopoverBtn"
disabled={showAssistant.show}
onClick={fetchSuggestions}
>
{t('ai-assistant.opener.use-ai')}
</Button>
{createPortal(
<Popover
className="suggestions-popover"
open={popoverOpen}
onAfterClose={() => setPopoverOpen(false)}
opener="openPopoverBtn"
placementType="Bottom"
horizontalAlign="Right"
>
<Input
icon={<Icon name="paper-plane" onClick={onSubmitInput} />}
value={inputValue}
onKeyDown={e => e.key === 'Enter' && onSubmitInput()}
onInput={e => setInputValue(e.target.value)}
placeholder={t('ai-assistant.opener.input-placeholder')}
className="popover-input"
/>
<Title level="H5" style={spacing.sapUiTinyMargin}>
{t('ai-assistant.opener.suggestions')}
</Title>
{errorOccured || (!isLoading && suggestions.length === 0) ? (
<FlexBox
alignItems="Center"
direction="Column"
style={{ gap: '8px', ...spacing.sapUiTinyMarginBottom }}
>
<Text style={spacing.sapUiTinyMargin}>
{t('ai-assistant.opener.error-message')}
</Text>
<Button onClick={fetchSuggestions}>
{t('common.buttons.retry')}
</Button>
</FlexBox>
) : isLoading ? (
<div
style={{
...spacing.sapUiTinyMargin,
...spacing.sapUiSmallMarginTop,
}}
>
<Loader progress="60%" />
</div>
) : (
<List style={spacing.sapUiTinyMarginTop}>
{suggestions.map((suggestion, index) => (
<CustomListItem
key={index}
onClick={() => sendInitialPrompt(suggestion)}
className="custom-list-item"
>
<Text className="text">{suggestion}</Text>
<Icon name="navigation-right-arrow" />
</CustomListItem>
))}
</List>
)}
</Popover>,
document.body,
)}
</>
);
}
Loading
Loading