Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 27 additions & 25 deletions crates/goose/src/providers/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, ProviderError> {
// 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<String> = messages
/// Returns the first 3 user messages as strings for session naming
fn get_initial_user_messages(&self, messages: &[Message]) -> Vec<String> {
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<String, ProviderError> {
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",
Expand All @@ -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
}
}

Expand Down
65 changes: 65 additions & 0 deletions crates/goose/src/providers/ollama.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, ProviderError> {
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"<think>.*?</think>",
r"<thinking>.*?</thinking>",
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("<think>", "")
.replace("</think>", "")
.replace("<thinking>", "")
.replace("</thinking>", "");
// Clean up extra whitespace
filtered = filtered
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join(" ");

filtered
}
}
Loading