portalis-orchestration 0.1.0

Pipeline orchestration for multi-agent workflows
Documentation
//! Pipeline Orchestration
//!
//! Coordinates all agents to execute the complete translation pipeline.

pub mod workspace_generator;

use portalis_core::{Agent, Error, Phase, PipelineState, Result};
pub use workspace_generator::{WorkspaceGenerator, WorkspaceConfig, CrateInfo, ExternalDependency};
use portalis_ingest::{IngestAgent, IngestInput};
use portalis_analysis::{AnalysisAgent, AnalysisInput};
use portalis_transpiler::{TranspilerAgent, TranspilerInput};
use portalis_build::{BuildAgent, BuildInput};
use portalis_test::{TestAgent, TestInput};
use portalis_packaging::{PackagingAgent, PackagingInput};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// Pipeline orchestrator
pub struct Pipeline {
    ingest: IngestAgent,
    analysis: AnalysisAgent,
    transpiler: TranspilerAgent,
    build: BuildAgent,
    test: TestAgent,
    packaging: PackagingAgent,
    state: PipelineState,
}

impl Pipeline {
    /// Create a new pipeline with default agents
    pub fn new() -> Self {
        Self {
            ingest: IngestAgent::new(),
            analysis: AnalysisAgent::new(),
            transpiler: TranspilerAgent::new(),
            build: BuildAgent::new(),
            test: TestAgent::new(),
            packaging: PackagingAgent::new(),
            state: PipelineState::new(),
        }
    }

    /// Execute the complete translation pipeline
    pub async fn translate(&mut self, source_path: PathBuf, source_code: String) -> Result<PipelineOutput> {
        tracing::info!("Starting translation pipeline for {:?}", source_path);

        // Phase 1: Ingest
        self.state.transition(Phase::Ingesting);
        let ingest_output = self.ingest.execute(IngestInput {
            source_path: source_path.clone(),
            source_code,
        }).await?;

        // Phase 2: Analysis
        self.state.transition(Phase::Analyzing);
        let ast_json = serde_json::to_value(&ingest_output.ast)
            .map_err(|e| Error::Pipeline(format!("Failed to serialize AST: {}", e)))?;

        let analysis_output = self.analysis.execute(AnalysisInput {
            ast: ast_json,
        }).await?;

        // Phase 3: Transpilation
        self.state.transition(Phase::Transpiling);
        let typed_functions = serde_json::to_value(&analysis_output.typed_functions)
            .map_err(|e| Error::Pipeline(format!("Failed to serialize functions: {}", e)))?;

        let api_contract = serde_json::to_value(&analysis_output.api_contract)
            .map_err(|e| Error::Pipeline(format!("Failed to serialize API: {}", e)))?;

        let transpiler_output = self.transpiler.execute(TranspilerInput {
            typed_functions: typed_functions.as_array().cloned().unwrap_or_default(),
            typed_classes: vec![],
            use_statements: vec![],
            cargo_dependencies: vec![],
            api_contract,
        }).await?;

        // Phase 4: Build
        self.state.transition(Phase::Building);
        let build_output = self.build.execute(BuildInput {
            rust_code: transpiler_output.rust_code.clone(),
        }).await?;

        // Phase 5: Test
        self.state.transition(Phase::Testing);
        let test_output = self.test.execute(TestInput {
            wasm_bytes: build_output.wasm_bytes.clone(),
        }).await?;

        // Phase 6: Package
        self.state.transition(Phase::Packaging);
        let test_results = serde_json::to_value(&test_output)
            .map_err(|e| Error::Pipeline(format!("Failed to serialize tests: {}", e)))?;

        let packaging_output = self.packaging.execute(PackagingInput {
            wasm_bytes: build_output.wasm_bytes.clone(),
            test_results,
        }).await?;

        // Complete
        self.state.transition(Phase::Complete);

        Ok(PipelineOutput {
            source_path,
            rust_code: transpiler_output.rust_code,
            wasm_bytes: packaging_output.package_bytes,
            manifest: packaging_output.manifest,
            test_passed: test_output.passed,
            test_failed: test_output.failed,
        })
    }

    /// Get the current pipeline state
    pub fn state(&self) -> &PipelineState {
        &self.state
    }
}

impl Default for Pipeline {
    fn default() -> Self {
        Self::new()
    }
}

/// Final pipeline output
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineOutput {
    pub source_path: PathBuf,
    pub rust_code: String,
    pub wasm_bytes: Vec<u8>,
    pub manifest: serde_json::Value,
    pub test_passed: usize,
    pub test_failed: usize,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_pipeline_simple_function() {
        let mut pipeline = Pipeline::new();

        let source = r#"
def add(a: int, b: int) -> int:
    return a + b
"#;

        let result = pipeline.translate(
            PathBuf::from("test.py"),
            source.to_string(),
        ).await;

        // Pipeline may fail on build (requires wasm32 target)
        // but we can verify it progresses through phases
        match result {
            Ok(output) => {
                assert!(!output.rust_code.is_empty());
                println!("Generated Rust:\n{}", output.rust_code);
            }
            Err(e) => {
                println!("Pipeline error (expected if wasm32 target not installed): {}", e);
            }
        }
    }

    #[tokio::test]
    async fn test_pipeline_fibonacci() {
        let mut pipeline = Pipeline::new();

        let source = r#"
def fibonacci(n: int) -> int:
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)
"#;

        let result = pipeline.translate(
            PathBuf::from("fibonacci.py"),
            source.to_string(),
        ).await;

        match result {
            Ok(output) => {
                assert!(!output.rust_code.is_empty());
                assert!(output.rust_code.contains("fibonacci"));
            }
            Err(e) => {
                println!("Pipeline error (expected if wasm32 target not installed): {}", e);
            }
        }
    }

    #[tokio::test]
    async fn test_pipeline_multiple_functions() {
        let mut pipeline = Pipeline::new();

        let source = r#"
def add(a: int, b: int) -> int:
    return a + b

def multiply(x: int, y: int) -> int:
    return x * y
"#;

        let result = pipeline.translate(
            PathBuf::from("multi.py"),
            source.to_string(),
        ).await;

        match result {
            Ok(output) => {
                assert!(output.rust_code.contains("add"));
                assert!(output.rust_code.contains("multiply"));
            }
            Err(e) => {
                println!("Pipeline error (expected if wasm32 target not installed): {}", e);
            }
        }
    }

    #[test]
    fn test_pipeline_state_tracking() {
        let pipeline = Pipeline::new();
        assert_eq!(pipeline.state().phase, Phase::Idle);
    }
}