Skip to main content

agentforge_parser/formats/
native.rs

1use agentforge_core::{
2    AgentFile, AgentForgeError, EvalHints, ModelConfig, ModelProvider, Result, ToolDefinition,
3};
4
5/// Normalize AgentForge native YAML/JSON format into `AgentFile`.
6pub 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}