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 @@ -17,6 +17,23 @@ workflow:
tool_names: [email_phishing_analyzer]
`;

const FABRIC_AGENT_YAML = `config_format: nemo-agents-spec-v1
name: email-phishing-fabric
default_harness: deepagents
harnesses:
deepagents:
kind: deepagents
models:
default:
provider: openai
model: \${NEMO_DEFAULT_MODEL}
mcp:
servers:
email-phishing-analyzer:
transport: stdio
url: /workspace/.venv/bin/email-phishing-analyzer-mcp
`;

const mockFetchText = (body: string, ok = true) =>
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok,
Expand Down Expand Up @@ -56,6 +73,36 @@ describe('loadSampleAgentConfig', () => {
);
});

it('injects the model into models.default for a Fabric config', async () => {
mockFetchText(FABRIC_AGENT_YAML);
const config = (await loadSampleAgentConfig('sample-agents/x/agent.yml', 'my-model')) as {
config_format: string;
default_harness: string;
models: { default: { model: string; provider: string } };
mcp: { servers: Record<string, { transport: string }> };
};
expect(config.models.default.model).toBe('my-model');
// The rest of the Fabric config is preserved and llms.llm is never required.
expect(config.config_format).toBe('nemo-agents-spec-v1');
expect(config.default_harness).toBe('deepagents');
expect(config.models.default.provider).toBe('openai');
expect(config.mcp.servers['email-phishing-analyzer'].transport).toBe('stdio');
});

it('throws when a Fabric config is missing models.default', async () => {
mockFetchText('config_format: nemo-agents-spec-v1\ndefault_harness: deepagents\n');
await expect(loadSampleAgentConfig('sample-agents/x/agent.yml', 'm')).rejects.toThrow(
/missing models\.default/
);
});

it('throws on an unsupported config_format instead of falling back to NAT', async () => {
mockFetchText('config_format: nemo-agents-spec-v2\nworkflow:\n _type: react_agent\n');
await expect(loadSampleAgentConfig('sample-agents/x/agent.yml', 'm')).rejects.toThrow(
/unsupported config_format: nemo-agents-spec-v2/
);
});

it('throws when the fetch fails', async () => {
mockFetchText('', false);
await expect(loadSampleAgentConfig('sample-agents/x/agent.yml', 'm')).rejects.toThrow(
Expand Down
51 changes: 47 additions & 4 deletions web/packages/studio/src/api/agents/loadSampleAgentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,64 @@ import { fetchSampleText } from '@studio/api/agents/fetchSampleText';
import YAML from 'yaml';

/**
* Loads a sample agent's NAT workflow config from a public static asset and
* injects the selected model. Parse-then-set: the fetched YAML's model_name
* literal is overwritten, so the asset can stay byte-identical to the plugin's
* Loads a sample agent's config from a public static asset and injects the
* selected model. Parse-then-set: the fetched YAML's model literal is
* overwritten, so the asset can stay byte-identical to the plugin's
* ${NEMO_DEFAULT_MODEL} version (the platform service doesn't resolve that).
*
* Branches on the config's own `config_format`:
* - NAT (`nat-workflow-v1`, the default when absent): model lives at
* `llms.llm.model_name`.
* - Fabric (`nemo-agents-spec-v1`): model lives at `models.default.model`; the
* selected harness inherits it when it declares no model of its own.
*/
export const loadSampleAgentConfig = async (
agentConfigPath: string,
modelName: string
): Promise<Record<string, unknown>> => {
const text = await fetchSampleText(agentConfigPath);
const config = YAML.parse(text) as Record<string, unknown>;

const configFormat = config?.config_format;

if (configFormat === 'nemo-agents-spec-v1') {
injectFabricModel(config, modelName, agentConfigPath);
return config;
}

if (configFormat === undefined || configFormat === 'nat-workflow-v1') {
injectNatModel(config, modelName, agentConfigPath);
return config;
}

throw new Error(
`Sample agent config ${agentConfigPath} has unsupported config_format: ${String(configFormat)}`
);
};

/** NAT workflow config: overwrite `llms.llm.model_name`. */
const injectNatModel = (
config: Record<string, unknown>,
modelName: string,
agentConfigPath: string
): void => {
const llm = (config?.llms as { llm?: unknown } | undefined)?.llm;
if (!llm || typeof llm !== 'object' || Array.isArray(llm)) {
throw new Error(`Sample agent config ${agentConfigPath} is missing llms.llm`);
}
(llm as Record<string, unknown>).model_name = modelName;
return config;
};

/** Fabric (nemo-agents-spec-v1) config: overwrite `models.default.model`. */
const injectFabricModel = (
config: Record<string, unknown>,
modelName: string,
agentConfigPath: string
): void => {
const models = config?.models as { default?: unknown } | undefined;
const defaultModel = models?.default;
if (!defaultModel || typeof defaultModel !== 'object' || Array.isArray(defaultModel)) {
throw new Error(`Sample agent config ${agentConfigPath} is missing models.default`);
}
(defaultModel as Record<string, unknown>).model = modelName;
};
4 changes: 4 additions & 0 deletions web/packages/studio/src/constants/sampleAgents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export interface SampleAgent {
/** Public path to a reusable nemo-evaluator eval-config.json. Samples without
* one remain available for agent creation but not evaluation seeding. */
evalConfigPath?: string;
/** Config format identifier sent to the create API. Defaults to
* `nat-workflow-v1` server-side when omitted; set to `nemo-agents-spec-v1`
* for Fabric-backed samples so the API validates them as Fabric, not NAT. */
configFormat?: string;
}

export const SAMPLE_AGENTS: SampleAgent[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ export const CreateExampleAgentModal: FC<CreateExampleAgentModalProps> = ({
name: buildSampleAgentName(example.namePrefix),
description: example.description,
config,
// Omitted for NAT samples (API defaults to nat-workflow-v1); set for
// Fabric samples so the API validates the config as nemo-agents-spec-v1.
...(example.configFormat ? { config_format: example.configFormat } : {}),
Comment thread
marcusds marked this conversation as resolved.
},
});
} catch {
Expand Down
Loading