agentforge_parser/formats/
openai.rs1use agentforge_core::{
2 AgentFile, AgentForgeError, EvalHints, ModelConfig, ModelProvider, Result, ToolDefinition,
3};
4
5pub fn normalize(value: &serde_json::Value) -> Result<AgentFile> {
8 let name = value
9 .get("name")
10 .and_then(|v| v.as_str())
11 .unwrap_or("openai-assistant")
12 .to_string();
13
14 let version = value
15 .get("metadata")
16 .and_then(|m| m.get("version"))
17 .and_then(|v| v.as_str())
18 .unwrap_or("1.0.0")
19 .to_string();
20
21 let system_prompt = value
23 .get("instructions")
24 .and_then(|v| v.as_str())
25 .ok_or_else(|| {
26 AgentForgeError::ValidationError("OpenAI: missing 'instructions' field".to_string())
27 })?
28 .to_string();
29
30 let model_id = value
31 .get("model")
32 .and_then(|v| v.as_str())
33 .ok_or_else(|| {
34 AgentForgeError::ValidationError("OpenAI: missing 'model' field".to_string())
35 })?
36 .to_string();
37
38 let temperature = value.get("temperature").and_then(|t| t.as_f64());
39 let top_p = value.get("top_p").and_then(|t| t.as_f64());
40
41 let model = ModelConfig {
42 provider: ModelProvider::Openai,
43 model_id,
44 temperature,
45 max_tokens: None,
46 top_p,
47 };
48
49 let tools = parse_openai_tools(value)?;
51
52 let output_schema = value
54 .get("response_format")
55 .and_then(|rf| rf.get("json_schema"))
56 .cloned();
57
58 Ok(AgentFile {
59 agentforge_schema_version: "1".to_string(),
60 name,
61 version,
62 model,
63 system_prompt,
64 tools,
65 output_schema,
66 constraints: vec![],
67 eval_hints: Some(EvalHints::default()),
68 metadata: None,
69 })
70}
71
72fn parse_openai_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 .filter_map(|t| {
84 if t.get("type").and_then(|ty| ty.as_str()) == Some("function") {
86 let func = t.get("function")?;
87 let name = func.get("name")?.as_str()?.to_string();
88 let description = func
89 .get("description")
90 .and_then(|d| d.as_str())
91 .unwrap_or("")
92 .to_string();
93 let parameters = func
94 .get("parameters")
95 .cloned()
96 .unwrap_or_else(|| serde_json::json!({"type": "object", "properties": {}}));
97 Some(Ok(ToolDefinition {
98 name,
99 description,
100 parameters,
101 }))
102 } else {
103 None
105 }
106 })
107 .collect()
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use serde_json::json;
114
115 #[test]
116 fn normalizes_openai_assistant() {
117 let v = json!({
118 "name": "support-bot",
119 "instructions": "You are a helpful support agent.",
120 "model": "gpt-4o",
121 "tools": [
122 {
123 "type": "function",
124 "function": {
125 "name": "get_order",
126 "description": "Get order details",
127 "parameters": {
128 "type": "object",
129 "properties": {"id": {"type": "string"}}
130 }
131 }
132 }
133 ]
134 });
135 let agent = normalize(&v).unwrap();
136 assert_eq!(agent.name, "support-bot");
137 assert_eq!(agent.system_prompt, "You are a helpful support agent.");
138 assert_eq!(agent.model.model_id, "gpt-4o");
139 assert_eq!(agent.tools.len(), 1);
140 assert_eq!(agent.tools[0].name, "get_order");
141 }
142
143 #[test]
144 fn filters_non_function_tools() {
145 let v = json!({
146 "instructions": "You are helpful.",
147 "model": "gpt-4o",
148 "tools": [
149 {"type": "code_interpreter"},
150 {"type": "function", "function": {"name": "search", "description": "Search"}}
151 ]
152 });
153 let agent = normalize(&v).unwrap();
154 assert_eq!(agent.tools.len(), 1);
155 }
156
157 #[test]
158 fn rejects_missing_instructions() {
159 let v = json!({ "model": "gpt-4o" });
160 assert!(normalize(&v).is_err());
161 }
162}