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: reuse basic framework of the POC #3512

Open
wants to merge 1 commit into
base: kyma-companion
Choose a base branch
from
Open
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
17 changes: 17 additions & 0 deletions public/i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ common:
remove-all: Remove all
reset: Reset
restart: Restart
retry: Retry
save: Save
submit: Submit
update: Update
Expand Down Expand Up @@ -748,6 +749,22 @@ jobs:
kubeconfig-id:
error: "Couldn't load kubeconfig ID; configuration not changed (Error: ${{error}})"
must-be-an-object: Kubeconfig must be a JSON or YAML object.
kyma-companion:
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
kyma-modules:
unmanaged-modules-info: One of the modules is not managed and may not work properly. We cannot guarantee any service level agreement (SLA) or provide updates and maintenance for the module.
unmanaged-modules-save-warning: Before proceeding, be aware that disabling module management may impact the stability and data integrity of your cluster. Once the management is disabled, reverting back may not be possible. Are you sure you want to continue?
Expand Down
8 changes: 8 additions & 0 deletions src/components/App/App.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
#splitter-layout {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}

#html-wrap {
position: absolute;
top: 0;
Expand Down
90 changes: 60 additions & 30 deletions src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ import useSidebarCondensed from 'sidebar/useSidebarCondensed';
import { useGetValidationEnabledSchemas } from 'state/validationEnabledSchemasAtom';
import { useGetKymaResources } from 'state/kymaResourcesAtom';

import { SplitterElement, SplitterLayout } from '@ui5/webcomponents-react';
import { showKymaCompanionState } from 'components/KymaCompanion/state/showKymaCompanionAtom';
import KymaCompanion from 'components/KymaCompanion/components/KymaCompanion';

export default function App() {
const language = useRecoilValue(languageAtom);
const cluster = useRecoilValue(clusterState);
Expand Down Expand Up @@ -72,37 +76,63 @@ export default function App() {
useAfterInitHook(kubeconfigIdState);
useGetKymaResources();

const showCompanion = useRecoilValue(showKymaCompanionState);

return (
<div id="html-wrap">
<Header />
<div id="page-wrap">
<Sidebar key={cluster?.name} />
<ContentWrapper>
<Routes key={cluster?.name}>
<Route
path="*"
element={
<IncorrectPath
to="clusters"
message={t('components.incorrect-path.message.clusters')}
<SplitterLayout id="splitter-layout">
<SplitterElement
resizable={showCompanion.show}
size={
showCompanion.show
? showCompanion.fullScreen
? '0%'
: '70%'
: '100%'
}
>
<div id="html-wrap">
<Header />
<div id="page-wrap">
<Sidebar key={cluster?.name} />
<ContentWrapper>
<Routes key={cluster?.name}>
<Route
path="*"
element={
<IncorrectPath
to="clusters"
message={t('components.incorrect-path.message.clusters')}
/>
}
/>
<Route path="/" />
<Route path="clusters" element={<ClusterList />} />
<Route
path="cluster/:currentClusterName"
element={<Navigate to="overview" />}
/>
<Route
path="cluster/:currentClusterName/*"
element={<ClusterRoutes />}
/>
}
/>
<Route path="/" />
<Route path="clusters" element={<ClusterList />} />
<Route
path="cluster/:currentClusterName"
element={<Navigate to="overview" />}
/>
<Route
path="cluster/:currentClusterName/*"
element={<ClusterRoutes />}
/>
{makeGardenerLoginRoute()}
</Routes>
<Preferences />
</ContentWrapper>
</div>
</div>
{makeGardenerLoginRoute()}
</Routes>
<Preferences />
</ContentWrapper>
</div>
</div>
</SplitterElement>
{showCompanion.show ? (
<SplitterElement
resizable={!showCompanion.fullScreen}
size={showCompanion.fullScreen ? '100%' : '30%'}
minSize={350}
>
<KymaCompanion />
</SplitterElement>
) : (
<></>
)}
</SplitterLayout>
);
}
87 changes: 87 additions & 0 deletions src/components/KymaCompanion/api/getChatResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { getClusterConfig } from 'state/utils/getBackendInfo';
import { parseWithNestedBrackets } from '../utils/parseNestedBrackets';

type GetChatResponseArgs = {
prompt: string;
handleChatResponse: (chunk: any) => void;
handleError: () => void;
sessionID: string;
clusterUrl: string;
token: string;
certificateAuthorityData: string;
};

export default async function getChatResponse({
prompt,
handleChatResponse,
handleError,
sessionID,
clusterUrl,
token,
certificateAuthorityData,
}: GetChatResponseArgs): Promise<void> {
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();
if (!reader) {
throw new Error('Failed to get reader from response body');
}
const decoder = new TextDecoder();
readChunk(reader, decoder, handleChatResponse, handleError, sessionID);
})
.catch(error => {
handleError();
console.error('Error fetching data:', error);
});
}

function readChunk(
reader: ReadableStreamDefaultReader<Uint8Array>,
decoder: TextDecoder,
handleChatResponse: (chunk: any) => void,
handleError: () => void,
sessionID: string,
) {
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);
});
}
40 changes: 40 additions & 0 deletions src/components/KymaCompanion/api/getFollowUpQuestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { getClusterConfig } from 'state/utils/getBackendInfo';

interface GetFollowUpQuestionsParams {
sessionID?: string;
handleFollowUpQuestions: (results: any) => void;
clusterUrl: string;
token: string;
certificateAuthorityData: string;
}

export default async function getFollowUpQuestions({
sessionID = '',
handleFollowUpQuestions,
clusterUrl,
token,
certificateAuthorityData,
}: GetFollowUpQuestionsParams): Promise<void> {
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);
}
}
55 changes: 55 additions & 0 deletions src/components/KymaCompanion/api/getPromptSuggestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { getClusterConfig } from 'state/utils/getBackendInfo';
import { extractApiGroup } from 'resources/Roles/helpers';

interface GetPromptSuggestionsParams {
namespace?: string;
resourceType?: string;
groupVersion?: string;
resourceName?: string;
sessionID?: string;
clusterUrl: string;
token: string;
certificateAuthorityData: string;
}

// TODO add return type

export default async function getPromptSuggestions({
namespace = '',
resourceType = '',
groupVersion = '',
resourceName = '',
sessionID = '',
clusterUrl,
token,
certificateAuthorityData,
}: GetPromptSuggestionsParams): Promise<any[] | false> {
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;
}
}
31 changes: 31 additions & 0 deletions src/components/KymaCompanion/components/Chat/Chat.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.chat-container {
height: 100%;
overflow: hidden;

.chat-list {
display: flex;
flex-direction: column;
overflow: auto;
gap: 8px;

&::-webkit-scrollbar {
display: none;
}

.left-aligned {
align-self: flex-start;
background-color: var(--sapBackgroundColor);
border-radius: 8px 8px 8px 0;
}

.right-aligned {
align-self: flex-end;
background-color: var(--sapContent_Illustrative_Color1);
border-radius: 8px 8px 0 8px;

.text {
color: white;
}
}
}
}
Loading
Loading