opensymphony 1.8.0

A Rust implementation of the OpenAI Symphony orchestration design
Documentation
use std::{fs, path::Path};

use super::{WorkflowDefinition, WorkflowFrontMatter, error::WorkflowLoadError};

pub(crate) fn load_workflow_from_path(
    path: &Path,
) -> Result<WorkflowDefinition, WorkflowLoadError> {
    let contents = fs::read_to_string(path).map_err(|source| match source.kind() {
        std::io::ErrorKind::NotFound => WorkflowLoadError::MissingWorkflowFile {
            path: path.to_path_buf(),
        },
        _ => WorkflowLoadError::ReadWorkflowFile {
            path: path.to_path_buf(),
            source,
        },
    })?;

    parse_workflow(&contents)
}

pub(crate) fn parse_workflow(source: &str) -> Result<WorkflowDefinition, WorkflowLoadError> {
    let Some((front_matter_source, prompt_source)) = split_front_matter(source) else {
        return Ok(WorkflowDefinition {
            front_matter: WorkflowFrontMatter::default(),
            prompt_template: source.to_owned(),
        });
    };

    let Some(front_matter) = parse_front_matter(front_matter_source)? else {
        return Ok(WorkflowDefinition {
            front_matter: WorkflowFrontMatter::default(),
            prompt_template: source.to_owned(),
        });
    };

    Ok(WorkflowDefinition {
        front_matter,
        prompt_template: prompt_source.to_owned(),
    })
}

fn parse_front_matter(
    front_matter: &str,
) -> Result<Option<WorkflowFrontMatter>, WorkflowLoadError> {
    let parsed = serde_yaml::from_str::<serde_yaml::Value>(front_matter)
        .map_err(|source| WorkflowLoadError::WorkflowParseError { source })?;

    match parsed {
        serde_yaml::Value::Null if front_matter.trim().is_empty() => {
            Ok(Some(WorkflowFrontMatter::default()))
        }
        serde_yaml::Value::Null => Ok(None),
        serde_yaml::Value::Mapping(_) => {
            let parsed: WorkflowFrontMatter = serde_yaml::from_value(parsed)
                .map_err(|source| WorkflowLoadError::WorkflowParseError { source })?;

            match parsed.extensions.keys().next() {
                Some(namespace) => Err(WorkflowLoadError::UnknownTopLevelNamespace {
                    namespace: namespace.clone(),
                }),
                None => Ok(Some(parsed)),
            }
        }
        _ => Ok(None),
    }
}

fn split_front_matter(source: &str) -> Option<(&str, &str)> {
    let mut lines = source.split_inclusive('\n');
    let first_line = lines.next()?;

    if trim_line(first_line) != "---" {
        return None;
    }

    let mut offset = first_line.len();
    for line in lines {
        let line_length = line.len();
        if trim_line(line) == "---" {
            let body_start = offset + line_length;
            return Some((&source[first_line.len()..offset], &source[body_start..]));
        }

        offset += line_length;
    }

    None
}

fn trim_line(line: &str) -> &str {
    line.trim_end_matches(['\r', '\n'])
}