agentforge_parser/formats/
native.rs1use agentforge_core::{
2 AgentFile, AgentForgeError, EvalHints, ModelConfig, ModelProvider, Result, ToolDefinition,
3};
4
5pub fn normalize(value: &serde_json::Value) -> Result<AgentFile> {
7 let name = string_field(value, "name")?;
8 let version = value
9 .get("version")
10 .and_then(|v| v.as_str())
11 .unwrap_or("1.0.0")
12 .to_string();
13
14 let schema_version = value
15 .get("agentforge_schema_version")
16 .and_then(|v| v.as_str())
17 .unwrap_or("1")
18 .to_string();
19
20 let model = parse_model(value)?;
21 let system_prompt = string_field(value, "system_prompt")?;
22 let tools = parse_tools(value)?;
23 let output_schema = value.get("output_schema").cloned();
24 let constraints = parse_constraints(value);
25 let eval_hints = parse_eval_hints(value);
26
27 Ok(AgentFile {
28 agentforge_schema_version: schema_version,
29 name,
30 version,
31 model,
32 system_prompt,
33 tools,
34 output_schema,
35 constraints,
36 eval_hints,
37 metadata: None,
38 })
39}
40
41fn parse_model(value: &serde_json::Value) -> Result<ModelConfig> {
42 let model_obj = value.get("model").ok_or_else(|| {
43 AgentForgeError::ValidationError("Missing required field: model".to_string())
44 })?;
45
46 let provider_str = model_obj
47 .get("provider")
48 .and_then(|p| p.as_str())
49 .unwrap_or("openai");
50
51 let provider = parse_provider(provider_str);
52 let model_id = model_obj
53 .get("model_id")
54 .and_then(|m| m.as_str())
55 .ok_or_else(|| {
56 AgentForgeError::ValidationError("Missing required field: model.model_id".to_string())
57 })?
58 .to_string();
59
60 Ok(ModelConfig {
61 provider,
62 model_id,
63 temperature: model_obj.get("temperature").and_then(|t| t.as_f64()),
64 max_tokens: model_obj
65 .get("max_tokens")
66 .and_then(|t| t.as_u64())
67 .map(|v| v as u32),
68 top_p: model_obj.get("top_p").and_then(|t| t.as_f64()),
69 })
70}
71
72fn parse_tools(value: &serde_json::Value) -> Result<Vec<ToolDefinition>> {
73 let tools_val = match value.get("tools") {
74 Some(t) => t,
75 None => return Ok(vec![]),
76 };
77
78 let arr = tools_val
79 .as_array()
80 .ok_or_else(|| AgentForgeError::ValidationError("tools must be an array".to_string()))?;
81
82 arr.iter()
83 .map(|t| {
84 let name = t
85 .get("name")
86 .and_then(|n| n.as_str())
87 .ok_or_else(|| AgentForgeError::ValidationError("Tool missing name".to_string()))?
88 .to_string();
89 let description = t
90 .get("description")
91 .and_then(|d| d.as_str())
92 .unwrap_or("")
93 .to_string();
94 let parameters = t
95 .get("parameters")
96 .cloned()
97 .unwrap_or_else(|| serde_json::json!({"type": "object", "properties": {}}));
98
99 Ok(ToolDefinition {
100 name,
101 description,
102 parameters,
103 })
104 })
105 .collect()
106}
107
108fn parse_constraints(value: &serde_json::Value) -> Vec<String> {
109 value
110 .get("constraints")
111 .and_then(|c| c.as_array())
112 .map(|arr| {
113 arr.iter()
114 .filter_map(|v| v.as_str().map(String::from))
115 .collect()
116 })
117 .unwrap_or_default()
118}
119
120fn parse_eval_hints(value: &serde_json::Value) -> Option<EvalHints> {
121 let hints = value.get("eval_hints")?;
122 Some(EvalHints {
123 domain: hints
124 .get("domain")
125 .and_then(|d| d.as_str())
126 .map(String::from),
127 typical_turns: hints
128 .get("typical_turns")
129 .and_then(|t| t.as_u64())
130 .map(|v| v as u32),
131 critical_tools: hints
132 .get("critical_tools")
133 .and_then(|ct| ct.as_array())
134 .map(|arr| {
135 arr.iter()
136 .filter_map(|v| v.as_str().map(String::from))
137 .collect()
138 })
139 .unwrap_or_default(),
140 pass_threshold: hints.get("pass_threshold").and_then(|t| t.as_f64()),
141 scenario_count: hints
142 .get("scenario_count")
143 .and_then(|s| s.as_u64())
144 .map(|v| v as u32),
145 })
146}
147
148pub(crate) fn parse_provider(s: &str) -> ModelProvider {
149 match s.to_lowercase().as_str() {
150 "openai" => ModelProvider::Openai,
151 "anthropic" => ModelProvider::Anthropic,
152 "ollama" => ModelProvider::Ollama,
153 "bedrock" => ModelProvider::Bedrock,
154 "nvidia" | "nvidia_nim" => ModelProvider::NvidiaNim,
155 _ => ModelProvider::Custom,
156 }
157}
158
159fn string_field(value: &serde_json::Value, field: &str) -> Result<String> {
160 value
161 .get(field)
162 .and_then(|v| v.as_str())
163 .map(String::from)
164 .ok_or_else(|| AgentForgeError::ValidationError(format!("Missing required field: {field}")))
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use serde_json::json;
171
172 #[test]
173 fn normalizes_valid_native_yaml() {
174 let v = json!({
175 "agentforge_schema_version": "1",
176 "name": "test-agent",
177 "version": "1.0.0",
178 "model": {
179 "provider": "openai",
180 "model_id": "gpt-4o",
181 "temperature": 0.2
182 },
183 "system_prompt": "You are a helpful assistant.",
184 "tools": [],
185 "constraints": ["Never hallucinate."]
186 });
187 let agent = normalize(&v).unwrap();
188 assert_eq!(agent.name, "test-agent");
189 assert_eq!(agent.model.model_id, "gpt-4o");
190 assert_eq!(agent.constraints.len(), 1);
191 }
192
193 #[test]
194 fn rejects_missing_model() {
195 let v = json!({
196 "agentforge_schema_version": "1",
197 "name": "test-agent",
198 "system_prompt": "You are helpful."
199 });
200 assert!(normalize(&v).is_err());
201 }
202
203 #[test]
204 fn parses_tools() {
205 let v = json!({
206 "agentforge_schema_version": "1",
207 "name": "test-agent",
208 "version": "1.0.0",
209 "model": {"provider": "openai", "model_id": "gpt-4o"},
210 "system_prompt": "You are helpful.",
211 "tools": [
212 {
213 "name": "get_order",
214 "description": "Get order details",
215 "parameters": {"type": "object", "properties": {"id": {"type": "string"}}}
216 }
217 ]
218 });
219 let agent = normalize(&v).unwrap();
220 assert_eq!(agent.tools.len(), 1);
221 assert_eq!(agent.tools[0].name, "get_order");
222 }
223}