Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import type { CustomNodeData } from "@/components/MainArea/Graph/Elements/types"
import {
getOasfSlugFromNodeData,
IDENTITY_UI_BY_STABLE_AGENT_UUID,
isDirectoryLabel,
isMcpServerLabel,
isRecruiterLabel,
normalizeAgentRecordUriToRepoPath,
parseStableAgentUuid,
resolveGithubFromAgentRecordUri,
Expand Down Expand Up @@ -162,4 +165,54 @@ describe("agenticTopologyIdentityUiMap", () => {
).toBe("recruiter")
})
})

describe("label predicates", () => {
it.each([
{ caseName: "weather mcp server", label: "Weather MCP Server", v: true },
{
caseName: "lowercased mcp server",
label: "payment mcp server",
v: true,
},
{
caseName: "trailing whitespace",
label: "Weather MCP Server ",
v: true,
},
{ caseName: "not at end", label: "MCP Server Gateway", v: false },
{ caseName: "plain agent", label: "Brazil Coffee Farm Agent", v: false },
])("isMcpServerLabel: $caseName", ({ label, v }) => {
expect(isMcpServerLabel(label)).toBe(v)
})

it.each([
{ caseName: "agentic recruiter", label: "Agentic Recruiter", v: true },
{
caseName: "split labels combined",
label: "agentic recruiter delegation",
v: true,
},
{ caseName: "recruiter alone", label: "Recruiter", v: false },
{ caseName: "directory", label: "AGNTCY Agent Directory", v: false },
])("isRecruiterLabel: $caseName", ({ label, v }) => {
expect(isRecruiterLabel(label)).toBe(v)
})

it.each([
{
caseName: "agntcy agent directory",
label: "AGNTCY Agent Directory",
v: true,
},
{
caseName: "combined labels",
label: "directory agntcy agent directory",
v: true,
},
{ caseName: "missing agntcy", label: "Agent Directory", v: false },
{ caseName: "recruiter", label: "Agentic Recruiter", v: false },
])("isDirectoryLabel: $caseName", ({ label, v }) => {
expect(isDirectoryLabel(label)).toBe(v)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,23 @@ export function splitTopologyNodeLabel(label: string): {
return { label1: t.slice(0, sp), label2: t.slice(sp + 1).trim() }
}

/** True when a node label denotes an MCP server (e.g. "Weather MCP Server"). */
export function isMcpServerLabel(label: string): boolean {
return /mcp\s+server$/i.test(label.trim())
}

/** True when a node label denotes the agentic recruiter. */
export function isRecruiterLabel(label: string): boolean {
const lower = label.toLowerCase()
return /\bagentic\b/.test(lower) && /\brecruiter\b/.test(lower)
}

/** True when a node label denotes the AGNTCY agent directory. */
export function isDirectoryLabel(label: string): boolean {
const lower = label.toLowerCase()
return lower.includes("agntcy") && lower.includes("agent directory")
}

/**
* Normalize catalog `agent_record_uri` relative paths to a repo path segment
* under `coffeeAGNTCY/coffee_agents` (for GitHub blob URLs).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* Copyright AGNTCY Contributors (https://github.com/agntcy)
* SPDX-License-Identifier: Apache-2.0
**/

import { isValidElement } from "react"
import { describe, expect, it } from "vitest"
import {
resolveTopologyNodeIcon,
topologyNodeIconKind,
TopologyNodeIconKind,
type TopologyNodeIconInput,
} from "@/utils/topologyNodeIcons"

describe("topologyNodeIconKind", () => {
it.each<{
caseName: string
input: TopologyNodeIconInput
expected: TopologyNodeIconKind
}>([
{
caseName: "slug wins over labels (auction supervisor)",
input: {
directoryAgentSlug: "auction-supervisor-agent",
label1: "Weather",
label2: "MCP Server",
},
expected: TopologyNodeIconKind.Supervisor,
},
{
caseName: "logistics supervisor slug -> supervisor",
input: { directoryAgentSlug: "logistics-supervisor-agent" },
expected: TopologyNodeIconKind.Supervisor,
},
{
caseName: "recruiter slug -> recruiter",
input: { directoryAgentSlug: "recruiter" },
expected: TopologyNodeIconKind.Recruiter,
},
{
caseName: "coffee farm slug -> farm",
input: { directoryAgentSlug: "colombia-coffee-farm" },
expected: TopologyNodeIconKind.Farm,
},
{
caseName: "tatooine farm slug -> farm",
input: { directoryAgentSlug: "tatooine-farm-agent" },
expected: TopologyNodeIconKind.Farm,
},
{
caseName: "weather mcp slug -> weatherMcp",
input: { directoryAgentSlug: "weather-mcp-server" },
expected: TopologyNodeIconKind.WeatherMcp,
},
{
caseName: "payment mcp slug -> paymentMcp",
input: { directoryAgentSlug: "payment-mcp-server" },
expected: TopologyNodeIconKind.PaymentMcp,
},
{
caseName: "shipping slug -> shipping",
input: { directoryAgentSlug: "shipping-agent" },
expected: TopologyNodeIconKind.Shipping,
},
{
caseName: "accountant slug -> accountant",
input: { directoryAgentSlug: "accountant-agent" },
expected: TopologyNodeIconKind.Accountant,
},
{
caseName: "unknown slug falls back to labels",
input: { directoryAgentSlug: "mystery", label1: "Shipper" },
expected: TopologyNodeIconKind.Shipping,
},
{
caseName: "weather mcp by labels",
input: { label1: "Weather", label2: "MCP Server" },
expected: TopologyNodeIconKind.WeatherMcp,
},
{
caseName: "payment mcp by labels",
input: { label1: "Payment", label2: "MCP Server" },
expected: TopologyNodeIconKind.PaymentMcp,
},
{
caseName: "generic mcp by labels -> default",
input: { label1: "Inventory", label2: "MCP Server" },
expected: TopologyNodeIconKind.Default,
},
{
caseName: "agentic recruiter by labels",
input: {
label1: "Agentic Recruiter",
label2: "Discovery and delegation",
},
expected: TopologyNodeIconKind.Recruiter,
},
{
caseName: "agntcy agent directory by labels",
input: { label1: "Directory", label2: "AGNTCY Agent Directory" },
expected: TopologyNodeIconKind.Directory,
},
{
caseName: "coffee farm by labels",
input: { label1: "Brazil", label2: "Coffee Farm Agent" },
expected: TopologyNodeIconKind.Farm,
},
{
caseName: "auction agent by labels",
input: { label1: "Auction Agent", label2: "Buyer" },
expected: TopologyNodeIconKind.Supervisor,
},
{
caseName: "logistics buyer by labels",
input: { label1: "Buyer", label2: "Logistics Agent" },
expected: TopologyNodeIconKind.Supervisor,
},
{
caseName: "shipper by labels",
input: { label1: "Shipper", label2: "Shipper Agent" },
expected: TopologyNodeIconKind.Shipping,
},
{
caseName: "accountant by labels",
input: { label1: "Accountant", label2: "Accountant Agent" },
expected: TopologyNodeIconKind.Accountant,
},
{
caseName: "unknown -> default",
input: { label1: "Mystery", label2: "Thing" },
expected: TopologyNodeIconKind.Default,
},
{
caseName: "empty input -> default",
input: {},
expected: TopologyNodeIconKind.Default,
},
])("$caseName", ({ input, expected }) => {
expect(topologyNodeIconKind(input)).toBe(expected)
})
})

describe("resolveTopologyNodeIcon", () => {
it("returns a renderable element for every input", () => {
expect(isValidElement(resolveTopologyNodeIcon({ label1: "Shipper" }))).toBe(
true,
)
expect(isValidElement(resolveTopologyNodeIcon({}))).toBe(true)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* Copyright AGNTCY Contributors (https://github.com/agntcy)
* SPDX-License-Identifier: Apache-2.0
*
* -----------------------------------------------------------------------------
* Presentation-only icon resolver for dynamically rendered topology nodes.
*
* Backend topology carries no icon field, so the dynamic renderer derives a
* branded icon from a node's directory slug (preferred) or its split label,
* using loose substring matching rather than an exhaustive agent map. This is
* presentation, not data: if the backend later supplies an icon/type hint, key
* off that.
* -----------------------------------------------------------------------------
*/

import React from "react"
import Air from "@mui/icons-material/Air"
import Calculate from "@mui/icons-material/Calculate"
import LocalShipping from "@mui/icons-material/LocalShipping"
import SmartToy from "@mui/icons-material/SmartToy"
import { GraphDiscoveryAssetImg } from "@/utils/GraphDiscoveryAssetImg"
import {
isDirectoryLabel,
isRecruiterLabel,
} from "@/utils/agenticTopologyIdentityUiMap"
import supervisorIcon from "@/assets/supervisor.png"
import farmAgentIcon from "@/assets/Grader-Agent.png"

export interface TopologyNodeIconInput {
label1?: string
label2?: string
directoryAgentSlug?: string
}

export enum TopologyNodeIconKind {
Supervisor = "supervisor",
Recruiter = "recruiter",
Directory = "directory",
Farm = "farm",
WeatherMcp = "weatherMcp",
PaymentMcp = "paymentMcp",
Shipping = "shipping",
Accountant = "accountant",
Default = "default",
}

function normalize(value: string | undefined): string {
return typeof value === "string" ? value.trim().toLowerCase() : ""
}

function iconKindFromSlug(slug: string): TopologyNodeIconKind | null {
if (slug.includes(TopologyNodeIconKind.Supervisor)) {
return TopologyNodeIconKind.Supervisor
}
if (slug.includes(TopologyNodeIconKind.Recruiter)) {
return TopologyNodeIconKind.Recruiter
}
if (slug.includes(TopologyNodeIconKind.Farm)) {
return TopologyNodeIconKind.Farm
}
if (slug.includes("weather")) {
return TopologyNodeIconKind.WeatherMcp
}
if (slug.includes("payment")) {
return TopologyNodeIconKind.PaymentMcp
}
if (slug.includes(TopologyNodeIconKind.Shipping)) {
return TopologyNodeIconKind.Shipping
}
if (slug.includes(TopologyNodeIconKind.Accountant)) {
return TopologyNodeIconKind.Accountant
}
return null
}

function iconKindFromLabels(
label1: string,
label2: string,
): TopologyNodeIconKind {
const combined = `${label1} ${label2}`.trim()

if (label2 === "mcp server" || label1.endsWith("mcp server")) {
if (label1.includes("weather")) return TopologyNodeIconKind.WeatherMcp
if (label1.includes("payment")) return TopologyNodeIconKind.PaymentMcp
return TopologyNodeIconKind.Default
}

if (isRecruiterLabel(combined)) {
return TopologyNodeIconKind.Recruiter
}

if (label1 === "directory" || isDirectoryLabel(combined)) {
return TopologyNodeIconKind.Directory
}

if (label2.includes("coffee farm")) return TopologyNodeIconKind.Farm
if (label1 === "shipper") return TopologyNodeIconKind.Shipping
if (label1 === "accountant") return TopologyNodeIconKind.Accountant

if (
label1 === "auction agent" ||
(label1 === "auction" && label2.includes("agent")) ||
label1 === "buyer" ||
label2.includes("buyer") ||
label2.includes("logistics agent") ||
combined === "logistics group"
) {
return TopologyNodeIconKind.Supervisor
}

return TopologyNodeIconKind.Default
}

export function topologyNodeIconKind(
input: TopologyNodeIconInput,
): TopologyNodeIconKind {
const slug = normalize(input.directoryAgentSlug)
if (slug) {
const bySlug = iconKindFromSlug(slug)
if (bySlug) return bySlug
}
return iconKindFromLabels(normalize(input.label1), normalize(input.label2))
}

function brandedImg(src: string, alt: string): React.ReactNode {
return <GraphDiscoveryAssetImg src={src} alt={alt} invertInDarkMode />
}

export function resolveTopologyNodeIcon(
input: TopologyNodeIconInput,
): React.ReactNode {
switch (topologyNodeIconKind(input)) {
case TopologyNodeIconKind.Supervisor:
return brandedImg(supervisorIcon, "Supervisor Icon")
case TopologyNodeIconKind.Recruiter:
return brandedImg(supervisorIcon, "Recruiter Icon")
case TopologyNodeIconKind.Directory:
return brandedImg(supervisorIcon, "Directory Icon")
case TopologyNodeIconKind.Farm:
return brandedImg(farmAgentIcon, "Farm Agent Icon")
case TopologyNodeIconKind.WeatherMcp:
return <Air aria-hidden />
case TopologyNodeIconKind.PaymentMcp:
return <Calculate aria-hidden />
case TopologyNodeIconKind.Shipping:
return <LocalShipping aria-hidden />
case TopologyNodeIconKind.Accountant:
return <Calculate aria-hidden />
default:
return <SmartToy aria-hidden />
}
}
Loading
Loading