Skip to main content

soma_core/
feedback_loop.rs

1// feedback_loop.rs: Multi-agent symbolic feedback and merge system for soma-core
2// All comments in English (US) per coding_guidelines.md
3
4use crate::agents::claude_agent::ClaudeAgent;
5use crate::agents::gemini_agent::GeminiAgent;
6use crate::agents::gpt4_agent::GPT4Agent;
7use crate::agents::gpt4_agent::{CognitiveAgent, ProposedEdit};
8use crate::dag::Node;
9use crate::memory::SymbolicContext;
10use std::collections::HashMap;
11use std::fs;
12
13/// Loads the current code context (e.g., from a file)
14#[allow(dead_code)]
15pub fn load_code_context(file: &str) -> Vec<String> {
16    fs::read_to_string(file)
17        .map(|s| s.lines().map(|l| l.to_string()).collect())
18        .unwrap_or_default()
19}
20
21/// Creates a backup of a file before editing
22#[allow(dead_code)]
23pub fn backup_file(file_path: &str) -> Result<String, std::io::Error> {
24    let backup_path = format!("{}.backup", file_path);
25    fs::copy(file_path, &backup_path)?;
26    Ok(backup_path)
27}
28
29/// Restores a file from its backup
30#[allow(dead_code)]
31pub fn restore_file(file_path: &str, backup_path: &str) -> Result<(), std::io::Error> {
32    fs::copy(backup_path, file_path)?;
33    fs::remove_file(backup_path)?;
34    Ok(())
35}
36
37/// Applies a proposed edit to a file by replacing the specified line range
38#[allow(dead_code)]
39pub fn apply_edit_to_file(
40    file_path: &str,
41    edit: &ProposedEdit,
42) -> Result<(), Box<dyn std::error::Error>> {
43    let lines = load_code_context(file_path);
44    let mut new_lines = lines.clone();
45
46    // Validate line range
47    if edit.line_range.0 == 0 || edit.line_range.1 > lines.len() {
48        return Err(format!(
49            "Invalid line range {:?} for file with {} lines",
50            edit.line_range,
51            lines.len()
52        )
53        .into());
54    }
55
56    // Convert 1-based line numbers to 0-based indices
57    let start_idx = edit.line_range.0 - 1;
58    let end_idx = edit.line_range.1.min(lines.len());
59
60    // Replace the lines
61    let edit_lines: Vec<String> = edit.new_code.lines().map(|s| s.to_string()).collect();
62    new_lines.splice(start_idx..end_idx, edit_lines);
63
64    // Write back to file
65    let content = new_lines.join("\n");
66    fs::write(file_path, content)?;
67
68    println!("āœ… Applied edit to {}: {}", file_path, edit.reason);
69    Ok(())
70}
71
72/// Applies all consensus edits to their respective files with backup/restore
73#[allow(dead_code)]
74pub fn apply_consensus_edits(
75    edits: &[&ProposedEdit],
76) -> Result<Vec<String>, Box<dyn std::error::Error>> {
77    let mut backups = Vec::new();
78
79    // Group edits by file
80    let mut edits_by_file: HashMap<String, Vec<&ProposedEdit>> = HashMap::new();
81    for edit in edits {
82        edits_by_file
83            .entry(edit.file.clone())
84            .or_default()
85            .push(edit);
86    }
87
88    for (file_path, file_edits) in edits_by_file {
89        // Create backup
90        if std::path::Path::new(&file_path).exists() {
91            let backup_path = backup_file(&file_path)?;
92            backups.push((file_path.clone(), backup_path));
93
94            // Apply edits in reverse order (from bottom to top) to maintain line numbers
95            let mut sorted_edits = file_edits;
96            sorted_edits.sort_by(|a, b| b.line_range.0.cmp(&a.line_range.0));
97
98            for edit in sorted_edits {
99                apply_edit_to_file(&file_path, edit)?;
100            }
101        } else {
102            println!("āš ļø  File {} does not exist, skipping edit", file_path);
103        }
104    }
105
106    Ok(backups.into_iter().map(|(_, backup)| backup).collect())
107}
108
109/// Shows the diff between original and edited files
110#[allow(dead_code)]
111pub fn show_file_diff(
112    file_path: &str,
113    backup_path: &str,
114) -> Result<(), Box<dyn std::error::Error>> {
115    let original = fs::read_to_string(backup_path)?;
116    let edited = fs::read_to_string(file_path)?;
117
118    println!("\nšŸ“„ File: {}", file_path);
119    println!("{}", "=".repeat(60));
120
121    let original_lines: Vec<&str> = original.lines().collect();
122    let edited_lines: Vec<&str> = edited.lines().collect();
123
124    let max_lines = original_lines.len().max(edited_lines.len());
125
126    for i in 0..max_lines {
127        let orig_line = original_lines.get(i).unwrap_or(&"");
128        let edit_line = edited_lines.get(i).unwrap_or(&"");
129
130        if orig_line != edit_line {
131            if !orig_line.is_empty() {
132                println!("- {}: {}", i + 1, orig_line);
133            }
134            if !edit_line.is_empty() {
135                println!("+ {}: {}", i + 1, edit_line);
136            }
137        } else if !orig_line.is_empty() {
138            println!("  {}: {}", i + 1, orig_line);
139        }
140
141        // Limit output for readability
142        if i > 20 {
143            println!("  ... ({} more lines)", max_lines - i - 1);
144            break;
145        }
146    }
147
148    Ok(())
149}
150
151/// Merge logic: Accepts edits if at least 2 agents agree on the same file/line range
152pub fn merge_edits(edits: &[ProposedEdit]) -> Vec<&ProposedEdit> {
153    let mut votes: HashMap<(String, (usize, usize)), Vec<&ProposedEdit>> = HashMap::new();
154    for edit in edits {
155        votes
156            .entry((edit.file.clone(), edit.line_range))
157            .or_default()
158            .push(edit);
159    }
160    votes
161        .values()
162        .filter(|v| v.len() >= 2)
163        .map(|v| v[0])
164        .collect()
165}
166
167/// Coordination function: runs the feedback loop and logs meta information
168pub fn run_feedback_loop(ctx: &SymbolicContext, _dag: &[Node]) {
169    let agents: Vec<Box<dyn CognitiveAgent>> = vec![
170        Box::new(GPT4Agent),
171        Box::new(ClaudeAgent),
172        Box::new(GeminiAgent),
173    ];
174    let mut all_edits = vec![];
175    for agent in &agents {
176        let edit = agent.propose_edit(ctx);
177        all_edits.push(edit);
178    }
179    let merged = merge_edits(&all_edits);
180    // Log all proposals and merged edits
181    let meta_log = serde_json::json!({
182        "timestamp": chrono::Utc::now().to_rfc3339(),
183        "proposals": all_edits.iter().map(|e| serde_json::json!({
184            "file": e.file,
185            "line_range": e.line_range,
186            "new_code": e.new_code,
187            "reason": e.reason,
188            "confidence": e.confidence,
189        })).collect::<Vec<_>>(),
190        "merged": merged.iter().map(|e| serde_json::json!({
191            "file": e.file,
192            "line_range": e.line_range,
193            "new_code": e.new_code,
194            "reason": e.reason,
195            "confidence": e.confidence,
196        })).collect::<Vec<_>>()
197    });
198    fs::write(
199        "meta_log.json",
200        serde_json::to_string_pretty(&meta_log).unwrap(),
201    )
202    .unwrap();
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::agents::gpt4_agent::ProposedEdit;
209    use crate::memory::SymbolicContext;
210
211    #[test]
212    fn test_merge_edits_consensus() {
213        let edits = vec![
214            ProposedEdit {
215                file: "test.rs".to_string(),
216                line_range: (10, 12),
217                new_code: "// Improved code".to_string(),
218                reason: "Better readability".to_string(),
219                confidence: 0.95,
220            },
221            ProposedEdit {
222                file: "test.rs".to_string(),
223                line_range: (10, 12),
224                new_code: "// Enhanced code".to_string(),
225                reason: "Better performance".to_string(),
226                confidence: 0.90,
227            },
228            ProposedEdit {
229                file: "other.rs".to_string(),
230                line_range: (5, 7),
231                new_code: "// Solo edit".to_string(),
232                reason: "Isolated change".to_string(),
233                confidence: 0.85,
234            },
235        ];
236
237        let merged = merge_edits(&edits);
238        assert_eq!(merged.len(), 1); // Only one edit should be merged (2+ consensus)
239        assert_eq!(merged[0].file, "test.rs");
240        assert_eq!(merged[0].line_range, (10, 12));
241    }
242
243    #[test]
244    fn test_feedback_loop_execution() {
245        let mut ctx = SymbolicContext::new();
246        ctx.set("current_task", "improve code quality");
247
248        let nodes = vec![];
249
250        // This should create a meta_log.json file
251        run_feedback_loop(&ctx, &nodes);
252
253        // Verify the log file was created
254        assert!(std::path::Path::new("meta_log.json").exists());
255
256        // Read and verify the log content
257        let log_content = std::fs::read_to_string("meta_log.json").unwrap();
258        let log: serde_json::Value = serde_json::from_str(&log_content).unwrap();
259
260        assert!(log["proposals"].is_array());
261        assert_eq!(log["proposals"].as_array().unwrap().len(), 3); // 3 agents
262        assert!(log["merged"].is_array());
263
264        // Clean up
265        std::fs::remove_file("meta_log.json").ok();
266    }
267}