Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
45 changes: 25 additions & 20 deletions crates/goose/src/providers/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,26 +344,8 @@ pub trait Provider: Send + Sync {
/// 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
.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
);
}

let message = Message::user().with_text(&description_prompt);
let prompt = self.create_session_name_prompt(messages);
let message = Message::user().with_text(&prompt);
let result = self
.complete(
"Reply with only a description in four words or less",
Expand All @@ -381,6 +363,29 @@ pub trait Provider: Send + Sync {

Ok(sanitized_description)
}

// Generate a prompt for a session name based on the conversation history
fn create_session_name_prompt(&self, messages: &[Message]) -> 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();

// Get context from the first 3 user messages
let context: Vec<String> = messages
.iter()
.filter(|m| m.role == rmcp::model::Role::User)
.take(3)
.map(|m| m.as_concat_text())
.collect();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we pass this in, instead of messages? the outer callers are only going to call us with the first three messages anyway (which I guess also means we should probably not cut at 3 here at all)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure.


if !context.is_empty() {
prompt = format!(
"Here are the first few user messages:\n{}\n\n{}",
context.join("\n"),
prompt
);
}
return prompt;
}
}

/// A message stream yields partial text content but complete tool calls, all within the Message object
Expand Down
75 changes: 75 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,77 @@ 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 message = Message::user().with_text(&self.create_session_name_prompt(messages));
let result = self
.complete(
"You are a title generator. Output only the requested title with no additional text, reasoning, or explanations.",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we tell it to keep it to 4 words here too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we tell it in the prompt already. I can also add it here.

&[message],
&[],
)
.await?;

let mut description = result.0.as_concat_text();
description = Self::filter_reasoning_tokens(&description);

let sanitized_description = if description.chars().count() > 100 {
safe_truncate(&description, 100)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safe_truncate already does check for length, so need to do that here. good to use chars().count) though

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh looks like we do that in the original code too. Let me clean both up

} else {
description
};

// If the description is too long, the model failed to give us a short description, provide a fallback instead
if sanitized_description.split_whitespace().count() > 6 {
Comment thread
angelahning marked this conversation as resolved.
Outdated
Ok("Ollama Chat Session".to_string())
} else {
Ok(sanitized_description)
}
}
}

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