feat: add /plan command in CLI to invoke reasoner with plan system prompt (#1616)

This commit is contained in:
Salman Mohammed
2025-03-20 10:10:01 -04:00
committed by GitHub
parent 3a4866cb7d
commit e273f8ebce
10 changed files with 369 additions and 51 deletions
+187 -11
View File
@@ -6,6 +6,7 @@ mod prompt;
mod thinking;
pub use builder::build_session;
use goose::providers::base::Provider;
pub use goose::session::Identifier;
use anyhow::Result;
@@ -28,6 +29,11 @@ use std::sync::Arc;
use std::time::Instant;
use tokio;
pub enum RunMode {
Normal,
Plan,
}
pub struct Session {
agent: Box<dyn Agent>,
messages: Vec<Message>,
@@ -35,6 +41,7 @@ pub struct Session {
// Cache for completion data - using std::sync for thread safety without async
completion_cache: Arc<std::sync::RwLock<CompletionCache>>,
debug: bool, // New field for debug mode
run_mode: RunMode,
}
// Cache structure for completion data
@@ -54,6 +61,42 @@ impl CompletionCache {
}
}
pub enum PlannerResponseType {
Plan,
ClarifyingQuestions,
}
/// Decide if the planner's reponse is a plan or a clarifying question
///
/// This function is called after the planner has generated a response
/// to the user's message. The response is either a plan or a clarifying
/// question.
pub async fn classify_planner_response(
message_text: String,
provider: Arc<Box<dyn Provider>>,
) -> Result<PlannerResponseType> {
let prompt = format!("The text below is the output from an AI model which can either provide a plan or list of clarifying questions. Based on the text below, decide if the output is a \"plan\" or \"clarifying questions\".\n---\n{message_text}");
// Generate the description
let message = Message::user().with_text(&prompt);
let (result, _usage) = provider
.complete(
"Reply only with the classification label: \"plan\" or \"clarifying questions\"",
&[message],
&[],
)
.await?;
// println!("classify_planner_response: {result:?}\n"); // TODO: remove
let predicted = result.as_concat_text();
if predicted.to_lowercase().contains("plan") {
Ok(PlannerResponseType::Plan)
} else {
Ok(PlannerResponseType::ClarifyingQuestions)
}
}
impl Session {
pub fn new(agent: Box<dyn Agent>, session_file: PathBuf, debug: bool) -> Self {
let messages = match session::read_messages(&session_file) {
@@ -70,6 +113,7 @@ impl Session {
session_file,
completion_cache: Arc::new(std::sync::RwLock::new(CompletionCache::new())),
debug,
run_mode: RunMode::Normal,
}
}
@@ -265,20 +309,35 @@ impl Session {
loop {
match input::get_input(&mut editor)? {
input::InputResult::Message(content) => {
save_history(&mut editor);
match self.run_mode {
RunMode::Normal => {
save_history(&mut editor);
self.messages.push(Message::user().with_text(&content));
self.messages.push(Message::user().with_text(&content));
// Get the provider from the agent for description generation
let provider = self.agent.provider().await;
// Get the provider from the agent for description generation
let provider = self.agent.provider().await;
// Persist messages with provider for automatic description generation
session::persist_messages(&self.session_file, &self.messages, Some(provider))
.await?;
// Persist messages with provider for automatic description generation
session::persist_messages(
&self.session_file,
&self.messages,
Some(provider),
)
.await?;
output::show_thinking();
self.process_agent_response(true).await?;
output::hide_thinking();
output::show_thinking();
self.process_agent_response(true).await?;
output::hide_thinking();
}
RunMode::Plan => {
let mut plan_messages = self.messages.clone();
plan_messages.push(Message::user().with_text(&content));
let reasoner = get_reasoner()?;
self.plan_with_reasoner_model(plan_messages, reasoner)
.await?;
}
}
}
input::InputResult::Exit => break,
input::InputResult::AddExtension(cmd) => {
@@ -345,7 +404,27 @@ impl Session {
config
.set_param("GOOSE_MODE", Value::String(mode.to_string()))
.unwrap();
println!("Goose mode set to '{}'", mode);
output::goose_mode_message(&format!("Goose mode set to '{}'", mode));
continue;
}
input::InputResult::Plan(options) => {
self.run_mode = RunMode::Plan;
output::render_enter_plan_mode();
let message_text = options.message_text;
if message_text.is_empty() {
continue;
}
let mut plan_messages = self.messages.clone();
plan_messages.push(Message::user().with_text(&message_text));
let reasoner = get_reasoner()?;
self.plan_with_reasoner_model(plan_messages, reasoner)
.await?;
}
input::InputResult::EndPlan => {
self.run_mode = RunMode::Normal;
output::render_exit_plan_mode();
continue;
}
input::InputResult::PromptCommand(opts) => {
@@ -419,6 +498,72 @@ impl Session {
Ok(())
}
async fn plan_with_reasoner_model(
&mut self,
plan_messages: Vec<Message>,
reasoner: Box<dyn Provider + Send + Sync>,
) -> Result<(), anyhow::Error> {
let plan_prompt = self.agent.get_plan_prompt().await?;
output::show_thinking();
let (plan_response, _usage) = reasoner.complete(&plan_prompt, &plan_messages, &[]).await?;
output::render_message(&plan_response, self.debug);
output::hide_thinking();
let planner_response_type =
classify_planner_response(plan_response.as_concat_text(), self.agent.provider().await)
.await?;
match planner_response_type {
PlannerResponseType::Plan => {
println!();
let should_act =
cliclack::confirm("Do you want to clear message history & act on this plan?")
.initial_value(true)
.interact()?;
if should_act {
output::render_act_on_plan();
self.run_mode = RunMode::Normal;
// set goose mode: auto if that isn't already the case
let config = Config::global();
let curr_goose_mode =
config.get_param("GOOSE_MODE").unwrap_or("auto".to_string());
if curr_goose_mode != "auto" {
config
.set_param("GOOSE_MODE", Value::String("auto".to_string()))
.unwrap();
}
// clear the messages before acting on the plan
self.messages.clear();
// add the plan response as a user message
let plan_message = Message::user().with_text(plan_response.as_concat_text());
self.messages.push(plan_message);
// act on the plan
output::show_thinking();
self.process_agent_response(true).await?;
output::hide_thinking();
// Reset run & goose mode
if curr_goose_mode != "auto" {
config
.set_param("GOOSE_MODE", Value::String(curr_goose_mode.to_string()))
.unwrap();
}
} else {
// add the plan response (assistant message) & carry the conversation forward
// in the next round, the user might wanna slightly modify the plan
self.messages.push(plan_response);
}
}
PlannerResponseType::ClarifyingQuestions => {
// add the plan response (assistant message) & carry the conversation forward
// in the next round, the user will answer the clarifying questions
self.messages.push(plan_response);
}
}
Ok(())
}
/// Process a single message and exit
pub async fn headless(&mut self, message: String) -> Result<()> {
self.process_message(message).await
@@ -650,3 +795,34 @@ impl Session {
Ok(metadata.total_tokens)
}
}
fn get_reasoner() -> Result<Box<dyn Provider + Send + Sync>, anyhow::Error> {
use goose::model::ModelConfig;
use goose::providers::create;
let (reasoner_provider, reasoner_model) = match (
std::env::var("GOOSE_PLANNER_PROVIDER"),
std::env::var("GOOSE_PLANNER_MODEL"),
) {
(Ok(provider), Ok(model)) => (provider, model),
_ => {
println!(
"WARNING: GOOSE_PLANNER_PROVIDER or GOOSE_PLANNER_MODEL is not set. \
Using default model from config..."
);
let config = Config::global();
let provider = config
.get_param("GOOSE_PROVIDER")
.expect("No provider configured. Run 'goose configure' first");
let model = config
.get_param("GOOSE_MODEL")
.expect("No model configured. Run 'goose configure' first");
(provider, model)
}
};
let model_config = ModelConfig::new(reasoner_model);
let reasoner = create(&reasoner_provider, model_config)?;
Ok(reasoner)
}