Skip to content

Commit ff20aa6

Browse files
authored
feat(ai-integrations): Initial code drop of model catalog backend module (#579)
* feat(ai-integrations): Initial code drop of model catalog backend module Signed-off-by: John Collier <jcollier@redhat.com> * fix: add comments to app-config example Signed-off-by: John Collier <jcollier@redhat.com> --------- Signed-off-by: John Collier <jcollier@redhat.com>
1 parent 37f943a commit ff20aa6

24 files changed

Lines changed: 1776 additions & 19 deletions
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-catalog-backend-module-model-catalog': patch
3+
---
4+
5+
Initial code drop of prototype model catalog plugin originally from redhat-ai-dev fork

workspaces/ai-integrations/app-config.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ backend:
4343
- host: '*.mozilla.org'
4444
- host: '*.openshift.com'
4545
- host: '*.openshiftapps.com'
46+
- host: '${RHDH_BRIDGE_HOST}'
4647

4748
integrations:
4849
github:
@@ -91,5 +92,7 @@ catalog:
9192
- allow: [Component, System, API, Resource, Location]
9293
providers:
9394
modelCatalog:
94-
ollama:
95+
# The field underneath here can be any value that you feel represents your model catalog bridge instance
96+
development:
97+
# If testing locally, this value should be set to `localhost:9090`
9598
baseUrl: '${RHDH_AI_BRIDGE_SERVER}'
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
module.exports = {
17+
presets: [
18+
[
19+
'@babel/preset-env',
20+
{
21+
targets: {
22+
node: 'current',
23+
},
24+
},
25+
],
26+
],
27+
};

workspaces/ai-integrations/packages/backend/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
*/
1616

1717
import { createBackend } from '@backstage/backend-defaults';
18+
import {
19+
catalogModuleRHDHRHOAIReaderProcessor,
20+
catalogModuleRHDHRHOAILocationsExtensionPoint,
21+
catalogModuleRHDHRHOAIEntityProvider,
22+
} from '@red-hat-developer-hub/backstage-plugin-catalog-backend-module-model-catalog';
1823

1924
const backend = createBackend();
2025

@@ -64,4 +69,7 @@ backend.add(
6469
'@red-hat-developer-hub/backstage-plugin-catalog-backend-module-model-catalog'
6570
),
6671
);
72+
backend.add(catalogModuleRHDHRHOAILocationsExtensionPoint);
73+
backend.add(catalogModuleRHDHRHOAIReaderProcessor);
74+
backend.add(catalogModuleRHDHRHOAIEntityProvider);
6775
backend.start();
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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 { SchedulerServiceTaskScheduleDefinitionConfig } from '@backstage/backend-plugin-api';
17+
18+
export interface Config {
19+
catalog?: {
20+
providers?: {
21+
modelCatalog?: {
22+
[key: string]: {
23+
/**
24+
* ModelCatalogConfig
25+
*/
26+
baseUrl: string;
27+
name?: string;
28+
system?: string;
29+
owner?: string;
30+
schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
31+
};
32+
};
33+
};
34+
};
35+
}

workspaces/ai-integrations/plugins/catalog-backend-module-model-catalog/package.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
"backstage": {
2020
"role": "backend-plugin-module",
2121
"pluginId": "catalog",
22-
"pluginPackage": "@backstage/plugin-catalog-backend"
22+
"pluginPackage": "@backstage/plugin-catalog-backend",
23+
"pluginPackages": [
24+
"@red-hat-developer-hub/backstage-plugin-catalog-backend-module-model-catalog"
25+
]
2326
},
2427
"scripts": {
2528
"start": "backstage-cli package start",
@@ -31,7 +34,16 @@
3134
"postpack": "backstage-cli package postpack"
3235
},
3336
"dependencies": {
34-
"@backstage/backend-plugin-api": "^1.2.1"
37+
"@backstage/backend-plugin-api": "^1.1.1",
38+
"@backstage/catalog-client": "^1.9.1",
39+
"@backstage/catalog-model": "^1.7.2",
40+
"@backstage/errors": "^1.2.7",
41+
"@backstage/plugin-catalog-backend": "^1.30.0",
42+
"@backstage/plugin-catalog-common": "^1.1.3",
43+
"@backstage/plugin-catalog-node": "^1.15.1",
44+
"@redhat-ai-dev/model-catalog-types": "^1.0.1",
45+
"stream": "^0.0.3",
46+
"yaml": "^2.7.0"
3547
},
3648
"devDependencies": {
3749
"@backstage/backend-test-utils": "^1.3.1",

workspaces/ai-integrations/plugins/catalog-backend-module-model-catalog/report.api.md

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,70 @@
44
55
```ts
66
import { BackendFeature } from '@backstage/backend-plugin-api';
7+
import { CatalogProcessor } from '@backstage/plugin-catalog-node';
8+
import { CatalogProcessorEmit } from '@backstage/plugin-catalog-node';
9+
import { CatalogProcessorParser } from '@backstage/plugin-catalog-node';
10+
import type { Config } from '@backstage/config';
11+
import { Entity } from '@backstage/catalog-model';
12+
import { EntityProvider } from '@backstage/plugin-catalog-node';
13+
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
14+
import { LocationSpec } from '@backstage/plugin-catalog-common';
15+
import { LoggerService } from '@backstage/backend-plugin-api';
16+
import { RootConfigService } from '@backstage/backend-plugin-api';
17+
import type { SchedulerService } from '@backstage/backend-plugin-api';
18+
import type { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api';
19+
import { UrlReaderService } from '@backstage/backend-plugin-api';
720

821
// @public
9-
const catalogModuleModelCatalog: BackendFeature;
10-
export default catalogModuleModelCatalog;
22+
const catalogModuleModelCatalogResourceEntityProvider: BackendFeature;
23+
export default catalogModuleModelCatalogResourceEntityProvider;
24+
25+
// @public
26+
export const catalogModuleRHDHRHOAIEntityProvider: BackendFeature;
27+
28+
// @public
29+
export const catalogModuleRHDHRHOAILocationsExtensionPoint: BackendFeature;
30+
31+
// @public
32+
export const catalogModuleRHDHRHOAIReaderProcessor: BackendFeature;
33+
34+
// @public
35+
export function fetchCatalogEntities(baseUrl: string): Promise<Entity[]>;
36+
37+
// @public
38+
export class ModelCatalogResourceEntityProvider implements EntityProvider {
39+
connect(connection: EntityProviderConnection): Promise<void>;
40+
createScheduleFn(taskRunner: SchedulerServiceTaskRunner): () => Promise<void>;
41+
static fromConfig(
42+
deps: {
43+
config: Config;
44+
logger: LoggerService;
45+
},
46+
options:
47+
| {
48+
schedule: SchedulerServiceTaskRunner;
49+
}
50+
| {
51+
scheduler: SchedulerService;
52+
},
53+
): ModelCatalogResourceEntityProvider[];
54+
getProviderName(): string;
55+
run(): Promise<void>;
56+
}
57+
58+
// @public
59+
export class RHDHRHOAIReaderProcessor implements CatalogProcessor {
60+
constructor(
61+
reader: UrlReaderService,
62+
config: RootConfigService,
63+
logger: LoggerService,
64+
);
65+
getProcessorName(): string;
66+
readLocation(
67+
location: LocationSpec,
68+
_optional: boolean,
69+
emit: CatalogProcessorEmit,
70+
parser: CatalogProcessorParser,
71+
): Promise<boolean>;
72+
}
1173
```
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
17+
import YAML from 'yaml';
18+
import { Entity, ResourceEntity } from '@backstage/catalog-model';
19+
import { Stream } from 'stream';
20+
21+
const fakeEntity: ResourceEntity = {
22+
apiVersion: 'backstage.io/v1beta1',
23+
kind: 'Resource',
24+
metadata: {
25+
name: 'ibm-granite',
26+
description: 'IBM Granite code model',
27+
tags: [],
28+
links: [],
29+
},
30+
spec: {
31+
dependencyOf: [],
32+
owner: 'example-user',
33+
type: 'ai-model',
34+
},
35+
};
36+
const blob = new Blob([YAML.stringify(fakeEntity)], {
37+
type: 'application/json',
38+
});
39+
40+
// Mock different fetch results based on the url passed in, to trigger success vs. error scenarios
41+
global.fetch = jest.fn(url => {
42+
if (url === 'errorTest') {
43+
return Promise.resolve({
44+
ok: false,
45+
status: 401,
46+
json: () => 'error',
47+
});
48+
}
49+
return Promise.resolve({
50+
ok: true,
51+
status: 200,
52+
blob: () => Promise.resolve(blob),
53+
});
54+
}) as jest.Mock;
55+
56+
import { fetchCatalogEntities } from './BridgeResourceConnector';
57+
58+
const httpsMock = require('https');
59+
60+
describe('Bridge Resource Connector', () => {
61+
it('should fetch catallog entities successfully', async () => {
62+
const streamStream = new Stream();
63+
httpsMock.get = jest.fn().mockImplementation(cb => {
64+
cb(streamStream);
65+
streamStream.emit('data', 'test');
66+
streamStream.emit('end');
67+
});
68+
const entities: Entity[] = await fetchCatalogEntities('fake-url');
69+
expect(entities.length).toEqual(1);
70+
expect(entities[0].metadata.name).toEqual('ibm-granite');
71+
});
72+
it('should error out if error encountered', async () => {
73+
const streamStream = new Stream();
74+
httpsMock.get = jest.fn().mockImplementation(cb => {
75+
cb(streamStream);
76+
streamStream.emit('data', 'test');
77+
streamStream.emit('end');
78+
});
79+
await expect(
80+
async () => await fetchCatalogEntities('errorTest'),
81+
).rejects.toThrow();
82+
});
83+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 { Entity } from '@backstage/catalog-model';
17+
18+
import YAML from 'yaml';
19+
20+
// listModels will list the models from an OpenAI compatible endpoint
21+
/*
22+
export async function listModels(
23+
baseUrl: string,
24+
access_token: string,
25+
): Promise<ModelList> {
26+
const res = await fetch(`${baseUrl}/v1/models`, {
27+
headers: {
28+
'Content-Type': 'application/json',
29+
Authorization: access_token,
30+
},
31+
method: 'GET',
32+
});
33+
34+
if (!res.ok) {
35+
throw new Error(res.statusText);
36+
}
37+
38+
const data = await res.json();
39+
return data;
40+
}*/
41+
42+
/**
43+
* fetchCatalogEntities retrieves model catalog entities from the Model Catalog Bridge services
44+
* @public
45+
*/
46+
export async function fetchCatalogEntities(baseUrl: string): Promise<Entity[]> {
47+
// ToDo: Discover catalog-info endpoint?
48+
const res = await fetch(`${baseUrl}`, {
49+
method: 'GET',
50+
});
51+
52+
if (!res.ok) {
53+
throw new Error(res.statusText);
54+
}
55+
56+
// ToDo: Look at seeing if we can use the parser provided by the CatalogProcessorProvider package
57+
const data = await res
58+
.blob()
59+
.then(blob => blob.text())
60+
.then(yamlStr => YAML.parseAllDocuments(yamlStr))
61+
.then(yamlData => yamlData.map(item => item.toJS()));
62+
return data;
63+
}

0 commit comments

Comments
 (0)