Skip to main content

portalis_orchestration/
lib.rs

1//! Pipeline Orchestration
2//!
3//! Coordinates all agents to execute the complete translation pipeline.
4
5pub mod workspace_generator;
6
7use portalis_core::{Agent, Error, Phase, PipelineState, Result};
8pub use workspace_generator::{WorkspaceGenerator, WorkspaceConfig, CrateInfo, ExternalDependency};
9use portalis_ingest::{IngestAgent, IngestInput};
10use portalis_analysis::{AnalysisAgent, AnalysisInput};
11use portalis_transpiler::{TranspilerAgent, TranspilerInput};
12use portalis_build::{BuildAgent, BuildInput};
13use portalis_test::{TestAgent, TestInput};
14use portalis_packaging::{PackagingAgent, PackagingInput};
15use serde::{Deserialize, Serialize};
16use std::path::PathBuf;
17
18/// Pipeline orchestrator
19pub struct Pipeline {
20    ingest: IngestAgent,
21    analysis: AnalysisAgent,
22    transpiler: TranspilerAgent,
23    build: BuildAgent,
24    test: TestAgent,
25    packaging: PackagingAgent,
26    state: PipelineState,
27}
28
29impl Pipeline {
30    /// Create a new pipeline with default agents
31    pub fn new() -> Self {
32        Self {
33            ingest: IngestAgent::new(),
34            analysis: AnalysisAgent::new(),
35            transpiler: TranspilerAgent::new(),
36            build: BuildAgent::new(),
37            test: TestAgent::new(),
38            packaging: PackagingAgent::new(),
39            state: PipelineState::new(),
40        }
41    }
42
43    /// Execute the complete translation pipeline
44    pub async fn translate(&mut self, source_path: PathBuf, source_code: String) -> Result<PipelineOutput> {
45        tracing::info!("Starting translation pipeline for {:?}", source_path);
46
47        // Phase 1: Ingest
48        self.state.transition(Phase::Ingesting);
49        let ingest_output = self.ingest.execute(IngestInput {
50            source_path: source_path.clone(),
51            source_code,
52        }).await?;
53
54        // Phase 2: Analysis
55        self.state.transition(Phase::Analyzing);
56        let ast_json = serde_json::to_value(&ingest_output.ast)
57            .map_err(|e| Error::Pipeline(format!("Failed to serialize AST: {}", e)))?;
58
59        let analysis_output = self.analysis.execute(AnalysisInput {
60            ast: ast_json,
61        }).await?;
62
63        // Phase 3: Transpilation
64        self.state.transition(Phase::Transpiling);
65        let typed_functions = serde_json::to_value(&analysis_output.typed_functions)
66            .map_err(|e| Error::Pipeline(format!("Failed to serialize functions: {}", e)))?;
67
68        let api_contract = serde_json::to_value(&analysis_output.api_contract)
69            .map_err(|e| Error::Pipeline(format!("Failed to serialize API: {}", e)))?;
70
71        let transpiler_output = self.transpiler.execute(TranspilerInput {
72            typed_functions: typed_functions.as_array().cloned().unwrap_or_default(),
73            typed_classes: vec![],
74            use_statements: vec![],
75            cargo_dependencies: vec![],
76            api_contract,
77        }).await?;
78
79        // Phase 4: Build
80        self.state.transition(Phase::Building);
81        let build_output = self.build.execute(BuildInput {
82            rust_code: transpiler_output.rust_code.clone(),
83        }).await?;
84
85        // Phase 5: Test
86        self.state.transition(Phase::Testing);
87        let test_output = self.test.execute(TestInput {
88            wasm_bytes: build_output.wasm_bytes.clone(),
89        }).await?;
90
91        // Phase 6: Package
92        self.state.transition(Phase::Packaging);
93        let test_results = serde_json::to_value(&test_output)
94            .map_err(|e| Error::Pipeline(format!("Failed to serialize tests: {}", e)))?;
95
96        let packaging_output = self.packaging.execute(PackagingInput {
97            wasm_bytes: build_output.wasm_bytes.clone(),
98            test_results,
99        }).await?;
100
101        // Complete
102        self.state.transition(Phase::Complete);
103
104        Ok(PipelineOutput {
105            source_path,
106            rust_code: transpiler_output.rust_code,
107            wasm_bytes: packaging_output.package_bytes,
108            manifest: packaging_output.manifest,
109            test_passed: test_output.passed,
110            test_failed: test_output.failed,
111        })
112    }
113
114    /// Get the current pipeline state
115    pub fn state(&self) -> &PipelineState {
116        &self.state
117    }
118}
119
120impl Default for Pipeline {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126/// Final pipeline output
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct PipelineOutput {
129    pub source_path: PathBuf,
130    pub rust_code: String,
131    pub wasm_bytes: Vec<u8>,
132    pub manifest: serde_json::Value,
133    pub test_passed: usize,
134    pub test_failed: usize,
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[tokio::test]
142    async fn test_pipeline_simple_function() {
143        let mut pipeline = Pipeline::new();
144
145        let source = r#"
146def add(a: int, b: int) -> int:
147    return a + b
148"#;
149
150        let result = pipeline.translate(
151            PathBuf::from("test.py"),
152            source.to_string(),
153        ).await;
154
155        // Pipeline may fail on build (requires wasm32 target)
156        // but we can verify it progresses through phases
157        match result {
158            Ok(output) => {
159                assert!(!output.rust_code.is_empty());
160                println!("Generated Rust:\n{}", output.rust_code);
161            }
162            Err(e) => {
163                println!("Pipeline error (expected if wasm32 target not installed): {}", e);
164            }
165        }
166    }
167
168    #[tokio::test]
169    async fn test_pipeline_fibonacci() {
170        let mut pipeline = Pipeline::new();
171
172        let source = r#"
173def fibonacci(n: int) -> int:
174    if n <= 1:
175        return n
176    return fibonacci(n - 1) + fibonacci(n - 2)
177"#;
178
179        let result = pipeline.translate(
180            PathBuf::from("fibonacci.py"),
181            source.to_string(),
182        ).await;
183
184        match result {
185            Ok(output) => {
186                assert!(!output.rust_code.is_empty());
187                assert!(output.rust_code.contains("fibonacci"));
188            }
189            Err(e) => {
190                println!("Pipeline error (expected if wasm32 target not installed): {}", e);
191            }
192        }
193    }
194
195    #[tokio::test]
196    async fn test_pipeline_multiple_functions() {
197        let mut pipeline = Pipeline::new();
198
199        let source = r#"
200def add(a: int, b: int) -> int:
201    return a + b
202
203def multiply(x: int, y: int) -> int:
204    return x * y
205"#;
206
207        let result = pipeline.translate(
208            PathBuf::from("multi.py"),
209            source.to_string(),
210        ).await;
211
212        match result {
213            Ok(output) => {
214                assert!(output.rust_code.contains("add"));
215                assert!(output.rust_code.contains("multiply"));
216            }
217            Err(e) => {
218                println!("Pipeline error (expected if wasm32 target not installed): {}", e);
219            }
220        }
221    }
222
223    #[test]
224    fn test_pipeline_state_tracking() {
225        let pipeline = Pipeline::new();
226        assert_eq!(pipeline.state().phase, Phase::Idle);
227    }
228}