agentforge_parser/
detect.rs1use agentforge_core::{AgentFileFormat, AgentForgeError, Result};
2
3pub fn detect_format(content: &str) -> Result<AgentFileFormat> {
6 let trimmed = content.trim();
7
8 if trimmed.starts_with('{') {
10 let value: serde_json::Value = serde_json::from_str(trimmed)
11 .map_err(|e| AgentForgeError::ParseError(format!("Invalid JSON: {e}")))?;
12
13 return Ok(classify_json_format(&value));
14 }
15
16 if trimmed.starts_with("---") {
18 let yaml_body = extract_frontmatter(trimmed)?;
19 let value: serde_json::Value = serde_yaml::from_str(&yaml_body)
20 .map_err(|e| AgentForgeError::ParseError(format!("Invalid frontmatter YAML: {e}")))?;
21
22 let has_name = value.get("name").is_some();
25 let is_native = value.get("agentforge_schema_version").is_some();
26 let has_markdown_body = has_markdown_body_after_frontmatter(trimmed);
27
28 if has_name && !is_native && has_markdown_body {
29 return Ok(AgentFileFormat::CopilotAgentMd);
30 }
31
32 return Ok(classify_yaml_format(&value));
33 }
34
35 if let Ok(value) = serde_yaml::from_str::<serde_json::Value>(trimmed) {
37 return Ok(classify_yaml_format(&value));
38 }
39
40 Err(AgentForgeError::InvalidFormat(
41 "Cannot detect format: not valid JSON, YAML, or Markdown frontmatter".to_string(),
42 ))
43}
44
45fn has_markdown_body_after_frontmatter(content: &str) -> bool {
47 let mut parts = content.splitn(3, "---");
48 parts.next(); parts.next(); match parts.next() {
51 Some(body) => !body.trim().is_empty(),
52 None => false,
53 }
54}
55
56fn classify_json_format(v: &serde_json::Value) -> AgentFileFormat {
57 if v.get("instructions").is_some()
59 && v.get("tools").is_some()
60 && v.get("system_prompt").is_none()
61 {
62 return AgentFileFormat::OpenaiJson;
63 }
64
65 if (v.get("system").is_some() || v.get("system_prompt").is_some())
67 && v.get("model").is_some()
68 && v.get("agentforge_schema_version").is_none()
69 {
70 if let Some(model) = v.get("model").and_then(|m| m.as_str()) {
72 if model.contains("claude") {
73 return AgentFileFormat::AnthropicJson;
74 }
75 }
76 return AgentFileFormat::AnthropicJson;
77 }
78
79 AgentFileFormat::NativeYaml
81}
82
83fn classify_yaml_format(v: &serde_json::Value) -> AgentFileFormat {
84 if v.get("agentforge_schema_version").is_some() {
86 return AgentFileFormat::NativeYaml;
87 }
88
89 if let Some(t) = v.get("_type").and_then(|t| t.as_str()) {
91 if t.contains("langchain") || t.contains("lang_chain") {
92 return AgentFileFormat::LangchainYaml;
93 }
94 }
95
96 if v.get("agents").is_some()
98 || (v.get("role").is_some() && v.get("goal").is_some() && v.get("backstory").is_some())
99 {
100 return AgentFileFormat::CrewaiYaml;
101 }
102
103 AgentFileFormat::NativeYaml
105}
106
107fn extract_frontmatter(content: &str) -> Result<String> {
108 let parts: Vec<&str> = content.splitn(3, "---").collect();
109 if parts.len() < 3 {
110 return Err(AgentForgeError::ParseError(
111 "Malformed Markdown frontmatter: missing closing ---".to_string(),
112 ));
113 }
114 Ok(parts[1].to_string())
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn detects_native_yaml() {
123 let content = r#"
124agentforge_schema_version: "1"
125name: test-agent
126version: "1.0.0"
127"#;
128 assert_eq!(detect_format(content).unwrap(), AgentFileFormat::NativeYaml);
129 }
130
131 #[test]
132 fn detects_openai_json() {
133 let content = r#"{"instructions": "You are helpful.", "tools": [], "model": "gpt-4o"}"#;
134 assert_eq!(detect_format(content).unwrap(), AgentFileFormat::OpenaiJson);
135 }
136
137 #[test]
138 fn detects_anthropic_json() {
139 let content =
140 r#"{"system": "You are helpful.", "tools": [], "model": "claude-3-5-sonnet-20241022"}"#;
141 assert_eq!(
142 detect_format(content).unwrap(),
143 AgentFileFormat::AnthropicJson
144 );
145 }
146
147 #[test]
148 fn detects_crewai_yaml() {
149 let content = r#"
150role: Support Agent
151goal: Help customers
152backstory: You are an expert support agent.
153"#;
154 assert_eq!(detect_format(content).unwrap(), AgentFileFormat::CrewaiYaml);
155 }
156
157 #[test]
158 fn detects_markdown_frontmatter() {
159 let content = r#"---
160agentforge_schema_version: "1"
161name: test-agent
162version: "1.0.0"
163---
164# Documentation
165"#;
166 assert_eq!(detect_format(content).unwrap(), AgentFileFormat::NativeYaml);
167 }
168
169 #[test]
170 fn detects_copilot_agent_md() {
171 let content = r#"---
172name: 'GitHub Actions Expert'
173description: 'CI/CD workflow specialist'
174model: GPT-4.1
175tools: ['github/*', 'read']
176---
177
178# GitHub Actions Expert
179
180You are a GitHub Actions specialist.
181"#;
182 assert_eq!(
183 detect_format(content).unwrap(),
184 AgentFileFormat::CopilotAgentMd
185 );
186 }
187
188 #[test]
189 fn copilot_md_without_body_falls_through_to_yaml() {
190 let content = "---\nname: 'Just Frontmatter'\n---\n";
192 assert_eq!(detect_format(content).unwrap(), AgentFileFormat::NativeYaml);
194 }
195
196 #[test]
197 fn rejects_invalid_json() {
198 let result = detect_format("{invalid json}");
199 assert!(result.is_err());
200 }
201}