Skip to content

Commit ab6a532

Browse files
feat(lightspeed): add configurable sample prompts in lightspeed (#800)
* add configurable sample prompts in lightspeed * udpate configuration name * choose default prompts based on questionValidation setting
1 parent d7a0dd1 commit ab6a532

10 files changed

Lines changed: 320 additions & 29 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-lightspeed': patch
3+
---
4+
5+
Add configurable sample prompts

workspaces/lightspeed/plugins/lightspeed/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ lightspeed:
123123
url: <server_URL>
124124
token: <api_key>
125125
questionValidation: true # Optional - To disable question (prompt) validation set it to false.
126+
prompts: # optional
127+
- title: <prompt_title>
128+
- message: <prompt_message>
126129
```
127130

128131
`questionValidation` is default to be enabled with topic restriction on RHDH related topics.

workspaces/lightspeed/plugins/lightspeed/config.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,21 @@ export interface Config {
4343
* @visibility frontend
4444
*/
4545
questionValidation?: boolean;
46+
prompts?: Array</**
47+
* @visibility frontend
48+
*/
49+
{
50+
/**
51+
* The title of the prompt.
52+
* Displayed as the heading of the prompt.
53+
* @visibility frontend
54+
*/
55+
title: string;
56+
/**
57+
* The main question or message shown in the prompt.
58+
* @visibility frontend
59+
*/
60+
message: string;
61+
}>;
4662
};
4763
}

workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
useLastOpenedConversation,
4747
useLightspeedDeletePermission,
4848
} from '../hooks';
49+
import { useWelcomePrompts } from '../hooks/useWelcomePrompts';
4950
import { ConversationSummary } from '../types';
5051
import { getAttachments } from '../utils/attachment-utils';
5152
import {
@@ -143,7 +144,7 @@ export const LightspeedChat = ({
143144
const { data: conversations = [] } = useConversations();
144145
const { mutateAsync: deleteConversation } = useDeleteConversation();
145146
const { allowed: hasDeleteAccess } = useLightspeedDeletePermission();
146-
147+
const samplePrompts = useWelcomePrompts();
147148
React.useEffect(() => {
148149
if (user && lastOpenedId === null && isReady) {
149150
setConversationId(TEMP_CONVERSATION_ID);
@@ -307,35 +308,13 @@ export const LightspeedChat = ({
307308
const welcomePrompts =
308309
(newChatCreated && conversationMessages.length === 0) ||
309310
(!conversationFound && conversationMessages.length === 0)
310-
? [
311-
{
312-
title: 'Getting Started with Backstage',
313-
message:
314-
'Can you guide me through the first steps to start using Backstage as a developer, like exploring the Software Catalog and adding my service?',
315-
onClick: () =>
316-
sendMessage(
317-
'Can you guide me through the first steps to start using Backstage as a developer, like exploring the Software Catalog and adding my service?',
318-
),
319-
},
320-
{
321-
title: 'Get Help On Code Readability',
322-
message:
323-
'Can you suggest techniques I can use to make my code more readable and maintainable?',
324-
onClick: () =>
325-
sendMessage(
326-
'Can you suggest techniques I can use to make my code more readable and maintainable?',
327-
),
328-
},
329-
{
330-
title: 'Create An OpenShift Deployment',
331-
message:
332-
'Can you guide me through creating a new deployment in OpenShift for a containerized application?',
333-
onClick: () =>
334-
sendMessage(
335-
'Can you guide me through creating a new deployment in OpenShift for a containerized application?',
336-
),
311+
? samplePrompts?.map(prompt => ({
312+
title: prompt.title,
313+
message: prompt.message,
314+
onClick: () => {
315+
sendMessage(prompt.message);
337316
},
338-
]
317+
}))
339318
: [];
340319

341320
const handleFilter = React.useCallback((value: string) => {

workspaces/lightspeed/plugins/lightspeed/src/const.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { SamplePrompts } from './types';
2+
13
/*
24
* Copyright Red Hat, Inc.
35
*
@@ -18,3 +20,61 @@ export const TEMP_CONVERSATION_ID = 'temp-conversation-id';
1820
export const FUNCTION_DISCLAIMER_WITHOUT_QUESTION_VALIDATION = `Red Hat Developer Hub Lightspeed can answer questions on many topics using your configured models. Lightspeed's responses are influenced by the Red Hat Developer Hub documentation but Lightspeed does not have access to your Software Catalog, TechDocs, or Templates etc. Do not include personal or sensitive information in your input. Interactions with Developer Hub Lightspeed may be reviewed and used to improve products or services.`;
1921

2022
export const FUNCTION_DISCLAIMER = `Red Hat Developer Hub Lightspeed can answer questions on many topics using your configured models. Lightspeed's responses are influenced by the Red Hat Developer Hub documentation but Lightspeed does not have access to your Software Catalog, TechDocs, or Templates etc. Lightspeed uses question (prompt) validation to ensure that conversations remain focused on technical topics relevant to Red Hat Developer Hub, such as Backstage, Kubernetes, and OpenShift. Do not include personal or sensitive information in your input. Interactions with Developer Hub Lightspeed may be reviewed and used to improve products or services.`;
23+
24+
const createPrompt = (title: string, message: string) => {
25+
return { title, message };
26+
};
27+
28+
export const DEFAULT_SAMPLE_PROMPTS: SamplePrompts = [
29+
createPrompt(
30+
'Get Help On Code Readability',
31+
'Can you suggest techniques I can use to make my code more readable and maintainable?',
32+
),
33+
createPrompt(
34+
'Get Help With Debugging',
35+
'My application is throwing an error when trying to connect to the database. Can you help me identify the issue?',
36+
),
37+
createPrompt(
38+
'Explain a Development Concept',
39+
'Can you explain how microservices architecture works and its advantages over a monolithic design?',
40+
),
41+
createPrompt(
42+
'Suggest Code Optimizations',
43+
'Can you suggest common ways to optimize code to achieve better performance?',
44+
),
45+
createPrompt(
46+
'Documentation Summary',
47+
'Can you summarize the documentation for implementing OAuth 2.0 authentication in a web app?',
48+
),
49+
createPrompt(
50+
'Workflows With Git',
51+
'I want to make changes to code on another branch without loosing my existing work. What is the procedure to do this using Git?',
52+
),
53+
createPrompt(
54+
'Suggest Testing Strategies',
55+
'Can you recommend some common testing strategies that will make my application robust and error-free?',
56+
),
57+
createPrompt(
58+
'Demystify Sorting Algorithms',
59+
'Can you explain the difference between a quicksort and a mergesort algorithm, and when to use each?',
60+
),
61+
createPrompt(
62+
'Understand Event-Driven Architecture',
63+
'Can you explain what event-driven architecture is and when it’s beneficial to use it in software development?',
64+
),
65+
];
66+
67+
export const RHDH_SAMPLE_PROMPTS: SamplePrompts = [
68+
createPrompt(
69+
'Deploy With Tekton',
70+
'Can you help me automate the deployment of my application using Tekton pipelines?',
71+
),
72+
createPrompt(
73+
'Create An OpenShift Deployment',
74+
'Can you guide me through creating a new deployment in OpenShift for a containerized application?',
75+
),
76+
createPrompt(
77+
'Getting Started with Backstage',
78+
'Can you guide me through the first steps to start using Backstage as a developer, like exploring the Software Catalog and adding my service?',
79+
),
80+
];
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { ConfigApi, useApi } from '@backstage/core-plugin-api';
17+
18+
import { renderHook, waitFor } from '@testing-library/react';
19+
20+
import { useWelcomePrompts } from '../useWelcomePrompts';
21+
22+
jest.mock('@backstage/core-plugin-api', () => ({
23+
useApi: jest.fn(),
24+
}));
25+
26+
describe('useWelcomePrompts', () => {
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
(useApi as jest.Mock).mockReturnValue({
30+
getOptionalConfigArray: jest.fn(),
31+
getOptionalBoolean: jest.fn().mockReturnValue(false),
32+
});
33+
});
34+
35+
it('should return welcome prompts from user prompts', async () => {
36+
const getMockString = (prompt: { title: string; message: string }) =>
37+
({
38+
getString: jest.fn().mockImplementation(key => {
39+
return prompt[key as keyof typeof prompt];
40+
}),
41+
}) as unknown as ConfigApi;
42+
43+
const userPrompts = [
44+
{ title: 'User Prompt 1', message: 'Message 1' },
45+
{ title: 'User Prompt 2', message: 'Message 2' },
46+
{ title: 'User Prompt 3', message: 'Message 3' },
47+
];
48+
49+
(useApi as jest.Mock).mockReturnValue({
50+
getOptionalBoolean: jest.fn().mockReturnValue(true),
51+
getOptionalConfigArray: jest
52+
.fn()
53+
.mockReturnValue(userPrompts.map(getMockString)),
54+
});
55+
const { result } = renderHook(() => useWelcomePrompts());
56+
await waitFor(() => {
57+
expect(result.current).toBeDefined();
58+
expect(result.current.length).toBe(3);
59+
const userPromptsTitles = userPrompts.map(p => p.title);
60+
const [prompt1, prompt2, prompt3] = result.current;
61+
62+
expect(userPromptsTitles).toContain(prompt1.title);
63+
expect(userPromptsTitles).toContain(prompt2.title);
64+
expect(userPromptsTitles).toContain(prompt3.title);
65+
});
66+
});
67+
68+
it('should return welcome prompts from default prompts', async () => {
69+
const { result } = renderHook(() => useWelcomePrompts());
70+
await waitFor(() => {
71+
expect(result.current).toBeDefined();
72+
expect(result.current.length).toBe(3);
73+
});
74+
});
75+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import React from 'react';
17+
18+
import { ConfigApi, configApiRef, useApi } from '@backstage/core-plugin-api';
19+
20+
import { DEFAULT_SAMPLE_PROMPTS, RHDH_SAMPLE_PROMPTS } from '../const';
21+
import { SamplePrompts } from '../types';
22+
import { getRandomSamplePrompts } from '../utils/prompt-utils';
23+
24+
export const useWelcomePrompts = (): SamplePrompts => {
25+
const configApi: ConfigApi = useApi(configApiRef);
26+
27+
return React.useMemo(() => {
28+
const questionValidationEnabled =
29+
configApi.getOptionalBoolean('lightspeed.questionValidation') ?? true;
30+
31+
const DEFAULT_PROMPTS = questionValidationEnabled
32+
? RHDH_SAMPLE_PROMPTS
33+
: [...DEFAULT_SAMPLE_PROMPTS, ...RHDH_SAMPLE_PROMPTS];
34+
35+
const samplePrompts: SamplePrompts = (
36+
configApi?.getOptionalConfigArray('lightspeed.prompts') ?? []
37+
).map(config => ({
38+
title: config.getString('title') ?? '',
39+
message: config.getString('message') ?? '',
40+
}));
41+
return getRandomSamplePrompts(samplePrompts, DEFAULT_PROMPTS);
42+
}, [configApi]);
43+
};

workspaces/lightspeed/plugins/lightspeed/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,8 @@ export type Attachment = {
6969
};
7070

7171
export type ConversationList = ConversationSummary[];
72+
73+
export type SamplePrompts = {
74+
title: string;
75+
message: string;
76+
}[];
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { SamplePrompts } from '../../types';
17+
import { getRandomSamplePrompts } from '../prompt-utils';
18+
19+
describe('getRandomSamplePrompts', () => {
20+
it('should return empty array from default prompts and userPrompts is undefined', () => {
21+
const result = getRandomSamplePrompts(undefined, undefined);
22+
expect(result.length).toBe(0);
23+
});
24+
25+
it('should return empty array from default prompts and userPrompts length is equal to 0', () => {
26+
const userPrompts: SamplePrompts = [];
27+
const defaultPrompts: SamplePrompts = [];
28+
29+
const result = getRandomSamplePrompts(userPrompts, defaultPrompts);
30+
expect(result.length).toBe(0);
31+
});
32+
33+
it('should return 3 random prompts from userPrompts if userPrompts length is greater than or equal to 3', () => {
34+
const userPrompts: SamplePrompts = [
35+
{ title: 'Prompt 1', message: 'Message 1' },
36+
{ title: 'Prompt 2', message: 'Message 2' },
37+
{ title: 'Prompt 3', message: 'Message 3' },
38+
{ title: 'Prompt 4', message: 'Message 4' },
39+
];
40+
const defaultPrompts: SamplePrompts = [
41+
{ title: 'Default Prompt 1', message: 'Default Message 1' },
42+
{ title: 'Default Prompt 2', message: 'Default Message 2' },
43+
];
44+
45+
const result = getRandomSamplePrompts(userPrompts, defaultPrompts, 3);
46+
expect(result.length).toBe(3);
47+
const [prompt1, prompt2, prompt3] = result;
48+
const userPromptsTitles = userPrompts.map(prompt => prompt.title);
49+
expect(userPromptsTitles).toContain(prompt1.title);
50+
expect(userPromptsTitles).toContain(prompt2.title);
51+
expect(userPromptsTitles).toContain(prompt3.title);
52+
});
53+
54+
it('should return 2 random prompts from default prompts if userPrompts length is equal to 0', () => {
55+
const userPrompts: SamplePrompts = [];
56+
const defaultPrompts: SamplePrompts = [
57+
{ title: 'Default Prompt 1', message: 'Default Message 1' },
58+
{ title: 'Default Prompt 2', message: 'Default Message 2' },
59+
];
60+
61+
const result = getRandomSamplePrompts(userPrompts, defaultPrompts);
62+
expect(result.length).toBe(2);
63+
const [prompt1, prompt2] = result;
64+
const defaultPromptsTitles = defaultPrompts.map(prompt => prompt.title);
65+
expect(defaultPromptsTitles).toContain(prompt1.title);
66+
expect(defaultPromptsTitles).toContain(prompt2.title);
67+
});
68+
});

0 commit comments

Comments
 (0)