feat: better support for gpt 4.1 with per model prompts (#2201)

This commit is contained in:
Michael Neale
2025-04-17 11:39:41 +10:00
committed by GitHub
parent e073e32178
commit 9524bc8b8d
5 changed files with 212 additions and 6 deletions
+10 -3
View File
@@ -609,9 +609,16 @@ impl Agent {
pub async fn create_recipe(&self, mut messages: Vec<Message>) -> Result<Recipe> {
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?;
+93
View File
@@ -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<ExtensionInfo>,
frontend_instructions: Option<String>,
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<String> =
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"
);
}
}
+13 -3
View File
@@ -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| {
@@ -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 users 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 users 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., <http://example.com/>).
- 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.
+35
View File
@@ -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<Mutex<Option<String>>> = 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<String> {
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()));
}
}