Skip to main content

agentforge_parser/formats/
openai.rs

1use agentforge_core::{
2    AgentFile, AgentForgeError, EvalHints, ModelConfig, ModelProvider, Result, ToolDefinition,
3};
4
5/// Normalize OpenAI Assistants API JSON into `AgentFile`.
6/// https://platform.openai.com/docs/api-reference/assistants
7pub 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    // OpenAI uses "instructions" as the system prompt
22    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    // OpenAI tools are in a different format — they wrap function definitions
50    let tools = parse_openai_tools(value)?;
51
52    // OpenAI response format → output schema
53    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            // OpenAI tool format: {"type": "function", "function": {...}}
85            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                // Ignore non-function tools (code_interpreter, file_search, etc.)
104                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}