@acarmisc/backstage-plugin-ai-agents 0.1.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.
@@ -0,0 +1,999 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+
16
+ // src/types.ts
17
+ function annotation(entity, key) {
18
+ return entity.metadata.annotations?.[`${AI_AGENT_ANNOTATION_PREFIX}/${key}`];
19
+ }
20
+ function parseFloatSafe(value) {
21
+ if (!value) return void 0;
22
+ const n = Number.parseFloat(value);
23
+ return Number.isFinite(n) ? n : void 0;
24
+ }
25
+ function parseCapabilities(raw) {
26
+ if (!raw) return [];
27
+ return raw.split(/[,\n]/).map((s) => s.trim()).filter(Boolean).map((token) => {
28
+ const [label, catRaw] = token.split(":").map((s) => s.trim());
29
+ const category = catRaw ? catRaw : void 0;
30
+ return {
31
+ label,
32
+ category: category && VALID_CATEGORIES.has(category) ? category : void 0
33
+ };
34
+ });
35
+ }
36
+ function entityRef(entity) {
37
+ const ns = entity.metadata.namespace ?? "default";
38
+ return `${entity.kind.toLowerCase()}:${ns}/${entity.metadata.name}`;
39
+ }
40
+ function purpose(entity) {
41
+ const explicit = annotation(entity, "purpose");
42
+ return (explicit ?? entity.metadata.description ?? "").trim();
43
+ }
44
+ function entityToAgent(entity, status) {
45
+ if (entity.spec?.type !== AI_AGENT_TYPE) return void 0;
46
+ const links = (entity.metadata.links ?? []).map((l) => ({
47
+ url: l.url,
48
+ title: l.title ?? l.url,
49
+ icon: l.icon
50
+ }));
51
+ return {
52
+ entityRef: entityRef(entity),
53
+ name: entity.metadata.name,
54
+ title: entity.metadata.title,
55
+ description: entity.metadata.description,
56
+ avatarUrl: annotation(entity, "avatar"),
57
+ owner: entity.spec?.owner,
58
+ system: entity.spec?.system,
59
+ lifecycle: entity.spec?.lifecycle,
60
+ version: annotation(entity, "version"),
61
+ purpose: purpose(entity),
62
+ runtime: {
63
+ runtime: annotation(entity, "runtime") ?? "custom",
64
+ runtimeHandle: annotation(entity, "runtime-handle"),
65
+ endpoint: annotation(entity, "endpoint"),
66
+ healthUrl: annotation(entity, "health")
67
+ },
68
+ billing: {
69
+ model: annotation(entity, "billing-model") ?? "free",
70
+ unitCost: parseFloatSafe(annotation(entity, "cost-per-1k")),
71
+ budget: parseFloatSafe(annotation(entity, "budget"))
72
+ },
73
+ capabilities: parseCapabilities(annotation(entity, "capabilities")),
74
+ tags: entity.metadata.tags ?? [],
75
+ links,
76
+ status,
77
+ rawEntity: entity
78
+ };
79
+ }
80
+ var AI_AGENT_TYPE, AI_AGENT_ANNOTATION_PREFIX, VALID_CATEGORIES;
81
+ var init_types = __esm({
82
+ "src/types.ts"() {
83
+ "use strict";
84
+ AI_AGENT_TYPE = "ai-agent";
85
+ AI_AGENT_ANNOTATION_PREFIX = "ai-agent.acarmisc.org";
86
+ VALID_CATEGORIES = /* @__PURE__ */ new Set([
87
+ "reasoning",
88
+ "retrieval",
89
+ "tools",
90
+ "vision",
91
+ "voice",
92
+ "data",
93
+ "safety"
94
+ ]);
95
+ }
96
+ });
97
+
98
+ // src/api.ts
99
+ import { createApiRef } from "@backstage/core-plugin-api";
100
+ var aiAgentsApiRef, AiAgentsApi;
101
+ var init_api = __esm({
102
+ "src/api.ts"() {
103
+ "use strict";
104
+ init_types();
105
+ aiAgentsApiRef = createApiRef({
106
+ id: "plugin.ai-agents.api"
107
+ });
108
+ AiAgentsApi = class {
109
+ constructor(opts, basePath = "/api/ai-agents") {
110
+ this.opts = opts;
111
+ this.basePath = basePath;
112
+ }
113
+ async listAgents() {
114
+ const result = await this.opts.catalogApi.getEntities({
115
+ filter: { kind: "Component", "spec.type": AI_AGENT_TYPE }
116
+ });
117
+ return result.items.map((e) => entityToAgent(e)).filter((a) => a !== void 0);
118
+ }
119
+ async getAgent(entityRef2) {
120
+ const entity = await this.opts.catalogApi.getEntityByRef(entityRef2);
121
+ return entity ? entityToAgent(entity) : void 0;
122
+ }
123
+ async getStatuses(entityRefs) {
124
+ if (!entityRefs.length) return {};
125
+ try {
126
+ const res = await this.opts.fetchApi.fetch(
127
+ `${this.basePath}/statuses?refs=${encodeURIComponent(entityRefs.join(","))}`
128
+ );
129
+ if (!res.ok) return {};
130
+ return await res.json();
131
+ } catch {
132
+ return {};
133
+ }
134
+ }
135
+ };
136
+ }
137
+ });
138
+
139
+ // src/hooks/useAgents.ts
140
+ import { useCallback, useMemo, useState } from "react";
141
+ import { useApi } from "@backstage/core-plugin-api";
142
+ import { useAsyncRetry } from "react-use";
143
+ function applyFilters(agents, filters) {
144
+ const search = filters.search.trim().toLowerCase();
145
+ return agents.filter((a) => {
146
+ if (search) {
147
+ const haystack = [a.name, a.title, a.description, a.purpose].filter(Boolean).join(" ").toLowerCase();
148
+ if (!haystack.includes(search)) return false;
149
+ }
150
+ if (filters.runtime.length && !filters.runtime.includes(a.runtime.runtime)) {
151
+ return false;
152
+ }
153
+ if (filters.capability.length) {
154
+ const caps = a.capabilities.map((c) => c.label);
155
+ if (!filters.capability.every((c) => caps.includes(c))) return false;
156
+ }
157
+ if (filters.lifecycle.length && !filters.lifecycle.includes(a.lifecycle ?? "")) {
158
+ return false;
159
+ }
160
+ if (filters.owner.length && !filters.owner.includes(a.owner ?? "")) {
161
+ return false;
162
+ }
163
+ return true;
164
+ });
165
+ }
166
+ function useAgents() {
167
+ const api = useApi(aiAgentsApiRef);
168
+ const {
169
+ value: agents,
170
+ loading,
171
+ error,
172
+ retry
173
+ } = useAsyncRetry(() => api.listAgents(), [api]);
174
+ const [filters, setFilters] = useState(initialFilters);
175
+ const filtered = useMemo(
176
+ () => applyFilters(agents ?? [], filters),
177
+ [agents, filters]
178
+ );
179
+ const update = useCallback(
180
+ (patch) => setFilters((prev) => ({ ...prev, ...patch })),
181
+ []
182
+ );
183
+ const reset = useCallback(() => setFilters(initialFilters), []);
184
+ return {
185
+ agents: filtered,
186
+ allAgents: agents ?? [],
187
+ loading,
188
+ error,
189
+ retry,
190
+ filters,
191
+ update,
192
+ reset
193
+ };
194
+ }
195
+ var initialFilters;
196
+ var init_useAgents = __esm({
197
+ "src/hooks/useAgents.ts"() {
198
+ "use strict";
199
+ init_api();
200
+ initialFilters = {
201
+ search: "",
202
+ runtime: [],
203
+ capability: [],
204
+ lifecycle: [],
205
+ owner: []
206
+ };
207
+ }
208
+ });
209
+
210
+ // src/components/AgentFilters.tsx
211
+ import React from "react";
212
+ import {
213
+ Box,
214
+ IconButton,
215
+ InputAdornment,
216
+ MenuItem,
217
+ Paper,
218
+ TextField,
219
+ Tooltip
220
+ } from "@mui/material";
221
+ import SearchIcon from "@mui/icons-material/Search";
222
+ import ClearIcon from "@mui/icons-material/Clear";
223
+ var SELECT_PROPS, AgentFiltersBar;
224
+ var init_AgentFilters = __esm({
225
+ "src/components/AgentFilters.tsx"() {
226
+ "use strict";
227
+ SELECT_PROPS = {
228
+ SelectProps: { multiple: true, renderValue: (v) => v.join(", ") || "All" }
229
+ };
230
+ AgentFiltersBar = ({
231
+ agents,
232
+ filters,
233
+ onChange,
234
+ onReset
235
+ }) => {
236
+ const runtimes = Array.from(new Set(agents.map((a) => a.runtime.runtime))).sort();
237
+ const capabilities = Array.from(
238
+ new Set(agents.flatMap((a) => a.capabilities.map((c) => c.label)))
239
+ ).sort();
240
+ const lifecycles = Array.from(new Set(agents.map((a) => a.lifecycle).filter(Boolean)));
241
+ const owners = Array.from(new Set(agents.map((a) => a.owner).filter(Boolean)));
242
+ const hasFilters = filters.search || filters.runtime.length || filters.capability.length || filters.lifecycle.length || filters.owner.length;
243
+ return /* @__PURE__ */ React.createElement(Paper, { sx: { p: 1.5, mb: 2, display: "flex", gap: 1.5, flexWrap: "wrap", alignItems: "center" } }, /* @__PURE__ */ React.createElement(
244
+ TextField,
245
+ {
246
+ size: "small",
247
+ placeholder: "Search agents\u2026",
248
+ value: filters.search,
249
+ onChange: (e) => onChange({ search: e.target.value }),
250
+ sx: { minWidth: 220, flexGrow: 1 },
251
+ InputProps: {
252
+ startAdornment: /* @__PURE__ */ React.createElement(InputAdornment, { position: "start" }, /* @__PURE__ */ React.createElement(SearchIcon, { fontSize: "small" })),
253
+ endAdornment: filters.search ? /* @__PURE__ */ React.createElement(InputAdornment, { position: "end" }, /* @__PURE__ */ React.createElement(IconButton, { size: "small", onClick: () => onChange({ search: "" }) }, /* @__PURE__ */ React.createElement(ClearIcon, { fontSize: "small" }))) : void 0
254
+ }
255
+ }
256
+ ), /* @__PURE__ */ React.createElement(
257
+ TextField,
258
+ {
259
+ select: true,
260
+ size: "small",
261
+ label: "Runtime",
262
+ value: filters.runtime,
263
+ onChange: (e) => onChange({ runtime: e.target.value }),
264
+ sx: { minWidth: 140 },
265
+ ...SELECT_PROPS
266
+ },
267
+ runtimes.map((r) => /* @__PURE__ */ React.createElement(MenuItem, { key: r, value: r }, r))
268
+ ), /* @__PURE__ */ React.createElement(
269
+ TextField,
270
+ {
271
+ select: true,
272
+ size: "small",
273
+ label: "Capability",
274
+ value: filters.capability,
275
+ onChange: (e) => onChange({ capability: e.target.value }),
276
+ sx: { minWidth: 150 },
277
+ ...SELECT_PROPS
278
+ },
279
+ capabilities.map((c) => /* @__PURE__ */ React.createElement(MenuItem, { key: c, value: c }, c))
280
+ ), /* @__PURE__ */ React.createElement(
281
+ TextField,
282
+ {
283
+ select: true,
284
+ size: "small",
285
+ label: "Lifecycle",
286
+ value: filters.lifecycle,
287
+ onChange: (e) => onChange({ lifecycle: e.target.value }),
288
+ sx: { minWidth: 130 },
289
+ ...SELECT_PROPS
290
+ },
291
+ lifecycles.map((l) => /* @__PURE__ */ React.createElement(MenuItem, { key: l, value: l }, l))
292
+ ), /* @__PURE__ */ React.createElement(
293
+ TextField,
294
+ {
295
+ select: true,
296
+ size: "small",
297
+ label: "Owner",
298
+ value: filters.owner,
299
+ onChange: (e) => onChange({ owner: e.target.value }),
300
+ sx: { minWidth: 140 },
301
+ ...SELECT_PROPS
302
+ },
303
+ owners.map((o) => /* @__PURE__ */ React.createElement(MenuItem, { key: o, value: o }, o))
304
+ ), hasFilters && /* @__PURE__ */ React.createElement(Tooltip, { title: "Clear filters" }, /* @__PURE__ */ React.createElement(IconButton, { size: "small", onClick: onReset }, /* @__PURE__ */ React.createElement(ClearIcon, { fontSize: "small" }))), /* @__PURE__ */ React.createElement(Box, { sx: { ml: "auto", alignSelf: "center" } }, /* @__PURE__ */ React.createElement("strong", null, agents.length), " agent", agents.length !== 1 ? "s" : ""));
305
+ };
306
+ }
307
+ });
308
+
309
+ // src/components/AgentAvatar.tsx
310
+ import React2, { useState as useState2 } from "react";
311
+ import { Box as Box2 } from "@mui/material";
312
+ function initialsOf(name) {
313
+ const parts = name.replace(/[-_]+/g, " ").trim().split(/\s+/);
314
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
315
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
316
+ }
317
+ function colorFor(name) {
318
+ let hash = 0;
319
+ for (let i = 0; i < name.length; i++) hash = hash * 31 + name.charCodeAt(i) | 0;
320
+ return PALETTE[Math.abs(hash) % PALETTE.length];
321
+ }
322
+ var PALETTE, AgentAvatar;
323
+ var init_AgentAvatar = __esm({
324
+ "src/components/AgentAvatar.tsx"() {
325
+ "use strict";
326
+ PALETTE = [
327
+ "#1976d2",
328
+ "#388e3c",
329
+ "#f57c00",
330
+ "#7b1fa2",
331
+ "#c62828",
332
+ "#0097a7",
333
+ "#5d4037",
334
+ "#455a64"
335
+ ];
336
+ AgentAvatar = ({
337
+ name,
338
+ avatarUrl,
339
+ size = 44
340
+ }) => {
341
+ const [broken, setBroken] = useState2(false);
342
+ const showImage = avatarUrl && !broken;
343
+ return /* @__PURE__ */ React2.createElement(
344
+ Box2,
345
+ {
346
+ sx: {
347
+ width: size,
348
+ height: size,
349
+ flexShrink: 0,
350
+ borderRadius: "50%",
351
+ overflow: "hidden",
352
+ display: "flex",
353
+ alignItems: "center",
354
+ justifyContent: "center",
355
+ bgcolor: showImage ? "transparent" : colorFor(name),
356
+ color: "common.white",
357
+ fontSize: size * 0.36,
358
+ fontWeight: 700
359
+ }
360
+ },
361
+ showImage ? /* @__PURE__ */ React2.createElement(
362
+ "img",
363
+ {
364
+ src: avatarUrl,
365
+ alt: name,
366
+ width: size,
367
+ height: size,
368
+ onError: () => setBroken(true),
369
+ style: { objectFit: "cover", borderRadius: "50%" }
370
+ }
371
+ ) : initialsOf(name)
372
+ );
373
+ };
374
+ }
375
+ });
376
+
377
+ // src/components/AgentStatusBadge.tsx
378
+ import React3 from "react";
379
+ import { Box as Box3, Tooltip as Tooltip2, Typography } from "@mui/material";
380
+ var STATE_COLOR, AgentStatusBadge;
381
+ var init_AgentStatusBadge = __esm({
382
+ "src/components/AgentStatusBadge.tsx"() {
383
+ "use strict";
384
+ STATE_COLOR = {
385
+ healthy: "success.main",
386
+ degraded: "warning.main",
387
+ down: "error.main",
388
+ unknown: "text.disabled"
389
+ };
390
+ AgentStatusBadge = ({ status }) => {
391
+ const state = status?.state ?? "unknown";
392
+ const color = STATE_COLOR[state];
393
+ const ring = state === "unknown" ? `1px dashed ${color}` : "none";
394
+ const title = status ? [
395
+ `Status: ${state}`,
396
+ status.lastChecked ? `Last checked: ${new Date(status.lastChecked).toLocaleString()}` : null,
397
+ status.latencyMs != null ? `Latency: ${status.latencyMs}ms` : null,
398
+ status.message ? status.message : null
399
+ ].filter(Boolean).join(" \xB7 ") : "Status: unknown";
400
+ return /* @__PURE__ */ React3.createElement(Tooltip2, { title, arrow: true }, /* @__PURE__ */ React3.createElement(
401
+ Box3,
402
+ {
403
+ sx: {
404
+ display: "inline-flex",
405
+ alignItems: "center",
406
+ gap: 0.75,
407
+ cursor: "help"
408
+ }
409
+ },
410
+ /* @__PURE__ */ React3.createElement(
411
+ Box3,
412
+ {
413
+ component: "span",
414
+ sx: {
415
+ width: 10,
416
+ height: 10,
417
+ borderRadius: "50%",
418
+ bgcolor: color,
419
+ border: ring,
420
+ flexShrink: 0
421
+ }
422
+ }
423
+ ),
424
+ status?.latencyMs != null && /* @__PURE__ */ React3.createElement(Typography, { variant: "caption", color: "text.secondary" }, status.latencyMs, "ms")
425
+ ));
426
+ };
427
+ }
428
+ });
429
+
430
+ // src/components/AgentCapabilities.tsx
431
+ import React4, { useState as useState3 } from "react";
432
+ import { Chip, Box as Box4, Popover } from "@mui/material";
433
+ var CATEGORY_COLOR, MAX_VISIBLE, AgentCapabilities;
434
+ var init_AgentCapabilities = __esm({
435
+ "src/components/AgentCapabilities.tsx"() {
436
+ "use strict";
437
+ CATEGORY_COLOR = {
438
+ reasoning: "primary",
439
+ retrieval: "info",
440
+ tools: "secondary",
441
+ vision: "success",
442
+ voice: "warning",
443
+ data: "default",
444
+ safety: "error"
445
+ };
446
+ MAX_VISIBLE = 5;
447
+ AgentCapabilities = ({
448
+ capabilities,
449
+ max = MAX_VISIBLE,
450
+ size = "small"
451
+ }) => {
452
+ const [anchor, setAnchor] = useState3(null);
453
+ if (!capabilities.length) return null;
454
+ const visible = capabilities.slice(0, max);
455
+ const overflow = capabilities.length - visible.length;
456
+ return /* @__PURE__ */ React4.createElement(Box4, { sx: { display: "flex", flexWrap: "wrap", gap: 0.5 } }, visible.map((c, i) => /* @__PURE__ */ React4.createElement(
457
+ Chip,
458
+ {
459
+ key: `${c.label}-${i}`,
460
+ label: c.label,
461
+ size,
462
+ color: c.category ? CATEGORY_COLOR[c.category] : "default",
463
+ variant: c.category ? "filled" : "outlined"
464
+ }
465
+ )), overflow > 0 && /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(
466
+ Chip,
467
+ {
468
+ label: `+${overflow}`,
469
+ size,
470
+ clickable: true,
471
+ onClick: (e) => {
472
+ e.stopPropagation();
473
+ setAnchor(e.currentTarget);
474
+ }
475
+ }
476
+ ), /* @__PURE__ */ React4.createElement(
477
+ Popover,
478
+ {
479
+ open: Boolean(anchor),
480
+ anchorEl: anchor,
481
+ onClose: () => setAnchor(null),
482
+ anchorOrigin: { vertical: "bottom", horizontal: "left" }
483
+ },
484
+ /* @__PURE__ */ React4.createElement(Box4, { sx: { p: 1, maxWidth: 280, display: "flex", flexWrap: "wrap", gap: 0.5 } }, capabilities.map((c, i) => /* @__PURE__ */ React4.createElement(
485
+ Chip,
486
+ {
487
+ key: `${c.label}-${i}`,
488
+ label: c.label,
489
+ size: "small",
490
+ color: c.category ? CATEGORY_COLOR[c.category] : "default",
491
+ variant: c.category ? "filled" : "outlined"
492
+ }
493
+ )))
494
+ )));
495
+ };
496
+ }
497
+ });
498
+
499
+ // src/components/RuntimeBadge.tsx
500
+ import React5 from "react";
501
+ import { Chip as Chip2, Box as Box5 } from "@mui/material";
502
+ import MemoryIcon from "@mui/icons-material/Memory";
503
+ import CloudQueueIcon from "@mui/icons-material/CloudQueue";
504
+ import FunctionsIcon from "@mui/icons-material/Functions";
505
+ import ExtensionIcon from "@mui/icons-material/Extension";
506
+ var RUNTIME_META, RuntimeBadge;
507
+ var init_RuntimeBadge = __esm({
508
+ "src/components/RuntimeBadge.tsx"() {
509
+ "use strict";
510
+ RUNTIME_META = {
511
+ "bedrock-agentcore": { label: "Bedrock AgentCore", icon: /* @__PURE__ */ React5.createElement(CloudQueueIcon, { fontSize: "small" }) },
512
+ litellm: { label: "LiteLLM", icon: /* @__PURE__ */ React5.createElement(MemoryIcon, { fontSize: "small" }) },
513
+ lambda: { label: "AWS Lambda", icon: /* @__PURE__ */ React5.createElement(FunctionsIcon, { fontSize: "small" }) },
514
+ custom: { label: "Custom", icon: /* @__PURE__ */ React5.createElement(ExtensionIcon, { fontSize: "small" }) }
515
+ };
516
+ RuntimeBadge = ({
517
+ runtime,
518
+ size = "small",
519
+ onClick
520
+ }) => {
521
+ const meta = RUNTIME_META[runtime] ?? {
522
+ label: String(runtime),
523
+ icon: /* @__PURE__ */ React5.createElement(ExtensionIcon, { fontSize: "small" })
524
+ };
525
+ return /* @__PURE__ */ React5.createElement(
526
+ Chip2,
527
+ {
528
+ size,
529
+ variant: "outlined",
530
+ label: /* @__PURE__ */ React5.createElement(Box5, { sx: { display: "inline-flex", alignItems: "center", gap: 0.5 } }, meta.icon, meta.label),
531
+ onClick: onClick ? () => onClick(runtime) : void 0,
532
+ clickable: Boolean(onClick)
533
+ }
534
+ );
535
+ };
536
+ }
537
+ });
538
+
539
+ // src/components/BillingBadge.tsx
540
+ import React6 from "react";
541
+ import { Chip as Chip3, Box as Box6, Typography as Typography2 } from "@mui/material";
542
+ var BILLING_COLOR, BillingBadge;
543
+ var init_BillingBadge = __esm({
544
+ "src/components/BillingBadge.tsx"() {
545
+ "use strict";
546
+ BILLING_COLOR = {
547
+ "per-invocation": "primary",
548
+ "per-token": "secondary",
549
+ subscription: "success",
550
+ free: "default"
551
+ };
552
+ BillingBadge = ({ billing }) => {
553
+ const color = BILLING_COLOR[billing.model] ?? "default";
554
+ const unitLabel = billing.model === "per-token" ? "per 1M tokens" : billing.model === "per-invocation" ? "per 1k calls" : null;
555
+ return /* @__PURE__ */ React6.createElement(Box6, { sx: { display: "flex", flexDirection: "column", gap: 0.25 } }, /* @__PURE__ */ React6.createElement(Chip3, { size: "small", color, label: billing.model, variant: "outlined" }), billing.unitCost != null && unitLabel && /* @__PURE__ */ React6.createElement(Typography2, { variant: "caption", color: "text.secondary" }, "~$", billing.unitCost, " ", unitLabel), billing.budget != null && /* @__PURE__ */ React6.createElement(Typography2, { variant: "caption", color: "text.secondary" }, "budget: $", billing.budget));
556
+ };
557
+ }
558
+ });
559
+
560
+ // src/components/AgentCard.tsx
561
+ import React7 from "react";
562
+ import { Box as Box7, Card, CardActionArea, Chip as Chip4, Link, Tooltip as Tooltip3, Typography as Typography3 } from "@mui/material";
563
+ import DashboardIcon from "@mui/icons-material/Dashboard";
564
+ import DescriptionIcon from "@mui/icons-material/Description";
565
+ import ArticleIcon from "@mui/icons-material/Article";
566
+ import BugReportIcon from "@mui/icons-material/BugReport";
567
+ var LIFECYCLE_COLOR, LINK_ICON, AgentCard;
568
+ var init_AgentCard = __esm({
569
+ "src/components/AgentCard.tsx"() {
570
+ "use strict";
571
+ init_AgentAvatar();
572
+ init_AgentStatusBadge();
573
+ init_AgentCapabilities();
574
+ init_RuntimeBadge();
575
+ init_BillingBadge();
576
+ LIFECYCLE_COLOR = {
577
+ production: "success",
578
+ experimental: "warning",
579
+ deprecated: "error"
580
+ };
581
+ LINK_ICON = {
582
+ dashboard: /* @__PURE__ */ React7.createElement(DashboardIcon, { fontSize: "small" }),
583
+ docs: /* @__PURE__ */ React7.createElement(DescriptionIcon, { fontSize: "small" }),
584
+ playbook: /* @__PURE__ */ React7.createElement(ArticleIcon, { fontSize: "small" }),
585
+ issues: /* @__PURE__ */ React7.createElement(BugReportIcon, { fontSize: "small" })
586
+ };
587
+ AgentCard = ({
588
+ agent,
589
+ onClick,
590
+ onRuntimeClick
591
+ }) => {
592
+ const title = agent.title ?? agent.name;
593
+ const lifecycleColor = agent.lifecycle ? LIFECYCLE_COLOR[agent.lifecycle] ?? "default" : "default";
594
+ return /* @__PURE__ */ React7.createElement(
595
+ Card,
596
+ {
597
+ variant: "outlined",
598
+ sx: {
599
+ height: "100%",
600
+ display: "flex",
601
+ flexDirection: "column",
602
+ transition: (theme) => `box-shadow ${theme.transitions.duration.short}ms`,
603
+ "&:hover": { boxShadow: 6 }
604
+ }
605
+ },
606
+ /* @__PURE__ */ React7.createElement(
607
+ CardActionArea,
608
+ {
609
+ onClick: () => onClick?.(agent),
610
+ sx: { flexGrow: 1, p: 2, alignItems: "stretch", display: "flex" }
611
+ },
612
+ /* @__PURE__ */ React7.createElement(Box7, { sx: { display: "flex", flexDirection: "column", width: "100%" } }, /* @__PURE__ */ React7.createElement(Box7, { sx: { display: "flex", alignItems: "center", gap: 1.5, mb: 1 } }, /* @__PURE__ */ React7.createElement(AgentAvatar, { name: agent.name, avatarUrl: agent.avatarUrl }), /* @__PURE__ */ React7.createElement(Box7, { sx: { flexGrow: 1, minWidth: 0 } }, /* @__PURE__ */ React7.createElement(Typography3, { variant: "subtitle1", fontWeight: 700, noWrap: true, title }, title), /* @__PURE__ */ React7.createElement(Typography3, { variant: "caption", color: "text.secondary", noWrap: true }, agent.name)), /* @__PURE__ */ React7.createElement(AgentStatusBadge, { status: agent.status })), /* @__PURE__ */ React7.createElement(
613
+ Typography3,
614
+ {
615
+ variant: "body2",
616
+ color: "text.secondary",
617
+ sx: {
618
+ display: "-webkit-box",
619
+ WebkitLineClamp: 2,
620
+ WebkitBoxOrient: "vertical",
621
+ overflow: "hidden",
622
+ minHeight: "2.6em",
623
+ mb: 1.5
624
+ }
625
+ },
626
+ agent.purpose || "No description provided."
627
+ ), /* @__PURE__ */ React7.createElement(Box7, { sx: { mb: 1 } }, /* @__PURE__ */ React7.createElement(
628
+ RuntimeBadge,
629
+ {
630
+ runtime: agent.runtime.runtime,
631
+ onClick: onRuntimeClick
632
+ }
633
+ )), agent.capabilities.length > 0 && /* @__PURE__ */ React7.createElement(Box7, { sx: { mb: 1.5 } }, /* @__PURE__ */ React7.createElement(AgentCapabilities, { capabilities: agent.capabilities })), /* @__PURE__ */ React7.createElement(Box7, { sx: { mb: 1.5 } }, /* @__PURE__ */ React7.createElement(BillingBadge, { billing: agent.billing })), /* @__PURE__ */ React7.createElement(
634
+ Box7,
635
+ {
636
+ sx: {
637
+ mt: "auto",
638
+ display: "flex",
639
+ alignItems: "center",
640
+ gap: 0.75,
641
+ flexWrap: "wrap"
642
+ }
643
+ },
644
+ agent.owner && /* @__PURE__ */ React7.createElement(Typography3, { variant: "caption", color: "text.secondary" }, agent.owner),
645
+ agent.lifecycle && /* @__PURE__ */ React7.createElement(
646
+ Chip4,
647
+ {
648
+ size: "small",
649
+ color: lifecycleColor,
650
+ label: agent.lifecycle,
651
+ variant: "outlined",
652
+ sx: { height: 20, "& .MuiChip-label": { px: 0.75, fontSize: "0.7rem" } }
653
+ }
654
+ ),
655
+ agent.version && /* @__PURE__ */ React7.createElement(Typography3, { variant: "caption", color: "text.secondary" }, "v", agent.version)
656
+ ), agent.links.length > 0 && /* @__PURE__ */ React7.createElement(Box7, { sx: { display: "flex", gap: 1.5, mt: 1.5, pt: 1, borderTop: "1px solid", borderColor: "divider" } }, agent.links.slice(0, 4).map((l, i) => /* @__PURE__ */ React7.createElement(Tooltip3, { key: i, title: l.title }, /* @__PURE__ */ React7.createElement(
657
+ Link,
658
+ {
659
+ href: l.url,
660
+ target: "_blank",
661
+ rel: "noopener noreferrer",
662
+ onClick: (e) => e.stopPropagation(),
663
+ sx: { display: "inline-flex", color: "text.secondary" }
664
+ },
665
+ LINK_ICON[l.icon ?? ""] ?? /* @__PURE__ */ React7.createElement(DescriptionIcon, { fontSize: "small" })
666
+ )))))
667
+ )
668
+ );
669
+ };
670
+ }
671
+ });
672
+
673
+ // src/components/AgentsGrid.tsx
674
+ import React8 from "react";
675
+ import Box8 from "@mui/material/Box";
676
+ var AgentsGrid;
677
+ var init_AgentsGrid = __esm({
678
+ "src/components/AgentsGrid.tsx"() {
679
+ "use strict";
680
+ init_AgentCard();
681
+ AgentsGrid = ({
682
+ agents,
683
+ onAgentClick,
684
+ onRuntimeClick
685
+ }) => {
686
+ return /* @__PURE__ */ React8.createElement(
687
+ Box8,
688
+ {
689
+ sx: {
690
+ display: "grid",
691
+ gridTemplateColumns: "repeat(auto-fill, minmax(320px, 1fr))",
692
+ gap: 2
693
+ }
694
+ },
695
+ agents.map((a) => /* @__PURE__ */ React8.createElement(
696
+ AgentCard,
697
+ {
698
+ key: a.entityRef,
699
+ agent: a,
700
+ onClick: onAgentClick,
701
+ onRuntimeClick
702
+ }
703
+ ))
704
+ );
705
+ };
706
+ }
707
+ });
708
+
709
+ // src/components/AgentDetailDrawer.tsx
710
+ import React9 from "react";
711
+ import {
712
+ Box as Box9,
713
+ Chip as Chip5,
714
+ Drawer,
715
+ IconButton as IconButton2,
716
+ Link as Link2,
717
+ List,
718
+ ListItem,
719
+ ListItemIcon,
720
+ ListItemText,
721
+ Stack,
722
+ Typography as Typography4
723
+ } from "@mui/material";
724
+ import CloseIcon from "@mui/icons-material/Close";
725
+ import OpenInNewIcon from "@mui/icons-material/OpenInNew";
726
+ import RefreshIcon from "@mui/icons-material/Refresh";
727
+ import DashboardIcon2 from "@mui/icons-material/Dashboard";
728
+ import DescriptionIcon2 from "@mui/icons-material/Description";
729
+ import ArticleIcon2 from "@mui/icons-material/Article";
730
+ import BugReportIcon2 from "@mui/icons-material/BugReport";
731
+ import ContentCopyIcon from "@mui/icons-material/ContentCopy";
732
+ var LINK_ICON2, Row, AgentDetailDrawer;
733
+ var init_AgentDetailDrawer = __esm({
734
+ "src/components/AgentDetailDrawer.tsx"() {
735
+ "use strict";
736
+ init_AgentAvatar();
737
+ init_AgentStatusBadge();
738
+ init_AgentCapabilities();
739
+ init_RuntimeBadge();
740
+ init_BillingBadge();
741
+ LINK_ICON2 = {
742
+ dashboard: /* @__PURE__ */ React9.createElement(DashboardIcon2, { fontSize: "small" }),
743
+ docs: /* @__PURE__ */ React9.createElement(DescriptionIcon2, { fontSize: "small" }),
744
+ playbook: /* @__PURE__ */ React9.createElement(ArticleIcon2, { fontSize: "small" }),
745
+ issues: /* @__PURE__ */ React9.createElement(BugReportIcon2, { fontSize: "small" })
746
+ };
747
+ Row = ({
748
+ label,
749
+ children
750
+ }) => /* @__PURE__ */ React9.createElement(Box9, { sx: { display: "flex", gap: 1, mb: 1 } }, /* @__PURE__ */ React9.createElement(Typography4, { variant: "body2", color: "text.secondary", sx: { minWidth: 110 } }, label), /* @__PURE__ */ React9.createElement(Box9, { sx: { flexGrow: 1 } }, children));
751
+ AgentDetailDrawer = ({
752
+ agent,
753
+ open,
754
+ onClose,
755
+ onRefreshStatus
756
+ }) => {
757
+ if (!agent) return null;
758
+ const title = agent.title ?? agent.name;
759
+ const copyRef = () => {
760
+ navigator.clipboard?.writeText(agent.entityRef).catch(() => {
761
+ });
762
+ };
763
+ return /* @__PURE__ */ React9.createElement(
764
+ Drawer,
765
+ {
766
+ anchor: "right",
767
+ open,
768
+ onClose,
769
+ sx: { "& .MuiDrawer-paper": { width: { xs: "100%", sm: 480 } } }
770
+ },
771
+ /* @__PURE__ */ React9.createElement(Box9, { sx: { p: 2, display: "flex", alignItems: "flex-start", gap: 1.5 } }, /* @__PURE__ */ React9.createElement(AgentAvatar, { name: agent.name, avatarUrl: agent.avatarUrl, size: 56 }), /* @__PURE__ */ React9.createElement(Box9, { sx: { flexGrow: 1 } }, /* @__PURE__ */ React9.createElement(Typography4, { variant: "h6" }, title), /* @__PURE__ */ React9.createElement(
772
+ Typography4,
773
+ {
774
+ variant: "caption",
775
+ color: "text.secondary",
776
+ sx: {
777
+ display: "inline-flex",
778
+ alignItems: "center",
779
+ gap: 0.5,
780
+ cursor: "pointer"
781
+ },
782
+ onClick: copyRef
783
+ },
784
+ agent.entityRef,
785
+ /* @__PURE__ */ React9.createElement(ContentCopyIcon, { sx: { fontSize: 12 } })
786
+ )), /* @__PURE__ */ React9.createElement(IconButton2, { size: "small", onClick: onClose }, /* @__PURE__ */ React9.createElement(CloseIcon, { fontSize: "small" }))),
787
+ /* @__PURE__ */ React9.createElement(Box9, { sx: { px: 2, pb: 2, display: "flex", alignItems: "center", gap: 1 } }, /* @__PURE__ */ React9.createElement(AgentStatusBadge, { status: agent.status }), onRefreshStatus && /* @__PURE__ */ React9.createElement(
788
+ IconButton2,
789
+ {
790
+ size: "small",
791
+ title: "Refresh status",
792
+ onClick: () => onRefreshStatus(agent.entityRef)
793
+ },
794
+ /* @__PURE__ */ React9.createElement(RefreshIcon, { fontSize: "small" })
795
+ )),
796
+ /* @__PURE__ */ React9.createElement(Box9, { sx: { px: 2, pb: 3 } }, /* @__PURE__ */ React9.createElement(Typography4, { variant: "body2", sx: { mb: 2 } }, agent.purpose || agent.description || "No description provided."), /* @__PURE__ */ React9.createElement(Row, { label: "Runtime" }, /* @__PURE__ */ React9.createElement(Stack, { direction: "row", spacing: 1, alignItems: "center" }, /* @__PURE__ */ React9.createElement(RuntimeBadge, { runtime: agent.runtime.runtime }), agent.runtime.endpoint && /* @__PURE__ */ React9.createElement(
797
+ Link2,
798
+ {
799
+ href: agent.runtime.endpoint,
800
+ target: "_blank",
801
+ rel: "noopener noreferrer",
802
+ sx: { display: "inline-flex", alignItems: "center", gap: 0.5 }
803
+ },
804
+ "endpoint ",
805
+ /* @__PURE__ */ React9.createElement(OpenInNewIcon, { sx: { fontSize: 12 } })
806
+ )), agent.runtime.runtimeHandle && /* @__PURE__ */ React9.createElement(Typography4, { variant: "caption", display: "block", color: "text.secondary", sx: { mt: 0.5, wordBreak: "break-all" } }, agent.runtime.runtimeHandle)), /* @__PURE__ */ React9.createElement(Row, { label: "Capabilities" }, agent.capabilities.length ? /* @__PURE__ */ React9.createElement(AgentCapabilities, { capabilities: agent.capabilities, max: 20 }) : /* @__PURE__ */ React9.createElement(Typography4, { variant: "body2", color: "text.secondary" }, "\u2014")), /* @__PURE__ */ React9.createElement(Row, { label: "Billing" }, /* @__PURE__ */ React9.createElement(BillingBadge, { billing: agent.billing })), /* @__PURE__ */ React9.createElement(Row, { label: "Owner" }, agent.owner ?? "\u2014"), /* @__PURE__ */ React9.createElement(Row, { label: "System" }, agent.system ?? "\u2014"), /* @__PURE__ */ React9.createElement(Row, { label: "Lifecycle" }, agent.lifecycle ?? "\u2014"), /* @__PURE__ */ React9.createElement(Row, { label: "Version" }, agent.version ?? "\u2014"), /* @__PURE__ */ React9.createElement(Row, { label: "Tags" }, agent.tags.length ? /* @__PURE__ */ React9.createElement(Box9, { sx: { display: "flex", flexWrap: "wrap", gap: 0.5 } }, agent.tags.map((t) => /* @__PURE__ */ React9.createElement(Chip5, { key: t, size: "small", label: t, variant: "outlined" }))) : /* @__PURE__ */ React9.createElement(Typography4, { variant: "body2", color: "text.secondary" }, "\u2014")), agent.links.length > 0 && /* @__PURE__ */ React9.createElement(Row, { label: "Links" }, /* @__PURE__ */ React9.createElement(List, { dense: true, disablePadding: true }, agent.links.map((l, i) => /* @__PURE__ */ React9.createElement(
807
+ ListItem,
808
+ {
809
+ key: i,
810
+ component: "a",
811
+ href: l.url,
812
+ target: "_blank",
813
+ rel: "noopener noreferrer",
814
+ sx: { color: "text.primary", borderRadius: 1, "&:hover": { bgcolor: "action.hover" } }
815
+ },
816
+ /* @__PURE__ */ React9.createElement(ListItemIcon, { sx: { minWidth: 28 } }, LINK_ICON2[l.icon ?? ""] ?? /* @__PURE__ */ React9.createElement(DescriptionIcon2, { fontSize: "small" })),
817
+ /* @__PURE__ */ React9.createElement(
818
+ ListItemText,
819
+ {
820
+ primary: l.title,
821
+ secondary: l.url,
822
+ secondaryTypographyProps: { sx: { fontSize: "0.7rem" }, noWrap: true }
823
+ }
824
+ )
825
+ )))), /* @__PURE__ */ React9.createElement(Row, { label: "Catalog" }, /* @__PURE__ */ React9.createElement(Link2, { href: `/catalog/${agent.entityRef.replace(":", "/")}` }, "Open in catalog ", /* @__PURE__ */ React9.createElement(OpenInNewIcon, { sx: { fontSize: 12, verticalAlign: "middle" } }))))
826
+ );
827
+ };
828
+ }
829
+ });
830
+
831
+ // src/components/AgentsPage.tsx
832
+ var AgentsPage_exports = {};
833
+ __export(AgentsPage_exports, {
834
+ AgentsPage: () => AgentsPage
835
+ });
836
+ import React10, { useCallback as useCallback2, useEffect, useState as useState4 } from "react";
837
+ import { Box as Box10, CircularProgress, Typography as Typography5 } from "@mui/material";
838
+ import { EmptyState } from "@backstage/core-components";
839
+ import { useApi as useApi2 } from "@backstage/core-plugin-api";
840
+ var POLL_INTERVAL_MS, AgentsPage;
841
+ var init_AgentsPage = __esm({
842
+ "src/components/AgentsPage.tsx"() {
843
+ "use strict";
844
+ init_api();
845
+ init_useAgents();
846
+ init_AgentFilters();
847
+ init_AgentsGrid();
848
+ init_AgentDetailDrawer();
849
+ POLL_INTERVAL_MS = 3e4;
850
+ AgentsPage = () => {
851
+ const api = useApi2(aiAgentsApiRef);
852
+ const { agents, allAgents, loading, error, retry, filters, update, reset } = useAgents();
853
+ const [selected, setSelected] = useState4(null);
854
+ const [drawerOpen, setDrawerOpen] = useState4(false);
855
+ const [statuses, setStatuses] = useState4({});
856
+ const refs = allAgents.map((a) => a.entityRef);
857
+ useEffect(() => {
858
+ let cancelled = false;
859
+ if (!refs.length) return;
860
+ api.getStatuses(refs).then((result) => {
861
+ if (!cancelled) setStatuses(result);
862
+ });
863
+ return () => {
864
+ cancelled = true;
865
+ };
866
+ }, [api, refs.join(",")]);
867
+ useEffect(() => {
868
+ if (!refs.length) return;
869
+ const id = setInterval(async () => {
870
+ const result = await api.getStatuses(refs);
871
+ setStatuses((prev) => ({ ...prev, ...result }));
872
+ }, POLL_INTERVAL_MS);
873
+ return () => clearInterval(id);
874
+ }, [api, refs.length]);
875
+ const agentsWithStatus = agents.map((a) => ({
876
+ ...a,
877
+ status: statuses[a.entityRef] ?? a.status
878
+ }));
879
+ const handleCardClick = useCallback2((agent) => {
880
+ setSelected(agent);
881
+ setDrawerOpen(true);
882
+ }, []);
883
+ const handleRuntimeClick = useCallback2(
884
+ (runtime) => {
885
+ if (!filters.runtime.includes(runtime)) {
886
+ update({ runtime: [...filters.runtime, runtime] });
887
+ }
888
+ },
889
+ [filters.runtime, update]
890
+ );
891
+ const handleRefreshStatus = useCallback2(
892
+ async (entityRef2) => {
893
+ const result = await api.getStatuses([entityRef2]);
894
+ setStatuses((prev) => ({ ...prev, ...result }));
895
+ setSelected(
896
+ (prev) => prev && prev.entityRef === entityRef2 ? { ...prev, status: result[entityRef2] ?? prev.status } : prev
897
+ );
898
+ },
899
+ [api]
900
+ );
901
+ if (loading && !allAgents.length) {
902
+ return /* @__PURE__ */ React10.createElement(Box10, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "40vh" }, /* @__PURE__ */ React10.createElement(CircularProgress, null));
903
+ }
904
+ if (error) {
905
+ return /* @__PURE__ */ React10.createElement(
906
+ EmptyState,
907
+ {
908
+ title: "Failed to load AI agents",
909
+ description: error.message,
910
+ missing: "content"
911
+ }
912
+ );
913
+ }
914
+ return /* @__PURE__ */ React10.createElement(Box10, { sx: { p: 3 } }, /* @__PURE__ */ React10.createElement(Box10, { sx: { mb: 2 } }, /* @__PURE__ */ React10.createElement(Typography5, { variant: "h4" }, "AI Agents"), /* @__PURE__ */ React10.createElement(Typography5, { variant: "body2", color: "text.secondary" }, "AI agents registered in the catalog as ", /* @__PURE__ */ React10.createElement("code", null, "Component"), " entities with ", /* @__PURE__ */ React10.createElement("code", null, "spec.type: ai-agent"), ".")), /* @__PURE__ */ React10.createElement(
915
+ AgentFiltersBar,
916
+ {
917
+ agents: allAgents,
918
+ filters,
919
+ onChange: update,
920
+ onReset: reset
921
+ }
922
+ ), agentsWithStatus.length === 0 ? /* @__PURE__ */ React10.createElement(
923
+ EmptyState,
924
+ {
925
+ title: "No AI agents registered",
926
+ description: "Add a Component with spec.type: ai-agent to the catalog, or adjust your filters.",
927
+ missing: "content",
928
+ action: /* @__PURE__ */ React10.createElement("button", { onClick: () => retry() }, "Retry")
929
+ }
930
+ ) : /* @__PURE__ */ React10.createElement(
931
+ AgentsGrid,
932
+ {
933
+ agents: agentsWithStatus,
934
+ onAgentClick: handleCardClick,
935
+ onRuntimeClick: handleRuntimeClick
936
+ }
937
+ ), /* @__PURE__ */ React10.createElement(
938
+ AgentDetailDrawer,
939
+ {
940
+ agent: selected,
941
+ open: drawerOpen,
942
+ onClose: () => setDrawerOpen(false),
943
+ onRefreshStatus: handleRefreshStatus
944
+ }
945
+ ));
946
+ };
947
+ }
948
+ });
949
+
950
+ // src/plugin.tsx
951
+ init_api();
952
+ import React11 from "react";
953
+ import { SmartToy as AgentIcon } from "@mui/icons-material";
954
+ import {
955
+ ApiBlueprint,
956
+ PageBlueprint,
957
+ createFrontendPlugin,
958
+ fetchApiRef
959
+ } from "@backstage/frontend-plugin-api";
960
+ import { catalogApiRef } from "@backstage/plugin-catalog-react";
961
+ var aiAgentsApi = ApiBlueprint.make({
962
+ params: (defineParams) => defineParams({
963
+ api: aiAgentsApiRef,
964
+ deps: { fetchApi: fetchApiRef, catalogApi: catalogApiRef },
965
+ factory: ({ fetchApi, catalogApi }) => new AiAgentsApi({ fetchApi, catalogApi })
966
+ })
967
+ });
968
+ var aiAgentsPage = PageBlueprint.make({
969
+ params: {
970
+ path: "/ai-agents",
971
+ title: "AI Agents",
972
+ icon: /* @__PURE__ */ React11.createElement(AgentIcon, null),
973
+ loader: async () => {
974
+ const { AgentsPage: AgentsPage2 } = await Promise.resolve().then(() => (init_AgentsPage(), AgentsPage_exports));
975
+ return /* @__PURE__ */ React11.createElement(AgentsPage2, null);
976
+ }
977
+ }
978
+ });
979
+ var aiAgentsPlugin = createFrontendPlugin({
980
+ pluginId: "ai-agents",
981
+ extensions: [aiAgentsApi, aiAgentsPage]
982
+ });
983
+
984
+ // src/index.ts
985
+ init_AgentsPage();
986
+ init_AgentCard();
987
+ init_api();
988
+ init_types();
989
+ export {
990
+ AI_AGENT_ANNOTATION_PREFIX,
991
+ AI_AGENT_TYPE,
992
+ AgentCard,
993
+ AgentsPage,
994
+ AiAgentsApi,
995
+ aiAgentsApiRef,
996
+ aiAgentsPlugin,
997
+ entityToAgent
998
+ };
999
+ //# sourceMappingURL=index.esm.js.map