From 9524bc8b8d534653371e7a6a020943c39744d041 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 17 Apr 2025 11:39:41 +1000 Subject: [PATCH] feat: better support for gpt 4.1 with per model prompts (#2201) --- crates/goose/src/agents/agent.rs | 13 ++- crates/goose/src/agents/prompt_manager.rs | 93 ++++++++++++++++++++++ crates/goose/src/agents/reply_parts.rs | 16 +++- crates/goose/src/prompts/system_gpt_4.1.md | 61 ++++++++++++++ crates/goose/src/providers/base.rs | 35 ++++++++ 5 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 crates/goose/src/prompts/system_gpt_4.1.md diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 0b876bfb..f6a4d088 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -609,9 +609,16 @@ impl Agent { pub async fn create_recipe(&self, mut messages: Vec) -> Result { let extension_manager = self.extension_manager.lock().await; let extensions_info = extension_manager.get_extensions_info().await; - let system_prompt = self - .prompt_manager - .build_system_prompt(extensions_info, self.frontend_instructions.clone()); + + // Get model name from provider + let model_config = self.provider.get_model_config(); + let model_name = &model_config.model_name; + + let system_prompt = self.prompt_manager.build_system_prompt( + extensions_info, + self.frontend_instructions.clone(), + Some(model_name), + ); let recipe_prompt = self.prompt_manager.get_recipe_prompt().await; let tools = extension_manager.get_prefixed_tools(None).await?; diff --git a/crates/goose/src/agents/prompt_manager.rs b/crates/goose/src/agents/prompt_manager.rs index 76b511b9..62663ae9 100644 --- a/crates/goose/src/agents/prompt_manager.rs +++ b/crates/goose/src/agents/prompt_manager.rs @@ -3,6 +3,7 @@ use serde_json::Value; use std::collections::HashMap; use crate::agents::extension::ExtensionInfo; +use crate::providers::base::get_current_model; use crate::{config::Config, prompt_template}; pub struct PromptManager { @@ -34,6 +35,25 @@ impl PromptManager { self.system_prompt_override = Some(template); } + /// Normalize a model name (replace - and / with _, lower case) + fn normalize_model_name(name: &str) -> String { + name.replace(['-', '/', '.'], "_").to_lowercase() + } + + /// Map model (normalized) to prompt filenames; returns filename if a key is contained in the normalized model + fn model_prompt_map(model: &str) -> &'static str { + let mut map = HashMap::new(); + map.insert("gpt_4_1", "system_gpt_4_1.md"); + // Add more mappings as needed + let norm_model = Self::normalize_model_name(model); + for (key, val) in &map { + if norm_model.contains(key) { + return val; + } + } + "system.md" + } + /// Build the final system prompt /// /// * `extensions_info` – extension information for each extension/MCP @@ -42,6 +62,7 @@ impl PromptManager { &self, extensions_info: Vec, frontend_instructions: Option, + model_name: Option<&str>, ) -> String { let mut context: HashMap<&str, Value> = HashMap::new(); let mut extensions_info = extensions_info.clone(); @@ -60,10 +81,25 @@ impl PromptManager { let current_date_time = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(); context.insert("current_date_time", Value::String(current_date_time)); + // First check the global store, and only if it's not available, fall back to the provided model_name + let model_to_use: Option = + get_current_model().or_else(|| model_name.map(|s| s.to_string())); + // Conditionally load the override prompt or the global system prompt let base_prompt = if let Some(override_prompt) = &self.system_prompt_override { prompt_template::render_inline_once(override_prompt, &context) .expect("Prompt should render") + } else if let Some(model) = &model_to_use { + // Use the fuzzy mapping to determine the prompt file, or fall back to legacy logic + let prompt_file = Self::model_prompt_map(model); + match prompt_template::render_global_file(prompt_file, &context) { + Ok(prompt) => prompt, + Err(_) => { + // Fall back to the standard system.md if model-specific one doesn't exist + prompt_template::render_global_file("system.md", &context) + .expect("Prompt should render") + } + } } else { prompt_template::render_global_file("system.md", &context) .expect("Prompt should render") @@ -99,3 +135,60 @@ impl PromptManager { prompt_template::render_global_file("recipe.md", &context).expect("Prompt should render") } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_normalize_model_name() { + assert_eq!(PromptManager::normalize_model_name("gpt-4.1"), "gpt_4_1"); + assert_eq!(PromptManager::normalize_model_name("gpt/3.5"), "gpt_3_5"); + assert_eq!( + PromptManager::normalize_model_name("GPT-3.5/PLUS"), + "gpt_3_5_plus" + ); + } + + #[test] + fn test_model_prompt_map_matches() { + // should match prompts based on contained normalized keys + assert_eq!( + PromptManager::model_prompt_map("gpt-4.1"), + "system_gpt_4_1.md" + ); + + assert_eq!( + PromptManager::model_prompt_map("gpt-4.1-2025-04-14"), + "system_gpt_4_1.md" + ); + + assert_eq!( + PromptManager::model_prompt_map("openai/gpt-4.1"), + "system_gpt_4_1.md" + ); + assert_eq!( + PromptManager::model_prompt_map("goose-gpt-4-1"), + "system_gpt_4_1.md" + ); + assert_eq!( + PromptManager::model_prompt_map("gpt-4-1-huge"), + "system_gpt_4_1.md" + ); + } + + #[test] + fn test_model_prompt_map_none() { + // should return system.md for unrecognized/unsupported model names + assert_eq!(PromptManager::model_prompt_map("llama-3-70b"), "system.md"); + assert_eq!(PromptManager::model_prompt_map("goose"), "system.md"); + assert_eq!( + PromptManager::model_prompt_map("claude-3.7-sonnet"), + "system.md" + ); + assert_eq!( + PromptManager::model_prompt_map("xxx-unknown-model"), + "system.md" + ); + } +} diff --git a/crates/goose/src/agents/reply_parts.rs b/crates/goose/src/agents/reply_parts.rs index 6c11885a..e5a95647 100644 --- a/crates/goose/src/agents/reply_parts.rs +++ b/crates/goose/src/agents/reply_parts.rs @@ -29,9 +29,16 @@ impl Agent { // Prepare system prompt let extension_manager = self.extension_manager.lock().await; let extensions_info = extension_manager.get_extensions_info().await; - let mut system_prompt = self - .prompt_manager - .build_system_prompt(extensions_info, self.frontend_instructions.clone()); + + // Get model name from provider + let model_config = self.provider.get_model_config(); + let model_name = &model_config.model_name; + + let mut system_prompt = self.prompt_manager.build_system_prompt( + extensions_info, + self.frontend_instructions.clone(), + Some(model_name), + ); // Handle toolshim if enabled let mut toolshim_tools = vec![]; @@ -83,6 +90,9 @@ impl Agent { // Call the provider to get a response let (mut response, usage) = provider.complete(system_prompt, messages, tools).await?; + // Store the model information in the global store + crate::providers::base::set_current_model(&usage.model); + // Post-process / structure the response only if tool interpretation is enabled if config.toolshim { let interpreter = OllamaInterpreter::new().map_err(|e| { diff --git a/crates/goose/src/prompts/system_gpt_4.1.md b/crates/goose/src/prompts/system_gpt_4.1.md new file mode 100644 index 00000000..43af3f23 --- /dev/null +++ b/crates/goose/src/prompts/system_gpt_4.1.md @@ -0,0 +1,61 @@ +You are a general-purpose AI agent called Goose, created by Block, the parent company of Square, CashApp, and Tidal. Goose is being developed as an open-source software project. + +IMPORTANT INSTRUCTIONS: + +Please keep going until the user’s query is completely resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the problem is solved. + +If you are not sure about file content or codebase structure, or other information pertaining to the user’s request, use your tools to read files and gather the relevant information: do NOT guess or make up an answer. It is important you use tools that can assist with providing the right context. + +CRITIAL: The str_replace command in the text_editor tool (when available) should be used most of the time, with the write tool only for new files. ALWAYS check the content of the file before editing. NEVER overwrite the whole content of a file unless directed to, always edit carefully by adding and changing content. Never leave content unfinished with comments like "rest of the file here" + +The user may direct or imply that you are to take actions, in this case, it is important to note the following guidelines: + +* If you are directed to complete a task, you should see it through. +* Your thinking should be thorough and so it's fine if it's very long. You can think step by step before and after each action you decide to take. +* Only terminate your turn when you are sure that the problem is solved. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn. +* You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully. +* Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Your solution must be perfect. If not, continue working on it. When you are validating solutions with tools, it is important to iterate until you get success +* Do not stop and ask the user for confirmation for actions you should be taking to achieve the outcomes directed and with tools available. + + + +The current date is {{current_date_time}}. + +Goose uses LLM providers with tool calling capability. +Your model may have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10 months prior to the current date. + +# Extensions + +Extensions allow other applications to provide context to Goose. Extensions connect Goose to different data sources and tools. +You are capable of dynamically plugging into new extensions and learning how to use them. You solve higher level problems using the tools in these extensions, and can interact with multiple at once. +Use the search_available_extensions tool to find additional extensions to enable to help with your task. To enable extensions, use the enable_extension tool and provide the extension_name. You should only enable extensions found from the search_available_extensions tool. + +{% if (extensions is defined) and extensions %} +Because you dynamically load extensions, your conversation history may refer +to interactions with extensions that are not currently active. The currently +active extensions are below. Each of these extensions provides tools that are +in your tool specification. + +{% for extension in extensions %} +## {{extension.name}} +{% if extension.has_resources %} +{{extension.name}} supports resources, you can use platform__read_resource, +and platform__list_resources on this extension. +{% endif %} +{% if extension.instructions %}### Instructions +{{extension.instructions}}{% endif %} +{% endfor %} + +{% else %} +No extensions are defined. You should let the user know that they should add extensions. +{% endif %} + +# Response Guidelines + +- Use Markdown formatting for all responses. +- Follow best practices for Markdown, including: + - Using headers for organization. + - Bullet points for lists. + - Links formatted correctly, either as linked text (e.g., [this is linked text](https://example.com)) or automatic links using angle brackets (e.g., ). +- For code examples, use fenced code blocks by placing triple backticks (` ``` `) before and after the code. Include the language identifier after the opening backticks (e.g., ` ```python `) to enable syntax highlighting. +- Ensure clarity, conciseness, and proper formatting to enhance readability and usability. diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index a6f8dd9f..f0771455 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -7,6 +7,24 @@ use crate::model::ModelConfig; use mcp_core::tool::Tool; use utoipa::ToSchema; +use once_cell::sync::Lazy; +use std::sync::Mutex; + +/// A global store for the current model being used, we use this as when a provider returns, it tells us the real model, not an alias +pub static CURRENT_MODEL: Lazy>> = Lazy::new(|| Mutex::new(None)); + +/// Set the current model in the global store +pub fn set_current_model(model: &str) { + if let Ok(mut current_model) = CURRENT_MODEL.lock() { + *current_model = Some(model.to_string()); + } +} + +/// Get the current model from the global store, the real model, not an alias +pub fn get_current_model() -> Option { + CURRENT_MODEL.lock().ok().and_then(|model| model.clone()) +} + /// Metadata about a provider's configuration requirements and capabilities #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ProviderMetadata { @@ -179,4 +197,21 @@ mod tests { Ok(()) } + + #[test] + fn test_set_and_get_current_model() { + // Set the model + set_current_model("gpt-4o"); + + // Get the model and verify + let model = get_current_model(); + assert_eq!(model, Some("gpt-4o".to_string())); + + // Change the model + set_current_model("claude-3.5-sonnet"); + + // Get the updated model and verify + let model = get_current_model(); + assert_eq!(model, Some("claude-3.5-sonnet".to_string())); + } }