agentforge_parser/
parser.rs1use crate::{detect::detect_format, formats};
2use agentforge_core::{AgentFile, AgentFileFormat, AgentForgeError, AgentVersion, Result};
3use chrono::Utc;
4use sha2::{Digest, Sha256};
5use uuid::Uuid;
6
7#[derive(Debug, Clone)]
9pub struct ParsedAgentFile {
10 pub agent: AgentFile,
11 pub format: AgentFileFormat,
12 pub sha: String,
13 pub raw_content: String,
14}
15
16pub fn parse_agent_file(content: &str) -> Result<ParsedAgentFile> {
20 let format = detect_format(content)?;
21
22 let value = parse_to_value(content, &format)?;
24
25 let agent = formats::normalize(&format, &value, content)?;
26 let sha = compute_sha256(content);
27
28 Ok(ParsedAgentFile {
29 agent,
30 format,
31 sha,
32 raw_content: content.to_string(),
33 })
34}
35
36pub fn parse_agent_file_with_format(
39 content: &str,
40 format: AgentFileFormat,
41) -> Result<ParsedAgentFile> {
42 let value = parse_to_value(content, &format)?;
43 let agent = formats::normalize(&format, &value, content)?;
44 let sha = compute_sha256(content);
45 Ok(ParsedAgentFile {
46 agent,
47 format,
48 sha,
49 raw_content: content.to_string(),
50 })
51}
52
53pub fn to_agent_version(parsed: ParsedAgentFile) -> AgentVersion {
55 let now = Utc::now();
56 AgentVersion {
57 id: Uuid::new_v4(),
58 name: parsed.agent.name.clone(),
59 version: parsed.agent.version.clone(),
60 sha: parsed.sha,
61 file_content: parsed.agent,
62 raw_content: parsed.raw_content,
63 format: parsed.format,
64 promoted: false,
65 is_champion: false,
66 changelog: None,
67 parent_sha: None,
68 created_at: now,
69 updated_at: now,
70 }
71}
72
73fn parse_to_value(content: &str, format: &AgentFileFormat) -> Result<serde_json::Value> {
74 let trimmed = content.trim();
75
76 match format {
77 AgentFileFormat::OpenaiJson | AgentFileFormat::AnthropicJson => {
78 serde_json::from_str(trimmed).map_err(|e| AgentForgeError::ParseError(e.to_string()))
79 }
80 AgentFileFormat::NativeYaml
81 | AgentFileFormat::LangchainYaml
82 | AgentFileFormat::CrewaiYaml => {
83 let yaml_content = if trimmed.starts_with("---") {
85 extract_frontmatter(trimmed)?
86 } else {
87 trimmed.to_string()
88 };
89 serde_yaml::from_str(&yaml_content)
90 .map_err(|e| AgentForgeError::ParseError(e.to_string()))
91 }
92 AgentFileFormat::CopilotAgentMd => {
93 let frontmatter = extract_frontmatter(trimmed)?;
96 serde_yaml::from_str(&frontmatter)
97 .map_err(|e| AgentForgeError::ParseError(e.to_string()))
98 }
99 }
100}
101
102fn extract_frontmatter(content: &str) -> Result<String> {
103 let parts: Vec<&str> = content.splitn(3, "---").collect();
104 if parts.len() < 3 {
105 return Err(AgentForgeError::ParseError(
106 "Malformed Markdown frontmatter: missing closing ---".to_string(),
107 ));
108 }
109 Ok(parts[1].to_string())
110}
111
112pub fn compute_sha256(content: &str) -> String {
114 let mut hasher = Sha256::new();
115 hasher.update(content.as_bytes());
116 hex::encode(hasher.finalize())
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use indoc::indoc;
123
124 const NATIVE_YAML: &str = indoc! {r#"
125 agentforge_schema_version: "1"
126 name: customer-support-agent
127 version: "1.0.0"
128 model:
129 provider: openai
130 model_id: gpt-4o
131 temperature: 0.2
132 max_tokens: 2048
133 system_prompt: |
134 You are a helpful customer support agent.
135 Never share pricing without verifying entitlement first.
136 tools:
137 - name: get_order_status
138 description: "Retrieve status of a customer order by order ID."
139 parameters:
140 type: object
141 properties:
142 order_id:
143 type: string
144 required: [order_id]
145 output_schema:
146 type: object
147 properties:
148 response:
149 type: string
150 action_taken:
151 type: string
152 required: [response]
153 constraints:
154 - "Never mention competitor products."
155 - "Always confirm order ID before calling get_order_status."
156 eval_hints:
157 domain: customer_support
158 typical_turns: 3
159 critical_tools: [get_order_status]
160 pass_threshold: 0.85
161 scenario_count: 200
162 "#};
163
164 #[test]
165 fn parses_native_yaml() {
166 let result = parse_agent_file(NATIVE_YAML).unwrap();
167 assert_eq!(result.agent.name, "customer-support-agent");
168 assert_eq!(result.agent.model.model_id, "gpt-4o");
169 assert_eq!(result.agent.tools.len(), 1);
170 assert_eq!(result.agent.constraints.len(), 2);
171 assert_eq!(result.format, AgentFileFormat::NativeYaml);
172 assert!(!result.sha.is_empty());
173 }
174
175 #[test]
176 fn sha_is_deterministic() {
177 let a = compute_sha256("hello");
178 let b = compute_sha256("hello");
179 assert_eq!(a, b);
180 }
181
182 #[test]
183 fn sha_differs_for_different_content() {
184 let a = compute_sha256("hello");
185 let b = compute_sha256("world");
186 assert_ne!(a, b);
187 }
188
189 #[test]
190 fn parses_openai_json() {
191 let content = r#"{
192 "name": "support-bot",
193 "instructions": "You are a helpful support agent.",
194 "model": "gpt-4o",
195 "tools": []
196 }"#;
197 let result = parse_agent_file(content).unwrap();
198 assert_eq!(result.format, AgentFileFormat::OpenaiJson);
199 assert_eq!(
200 result.agent.system_prompt,
201 "You are a helpful support agent."
202 );
203 }
204
205 #[test]
206 fn to_agent_version_sets_fields() {
207 let parsed = parse_agent_file(NATIVE_YAML).unwrap();
208 let version = to_agent_version(parsed);
209 assert_eq!(version.name, "customer-support-agent");
210 assert!(!version.promoted);
211 assert!(!version.is_champion);
212 }
213}