Skip to content
Draft
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
@@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { Agent } from '@nemo/sdk/generated/agents/schema/Agent';
import { Accordion, Badge, Block, Flex, Stack, Text } from '@nvidia/foundations-react-core';
import type { AgentConfig } from '@studio/components/dataViews/AgentsDataView';
import { redactSecrets } from '@studio/components/sidePanels/AgentPanels/AgentPanel/redactSecrets';
import { summarizeAgentWorkflow } from '@studio/components/sidePanels/AgentPanels/AgentPanel/summarizeAgentWorkflow';
import { Workflow, Wrench } from 'lucide-react';
import type { FC } from 'react';
import YAML from 'yaml';

interface AgentWorkflowContentProps {
agent?: Agent;
}

export const AgentWorkflowContent: FC<AgentWorkflowContentProps> = ({ agent }) => {
const config = agent?.config as AgentConfig | undefined;
const summary = summarizeAgentWorkflow(config);

if (!config || (!summary.workflowType && summary.tools.length === 0)) {
return (
<Block padding="4">
<Text color="secondary">This agent has no workflow config to visualize.</Text>
</Block>
);
}

let yamlText = '';
try {
yamlText = YAML.stringify(redactSecrets(config));
} catch {
yamlText = 'Unable to render config as YAML.';
}

const wiredTools = summary.tools.filter((t) => t.wired);
const unwiredTools = summary.tools.filter((t) => !t.wired);

return (
<Stack className="overflow-auto" gap="4">
<Block padding="4">
<Stack gap="4">
<Flex align="center" gap="2">
<Workflow className="size-4 text-brand" />
<Text kind="body/semibold/md">{summary.workflowType ?? 'workflow'}</Text>
{summary.models.map((model) => (
<Badge key={model} kind="outline">
{model}
</Badge>
))}
</Flex>

<Stack gap="2" align="center">
<WorkflowNode label={summary.workflowType ?? 'workflow'} sub={summary.llmName} root />
{wiredTools.length > 0 && (
<>
<div className="h-4 w-px bg-base" />
<Flex gap="2" wrap="wrap" className="justify-center">
{wiredTools.map((tool) => (
<WorkflowNode key={tool.name} label={tool.name} sub={tool.type} />
))}
</Flex>
</>
)}
</Stack>

{unwiredTools.length > 0 && (
<Text kind="body/regular/xs" color="secondary">
Declared but not wired as tools: {unwiredTools.map((t) => t.name).join(', ')}
</Text>
)}
</Stack>
</Block>

<Accordion
multiple
className="w-full border-t border-base"
items={[
{
chevronPosition: 'start',
value: 'config',
slotTrigger: <Text kind="body/semibold/sm">Workflow config (YAML)</Text>,
slotContent: (
<Block>
<pre className="font-mono text-xs whitespace-pre-wrap break-all max-h-96 overflow-auto bg-surface-subtle p-density-lg rounded leading-relaxed">
{yamlText}
</pre>
</Block>
),
},
]}
/>
</Stack>
);
};

interface WorkflowNodeProps {
label: string;
sub?: string;
root?: boolean;
}

const WorkflowNode: FC<WorkflowNodeProps> = ({ label, sub, root }) => (
<Flex
align="center"
gap="2"
className={`rounded-md border px-3 py-2 ${
root ? 'border-brand bg-surface-subtle' : 'border-base'
}`}
>
{!root && <Wrench className="size-3 text-subtle" />}
<Stack gap="0">
<Text kind="body/semibold/sm">{label}</Text>
{sub && (
<Text kind="body/regular/xs" color="secondary">
{sub}
</Text>
)}
</Stack>
</Flex>
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { AgentConfig } from '@studio/components/dataViews/AgentsDataView';
import { getAgentModelNames } from '@studio/components/dataViews/AgentsDataView/utils';
import { DeleteConfirmationModal } from '@studio/components/DeleteConfirmationModal';
import { AgentDetailsContent } from '@studio/components/sidePanels/AgentPanels/AgentPanel/AgentDetailsContent';
import { AgentWorkflowContent } from '@studio/components/sidePanels/AgentPanels/AgentPanel/AgentWorkflowContent';
import { ChatPlaygroundContent } from '@studio/components/sidePanels/AgentPanels/AgentPanel/ChatPlaygroundContent';
import { DeploymentLogsView } from '@studio/components/sidePanels/AgentPanels/AgentPanel/DeploymentLogsView';
import type { AgentPanelTab } from '@studio/components/sidePanels/AgentPanels/AgentPanel/types';
Expand Down Expand Up @@ -61,6 +62,7 @@ export const AgentPanel: FC<AgentPanelProps> = ({
const tabItems = useMemo(
() => [
{ value: 'agent-details', children: 'Details' },
{ value: 'agent-workflow', children: 'Workflow' },
{ value: 'chat-playground', children: 'Chat Playground' },
{ value: 'deployment-logs', children: 'Logs' },
],
Expand Down Expand Up @@ -114,6 +116,8 @@ export const AgentPanel: FC<AgentPanelProps> = ({

if (selectedTab === 'deployment-logs') {
content = <DeploymentLogsView workspace={workspace} deployments={agentDeployments} />;
} else if (selectedTab === 'agent-workflow') {
content = <AgentWorkflowContent agent={agent} />;
} else if (selectedTab === 'chat-playground') {
content = (
<ChatPlaygroundContent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { redactSecrets } from '@studio/components/sidePanels/AgentPanels/AgentPanel/redactSecrets';

describe('redactSecrets', () => {
it('masks credential-like keys anywhere in the tree', () => {
expect(
redactSecrets({
llms: { llm: { _type: 'openai', model_name: 'gpt-4o', api_key: 'sk-secret' } },
auth: { token: 'abc', bearer_secret: 'xyz' },
password: 'hunter2',
})
).toEqual({
llms: { llm: { _type: 'openai', model_name: 'gpt-4o', api_key: '***' } },
auth: { token: '***', bearer_secret: '***' },
password: '***',
});
});

it('leaves non-secret values and structure intact', () => {
const input = { workflow: { _type: 'react_agent', tool_names: ['a', 'b'] } };
expect(redactSecrets(input)).toEqual(input);
});

it('handles arrays and primitives', () => {
expect(redactSecrets([{ api_key: 'k' }, 'plain'])).toEqual([{ api_key: '***' }, 'plain']);
expect(redactSecrets(undefined)).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

const SECRET_KEY = /(api[_-]?key|token|secret|password|credential|authorization)/i;
const MASK = '***';

/**
* Deep-copy a config/card object with credential-like values masked, so the
* Workflow tab never renders inline secrets (e.g. `llms.*.api_key`) to anyone
* who can view the agent. Only scalar values under a secret-looking key are
* masked; structure is preserved.
*/
export const redactSecrets = (value: unknown): unknown => {
if (Array.isArray(value)) return value.map(redactSecrets);
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value as Record<string, unknown>).map(([key, val]) => {
if (SECRET_KEY.test(key) && (typeof val === 'string' || typeof val === 'number')) {
return [key, MASK];
}
return [key, redactSecrets(val)];
})
);
}
return value;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { AgentConfig } from '@studio/components/dataViews/AgentsDataView';
import { summarizeAgentWorkflow } from '@studio/components/sidePanels/AgentPanels/AgentPanel/summarizeAgentWorkflow';

describe('summarizeAgentWorkflow', () => {
it('returns empty summary for undefined config', () => {
expect(summarizeAgentWorkflow(undefined)).toEqual({
workflowType: undefined,
llmName: undefined,
models: [],
tools: [],
});
});

it('extracts workflow type, model, and flags wired tools', () => {
const config: AgentConfig = {
functions: {
calculator: { _type: 'calculator' },
current_datetime: { _type: 'current_datetime' },
unused: { _type: 'internet_search' },
},
llms: { llm: { _type: 'openai', model_name: 'gpt-4o' } },
workflow: {
_type: 'react_agent',
llm_name: 'llm',
tool_names: ['calculator', 'current_datetime'],
},
};
expect(summarizeAgentWorkflow(config)).toEqual({
workflowType: 'react_agent',
llmName: 'llm',
models: ['gpt-4o'],
tools: [
{ name: 'calculator', type: 'calculator', wired: true },
{ name: 'current_datetime', type: 'current_datetime', wired: true },
{ name: 'unused', type: 'internet_search', wired: false },
],
});
});

it('dedupes repeated model names across llms', () => {
const config: AgentConfig = {
llms: {
a: { _type: 'openai', model_name: 'gpt-4o' },
b: { _type: 'openai', model_name: 'gpt-4o' },
},
workflow: { _type: 'react_agent' },
};
expect(summarizeAgentWorkflow(config).models).toEqual(['gpt-4o']);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { AgentConfig } from '@studio/components/dataViews/AgentsDataView';

export interface WorkflowToolNode {
name: string;
type: string;
/** True when the workflow's tool_names wires this function in as a tool. */
wired: boolean;
}

export interface AgentWorkflowSummary {
workflowType?: string;
llmName?: string;
models: string[];
tools: WorkflowToolNode[];
}

/**
* Derives a display-friendly view of a NAT workflow config: the top-level
* workflow type, its LLM/model, and the functions declared in the config —
* flagging which are wired into the agent via `workflow.tool_names`. Purely a
* read model over the stored config; it does not validate the NAT schema.
*/
export const summarizeAgentWorkflow = (config: AgentConfig | undefined): AgentWorkflowSummary => {
const workflow = config?.workflow;
const wiredNames = new Set(workflow?.tool_names ?? []);

const models = Object.values(config?.llms ?? {})
.map((llm) => llm?.model_name)
.filter((m): m is string => !!m);

const tools: WorkflowToolNode[] = Object.entries(config?.functions ?? {}).map(([name, fn]) => ({
name,
type: fn?._type ?? 'unknown',
wired: wiredNames.has(name),
}));

return {
workflowType: workflow?._type,
llmName: workflow?.llm_name,
models: [...new Set(models)],
tools,
};
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

export type AgentPanelTab = 'agent-details' | 'chat-playground' | 'deployment-logs';
export type AgentPanelTab =
| 'agent-details'
| 'agent-workflow'
| 'chat-playground'
| 'deployment-logs';