use std::fs;
use std::path::{Path, PathBuf};
use clap::ValueEnum;
use nemo_fabric_core::{FabricConfig, ModelConfig};
use serde_json::Value;
use crate::examples::Example;
use crate::presets;
const PYTHON_MAIN: &str = include_str!("../templates/python/main.py.tmpl");
const PYTHON_PROJECT: &str = include_str!("../templates/python/pyproject.toml.tmpl");
const RUST_MAIN: &str = include_str!("../templates/rust/main.rs.tmpl");
const RUST_PROJECT: &str = include_str!("../templates/rust/Cargo.toml.tmpl");
const README: &str = include_str!("../templates/README.md.tmpl");
#[derive(Debug)]
struct ScaffoldFile {
path: String,
contents: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Language {
Python,
Rust,
}
impl Language {
pub fn as_str(self) -> &'static str {
match self {
Self::Python => "python",
Self::Rust => "rust",
}
}
}
pub fn init(
example: Example,
variant: Option<&str>,
language: Language,
destination: impl AsRef<Path>,
) -> Result<PathBuf, String> {
let destination = destination.as_ref();
if destination.exists() {
return Err(format!(
"destination already exists: {}",
destination.display()
));
}
let variant = variant.unwrap_or(example.default_variant);
let preset =
presets::find(variant).ok_or_else(|| format!("unknown preset variant {variant:?}"))?;
let config = example.config(Some(variant))?;
let mut files = example
.embedded_files(preset)
.into_iter()
.map(|file| ScaffoldFile {
path: file.path.to_string(),
contents: file.contents.to_string(),
})
.collect::<Vec<_>>();
files.extend(language_files(language, &config, example.name));
files.push(ScaffoldFile {
path: "README.md".to_string(),
contents: README
.replace("{{EXAMPLE}}", example.name)
.replace("{{VARIANT}}", variant)
.replace("{{LANGUAGE}}", language.as_str()),
});
write_files(destination, &files)?;
Ok(destination.to_path_buf())
}
fn language_files(language: Language, config: &FabricConfig, example: &str) -> Vec<ScaffoldFile> {
let (project_path, project, main_path, main) = match language {
Language::Python => (
"pyproject.toml",
PYTHON_PROJECT.replace("{{PACKAGE}}", &package_name(example)),
"main.py",
render_python(config),
),
Language::Rust => (
"Cargo.toml",
RUST_PROJECT
.replace("{{PACKAGE}}", &package_name(example))
.replace("{{NEMO_FABRIC_CORE_DEPENDENCY}}", &rust_core_dependency()),
"src/main.rs",
render_rust(config),
),
};
vec![
ScaffoldFile {
path: project_path.to_string(),
contents: project,
},
ScaffoldFile {
path: main_path.to_string(),
contents: main,
},
]
}
fn rust_core_dependency() -> String {
let source_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../fabric-core");
rust_core_dependency_for_path(&source_path)
}
fn rust_core_dependency_for_path(source_path: &Path) -> String {
let version = rust_string(env!("CARGO_PKG_VERSION"));
if !source_path.join("Cargo.toml").is_file() {
return version;
}
let Ok(source_path) = source_path.canonicalize() else {
return version;
};
let Some(source_path) = source_path.to_str() else {
return version;
};
format!(
"{{ path = {}, version = {version} }}",
rust_string(source_path)
)
}
fn write_files(destination: &Path, files: &[ScaffoldFile]) -> Result<(), String> {
fs::create_dir(destination).map_err(|error| {
format!(
"failed to create destination {}: {error}",
destination.display()
)
})?;
for file in files {
let path = destination.join(&file.path);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("failed to create {}: {error}", parent.display()))?;
}
fs::write(&path, &file.contents)
.map_err(|error| format!("failed to write {}: {error}", path.display()))?;
}
Ok(())
}
fn render_python(config: &FabricConfig) -> String {
let harness = config.harness.as_ref().expect("scaffold preset harness");
PYTHON_MAIN
.replace("{{AGENT_NAME}}", &python_string(&config.metadata.name))
.replace(
"{{DESCRIPTION}}",
&python_string(config.metadata.description.as_deref().unwrap_or("")),
)
.replace("{{ADAPTER_ID}}", &python_string(&harness.adapter_id))
.replace(
"{{HARNESS_SETTINGS}}",
&python_value(&Value::Object(harness.settings.clone())),
)
.replace(
"{{INSTRUCTIONS}}",
&config
.instructions
.as_ref()
.and_then(|instructions| instructions.system.as_ref())
.map(|instruction| {
format!(
"InstructionsConfig(system=InstructionConfig(content={}, mode=\"replace\"))",
python_string(&instruction.content)
)
})
.unwrap_or_else(|| "None".to_string()),
)
.replace(
"{{MAX_TURNS}}",
&config
.runtime
.max_turns
.map(|value| value.to_string())
.unwrap_or_else(|| "None".to_string()),
)
.replace(
"{{TIMEOUT_SECONDS}}",
&config
.runtime
.timeout_seconds
.map(|value| value.to_string())
.unwrap_or_else(|| "None".to_string()),
)
.replace(
"{{ENVIRONMENT_ENV}}",
&python_value(&Value::Object(
config
.environment
.as_ref()
.map(|environment| {
environment
.env
.iter()
.map(|(name, value)| (name.clone(), Value::String(value.clone())))
.collect()
})
.unwrap_or_default(),
)),
)
.replace("{{MODELS}}", &python_models(config.models.get("default")))
}
fn python_models(model: Option<&ModelConfig>) -> String {
let Some(model) = model else {
return "{}".to_string();
};
let temperature = model
.temperature
.map(|value| value.to_string())
.unwrap_or_else(|| "None".to_string());
let top_p = model
.top_p
.map(|value| value.to_string())
.unwrap_or_else(|| "None".to_string());
let max_tokens = model
.max_tokens
.map(|value| value.to_string())
.unwrap_or_else(|| "None".to_string());
let base_url = model
.base_url
.as_deref()
.map(python_string)
.unwrap_or_else(|| "None".to_string());
format!(
"{{\"default\": ModelConfig(provider={}, model={}, temperature={temperature}, top_p={top_p}, max_tokens={max_tokens}, api_key_env={}, base_url={base_url}, settings={})}}",
python_string(&model.provider),
python_string(&model.model),
model
.api_key_env
.as_deref()
.map(python_string)
.unwrap_or_else(|| "None".to_string()),
python_value(&Value::Object(model.settings.clone())),
)
}
fn python_value(value: &Value) -> String {
match value {
Value::Null => "None".to_string(),
Value::Bool(value) => if *value { "True" } else { "False" }.to_string(),
Value::Number(value) => value.to_string(),
Value::String(value) => python_string(value),
Value::Array(values) => format!(
"[{}]",
values
.iter()
.map(python_value)
.collect::<Vec<_>>()
.join(", ")
),
Value::Object(values) => format!(
"{{{}}}",
values
.iter()
.map(|(key, value)| format!("{}: {}", python_string(key), python_value(value)))
.collect::<Vec<_>>()
.join(", ")
),
}
}
fn python_string(value: &str) -> String {
serde_json::to_string(value).expect("strings serialize")
}
fn render_rust(config: &FabricConfig) -> String {
let harness = config.harness.as_ref().expect("scaffold preset harness");
RUST_MAIN
.replace("{{AGENT_NAME}}", &rust_string(&config.metadata.name))
.replace(
"{{DESCRIPTION}}",
&rust_string(config.metadata.description.as_deref().unwrap_or("")),
)
.replace("{{ADAPTER_ID}}", &rust_string(&harness.adapter_id))
.replace(
"{{HARNESS_SETTINGS}}",
&rust_settings(&harness.settings),
)
.replace(
"{{INSTRUCTIONS}}",
&config
.instructions
.as_ref()
.and_then(|instructions| instructions.system.as_ref())
.map(|instruction| {
format!(
"Some(nemo_fabric_core::InstructionsConfig {{ system: Some(nemo_fabric_core::InstructionConfig {{ content: {}.to_string(), mode: nemo_fabric_core::InstructionMode::Replace, extensions: BTreeMap::new() }}), extensions: BTreeMap::new() }})",
rust_string(&instruction.content)
)
})
.unwrap_or_else(|| "None".to_string()),
)
.replace(
"{{MAX_TURNS}}",
&config
.runtime
.max_turns
.map(|value| format!("Some({value})"))
.unwrap_or_else(|| "None".to_string()),
)
.replace(
"{{TIMEOUT_SECONDS}}",
&config
.runtime
.timeout_seconds
.map(|value| format!("Some({value})"))
.unwrap_or_else(|| "None".to_string()),
)
.replace(
"{{ENVIRONMENT_ENV}}",
&rust_string_map(
config
.environment
.as_ref()
.map(|environment| &environment.env),
),
)
.replace("{{MODELS}}", &rust_models(config.models.get("default")))
}
fn rust_string_map(values: Option<&std::collections::BTreeMap<String, String>>) -> String {
let Some(values) = values.filter(|values| !values.is_empty()) else {
return "BTreeMap::new()".to_string();
};
format!(
"BTreeMap::from_iter([{}])",
values
.iter()
.map(|(key, value)| format!(
"({}.to_string(), {}.to_string())",
rust_string(key),
rust_string(value)
))
.collect::<Vec<_>>()
.join(", ")
)
}
fn rust_settings(settings: &serde_json::Map<String, Value>) -> String {
if settings.is_empty() {
return "Map::new()".to_string();
}
format!(
"Map::from_iter([{}])",
settings
.iter()
.map(|(key, value)| {
format!(
"({}.to_string(), serde_json::json!({}))",
rust_string(key),
value
)
})
.collect::<Vec<_>>()
.join(", ")
)
}
fn rust_models(model: Option<&ModelConfig>) -> String {
let Some(model) = model else {
return "BTreeMap::new()".to_string();
};
let api_key = model
.api_key_env
.as_deref()
.map(|value| format!("Some({}.to_string())", rust_string(value)))
.unwrap_or_else(|| "None".to_string());
let temperature = model
.temperature
.map(|value| format!("Some({value:?})"))
.unwrap_or_else(|| "None".to_string());
let top_p = model
.top_p
.map(|value| format!("Some({value:?})"))
.unwrap_or_else(|| "None".to_string());
let max_tokens = model
.max_tokens
.map(|value| format!("Some({value})"))
.unwrap_or_else(|| "None".to_string());
let base_url = model
.base_url
.as_deref()
.map(|value| format!("Some({}.to_string())", rust_string(value)))
.unwrap_or_else(|| "None".to_string());
format!(
"BTreeMap::from_iter([(\"default\".to_string(), nemo_fabric_core::ModelConfig {{ provider: {}.to_string(), model: {}.to_string(), temperature: {temperature}, top_p: {top_p}, max_tokens: {max_tokens}, api_key_env: {api_key}, base_url: {base_url}, settings: {}, extensions: BTreeMap::new() }})])",
rust_string(&model.provider),
rust_string(&model.model),
rust_settings(&model.settings),
)
}
fn rust_string(value: &str) -> String {
format!("{value:?}")
}
fn package_name(example: &str) -> String {
example.replace('_', "-")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::examples;
fn destination(test: &str, language: Language) -> PathBuf {
std::env::temp_dir().join(format!(
"nemo-fabric-scaffold-test-{}-{test}-{}",
std::process::id(),
language.as_str()
))
}
#[test]
fn generates_both_languages_from_the_same_example_assets_and_config() {
let example = examples::find("code-review").expect("example");
for language in [Language::Python, Language::Rust] {
let destination = destination("generate", language);
let _ = fs::remove_dir_all(&destination);
init(example, Some("hermes"), language, &destination).expect("generate scaffold");
assert!(destination.join("repo/calculator.py").is_file());
assert!(destination.join("skills/code-review.md").is_file());
assert!(
destination
.join("adapters/hermes/hermes.fabric-adapter.json")
.is_file()
);
assert_eq!(
fs::read_to_string(destination.join("repo/calculator.py")).expect("read workspace"),
crate::examples::CODE_REVIEW_WORKSPACE
);
let launcher = match language {
Language::Python => destination.join("main.py"),
Language::Rust => destination.join("src/main.rs"),
};
let source = fs::read_to_string(launcher).expect("read launcher");
assert!(source.contains("nvidia.fabric.hermes"));
assert!(source.contains("nvidia/nemotron-3-nano-omni-30b-a3b-reasoning"));
assert!(source.contains("https://integrate.api.nvidia.com/v1"));
if language == Language::Rust {
let manifest =
fs::read_to_string(destination.join("Cargo.toml")).expect("read manifest");
assert!(
manifest.contains(&format!("nemo-fabric-core = {}", rust_core_dependency()))
);
assert!(manifest.contains("[workspace]"));
}
fs::remove_dir_all(destination).expect("remove scaffold");
}
}
#[test]
fn source_checkout_rust_scaffold_builds_inside_the_repository() {
let destination = Path::new(env!("CARGO_MANIFEST_DIR")).join(format!(
".nemo-fabric-scaffold-test-{}-build",
std::process::id()
));
let _ = fs::remove_dir_all(&destination);
init(
examples::find("code-review").expect("example"),
None,
Language::Rust,
&destination,
)
.expect("generate scaffold");
let output = std::process::Command::new(env!("CARGO"))
.args(["check", "--offline"])
.current_dir(&destination)
.output()
.expect("run cargo check");
assert!(
output.status.success(),
"generated Rust scaffold did not build:\n{}",
String::from_utf8_lossy(&output.stderr)
);
fs::remove_dir_all(destination).expect("remove scaffold");
}
#[test]
fn rust_core_dependency_uses_local_checkout_when_available() {
let source_path = destination("core-dependency-local", Language::Rust);
let _ = fs::remove_dir_all(&source_path);
fs::create_dir_all(&source_path).expect("create source checkout");
fs::write(source_path.join("Cargo.toml"), "").expect("write Cargo manifest");
let canonical_path = source_path.canonicalize().expect("canonicalize checkout");
assert_eq!(
rust_core_dependency_for_path(&source_path),
format!(
"{{ path = {}, version = {} }}",
rust_string(canonical_path.to_str().expect("UTF-8 checkout path")),
rust_string(env!("CARGO_PKG_VERSION"))
)
);
fs::remove_dir_all(source_path).expect("remove source checkout");
}
#[test]
fn rust_core_dependency_falls_back_when_checkout_is_unavailable() {
let source_path = destination("core-dependency-missing", Language::Rust);
let _ = fs::remove_dir_all(&source_path);
assert_eq!(
rust_core_dependency_for_path(&source_path),
rust_string(env!("CARGO_PKG_VERSION"))
);
}
#[test]
fn renderers_preserve_normalized_model_settings() {
let mut config = presets::find("hermes")
.expect("hermes preset")
.config()
.expect("construct Hermes config");
let model = config.models.get_mut("default").expect("default model");
model.temperature = Some(1.0);
model.top_p = Some(1.0);
model.max_tokens = Some(512);
let python = render_python(&config);
assert!(python.contains("temperature=1"));
assert!(python.contains("top_p=1"));
assert!(python.contains("max_tokens=512"));
assert!(python.contains("base_url=\"https://integrate.api.nvidia.com/v1\""));
let rust = render_rust(&config);
assert!(rust.contains("temperature: Some(1.0)"));
assert!(rust.contains("top_p: Some(1.0)"));
assert!(rust.contains("max_tokens: Some(512)"));
assert!(
rust.contains("base_url: Some(\"https://integrate.api.nvidia.com/v1\".to_string())")
);
}
#[test]
fn refuses_to_overwrite_a_destination() {
let destination = destination("overwrite", Language::Python);
let _ = fs::remove_dir_all(&destination);
fs::create_dir(&destination).expect("create destination");
let error = init(
examples::find("code-review").expect("example"),
None,
Language::Python,
&destination,
)
.expect_err("must refuse overwrite");
assert!(error.contains("already exists"));
fs::remove_dir_all(destination).expect("remove destination");
}
}