diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index ea108e3fb9f0..e9afbed1fe2e 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -340,30 +340,22 @@ pub trait Provider: Send + Sync { } } - /// Generate a session name/description based on the conversation history - /// This method can be overridden by providers to implement custom session naming strategies. - /// The default implementation creates a prompt asking for a concise description in 4 words or less. - async fn generate_session_name(&self, messages: &[Message]) -> Result { - // Create a prompt for a concise description - let mut description_prompt = "Based on the conversation so far, provide a concise description of this session in 4 words or less. This will be used for finding the session later in a UI with limited space - reply *ONLY* with the description".to_string(); - - // Get context from the first 3 user messages - let context: Vec = messages + /// Returns the first 3 user messages as strings for session naming + fn get_initial_user_messages(&self, messages: &[Message]) -> Vec { + messages .iter() .filter(|m| m.role == rmcp::model::Role::User) .take(3) .map(|m| m.as_concat_text()) - .collect(); - - if !context.is_empty() { - description_prompt = format!( - "Here are the first few user messages:\n{}\n\n{}", - context.join("\n"), - description_prompt - ); - } + .collect() + } - let message = Message::user().with_text(&description_prompt); + /// Generate a session name/description based on the conversation history + /// Creates a prompt asking for a concise description in 4 words or less. + async fn generate_session_name(&self, messages: &[Message]) -> Result { + let context = self.get_initial_user_messages(messages); + let prompt = self.create_session_name_prompt(&context); + let message = Message::user().with_text(&prompt); let result = self .complete( "Reply with only a description in four words or less", @@ -373,13 +365,23 @@ pub trait Provider: Send + Sync { .await?; let description = result.0.as_concat_text(); - let sanitized_description = if description.chars().count() > 100 { - safe_truncate(&description, 100) - } else { - description - }; - Ok(sanitized_description) + Ok(safe_truncate(&description, 100)) + } + + // Generate a prompt for a session name based on the conversation history + fn create_session_name_prompt(&self, context: &[String]) -> String { + // Create a prompt for a concise description + let mut prompt = "Based on the conversation so far, provide a concise description of this session in 4 words or less. This will be used for finding the session later in a UI with limited space - reply *ONLY* with the description".to_string(); + + if !context.is_empty() { + prompt = format!( + "Here are the first few user messages:\n{}\n\n{}", + context.join("\n"), + prompt + ); + } + prompt } } diff --git a/crates/goose/src/providers/ollama.rs b/crates/goose/src/providers/ollama.rs index 4001ee857315..25fd1303fc44 100644 --- a/crates/goose/src/providers/ollama.rs +++ b/crates/goose/src/providers/ollama.rs @@ -4,8 +4,10 @@ use super::utils::{get_model, handle_response_openai_compat}; use crate::message::Message; use crate::model::ModelConfig; use crate::providers::formats::openai::{create_request, get_usage, response_to_message}; +use crate::utils::safe_truncate; use anyhow::Result; use async_trait::async_trait; +use regex::Regex; use reqwest::Client; use rmcp::model::Tool; use serde_json::Value; @@ -154,4 +156,67 @@ impl Provider for OllamaProvider { super::utils::emit_debug_trace(&self.model, &payload, &response, &usage); Ok((message, ProviderUsage::new(model, usage))) } + + /// Generate a session name based on the conversation history + /// This override filters out reasoning tokens that some Ollama models produce + async fn generate_session_name(&self, messages: &[Message]) -> Result { + let context = self.get_initial_user_messages(messages); + let message = Message::user().with_text(self.create_session_name_prompt(&context)); + let result = self + .complete( + "You are a title generator. Output only the requested title of 4 words or less, with no additional text, reasoning, or explanations.", + &[message], + &[], + ) + .await?; + + let mut description = result.0.as_concat_text(); + description = Self::filter_reasoning_tokens(&description); + + Ok(safe_truncate(&description, 100)) + } +} + +impl OllamaProvider { + /// Filter out reasoning tokens and thinking patterns from model responses + fn filter_reasoning_tokens(text: &str) -> String { + let mut filtered = text.to_string(); + + // Remove common reasoning patterns + let reasoning_patterns = [ + r".*?", + r".*?", + r"Let me think.*?\n", + r"I need to.*?\n", + r"First, I.*?\n", + r"Okay, .*?\n", + r"So, .*?\n", + r"Well, .*?\n", + r"Hmm, .*?\n", + r"Actually, .*?\n", + r"Based on.*?I think", + r"Looking at.*?I would say", + ]; + + for pattern in reasoning_patterns { + if let Ok(re) = Regex::new(pattern) { + filtered = re.replace_all(&filtered, "").to_string(); + } + } + // Remove any remaining thinking markers + filtered = filtered + .replace("", "") + .replace("", "") + .replace("", "") + .replace("", ""); + // Clean up extra whitespace + filtered = filtered + .lines() + .map(|line| line.trim()) + .filter(|line| !line.is_empty()) + .collect::>() + .join(" "); + + filtered + } }