diff --git a/crates/goose-mcp/src/developer/editor_models/EDITOR_API_EXAMPLE.md b/crates/goose-mcp/src/developer/editor_models/EDITOR_API_EXAMPLE.md index 073e34683dcf..9099eaff1747 100644 --- a/crates/goose-mcp/src/developer/editor_models/EDITOR_API_EXAMPLE.md +++ b/crates/goose-mcp/src/developer/editor_models/EDITOR_API_EXAMPLE.md @@ -36,7 +36,7 @@ export GOOSE_EDITOR_MODEL="claude-3-5-sonnet-20241022" ```bash export GOOSE_EDITOR_API_KEY="sk-..." export GOOSE_EDITOR_HOST="https://api.morphllm.com/v1" -export GOOSE_EDITOR_MODEL="morph-v0" +export GOOSE_EDITOR_MODEL="morph-v3-large" ``` **Relace** diff --git a/crates/goose-mcp/src/developer/editor_models/mod.rs b/crates/goose-mcp/src/developer/editor_models/mod.rs index d442aa56c32b..cdc56a3008a3 100644 --- a/crates/goose-mcp/src/developer/editor_models/mod.rs +++ b/crates/goose-mcp/src/developer/editor_models/mod.rs @@ -86,7 +86,7 @@ pub fn create_editor_model() -> Option { // Determine which editor to use based on the host if host.contains("relace.run") { Some(EditorModel::Relace(RelaceEditor::new(api_key, host, model))) - } else if host.contains("api.morphllm") { + } else if host.contains("api.morphllm") || model.contains("morph") { Some(EditorModel::MorphLLM(MorphLLMEditor::new( api_key, host, model, ))) diff --git a/crates/goose-mcp/src/developer/editor_models/morphllm_editor.rs b/crates/goose-mcp/src/developer/editor_models/morphllm_editor.rs index 8c5d60f8f813..3c6266d16ff6 100644 --- a/crates/goose-mcp/src/developer/editor_models/morphllm_editor.rs +++ b/crates/goose-mcp/src/developer/editor_models/morphllm_editor.rs @@ -19,6 +19,45 @@ impl MorphLLMEditor { model, } } + + /// Extract content between XML tags + fn extract_tag_content(text: &str, tag_name: &str) -> Option { + let start_tag = format!("<{}>", tag_name); + let end_tag = format!("", tag_name); + + if let (Some(start_pos), Some(end_pos)) = (text.find(&start_tag), text.find(&end_tag)) { + if start_pos < end_pos { + let content_start = start_pos + start_tag.len(); + let content = &text[content_start..end_pos]; + return Some(content.trim().to_string()); + } + } + None + } + + fn format_user_prompt(original_code: &str, update_snippet: &str) -> String { + if let Some(code_content) = Self::extract_tag_content(update_snippet, "code") { + // Look for instruction tags which help provide hints + if let Some(instruction_content) = + Self::extract_tag_content(update_snippet, "instruction") + { + // Both code and instruction tags found + return format!( + "{}\n{}\n{}", + instruction_content, original_code, code_content + ); + } + // Only code tags found, no instruction + return format!( + "{}\n{}", + original_code, code_content + ); + } + format!( + "{}\n{}", + original_code, update_snippet + ) + } } impl EditorModelImpl for MorphLLMEditor { @@ -42,11 +81,8 @@ impl EditorModelImpl for MorphLLMEditor { // Create the client let client = Client::new(); - // Format the prompt as specified in the Python example - let user_prompt = format!( - "{}\n{}", - original_code, update_snippet - ); + // Parse update_snippet for and tags + let user_prompt = Self::format_user_prompt(original_code, update_snippet); // Prepare the request body for OpenAI-compatible API let body = json!({ @@ -99,6 +135,26 @@ impl EditorModelImpl for MorphLLMEditor { fn get_str_replace_description(&self) -> &'static str { "Use the edit_file to propose an edit to an existing file. This will be read by a less intelligent model, which will quickly apply the edit. You should make it clear what the edit is, while also minimizing the unchanged code you write. + + **IMPORTANT**: in the new_str parameter, you must also provide an `instruction` - a single sentence written in the first person describing what you are going to do for the sketched edit. + This instruction helps the less intelligent model understand and apply your edit correctly. + + Examples of good instructions: + - I am adding error handling to the user authentication function and removing the old authentication method + - The instruction should be specific enough to disambiguate any uncertainty in your edit. + + + The format for new_str should be like this example: + + + new code here you want to add + + + adding new code with error handling + + + provide this to new_str as a single string. + When writing the edit, you should specify each edit in sequence, with the special comment // ... existing code ... to represent unchanged code in between edited lines. For example: @@ -117,3 +173,140 @@ impl EditorModelImpl for MorphLLMEditor { " } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_tag_content_valid() { + let text = "fn main() {}"; + let result = MorphLLMEditor::extract_tag_content(text, "code"); + assert_eq!(result, Some("fn main() {}".to_string())); + } + + #[test] + fn test_extract_tag_content_with_whitespace() { + let text = " I am adding a print statement "; + let result = MorphLLMEditor::extract_tag_content(text, "instruction"); + assert_eq!(result, Some("I am adding a print statement".to_string())); + } + + #[test] + fn test_extract_tag_content_invalid_order() { + let text = "Invalid"; + let result = MorphLLMEditor::extract_tag_content(text, "code"); + assert_eq!(result, None); + } + + #[test] + fn test_extract_tag_content_missing_end_tag() { + let text = "fn main() {}"; + let result = MorphLLMEditor::extract_tag_content(text, "code"); + assert_eq!(result, None); + } + + #[test] + fn test_extract_tag_content_missing_start_tag() { + let text = "fn main() {}"; + let result = MorphLLMEditor::extract_tag_content(text, "code"); + assert_eq!(result, None); + } + + #[test] + fn test_extract_tag_content_nested_tags() { + let text = "fn main() { nested }"; + let result = MorphLLMEditor::extract_tag_content(text, "code"); + assert_eq!(result, Some("fn main() { nested".to_string())); + } + + #[test] + fn test_format_user_prompt_no_tags() { + let original_code = "fn main() {}"; + let update_snippet = "Add error handling"; + let result = MorphLLMEditor::format_user_prompt(original_code, update_snippet); + assert_eq!( + result, + "fn main() {}\nAdd error handling" + ); + } + + #[test] + fn test_format_user_prompt_with_code_tags_only() { + let original_code = "fn main() {}"; + let update_snippet = "fn main() { println!(\"Hello\"); }"; + let result = MorphLLMEditor::format_user_prompt(original_code, update_snippet); + assert_eq!( + result, + "fn main() {}\nfn main() { println!(\"Hello\"); }" + ); + } + + #[test] + fn test_format_user_prompt_with_both_tags() { + let original_code = "fn main() {}"; + let update_snippet = "fn main() { println!(\"Hello\"); }I am adding a print statement"; + let result = MorphLLMEditor::format_user_prompt(original_code, update_snippet); + assert_eq!( + result, + "I am adding a print statement\nfn main() {}\nfn main() { println!(\"Hello\"); }" + ); + } + + #[test] + fn test_format_user_prompt_with_whitespace() { + let original_code = "fn main() {}"; + let update_snippet = " fn main() { println!(\"Hello\"); } I am adding a print statement "; + let result = MorphLLMEditor::format_user_prompt(original_code, update_snippet); + assert_eq!( + result, + "I am adding a print statement\nfn main() {}\nfn main() { println!(\"Hello\"); }" + ); + } + + #[test] + fn test_format_user_prompt_invalid_code_tags() { + let original_code = "fn main() {}"; + let update_snippet = "Invalid"; + let result = MorphLLMEditor::format_user_prompt(original_code, update_snippet); + assert_eq!( + result, + "fn main() {}\nInvalid" + ); + } + + #[test] + fn test_format_user_prompt_invalid_instruction_tags() { + let original_code = "fn main() {}"; + let update_snippet = + "fn main() { println!(\"Hello\"); }Invalid"; + let result = MorphLLMEditor::format_user_prompt(original_code, update_snippet); + assert_eq!( + result, + "fn main() {}\nfn main() { println!(\"Hello\"); }" + ); + } + + #[test] + fn test_format_user_prompt_nested_tags() { + let original_code = "fn main() {}"; + let update_snippet = "fn main() { nested }"; + let result = MorphLLMEditor::format_user_prompt(original_code, update_snippet); + // Should use the first occurrence of and find its matching + assert_eq!( + result, + "fn main() {}\nfn main() { nested" + ); + } + + #[test] + fn test_format_user_prompt_tags_in_different_order() { + let original_code = "fn main() {}"; + let update_snippet = "I am adding a print statementfn main() { println!(\"Hello\"); }"; + let result = MorphLLMEditor::format_user_prompt(original_code, update_snippet); + assert_eq!( + result, + "I am adding a print statement\nfn main() {}\nfn main() { println!(\"Hello\"); }" + ); + } +} diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index 7674a642a24d..9f607db9ce03 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -270,11 +270,13 @@ impl DeveloperRouter { To use the write command, you must specify `file_text` which will become the new content of the file. Be careful with existing files! This is a full overwrite, so you must include everything - not just sections you are modifying. - - To use the edit_file command, you must specify both `old_str` and `new_str` - {}. - + To use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning) and `new_str` (the text to insert). + + To use the edit_file command, you must specify both `old_str` and `new_str` + {} + "#, editor.get_str_replace_description()}, "edit_file", )