Skip to content

Commit

Permalink
feat(lightspeed): add a new lightspeed plugin with basic implementati…
Browse files Browse the repository at this point in the history
…on of chat (#1889)
  • Loading branch information
rohitkrai03 committed Jul 29, 2024
1 parent aec9eb8 commit cb80e38
Show file tree
Hide file tree
Showing 21 changed files with 609 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ yarn.lock @janus-idp/maintainers-plugins
/plugins/analytics-module-matomo @janus-idp/maintainers-plugins @janus-idp/devex-uxe
/plugins/kiali @janus-idp/maintainers-plugins @janus-idp/kiali
/plugins/kiali-backend @janus-idp/maintainers-plugins @janus-idp/kiali
/plugins/lightspeed @janus-idp/maintainers-plugins @rohitkrai03
/plugins/quay @janus-idp/rhtap
/plugins/tekton @janus-idp/rhtap
/plugins/argocd @janus-idp/rhtap
1 change: 1 addition & 0 deletions plugins/lightspeed/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
61 changes: 61 additions & 0 deletions plugins/lightspeed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Lightspeed plugin for Backstage

The Lightspeed plugin enables you to interact with any LLM server running a model with OpenAI's API compatibility.

## For administrators

### Installation

1. Install the Lightspeed plugin using the following command:

```console
yarn workspace app add @janus-idp/backstage-plugin-lightspeed
```

### Configuration

1. Set the proxy to the desired LLM server in the `app-config.yaml` file as follows:

```yaml title="app-config.yaml"
proxy:
'/lightspeed/api':
target: http://localhost:11434/v1/
headers:
Authorization: Bearer <token>
```

2. Add a new nav item **Lightspeed** in App `packages/app/src/App.tsx`:

```tsx title="packages/app/src/components/App.tsx"
/* highlight-add-next-line */ import { LightspeedPage } from '@janus-idp/backstage-plugin-lightspeed';

<Route path="/lightspeed" element={<LightspeedPage />} />;
```

3. Enable **Lightspeed** page in `packages/app/src/components/Root/Root.tsx`:

```tsx title="packages/app/src/components/Root/Root.tsx"
/* highlight-add-next-line */ import { LightspeedIcon } from '@janus-idp/backstage-plugin-lightspeed';

<SidebarItem
icon={LightspeedIcon as IconComponent}
to="lightspeed"
text="Lightspeed"
/>;
```

## For users

### Using the Lightspeed plugin in Backstage

Lightspeed is a front-end plugin that enables you to interact with any LLM server running a model with OpenAI's API compatibility.

#### Prerequisites

- Your Backstage application is installed and running.
- You have installed the Lightspeed plugin. For installation process, see [Installation](#installation).

#### Procedure

1. Open your Backstage application and select a Lightspeed nav item from the **Navigation**.
2. Ask you questions to the Lightspeed chatbot.
14 changes: 14 additions & 0 deletions plugins/lightspeed/app-config.janus-idp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
dynamicPlugins:
frontend:
janus-idp.backstage-plugin-lightspeed:
appIcons:
- name: LightspeedIcon
module: LightspeedPlugin
importName: LightspeedIcon
dynamicRoutes:
- path: /lightspeed
importName: LightspeedPage
module: LightspeedPlugin
menuItem:
icon: LightspeedIcon
text: Lightspeed
1 change: 1 addition & 0 deletions plugins/lightspeed/config.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export interface Config {}
14 changes: 14 additions & 0 deletions plugins/lightspeed/dev/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';

import { createDevApp } from '@backstage/dev-utils';

import { LightspeedPage, lightspeedPlugin } from '../src/plugin';

createDevApp()
.registerPlugin(lightspeedPlugin)
.addPage({
element: <LightspeedPage />,
title: 'Lightspeed Page',
path: '/lightspeed',
})
.render();
1 change: 1 addition & 0 deletions plugins/lightspeed/images/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 84 additions & 0 deletions plugins/lightspeed/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"name": "@janus-idp/backstage-plugin-lightspeed",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "frontend-plugin",
"supported-versions": "1.28.4",
"pluginId": "lightspeed",
"pluginPackages": [
"@janus-idp/backstage-plugin-lightspeed"
]
},
"sideEffects": false,
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"export-dynamic": "janus-cli package export-dynamic-plugin",
"lint": "backstage-cli package lint",
"postpack": "backstage-cli package postpack",
"postversion": "yarn run export-dynamic",
"prepack": "backstage-cli package prepack",
"start": "backstage-cli package start",
"test": "backstage-cli package test --passWithNoTests --coverage",
"tsc": "tsc"
},
"dependencies": {
"@backstage/core-components": "^0.14.9",
"@backstage/core-plugin-api": "^1.9.3",
"@backstage/theme": "^0.5.6",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.11.3",
"@material-ui/lab": "^4.0.0-alpha.61",
"@mui/icons-material": "^5.15.18",
"openai": "^4.52.6",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "16.13.1 || ^17.0.0 || ^18.0.0"
},
"devDependencies": {
"@backstage/cli": "0.26.11",
"@backstage/core-app-api": "1.14.1",
"@backstage/dev-utils": "1.0.36",
"@backstage/test-utils": "1.5.9",
"@janus-idp/cli": "1.13.0",
"@testing-library/jest-dom": "6.4.8",
"@testing-library/react": "14.3.1",
"@testing-library/react-hooks": "8.0.1",
"@testing-library/user-event": "14.5.2",
"msw": "1.3.3"
},
"files": [
"dist",
"config.d.ts",
"dist-scalprum",
"app-config.janus-idp.yaml"
],
"scalprum": {
"name": "janus-idp.backstage-plugin-lightspeed",
"exposedModules": {
"LightspeedPlugin": "./src/index.ts"
}
},
"configSchema": "config.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/janus-idp/backstage-plugins",
"directory": "plugins/lightspeed"
},
"keywords": [
"backstage",
"plugin"
],
"homepage": "https://janus-idp.io/",
"bugs": "https://github.com/janus-idp/backstage-plugins/issues"
}
9 changes: 9 additions & 0 deletions plugins/lightspeed/src/components/LightspeedIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as React from 'react';

import logo from '../../images/logo.svg';

export const LightspeedIcon = () => {
return (
<img src={logo as any} alt="lightspeed icon" style={{ height: '25px' }} />
);
};
77 changes: 77 additions & 0 deletions plugins/lightspeed/src/components/LightspeedInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';

import Button from '@material-ui/core/Button';
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import SendIcon from '@material-ui/icons/Send';

const useStyles = makeStyles((theme: Theme) =>
createStyles({
wrapForm: {
display: 'flex',
justifyContent: 'center',
width: '98%',
margin: `${theme.spacing(0)} auto`,
},
wrapText: {
width: '100%',
},
button: {
margin: theme.spacing(0),
},
}),
);

type LightspeedInputProps = {
onSubmit: (prompt: string) => void;
};

export const LightspeedInput: React.FC<LightspeedInputProps> = ({
onSubmit,
}) => {
const classes = useStyles();

const [prompt, setPrompt] = React.useState('');

const handleInputChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setPrompt(e.target.value);
},
[],
);

const handleSubmit = React.useCallback(
(e: React.FormEvent) => {
e.preventDefault();
onSubmit(prompt);
setPrompt('');
},
[onSubmit, prompt],
);

return (
<form
className={classes.wrapForm}
noValidate
autoComplete="off"
onSubmit={handleSubmit}
>
<TextField
id="standard-text"
label="Ask Lightspeed"
className={classes.wrapText}
value={prompt}
onChange={handleInputChange}
/>
<Button
type="submit"
variant="contained"
color="primary"
className={classes.button}
onSubmit={handleSubmit}
>
<SendIcon />
</Button>
</form>
);
};
114 changes: 114 additions & 0 deletions plugins/lightspeed/src/components/LightspeedPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React from 'react';

import { Content, Header, HeaderLabel, Page } from '@backstage/core-components';
import { configApiRef, useApi } from '@backstage/core-plugin-api';

import { Paper } from '@material-ui/core';
import { createStyles, makeStyles } from '@material-ui/core/styles';
import OpenAI from 'openai';

import { LightspeedInput } from './LightspeedInput';
import { SystemMessage, UserMessage } from './Message';

const useStyles = makeStyles(() =>
createStyles({
paper: {
width: '70%',
height: '100%',
maxHeight: '740px',
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
position: 'relative',
},
container: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
messagesBody: {
width: 'calc( 100% - 20px )',
padding: 10,
overflowY: 'scroll',
height: 'calc( 100% - 80px )',
},
}),
);

export const LightspeedPage = () => {
const classes = useStyles();

const configApi = useApi(configApiRef);

const [, setChunkIndex] = React.useState(0);
const [prompts, setPrompts] = React.useState<string[]>([]);
const [completions, setCompletions] = React.useState<{
[key: string]: string;
}>({});
const backendUrl = configApi.getString('backend.baseUrl');
const openai = new OpenAI({
baseURL: `${backendUrl}/api/proxy/lightspeed/api`,

// required but ignored
apiKey: 'random-key',
dangerouslyAllowBrowser: true,
});

const handleInputPrompt = React.useCallback(
async (prompt: string) => {
setPrompts(p => [...p, prompt]);
setChunkIndex(0);

const result = await openai.chat.completions.create({
messages: [
{
role: 'system',
content:
'You are a helpful assistant that can answer question in Red Hat Developer Hub.',
},
{ role: 'user', content: prompt },
],
model: 'llama3',
stream: true,
});

for await (const chunk of result) {
setChunkIndex(index => index + 1);
setCompletions(c => {
// console.log('string ---', s);
c[prompt] =
`${c[prompt] || ''}${chunk.choices[0]?.delta?.content || ''}`;
return c;
});
}
},
[openai.chat.completions],
);

return (
<Page themeId="tool">
<Header
title="Red Hat Developer Hub Lightspeed"
subtitle="A new way to interact with LLMs inside Developer Hub."
>
<HeaderLabel label="Owner" value="Team X" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
<Content className={classes.container}>
<Paper className={classes.paper} elevation={2}>
<Paper id="style-1" className={classes.messagesBody}>
{prompts.map(prompt => (
<>
{prompt && <UserMessage message={prompt} />}
{completions[prompt] && (
<SystemMessage message={completions[prompt]} />
)}
</>
))}
</Paper>
<LightspeedInput onSubmit={handleInputPrompt} />
</Paper>
</Content>
</Page>
);
};
Loading

0 comments on commit cb80e38

Please sign in to comment.