-
Notifications
You must be signed in to change notification settings - Fork 139
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(lightspeed): add a new lightspeed plugin with basic implementati…
…on of chat (#1889)
- Loading branch information
1 parent
aec9eb8
commit cb80e38
Showing
21 changed files
with
609 additions
and
1 deletion.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export interface Config {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' }} /> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
Oops, something went wrong.