@nospt/plugin-tech-radar-ng-backend 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -0
- package/dist/database/TechRadarDb.cjs.js +244 -0
- package/dist/database/TechRadarDb.cjs.js.map +1 -0
- package/dist/index.cjs.js +10 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/plugin.cjs.js +67 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/router.cjs.js +79 -0
- package/dist/router.cjs.js.map +1 -0
- package/dist/service/GitHubMetricsService.cjs.js +108 -0
- package/dist/service/GitHubMetricsService.cjs.js.map +1 -0
- package/dist/service/HuggingFaceMetricsService.cjs.js +114 -0
- package/dist/service/HuggingFaceMetricsService.cjs.js.map +1 -0
- package/dist/service/RadarMetricsService.cjs.js +139 -0
- package/dist/service/RadarMetricsService.cjs.js.map +1 -0
- package/migrations/001_create_segments_table.js +34 -0
- package/migrations/002_add_segments.js +124 -0
- package/migrations/003_create_rings_table.js +46 -0
- package/migrations/004_add_rings.js +30 -0
- package/migrations/005_create_radar_candidates_table.js +43 -0
- package/migrations/006_create_radar_candidates_snapshot_table.js +35 -0
- package/package.json +67 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var express = require('express');
|
|
4
|
+
var Router = require('express-promise-router');
|
|
5
|
+
var pluginTechRadarNgCommon = require('@nospt/plugin-tech-radar-ng-common');
|
|
6
|
+
var z = require('zod');
|
|
7
|
+
var errors = require('@backstage/errors');
|
|
8
|
+
|
|
9
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
12
|
+
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
13
|
+
var z__default = /*#__PURE__*/_interopDefaultCompat(z);
|
|
14
|
+
|
|
15
|
+
async function createRouter({
|
|
16
|
+
logger,
|
|
17
|
+
radarMetricsService
|
|
18
|
+
}) {
|
|
19
|
+
const router = Router__default.default();
|
|
20
|
+
router.use(express__default.default.json());
|
|
21
|
+
router.get("/segments", async (_req, res) => {
|
|
22
|
+
const segments = await radarMetricsService.fetchQuadrants();
|
|
23
|
+
res.status(200).json(segments);
|
|
24
|
+
});
|
|
25
|
+
router.get("/rings", async (_req, res) => {
|
|
26
|
+
const rings = await radarMetricsService.fetchRings();
|
|
27
|
+
res.status(200).json(rings);
|
|
28
|
+
});
|
|
29
|
+
router.get("/candidates", async (_req, res) => {
|
|
30
|
+
const parsedQuery = pluginTechRadarNgCommon.candidateQuerySchema.safeParse(_req.query);
|
|
31
|
+
if (!parsedQuery.success) {
|
|
32
|
+
logger.debug(`GET /candidates query: ${_req.query}`);
|
|
33
|
+
logger.error("Failed to parse query options!");
|
|
34
|
+
const queryErrors = z__default.default.treeifyError(parsedQuery.error);
|
|
35
|
+
res.status(400).json({
|
|
36
|
+
error: "Invalid query parameters",
|
|
37
|
+
details: queryErrors.errors
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const {
|
|
42
|
+
page,
|
|
43
|
+
pageSize,
|
|
44
|
+
unclassified,
|
|
45
|
+
in_radar,
|
|
46
|
+
orderBy,
|
|
47
|
+
orderDir,
|
|
48
|
+
platforms
|
|
49
|
+
} = parsedQuery.data;
|
|
50
|
+
const metrics = await radarMetricsService.getRadarMetrics({
|
|
51
|
+
page,
|
|
52
|
+
pageSize,
|
|
53
|
+
unclassified,
|
|
54
|
+
in_radar,
|
|
55
|
+
orderBy,
|
|
56
|
+
orderDir,
|
|
57
|
+
platforms
|
|
58
|
+
});
|
|
59
|
+
res.status(200).json(metrics);
|
|
60
|
+
});
|
|
61
|
+
router.put("/candidates/classify", async (req, res) => {
|
|
62
|
+
logger.debug(`PUT /candidates/classify body: ${JSON.stringify(req.body)}`);
|
|
63
|
+
const parsedBody = pluginTechRadarNgCommon.candidatePatchSchema.safeParse(req.body);
|
|
64
|
+
if (!parsedBody.success) {
|
|
65
|
+
logger.error("Failed to parse request body!");
|
|
66
|
+
const bodyErrors = z__default.default.treeifyError(parsedBody.error);
|
|
67
|
+
throw new errors.InputError("Invalid request body", bodyErrors.errors);
|
|
68
|
+
}
|
|
69
|
+
await radarMetricsService.patchCandidates(parsedBody.data);
|
|
70
|
+
res.status(200).end();
|
|
71
|
+
});
|
|
72
|
+
router.get("/githubDiscovery", async (_req, _) => {
|
|
73
|
+
await radarMetricsService.discover();
|
|
74
|
+
});
|
|
75
|
+
return router;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
exports.createRouter = createRouter;
|
|
79
|
+
//# sourceMappingURL=router.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.cjs.js","sources":["../src/router.ts"],"sourcesContent":["import express from 'express';\nimport Router from 'express-promise-router';\nimport { type RadarMetricsService } from './service/RadarMetricsService';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport {\n candidatePatchSchema,\n candidateQuerySchema,\n} from '@nospt/plugin-tech-radar-ng-common';\nimport z from 'zod';\n\nimport { InputError } from '@backstage/errors';\ninterface RouterOptions {\n logger: LoggerService;\n radarMetricsService: RadarMetricsService;\n}\n\nexport async function createRouter({\n logger,\n radarMetricsService,\n}: RouterOptions): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\n\n router.get('/segments', async (_req, res) => {\n const segments = await radarMetricsService.fetchQuadrants();\n\n res.status(200).json(segments);\n });\n\n router.get('/rings', async (_req, res) => {\n const rings = await radarMetricsService.fetchRings();\n\n res.status(200).json(rings);\n });\n\n router.get('/candidates', async (_req, res) => {\n const parsedQuery = candidateQuerySchema.safeParse(_req.query);\n\n if (!parsedQuery.success) {\n logger.debug(`GET /candidates query: ${_req.query}`);\n logger.error('Failed to parse query options!');\n\n const queryErrors = z.treeifyError(parsedQuery.error);\n res.status(400).json({\n error: 'Invalid query parameters',\n details: queryErrors.errors,\n });\n return;\n }\n\n const {\n page,\n pageSize,\n unclassified,\n in_radar,\n orderBy,\n orderDir,\n platforms,\n } = parsedQuery.data;\n\n const metrics = await radarMetricsService.getRadarMetrics({\n page,\n pageSize,\n unclassified,\n in_radar,\n orderBy,\n orderDir,\n platforms,\n });\n\n res.status(200).json(metrics);\n });\n\n router.put('/candidates/classify', async (req, res) => {\n logger.debug(`PUT /candidates/classify body: ${JSON.stringify(req.body)}`);\n\n const parsedBody = candidatePatchSchema.safeParse(req.body);\n\n if (!parsedBody.success) {\n logger.error('Failed to parse request body!');\n\n const bodyErrors = z.treeifyError(parsedBody.error);\n throw new InputError('Invalid request body', bodyErrors.errors);\n }\n\n await radarMetricsService.patchCandidates(parsedBody.data);\n res.status(200).end();\n });\n\n router.get('/githubDiscovery', async (_req, _) => {\n await radarMetricsService.discover(); // call your service\n });\n\n return router;\n}\n"],"names":["Router","express","candidateQuerySchema","z","candidatePatchSchema","InputError"],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
4
|
+
var errors = require('@backstage/errors');
|
|
5
|
+
var pluginTechRadarNgCommon = require('@nospt/plugin-tech-radar-ng-common');
|
|
6
|
+
var types = require('@backstage/types');
|
|
7
|
+
|
|
8
|
+
class DefaultGitHubMetricsService {
|
|
9
|
+
#logger;
|
|
10
|
+
#config;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.#logger = options.logger;
|
|
13
|
+
this.#config = options.config;
|
|
14
|
+
}
|
|
15
|
+
static create(options) {
|
|
16
|
+
return new DefaultGitHubMetricsService(options);
|
|
17
|
+
}
|
|
18
|
+
async discover() {
|
|
19
|
+
try {
|
|
20
|
+
const frequency = this.#config.getOptional("techRadarNg.discovery.frequency") ?? { days: 1 };
|
|
21
|
+
const since = new Date(Date.now() - types.durationToMilliseconds(frequency));
|
|
22
|
+
const searchQueries = this.#config.getOptionalStringArray(
|
|
23
|
+
"techRadarNg.github.searchQueries"
|
|
24
|
+
) ?? [];
|
|
25
|
+
const requests = [];
|
|
26
|
+
for (const query of searchQueries) {
|
|
27
|
+
const byMostStars = `q=${encodeURIComponent(
|
|
28
|
+
query
|
|
29
|
+
)}&sort=stars&per_page=50`;
|
|
30
|
+
const trending = `q=${encodeURIComponent(
|
|
31
|
+
`(${query}) AND created:>${since.toISOString().split("T")[0]}`
|
|
32
|
+
)}&sort=stars&per_page=50`;
|
|
33
|
+
requests.push(this.fetchReposByQuery(byMostStars));
|
|
34
|
+
requests.push(this.fetchReposByQuery(trending));
|
|
35
|
+
}
|
|
36
|
+
const repos = (await Promise.all(requests)).flat();
|
|
37
|
+
const uniqueRepos = repos.filter(
|
|
38
|
+
(repo, index, self) => self.findIndex((r) => r.node_id === repo.node_id) === index
|
|
39
|
+
);
|
|
40
|
+
let candidates = [];
|
|
41
|
+
for (const repo of uniqueRepos) {
|
|
42
|
+
const candidate = {
|
|
43
|
+
platform_id: repo.node_id,
|
|
44
|
+
full_name: repo.full_name,
|
|
45
|
+
name: repo.name,
|
|
46
|
+
description: repo.description,
|
|
47
|
+
url: repo.html_url,
|
|
48
|
+
homepage: repo.homepage,
|
|
49
|
+
primary_language: repo.language,
|
|
50
|
+
license: repo.license?.name,
|
|
51
|
+
popularity_score: repo.stargazers_count,
|
|
52
|
+
usage_score: repo.forks_count,
|
|
53
|
+
is_active: !repo.archived && !repo.disabled,
|
|
54
|
+
platform: pluginTechRadarNgCommon.Platform.GITHUB,
|
|
55
|
+
last_activity_at: new Date(repo.pushed_at)
|
|
56
|
+
};
|
|
57
|
+
if (candidate) {
|
|
58
|
+
candidates.push(candidate);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return candidates;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
this.#logger.error(
|
|
64
|
+
`GitHub GenAI discovery error: ${errors.stringifyError(error)}`
|
|
65
|
+
);
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async fetchReposByQuery(query) {
|
|
70
|
+
this.#logger.info(`Fetching GitHub repositories with query: ${query}`);
|
|
71
|
+
const response = await fetch(
|
|
72
|
+
`https://api.github.com/search/repositories?${query}`,
|
|
73
|
+
{
|
|
74
|
+
headers: {
|
|
75
|
+
Accept: "application/vnd.github.v3+json"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
if (response.ok) {
|
|
80
|
+
const data = await response.json();
|
|
81
|
+
this.#logger.info(`Fetched ${data.items.length} GitHub repositories`);
|
|
82
|
+
return data.items || [];
|
|
83
|
+
} else {
|
|
84
|
+
const errorText = await response.text();
|
|
85
|
+
this.#logger.error(
|
|
86
|
+
`GitHub API error: ${response.status} ${response.statusText} - ${errorText}`
|
|
87
|
+
);
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const gitHubMetricsServiceRef = backendPluginApi.createServiceRef({
|
|
93
|
+
id: "github.metrics",
|
|
94
|
+
defaultFactory: async (service) => backendPluginApi.createServiceFactory({
|
|
95
|
+
service,
|
|
96
|
+
deps: {
|
|
97
|
+
logger: backendPluginApi.coreServices.logger,
|
|
98
|
+
config: backendPluginApi.coreServices.rootConfig
|
|
99
|
+
},
|
|
100
|
+
async factory(deps) {
|
|
101
|
+
return DefaultGitHubMetricsService.create(deps);
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
exports.DefaultGitHubMetricsService = DefaultGitHubMetricsService;
|
|
107
|
+
exports.gitHubMetricsServiceRef = gitHubMetricsServiceRef;
|
|
108
|
+
//# sourceMappingURL=GitHubMetricsService.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GitHubMetricsService.cjs.js","sources":["../../src/service/GitHubMetricsService.ts"],"sourcesContent":["import {\n coreServices,\n createServiceFactory,\n createServiceRef,\n LoggerService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport { stringifyError } from '@backstage/errors';\nimport { Platform } from '@nospt/plugin-tech-radar-ng-common';\nimport type { GithubRepo, CandidateRow } from '../types';\nimport { durationToMilliseconds, HumanDuration } from '@backstage/types';\n\ninterface DefaultGitHubMetricsServiceOptions {\n logger: LoggerService;\n config: RootConfigService;\n}\n\nexport interface GitHubMetricsService {\n discover(): Promise<CandidateRow[]>;\n}\n\nexport class DefaultGitHubMetricsService implements GitHubMetricsService {\n readonly #logger: LoggerService;\n readonly #config: RootConfigService;\n\n private constructor(options: DefaultGitHubMetricsServiceOptions) {\n this.#logger = options.logger;\n this.#config = options.config;\n }\n\n static create(options: DefaultGitHubMetricsServiceOptions) {\n return new DefaultGitHubMetricsService(options);\n }\n\n async discover(): Promise<CandidateRow[]> {\n try {\n const frequency: HumanDuration =\n this.#config.getOptional('techRadarNg.discovery.frequency') ?? { days: 1 };\n const since = new Date(Date.now() - durationToMilliseconds(frequency));\n\n // Enhanced search for AI/ML repositories\n const searchQueries =\n this.#config.getOptionalStringArray(\n 'techRadarNg.github.searchQueries',\n ) ?? [];\n\n const requests: Promise<GithubRepo[]>[] = [];\n\n for (const query of searchQueries) {\n // Search for most starred repositories matching the query\n const byMostStars = `q=${encodeURIComponent(\n query,\n )}&sort=stars&per_page=50`;\n\n // Search for trending repositories created since the derived date\n const trending = `q=${encodeURIComponent(\n `(${query}) AND created:>${since.toISOString().split('T')[0]}`,\n )}&sort=stars&per_page=50`;\n\n requests.push(this.fetchReposByQuery(byMostStars));\n requests.push(this.fetchReposByQuery(trending));\n }\n\n const repos = (await Promise.all(requests)).flat();\n // Deduplicate repositories across multiple queries\n const uniqueRepos = repos.filter(\n (repo, index, self) =>\n self.findIndex(r => r.node_id === repo.node_id) === index,\n );\n\n let candidates: CandidateRow[] = [];\n\n for (const repo of uniqueRepos) {\n const candidate = {\n platform_id: repo.node_id,\n full_name: repo.full_name,\n name: repo.name,\n description: repo.description,\n url: repo.html_url,\n homepage: repo.homepage,\n primary_language: repo.language,\n license: repo.license?.name,\n popularity_score: repo.stargazers_count,\n usage_score: repo.forks_count,\n is_active: !repo.archived && !repo.disabled,\n platform: Platform.GITHUB,\n last_activity_at: new Date(repo.pushed_at),\n };\n if (candidate) {\n candidates.push(candidate);\n }\n }\n\n return candidates;\n } catch (error: unknown) {\n this.#logger.error(\n `GitHub GenAI discovery error: ${stringifyError(error)}`,\n );\n return [];\n }\n }\n\n private async fetchReposByQuery(query: string): Promise<GithubRepo[]> {\n this.#logger.info(`Fetching GitHub repositories with query: ${query}`);\n const response = await fetch(\n `https://api.github.com/search/repositories?${query}`,\n {\n headers: {\n Accept: 'application/vnd.github.v3+json',\n },\n },\n );\n\n if (response.ok) {\n const data: {\n incomplete_result: boolean;\n items: GithubRepo[];\n total_count: number;\n } = await response.json();\n\n this.#logger.info(`Fetched ${data.items.length} GitHub repositories`);\n return data.items || [];\n } else {\n const errorText = await response.text();\n this.#logger.error(\n `GitHub API error: ${response.status} ${response.statusText} - ${errorText}`,\n );\n return [];\n }\n }\n}\n\nexport const gitHubMetricsServiceRef = createServiceRef<GitHubMetricsService>({\n id: 'github.metrics',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n },\n async factory(deps) {\n return DefaultGitHubMetricsService.create(deps);\n },\n }),\n});\n"],"names":["durationToMilliseconds","Platform","stringifyError","createServiceRef","createServiceFactory","coreServices"],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
4
|
+
var errors = require('@backstage/errors');
|
|
5
|
+
|
|
6
|
+
class DefaultHuggingFaceMetricsService {
|
|
7
|
+
#logger;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.#logger = options.logger;
|
|
10
|
+
}
|
|
11
|
+
static create(options) {
|
|
12
|
+
return new DefaultHuggingFaceMetricsService(options);
|
|
13
|
+
}
|
|
14
|
+
async getGenAIMetrics(technology) {
|
|
15
|
+
try {
|
|
16
|
+
const modelsResponse = await fetch(
|
|
17
|
+
`https://huggingface.co/api/models?search=${encodeURIComponent(
|
|
18
|
+
technology
|
|
19
|
+
)}&limit=20`
|
|
20
|
+
);
|
|
21
|
+
if (!modelsResponse.ok) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
const models = await modelsResponse.json();
|
|
25
|
+
if (!Array.isArray(models) || models.length === 0) {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
const totalDownloads = models.reduce(
|
|
29
|
+
(sum, model) => sum + (model.downloads || 0),
|
|
30
|
+
0
|
|
31
|
+
);
|
|
32
|
+
const totalLikes = models.reduce(
|
|
33
|
+
(sum, model) => sum + (model.likes || 0),
|
|
34
|
+
0
|
|
35
|
+
);
|
|
36
|
+
const avgDownloads = totalDownloads / models.length;
|
|
37
|
+
const recentModels = models.filter((model) => {
|
|
38
|
+
if (!model.createdAt) return false;
|
|
39
|
+
const created = new Date(model.createdAt);
|
|
40
|
+
const sixMonthsAgo = /* @__PURE__ */ new Date();
|
|
41
|
+
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
|
|
42
|
+
return created > sixMonthsAgo;
|
|
43
|
+
}).length;
|
|
44
|
+
const adoption = Math.min(Math.log10(avgDownloads + 1) * 15, 100);
|
|
45
|
+
const innovation = recentModels / models.length * 100;
|
|
46
|
+
const communitySupport = Math.min(Math.log10(totalLikes + 1) * 20, 100);
|
|
47
|
+
return {
|
|
48
|
+
adoption: Math.round(adoption),
|
|
49
|
+
innovation: Math.round(innovation),
|
|
50
|
+
communitySupport: Math.round(communitySupport)
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
this.#logger.error(`HuggingFace metrics error: ${errors.stringifyError(error)}`);
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async discover() {
|
|
58
|
+
const candidates = [];
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(
|
|
61
|
+
"https://huggingface.co/api/trending?type=model&direction=-1&limit=20"
|
|
62
|
+
);
|
|
63
|
+
if (response.ok) {
|
|
64
|
+
const data = await response.json();
|
|
65
|
+
for (const repo of data.recentlyTrending.slice(0, 15)) {
|
|
66
|
+
const model = repo.repoData;
|
|
67
|
+
if (model.downloads > 1e3 || model.likes > 50) {
|
|
68
|
+
const candidate = {
|
|
69
|
+
name: model.id || "Unknown Model",
|
|
70
|
+
segment: this.categorizeHuggingFaceModel(model),
|
|
71
|
+
description: `HuggingFace model: ${model.pipeline_tag || "AI model"}`,
|
|
72
|
+
category: [model.pipeline_tag || "ml-model"],
|
|
73
|
+
pricing: "free"
|
|
74
|
+
};
|
|
75
|
+
candidates.push(candidate);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
this.#logger.error(
|
|
81
|
+
`HuggingFace discovery error: ${errors.stringifyError(error)}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return candidates;
|
|
85
|
+
}
|
|
86
|
+
categorizeHuggingFaceModel(model) {
|
|
87
|
+
const pipelineTag = model.pipeline_tag?.toLowerCase() || "";
|
|
88
|
+
if (pipelineTag.includes("text-generation") || pipelineTag.includes("conversational")) {
|
|
89
|
+
return "llm-techniques";
|
|
90
|
+
} else if (pipelineTag.includes("text-to-image") || pipelineTag.includes("image")) {
|
|
91
|
+
return "ai-low-code-no-code";
|
|
92
|
+
} else if (pipelineTag.includes("code") || pipelineTag.includes("translation")) {
|
|
93
|
+
return "ai-code-development";
|
|
94
|
+
} else {
|
|
95
|
+
return "infrastructure";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const huggingFaceMetricsServiceRef = backendPluginApi.createServiceRef({
|
|
100
|
+
id: "huggingface.metrics",
|
|
101
|
+
defaultFactory: async (service) => backendPluginApi.createServiceFactory({
|
|
102
|
+
service,
|
|
103
|
+
deps: {
|
|
104
|
+
logger: backendPluginApi.coreServices.logger
|
|
105
|
+
},
|
|
106
|
+
async factory(deps) {
|
|
107
|
+
return DefaultHuggingFaceMetricsService.create(deps);
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
exports.DefaultHuggingFaceMetricsService = DefaultHuggingFaceMetricsService;
|
|
113
|
+
exports.huggingFaceMetricsServiceRef = huggingFaceMetricsServiceRef;
|
|
114
|
+
//# sourceMappingURL=HuggingFaceMetricsService.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HuggingFaceMetricsService.cjs.js","sources":["../../src/service/HuggingFaceMetricsService.ts"],"sourcesContent":["import {\n coreServices,\n createServiceFactory,\n createServiceRef,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport { stringifyError } from '@backstage/errors';\nimport {\n GenAITechCandidate,\n GenAITechMetrics,\n HuggingFaceRepo,\n} from '../types';\n\ninterface DefaultHuggingFaceMetricsServiceOptions {\n logger: LoggerService;\n}\n\nexport interface HuggingFaceMetricsService {\n getGenAIMetrics(technology: string): Promise<Partial<GenAITechMetrics>>;\n discover(): Promise<GenAITechCandidate[]>;\n}\n\nexport class DefaultHuggingFaceMetricsService\n implements HuggingFaceMetricsService\n{\n readonly #logger: LoggerService;\n\n private constructor(options: DefaultHuggingFaceMetricsServiceOptions) {\n this.#logger = options.logger;\n }\n\n static create(options: DefaultHuggingFaceMetricsServiceOptions) {\n return new DefaultHuggingFaceMetricsService(options);\n }\n\n async getGenAIMetrics(\n technology: string,\n ): Promise<Partial<GenAITechMetrics>> {\n try {\n // Search HuggingFace models and datasets\n const modelsResponse = await fetch(\n `https://huggingface.co/api/models?search=${encodeURIComponent(\n technology,\n )}&limit=20`,\n );\n\n if (!modelsResponse.ok) {\n return {};\n }\n\n const models = await modelsResponse.json();\n\n if (!Array.isArray(models) || models.length === 0) {\n return {};\n }\n\n // Calculate metrics based on HuggingFace activity\n const totalDownloads = models.reduce(\n (sum, model) => sum + (model.downloads || 0),\n 0,\n );\n const totalLikes = models.reduce(\n (sum, model) => sum + (model.likes || 0),\n 0,\n );\n const avgDownloads = totalDownloads / models.length;\n\n // Check for recent models\n const recentModels = models.filter(model => {\n if (!model.createdAt) return false;\n const created = new Date(model.createdAt);\n const sixMonthsAgo = new Date();\n sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);\n return created > sixMonthsAgo;\n }).length;\n\n // HuggingFace specific metrics\n const adoption = Math.min(Math.log10(avgDownloads + 1) * 15, 100);\n const innovation = (recentModels / models.length) * 100;\n const communitySupport = Math.min(Math.log10(totalLikes + 1) * 20, 100);\n\n return {\n adoption: Math.round(adoption),\n innovation: Math.round(innovation),\n communitySupport: Math.round(communitySupport),\n };\n } catch (error) {\n this.#logger.error(`HuggingFace metrics error: ${stringifyError(error)}`);\n return {};\n }\n }\n\n async discover(): Promise<GenAITechCandidate[]> {\n const candidates: GenAITechCandidate[] = [];\n\n try {\n // Discover trending models\n const response = await fetch(\n 'https://huggingface.co/api/trending?type=model&direction=-1&limit=20',\n );\n\n if (response.ok) {\n const data: { recentlyTrending: HuggingFaceRepo[] } =\n await response.json();\n\n for (const repo of data.recentlyTrending.slice(0, 15)) {\n // No filter by 'repo.repoType' since we are already fetching only models\n const model = repo.repoData;\n\n // Increased from 10 to 15\n if (model.downloads > 1000 || model.likes > 50) {\n const candidate: GenAITechCandidate = {\n name: model.id || 'Unknown Model',\n segment: this.categorizeHuggingFaceModel(model),\n description: `HuggingFace model: ${\n model.pipeline_tag || 'AI model'\n }`,\n category: [model.pipeline_tag || 'ml-model'],\n pricing: 'free',\n };\n candidates.push(candidate);\n }\n }\n }\n } catch (error) {\n this.#logger.error(\n `HuggingFace discovery error: ${stringifyError(error)}`,\n );\n }\n\n return candidates;\n }\n\n private categorizeHuggingFaceModel(\n model: any,\n ): GenAITechCandidate['segment'] {\n const pipelineTag = model.pipeline_tag?.toLowerCase() || '';\n\n if (\n pipelineTag.includes('text-generation') ||\n pipelineTag.includes('conversational')\n ) {\n return 'llm-techniques';\n } else if (\n pipelineTag.includes('text-to-image') ||\n pipelineTag.includes('image')\n ) {\n return 'ai-low-code-no-code';\n } else if (\n pipelineTag.includes('code') ||\n pipelineTag.includes('translation')\n ) {\n return 'ai-code-development';\n } else {\n return 'infrastructure';\n }\n }\n}\n\nexport const huggingFaceMetricsServiceRef =\n createServiceRef<HuggingFaceMetricsService>({\n id: 'huggingface.metrics',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n logger: coreServices.logger,\n },\n async factory(deps) {\n return DefaultHuggingFaceMetricsService.create(deps);\n },\n }),\n });\n"],"names":["stringifyError","createServiceRef","createServiceFactory","coreServices"],"mappings":"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"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
4
|
+
var errors = require('@backstage/errors');
|
|
5
|
+
var GitHubMetricsService = require('./GitHubMetricsService.cjs.js');
|
|
6
|
+
var HuggingFaceMetricsService = require('./HuggingFaceMetricsService.cjs.js');
|
|
7
|
+
var TechRadarDb = require('../database/TechRadarDb.cjs.js');
|
|
8
|
+
|
|
9
|
+
class DefaultRadarMetricsService {
|
|
10
|
+
#logger;
|
|
11
|
+
#gitHubMetricsService;
|
|
12
|
+
#techRadarDb;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.#logger = options.logger;
|
|
15
|
+
this.#gitHubMetricsService = options.gitHubMetricsService;
|
|
16
|
+
this.#techRadarDb = options.techRadarDb;
|
|
17
|
+
}
|
|
18
|
+
static create(options) {
|
|
19
|
+
return new DefaultRadarMetricsService(options);
|
|
20
|
+
}
|
|
21
|
+
async getRadarMetrics(opts) {
|
|
22
|
+
try {
|
|
23
|
+
const { items: rows, ...pagination } = await this.#techRadarDb.fetchCandidates(opts);
|
|
24
|
+
const items = rows.map((row) => {
|
|
25
|
+
const popularity_score_variation = row.prev_popularity_score !== null && row.prev_popularity_score !== void 0 ? (row.popularity_score - row.prev_popularity_score) / row.prev_popularity_score * 100 : 0;
|
|
26
|
+
const usage_score_variation = row.prev_usage_score !== null && row.prev_usage_score !== void 0 ? (row.usage_score - row.prev_usage_score) / row.prev_usage_score * 100 : 0;
|
|
27
|
+
return {
|
|
28
|
+
id: row.id,
|
|
29
|
+
name: row.name,
|
|
30
|
+
description: row.description,
|
|
31
|
+
url: row.url,
|
|
32
|
+
homepage: row.homepage,
|
|
33
|
+
primary_language: row.primary_language,
|
|
34
|
+
license: row.license,
|
|
35
|
+
popularity_score: row.popularity_score,
|
|
36
|
+
usage_score: row.usage_score,
|
|
37
|
+
popularity_score_variation: parseFloat(
|
|
38
|
+
popularity_score_variation.toFixed(2)
|
|
39
|
+
),
|
|
40
|
+
usage_score_variation: parseFloat(usage_score_variation.toFixed(2)),
|
|
41
|
+
in_radar: row.in_radar ?? false,
|
|
42
|
+
is_active: row.is_active,
|
|
43
|
+
platform: row.platform,
|
|
44
|
+
segment: row.segment,
|
|
45
|
+
ring: row.ring
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
return { ...pagination, items };
|
|
49
|
+
} catch (error) {
|
|
50
|
+
this.#logger.error(`Radar metrics error: ${errors.stringifyError(error)}`);
|
|
51
|
+
return {
|
|
52
|
+
items: [],
|
|
53
|
+
total: 0,
|
|
54
|
+
page: opts.page,
|
|
55
|
+
pageSize: opts.pageSize,
|
|
56
|
+
totalPages: 0
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async discover() {
|
|
61
|
+
const discoveries = [];
|
|
62
|
+
try {
|
|
63
|
+
const [githubDiscoveries] = await Promise.allSettled([
|
|
64
|
+
this.#gitHubMetricsService.discover()
|
|
65
|
+
// this.#huggingFaceMetricsService.discover(),
|
|
66
|
+
// this.discoverFromAINews(),
|
|
67
|
+
]);
|
|
68
|
+
if (githubDiscoveries.status === "fulfilled") {
|
|
69
|
+
discoveries.push(...githubDiscoveries.value);
|
|
70
|
+
}
|
|
71
|
+
const insertedRecords = await this.#techRadarDb.insertCandidates(
|
|
72
|
+
discoveries
|
|
73
|
+
);
|
|
74
|
+
if (insertedRecords && insertedRecords.length > 0) {
|
|
75
|
+
const idByPlatformId = new Map(
|
|
76
|
+
insertedRecords.map((r) => [r.platform_id, r.id])
|
|
77
|
+
);
|
|
78
|
+
const candidatesWithIds = discoveries.map((c) => ({ ...c, id: idByPlatformId.get(c.platform_id) })).filter((c) => c.id !== void 0);
|
|
79
|
+
await this.#techRadarDb.insertCandidateSnapshots(candidatesWithIds);
|
|
80
|
+
this.#logger.info(
|
|
81
|
+
`Total candidates in database after discovery: ${insertedRecords.length}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (discoveries.length !== insertedRecords?.length) {
|
|
85
|
+
this.#logger.warn(
|
|
86
|
+
`Discovered ${discoveries.length} candidates but only ${insertedRecords?.length} were inserted. There may be duplicates or errors during insertion.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
this.#logger.error(
|
|
91
|
+
`GenAI technology discovery failed: ${errors.stringifyError(error)}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async fetchRings() {
|
|
96
|
+
try {
|
|
97
|
+
const rings = await this.#techRadarDb.findRings();
|
|
98
|
+
return rings;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
this.#logger.error(`Error fetching rings: ${errors.stringifyError(error)}`);
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async fetchQuadrants() {
|
|
105
|
+
try {
|
|
106
|
+
const segments = await this.#techRadarDb.findQuadrants();
|
|
107
|
+
return segments;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
this.#logger.error(`Error fetching segments: ${errors.stringifyError(error)}`);
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async patchCandidates(candidatesPatch) {
|
|
114
|
+
this.#logger.info(
|
|
115
|
+
`Patching candidates with data: ${JSON.stringify(candidatesPatch)}`
|
|
116
|
+
);
|
|
117
|
+
await this.#techRadarDb.patchCandidates(candidatesPatch);
|
|
118
|
+
this.#logger.info("Successfully patched candidates.");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const radarMetricsServiceRef = backendPluginApi.createServiceRef({
|
|
122
|
+
id: "radar.metrics",
|
|
123
|
+
defaultFactory: async (service) => backendPluginApi.createServiceFactory({
|
|
124
|
+
service,
|
|
125
|
+
deps: {
|
|
126
|
+
logger: backendPluginApi.coreServices.logger,
|
|
127
|
+
gitHubMetricsService: GitHubMetricsService.gitHubMetricsServiceRef,
|
|
128
|
+
huggingFaceMetricsService: HuggingFaceMetricsService.huggingFaceMetricsServiceRef,
|
|
129
|
+
techRadarDb: TechRadarDb.techRadarDbRef
|
|
130
|
+
},
|
|
131
|
+
async factory(deps) {
|
|
132
|
+
return DefaultRadarMetricsService.create(deps);
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
exports.DefaultRadarMetricsService = DefaultRadarMetricsService;
|
|
138
|
+
exports.radarMetricsServiceRef = radarMetricsServiceRef;
|
|
139
|
+
//# sourceMappingURL=RadarMetricsService.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RadarMetricsService.cjs.js","sources":["../../src/service/RadarMetricsService.ts"],"sourcesContent":["import {\n coreServices,\n createServiceFactory,\n createServiceRef,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport { stringifyError } from '@backstage/errors';\nimport {\n type GitHubMetricsService,\n gitHubMetricsServiceRef,\n} from './GitHubMetricsService';\nimport type {\n Candidate,\n CandidateQueryValues,\n CandidatesPatch,\n PaginatedResult,\n Segment,\n Ring,\n} from '@nospt/plugin-tech-radar-ng-common';\nimport type { CandidateRow } from '../types';\nimport {\n HuggingFaceMetricsService,\n huggingFaceMetricsServiceRef,\n} from './HuggingFaceMetricsService';\nimport { TechRadarDb, techRadarDbRef } from '../database/TechRadarDb';\n\ninterface RadarMetricsServiceOptions {\n logger: LoggerService;\n gitHubMetricsService: GitHubMetricsService;\n huggingFaceMetricsService: HuggingFaceMetricsService;\n techRadarDb: TechRadarDb;\n}\n\nexport interface RadarMetricsService {\n getRadarMetrics(\n opts: CandidateQueryValues,\n ): Promise<PaginatedResult<Candidate>>;\n discover(): Promise<void>;\n fetchRings(): Promise<Ring[]>;\n fetchQuadrants(): Promise<Segment[]>;\n patchCandidates(candidatesPatch: CandidatesPatch): Promise<void>;\n}\n\nexport class DefaultRadarMetricsService implements RadarMetricsService {\n readonly #logger: LoggerService;\n readonly #gitHubMetricsService: GitHubMetricsService;\n readonly #techRadarDb: TechRadarDb;\n\n private constructor(options: RadarMetricsServiceOptions) {\n this.#logger = options.logger;\n this.#gitHubMetricsService = options.gitHubMetricsService;\n this.#techRadarDb = options.techRadarDb;\n }\n\n static create(options: RadarMetricsServiceOptions) {\n return new DefaultRadarMetricsService(options);\n }\n\n async getRadarMetrics(\n opts: CandidateQueryValues,\n ): Promise<PaginatedResult<Candidate>> {\n try {\n const { items: rows, ...pagination } =\n await this.#techRadarDb.fetchCandidates(opts);\n\n const items: Candidate[] = rows.map(row => {\n const popularity_score_variation =\n row.prev_popularity_score !== null &&\n row.prev_popularity_score !== undefined\n ? ((row.popularity_score - row.prev_popularity_score) /\n row.prev_popularity_score) *\n 100\n : 0;\n\n const usage_score_variation =\n row.prev_usage_score !== null && row.prev_usage_score !== undefined\n ? ((row.usage_score - row.prev_usage_score) /\n row.prev_usage_score) *\n 100\n : 0;\n\n return {\n id: row.id as string,\n name: row.name,\n description: row.description,\n url: row.url,\n homepage: row.homepage,\n primary_language: row.primary_language,\n license: row.license,\n popularity_score: row.popularity_score,\n usage_score: row.usage_score,\n popularity_score_variation: parseFloat(\n popularity_score_variation.toFixed(2),\n ),\n usage_score_variation: parseFloat(usage_score_variation.toFixed(2)),\n in_radar: row.in_radar ?? false,\n is_active: row.is_active,\n platform: row.platform,\n segment: row.segment,\n ring: row.ring,\n };\n });\n\n return { ...pagination, items };\n } catch (error: unknown) {\n this.#logger.error(`Radar metrics error: ${stringifyError(error)}`);\n return {\n items: [],\n total: 0,\n page: opts.page,\n pageSize: opts.pageSize,\n totalPages: 0,\n };\n }\n }\n\n async discover(): Promise<void> {\n const discoveries: CandidateRow[] = [];\n\n try {\n // Multi-source discovery\n const [githubDiscoveries] = await Promise.allSettled([\n this.#gitHubMetricsService.discover(),\n // this.#huggingFaceMetricsService.discover(),\n // this.discoverFromAINews(),\n ]);\n\n // Aggregate discoveries\n if (githubDiscoveries.status === 'fulfilled') {\n discoveries.push(...githubDiscoveries.value);\n }\n\n // if (huggingFaceDiscoveries.status === 'fulfilled') {\n // discoveries.push(...huggingFaceDiscoveries.value);\n // }\n\n const insertedRecords = await this.#techRadarDb.insertCandidates(\n discoveries,\n );\n\n if (insertedRecords && insertedRecords.length > 0) {\n // Match DB-assigned IDs back to candidates by platform_id (order-safe)\n const idByPlatformId = new Map(\n insertedRecords.map(r => [r.platform_id, r.id]),\n );\n const candidatesWithIds = discoveries\n .map(c => ({ ...c, id: idByPlatformId.get(c.platform_id as string) }))\n .filter(c => c.id !== undefined) as typeof discoveries;\n await this.#techRadarDb.insertCandidateSnapshots(candidatesWithIds);\n\n this.#logger.info(\n `Total candidates in database after discovery: ${insertedRecords.length}`,\n );\n }\n\n if (discoveries.length !== insertedRecords?.length) {\n this.#logger.warn(\n `Discovered ${discoveries.length} candidates but only ${insertedRecords?.length} were inserted. There may be duplicates or errors during insertion.`,\n );\n }\n } catch (error) {\n this.#logger.error(\n `GenAI technology discovery failed: ${stringifyError(error)}`,\n );\n }\n }\n\n async fetchRings(): Promise<Ring[]> {\n try {\n const rings = await this.#techRadarDb.findRings();\n return rings;\n } catch (error) {\n this.#logger.error(`Error fetching rings: ${stringifyError(error)}`);\n return [];\n }\n }\n\n async fetchQuadrants(): Promise<Segment[]> {\n try {\n const segments = await this.#techRadarDb.findQuadrants();\n return segments;\n } catch (error) {\n this.#logger.error(`Error fetching segments: ${stringifyError(error)}`);\n return [];\n }\n }\n\n async patchCandidates(candidatesPatch: CandidatesPatch): Promise<void> {\n this.#logger.info(\n `Patching candidates with data: ${JSON.stringify(candidatesPatch)}`,\n );\n await this.#techRadarDb.patchCandidates(candidatesPatch);\n this.#logger.info('Successfully patched candidates.');\n }\n}\n\nexport const radarMetricsServiceRef = createServiceRef<RadarMetricsService>({\n id: 'radar.metrics',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n logger: coreServices.logger,\n gitHubMetricsService: gitHubMetricsServiceRef,\n huggingFaceMetricsService: huggingFaceMetricsServiceRef,\n techRadarDb: techRadarDbRef,\n },\n async factory(deps) {\n return DefaultRadarMetricsService.create(deps);\n },\n }),\n});\n"],"names":["stringifyError","createServiceRef","createServiceFactory","coreServices","gitHubMetricsServiceRef","huggingFaceMetricsServiceRef","techRadarDbRef"],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: create segments table
|
|
3
|
+
*
|
|
4
|
+
* Stores segment entries — categories for the Tech Radar NG.
|
|
5
|
+
*
|
|
6
|
+
* NOTE: Migrations must be plain JS (not TS). Backstage runs them
|
|
7
|
+
* at runtime without a TypeScript loader.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {import('knex').Knex} knex
|
|
12
|
+
*/
|
|
13
|
+
exports.up = async function (knex) {
|
|
14
|
+
await knex.schema.createTable('segments', table => {
|
|
15
|
+
table.string('id').primary();
|
|
16
|
+
|
|
17
|
+
// Core identity
|
|
18
|
+
table.string('name').notNullable().unique();
|
|
19
|
+
table.text('description');
|
|
20
|
+
|
|
21
|
+
// search_params is a string[] — stored as JSON text
|
|
22
|
+
table.text('search_params').notNullable().defaultTo('[]');
|
|
23
|
+
|
|
24
|
+
table.timestamp('created_at').defaultTo(knex.fn.now());
|
|
25
|
+
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {import('knex').Knex} knex
|
|
31
|
+
*/
|
|
32
|
+
exports.down = async function (knex) {
|
|
33
|
+
await knex.schema.dropTable('segments');
|
|
34
|
+
};
|