├── .gitignore ├── .eslintrc.js ├── config.d.ts ├── tsconfig.json ├── src ├── setupTests.ts ├── index.ts ├── service │ ├── standaloneServer.ts │ ├── samples.ts │ ├── router.ts │ ├── openai.ts │ └── router.test.ts └── run.ts ├── .github └── workflows │ └── stale-issues.yaml ├── package.json ├── README.md └── .scribe.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | dist/ 4 | dist-types 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /config.d.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | /** 3 | * @visibility backend 4 | */ 5 | openai?: { 6 | /** 7 | * @visibility backend 8 | */ 9 | apiKey: string 10 | } 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/index.ts"], 3 | "exclude":["node_modules"], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "outDir": "dist-types/src", 9 | "declarationMap": true, 10 | "esModuleInterop":true, 11 | } 12 | } -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Backstage Authors 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 | 17 | export {}; 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Backstage Authors 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 | 17 | export * from './service/router'; 18 | -------------------------------------------------------------------------------- /.github/workflows/stale-issues.yaml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | workflow_dispatch: 6 | 7 | 8 | jobs: 9 | close-issues: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | steps: 14 | - uses: actions/stale@v5 15 | with: 16 | days-before-issue-stale: 15 17 | days-before-issue-close: 7 18 | stale-issue-label: "stale" 19 | stale-issue-message: "This issue is stale because it has been open for 15 days with no activity." 20 | close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale." 21 | repo-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /src/service/standaloneServer.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { createServiceBuilder, loadBackendConfig } from '@backstage/backend-common'; 4 | import { Server } from 'http'; 5 | import { Logger } from 'winston'; 6 | import { createRouter } from './router'; 7 | 8 | export interface ServerOptions { 9 | port: number; 10 | enableCors: boolean; 11 | logger: Logger; 12 | } 13 | 14 | export async function startStandaloneServer( 15 | options: ServerOptions, 16 | ): Promise { 17 | const logger = options.logger.child({ service: 'chatgpt-backend' }); 18 | const config = await loadBackendConfig({ logger, argv: process.argv }); 19 | 20 | const router = await createRouter({ 21 | logger, 22 | config 23 | }); 24 | 25 | let service = createServiceBuilder(module) 26 | .setPort(options.port) 27 | .addRouter('/api/chatgpt', router); 28 | service = service.enableCors({ origin: 'http://localhost:3000' }); 29 | 30 | 31 | return await service.start().catch(err => { 32 | logger.error(err); 33 | process.exit(1); 34 | }); 35 | } 36 | 37 | module.hot?.accept(); 38 | -------------------------------------------------------------------------------- /src/run.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Backstage Authors 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 | 17 | import { getRootLogger } from '@backstage/backend-common'; 18 | import yn from 'yn'; 19 | import { startStandaloneServer } from './service/standaloneServer'; 20 | 21 | const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; 22 | const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); 23 | const logger = getRootLogger(); 24 | 25 | startStandaloneServer({ port, enableCors, logger }).catch(err => { 26 | logger.error(err); 27 | process.exit(1); 28 | }); 29 | 30 | process.on('SIGINT', () => { 31 | logger.info('CTRL+C pressed; exiting.'); 32 | process.exit(0); 33 | }); 34 | -------------------------------------------------------------------------------- /src/service/samples.ts: -------------------------------------------------------------------------------- 1 | 2 | export const frameworkSamples = { 3 | react : `const Button = ({ onClick, children }) => { 4 | return ( 5 | 8 | ); 9 | };`, 10 | angular: `@Component({ 11 | selector: 'app-hero-list', 12 | templateUrl: './hero-list.component.html', 13 | providers: [ HeroService ] 14 | }) 15 | export class HeroListComponent implements OnInit { 16 | heroes: Hero[] = []; 17 | selectedHero: Hero | undefined; 18 | constructor(private service: HeroService) { } 19 | ngOnInit() { 20 | this.heroes = this.service.getHeroes(); 21 | } 22 | selectHero(hero: Hero) { this.selectedHero = hero; } 23 | }`, 24 | vue: ` 25 | export default { 26 | data() { 27 | return { 28 | count: 0 29 | } 30 | }, 31 | template: \` 32 | \` 35 | }` 36 | } -------------------------------------------------------------------------------- /src/service/router.ts: -------------------------------------------------------------------------------- 1 | 2 | import { errorHandler } from '@backstage/backend-common'; 3 | import express from 'express'; 4 | import Router from 'express-promise-router'; 5 | import { Logger } from 'winston'; 6 | import { Config } from '@backstage/config'; 7 | 8 | import { openAPIResponse } from './openai'; 9 | 10 | export interface RouterOptions { 11 | logger: Logger; 12 | config: Config; 13 | } 14 | 15 | export async function createRouter( 16 | options: RouterOptions, 17 | 18 | 19 | ): Promise { 20 | const { config, logger } = options; 21 | 22 | const router = Router(); 23 | router.use(express.json()); 24 | 25 | router.get('/health', (_, response) => { 26 | logger.info('PONG!'); 27 | response.send({ status: 'ok' }); 28 | }); 29 | 30 | router.get('/completions', async (request, response) => { 31 | const model = request.query.model as string 32 | const parsedArray = (request.query.messages as []); 33 | const temperature = Number(request.query.temperature as string) 34 | const maxTokens = Number(request.query.maxTokens as string) 35 | const completion = await openAPIResponse(config.getString('openai.apiKey'),{model, messages:parsedArray, temperature, maxTokens}) 36 | 37 | response.send({completion: completion}) 38 | }) 39 | 40 | router.use(errorHandler()); 41 | return router; 42 | } 43 | -------------------------------------------------------------------------------- /src/service/openai.ts: -------------------------------------------------------------------------------- 1 | import { ChatCompletionRequestMessage, 2 | Configuration, 3 | CreateChatCompletionRequest, 4 | CreateCompletionRequest, 5 | OpenAIApi} from "openai"; 6 | import BadRequest from 'http-errors' 7 | 8 | interface ChatGPTUserInput { 9 | model? : string 10 | messages? : any[] 11 | temperature? : number 12 | maxTokens? : number 13 | } 14 | 15 | interface OpenAIConfig { 16 | apiKey : string 17 | } 18 | 19 | 20 | export const openAPIResponse = async (apiKey : string ,input : ChatGPTUserInput) => { 21 | const openAIConfiguration = { 22 | apiKey: apiKey 23 | } as OpenAIConfig 24 | const configuration = new Configuration({ 25 | apiKey: openAIConfiguration.apiKey, 26 | }); 27 | 28 | const openai = new OpenAIApi(configuration); 29 | let response 30 | if(input.model == 'gpt-3.5-turbo'){ 31 | const chatCompletionRequest: CreateChatCompletionRequest = { 32 | model: input.model, 33 | messages: input.messages, 34 | temperature: input.temperature, 35 | max_tokens: input.maxTokens, 36 | }; 37 | response = await openai.createChatCompletion(chatCompletionRequest); 38 | } 39 | else { 40 | throw BadRequest("Invalid model") 41 | } 42 | const completion = response.data.choices 43 | return completion 44 | 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@enfuse/plugin-chatgpt-backend", 3 | "version": "1.1.1", 4 | "main": "dist/index.cjs.js", 5 | "types": "dist/index.d.ts", 6 | "license": "Apache-2.0", 7 | "type": "commonjs", 8 | "publishConfig": { 9 | "access": "public", 10 | "main": "dist/index.cjs.js", 11 | "types": "dist/index.d.ts" 12 | }, 13 | "backstage": { 14 | "role": "backend-plugin" 15 | }, 16 | "scripts": { 17 | "start": "backstage-cli package start", 18 | "build": "backstage-cli package build", 19 | "lint": "backstage-cli package lint", 20 | "test": "backstage-cli package test", 21 | "clean": "backstage-cli package clean", 22 | "prepublish": "tsc && yarn run build" 23 | }, 24 | "dependencies": { 25 | "@backstage/backend-common": "^0.15.1", 26 | "@backstage/config": "^1.0.2", 27 | "@enfuse/plugin-chatgpt-backend": "^1.0.9", 28 | "@knuckleswtf/scribe-express": "^2.0.2", 29 | "@types/express": "*", 30 | "express": "^4.17.1", 31 | "express-promise-router": "^4.1.0", 32 | "node-fetch": "^2.6.7", 33 | "openai": "^3.1.0", 34 | "winston": "^3.2.1", 35 | "yn": "^4.0.0" 36 | }, 37 | "devDependencies": { 38 | "@backstage/cli": "^0.22.6", 39 | "@types/supertest": "^2.0.8", 40 | "msw": "^0.46.0", 41 | "supertest": "^4.0.2" 42 | }, 43 | "files": [ 44 | "dist", 45 | "config.d.ts" 46 | ], 47 | "configSchema": "config.d.ts" 48 | } 49 | -------------------------------------------------------------------------------- /src/service/router.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Backstage Authors 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 | 17 | import { getVoidLogger } from '@backstage/backend-common'; 18 | import express from 'express'; 19 | import request from 'supertest'; 20 | 21 | import { createRouter } from './router'; 22 | 23 | describe('createRouter', () => { 24 | let app: express.Express; 25 | 26 | beforeAll(async () => { 27 | const router = await createRouter({ 28 | logger: getVoidLogger(), 29 | }); 30 | app = express().use(router); 31 | }); 32 | 33 | beforeEach(() => { 34 | jest.resetAllMocks(); 35 | }); 36 | 37 | describe('GET /health', () => { 38 | it('returns ok', async () => { 39 | const response = await request(app).get('/health'); 40 | 41 | expect(response.status).toEqual(200); 42 | expect(response.body).toEqual({ status: 'ok' }); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Plugin Backend 2 | 3 | Plugin that exposes an API to interact with OpenAI and serve the [frontend](https://github.com/enfuse/backstage-chatgpt-plugin) chatgpt plugin 4 | 5 | 6 | # Installation 7 | Navigate to root of Backstage installation and run 8 | ```sh 9 | # From root directory of your Backstage installation 10 | yarn add --cwd packages/backend @enfuse/plugin-chatgpt-backend 11 | ``` 12 | 13 | # Configuration 14 | 1. This plugin requires an OpenAI API Key. This should be provided in the backstage configuration as shown below: 15 | 16 | ```yml 17 | //app-config.yml or app-config-local.yml 18 | 19 | openai: 20 | apiKey: 21 | 22 | ``` 23 | This can be generated here: [ChatGPT API keys](https://platform.openai.com/account/api-keys). 24 | 25 | 3. Create a `chatgpt.ts` file inside your `packages/backend/src/plugins` directory and include the following: 26 | 27 | 28 | ``` js 29 | 30 | import { createRouter } from '@enfuse/plugin-chatgpt-backend'; 31 | import { Router } from 'express'; 32 | import { PluginEnvironment } from '../types'; 33 | 34 | export default async function createPlugin( 35 | env: PluginEnvironment, 36 | ): Promise { 37 | return await createRouter({ 38 | logger: env.logger, 39 | config: env.config 40 | }); 41 | } 42 | ``` 43 | 44 | 4. Inside your `packages/backend/src/index.ts` file, find the section where backend environments and routes are set up and include the following: 45 | 46 | ``` js 47 | import chatGPTBackend from './plugins/chatgpt'; 48 | 49 | ... 50 | const chatgptEnv = useHotMemoize(module, () => createEnv('chatgpt-backend')); 51 | 52 | apiRouter.use('/chatgpt', await chatGPTBackend(chatgptEnv)); 53 | 54 | ``` 55 | 56 | 5. Test your backend plugin installation by having backstage running and curling the endpoint 57 | 58 | ``` bash 59 | curl localhost:7007/api/chatgpt/health 60 | ``` 61 | 62 | -------------------------------------------------------------------------------- /.scribe.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | theme: 'default', 4 | 5 | /* 6 | * The docs, Postman collection and OpenAPI spec will be generated to this folder. 7 | */ 8 | outputPath: 'public/docs', 9 | 10 | /* 11 | * The base URL displayed in the docs. 12 | */ 13 | baseUrl: "http://yourApi.dev", 14 | 15 | /* 16 | * The HTML for the generated documentation, and the name of the generated Postman collection and OpenAPI spec. 17 | */ 18 | title: "Chatgpt Backend Documentation", 19 | description: '', 20 | 21 | /* 22 | * Custom logo path. This will be used as the value of the src attribute for the <img> tag, 23 | * so make sure it points to a public URL or path accessible from your web server. For best results, the image width should be 230px. 24 | * Set this to false to not use a logo. 25 | */ 26 | logo: false, 27 | 28 | tryItOut: { 29 | /** 30 | * Add a Try It Out button to your endpoints so consumers can test endpoints right from their browser. 31 | * Don't forget to enable CORS headers for your endpoints. 32 | */ 33 | enabled: true, 34 | 35 | /** 36 | * The base URL for the API tester to use (for example, you can set this to your staging URL). 37 | * Leave as null to use the same URL displayed in the docs. 38 | */ 39 | baseUrl: null, 40 | }, 41 | 42 | /* 43 | * How is your API authenticated? This information will be used in the displayed docs, generated examples and response calls. 44 | */ 45 | auth: { 46 | /* 47 | * Set this to true if any endpoints in your API uses authentication. 48 | */ 49 | enabled: false, 50 | /* 51 | * Set this to true if your API should be authenticated by default. 52 | * You can then use @unauthenticated or @authenticated on individual endpoints to change their status. 53 | */ 54 | default: false, 55 | /* 56 | * Where is the auth value meant to be sent in a request? 57 | * Options: query, body, query_or_body, basic, bearer, header (for custom header) 58 | */ 59 | in: 'bearer', 60 | /* 61 | * The name of the parameter (eg token, key, apiKey) or header (eg Authorization, Api-Key). 62 | */ 63 | name: 'token', 64 | /* 65 | * The value of the auth parameter (in your auth section above) to be used by Scribe to authenticate response calls. 66 | * If this value is null, Scribe will use a random value. If you don't have authenticated endpoints, don't worry about this. 67 | */ 68 | useValue: () => process.env.SCRIBE_AUTH_KEY, 69 | /* 70 | * Placeholder your users will see for the auth parameter in the example requests. 71 | * Set this to null if you want Scribe to use a random value as placeholder instead. 72 | */ 73 | placeholder: '{YOUR_AUTH_KEY}', 74 | /* 75 | * Any extra authentication-related info for your users. For instance, you can describe how to find or generate their auth credentials. 76 | * Markdown and HTML are supported. 77 | */ 78 | extraInfo: 'You can retrieve your token by visiting your dashboard and clicking <b>Generate API token</b>.', 79 | }, 80 | 81 | /* 82 | * The routes for which documentation should be generated. 83 | * Each group contains rules defining which routes should be included ('include' and 'exclude' sections) 84 | * and settings which should be applied to them ('apply' section). 85 | */ 86 | routes: [ 87 | { 88 | /* 89 | * Include any routes whose paths match this pattern (use * as a wildcard to match any characters). Example: '/api/*.*'. 90 | */ 91 | include: ['*'], 92 | /* 93 | * Exclude any routes whose paths match this pattern (use * as a wildcard to match any characters). Example: '/admin/*.*'. 94 | */ 95 | exclude: ['*.websocket'], 96 | apply: { 97 | /* 98 | * Specify headers to be added to the example requests 99 | */ 100 | headers: { 101 | 'Content-Type': 'application/json', 102 | Accept: 'application/json', 103 | }, 104 | /* 105 | * If no @response declarations are found for the route, 106 | * we'll try to get a sample response by attempting an API call. 107 | * Configure the settings for the API call here. 108 | */ 109 | responseCalls: { 110 | /* 111 | * The base URL Scribe will make requests to. This should be the URL (+ port) you run on localhost. 112 | */ 113 | baseUrl: "http://localhost:3000", 114 | /* 115 | * API calls will be made only for routes in this group matching these HTTP methods (GET, POST, etc). 116 | * List the methods here or use '*' to mean all methods. Leave empty to disable API calls. 117 | */ 118 | methods: ['GET'], 119 | /* 120 | * Environment variables which should be set for the API call. 121 | * This is a good place to ensure that notifications, emails and other external services 122 | * are not triggered during the documentation API calls. 123 | * You can also create a `.env.docs` file instead. 124 | */ 125 | env: { 126 | // NODE_ENV: 'docs' 127 | }, 128 | 129 | bodyParams: {}, 130 | queryParams: {}, 131 | fileParams: {}, 132 | } 133 | } 134 | } 135 | ], 136 | 137 | /* 138 | * Generate a Postman collection in addition to HTML docs. 139 | * The collection will be generated to {outputPath}/collection.json. 140 | * Collection schema: https://schema.getpostman.com/json/collection/v2.1.0/collection.json 141 | */ 142 | postman: { 143 | enabled: true, 144 | // Override specific fields in the generated collection. Lodash set() notation is supported. 145 | overrides: { 146 | // 'info.version': '2.0.0', 147 | } 148 | }, 149 | 150 | /* 151 | * Generate an OpenAPI spec in addition to HTML docs. 152 | * The spec file will be generated to {outputPath}/openapi.yaml. 153 | * Specification schema: https://swagger.io/specification/ 154 | */ 155 | openapi: { 156 | enabled: false, 157 | // Override specific fields in the generated spec. Lodash set() notation is supported. 158 | overrides: { 159 | // 'info.version': '2.0.0', 160 | } 161 | }, 162 | 163 | /* 164 | * Example requests for each endpoint will be shown in each of these languages. 165 | * Supported options are: bash, javascript 166 | */ 167 | exampleLanguages: [ 168 | 'bash', 169 | 'javascript', 170 | ], 171 | 172 | /* 173 | * Name for the group of endpoints which do not have a @group set. 174 | */ 175 | defaultGroup: 'Endpoints', 176 | 177 | /* 178 | * Text to place in the "Introduction" section. Markdown and HTML are supported. 179 | */ 180 | introText: `This documentation aims to provide all the information you need to work with our API. 181 | 182 | <aside>As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile). 183 | You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside>`, 184 | 185 | /* 186 | * If you would like the package to generate the same example values for parameters on each run, 187 | * set this to any number (eg. 1234) 188 | */ 189 | fakerSeed: null, 190 | }; --------------------------------------------------------------------------------