mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
More slash commands (#5858)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -31,7 +31,7 @@ use anyhow::{Context, Result};
|
||||
use completion::GooseCompleter;
|
||||
use goose::agents::extension::{Envs, ExtensionConfig, PLATFORM_EXTENSIONS};
|
||||
use goose::agents::types::RetryConfig;
|
||||
use goose::agents::{Agent, SessionConfig, MANUAL_COMPACT_TRIGGERS};
|
||||
use goose::agents::{Agent, SessionConfig, COMPACT_TRIGGERS};
|
||||
use goose::config::{Config, GooseMode};
|
||||
use goose::providers::pricing::initialize_pricing_cache;
|
||||
use goose::session::SessionManager;
|
||||
@@ -704,7 +704,7 @@ impl CliSession {
|
||||
};
|
||||
|
||||
if should_summarize {
|
||||
self.push_message(Message::user().with_text(MANUAL_COMPACT_TRIGGERS[0]));
|
||||
self.push_message(Message::user().with_text(COMPACT_TRIGGERS[0]));
|
||||
output::show_thinking();
|
||||
self.process_agent_response(true, CancellationToken::default())
|
||||
.await?;
|
||||
|
||||
@@ -18,7 +18,10 @@ use goose::providers::pricing::{
|
||||
get_all_pricing, get_model_pricing, parse_model_id, refresh_pricing,
|
||||
};
|
||||
use goose::providers::providers as get_providers;
|
||||
use goose::{agents::ExtensionConfig, config::permission::PermissionLevel, slash_commands};
|
||||
use goose::{
|
||||
agents::execute_commands, agents::ExtensionConfig, config::permission::PermissionLevel,
|
||||
slash_commands,
|
||||
};
|
||||
use http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -437,11 +440,15 @@ pub async fn get_slash_commands() -> Result<Json<SlashCommandsResponse>, StatusC
|
||||
command_type: CommandType::Recipe,
|
||||
})
|
||||
.collect();
|
||||
commands.push(SlashCommand {
|
||||
command: "compact".to_string(),
|
||||
help: "Compact the current conversation to save tokens".to_string(),
|
||||
command_type: CommandType::Builtin,
|
||||
});
|
||||
|
||||
for cmd_def in execute_commands::list_commands() {
|
||||
commands.push(SlashCommand {
|
||||
command: cmd_def.name.to_string(),
|
||||
help: cmd_def.description.to_string(),
|
||||
command_type: CommandType::Builtin,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Json(SlashCommandsResponse { commands }))
|
||||
}
|
||||
|
||||
|
||||
@@ -62,8 +62,6 @@ use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
const DEFAULT_MAX_TURNS: u32 = 1000;
|
||||
const COMPACTION_THINKING_TEXT: &str = "goose is compacting the conversation...";
|
||||
pub const MANUAL_COMPACT_TRIGGERS: &[&str] =
|
||||
&["Please compact this conversation", "/compact", "/summarize"];
|
||||
|
||||
/// Context needed for the reply function
|
||||
pub struct ReplyContext {
|
||||
@@ -777,50 +775,70 @@ impl Agent {
|
||||
}
|
||||
|
||||
let message_text = user_message.as_concat_text();
|
||||
let is_manual_compact = MANUAL_COMPACT_TRIGGERS.contains(&message_text.trim());
|
||||
|
||||
let slash_command_recipe = if message_text.trim().starts_with('/') {
|
||||
// Track custom slash command usage (don't track command name for privacy)
|
||||
if message_text.trim().starts_with('/') {
|
||||
let command = message_text.split_whitespace().next();
|
||||
|
||||
// Check if it's a builtin command first
|
||||
let is_builtin = command
|
||||
.map(|cmd| MANUAL_COMPACT_TRIGGERS.contains(&cmd))
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_builtin {
|
||||
None
|
||||
} else {
|
||||
// Try to resolve as recipe command
|
||||
let recipe = command.and_then(crate::slash_commands::resolve_slash_command);
|
||||
|
||||
// Track non-builtin slash command usage (don't track command name for privacy)
|
||||
if recipe.is_some() {
|
||||
if let Some(cmd) = command {
|
||||
if crate::slash_commands::get_recipe_for_command(cmd).is_some() {
|
||||
crate::posthog::emit_custom_slash_command_used();
|
||||
}
|
||||
|
||||
recipe
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(recipe) = slash_command_recipe {
|
||||
let prompt = [recipe.instructions.as_deref(), recipe.prompt.as_deref()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
let prompt_message = Message::user()
|
||||
.with_text(prompt)
|
||||
.with_visibility(false, true);
|
||||
SessionManager::add_message(&session_config.id, &prompt_message).await?;
|
||||
SessionManager::add_message(
|
||||
&session_config.id,
|
||||
&user_message.with_visibility(true, false),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
SessionManager::add_message(&session_config.id, &user_message).await?;
|
||||
let command_result = self
|
||||
.execute_command(&message_text, &session_config.id)
|
||||
.await;
|
||||
|
||||
match command_result {
|
||||
Some(response) if response.role == rmcp::model::Role::Assistant => {
|
||||
SessionManager::add_message(
|
||||
&session_config.id,
|
||||
&user_message.clone().with_visibility(true, false),
|
||||
)
|
||||
.await?;
|
||||
SessionManager::add_message(
|
||||
&session_config.id,
|
||||
&response.clone().with_visibility(true, false),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Check if this was a command that modifies conversation history
|
||||
let modifies_history = crate::agents::execute_commands::COMPACT_TRIGGERS
|
||||
.contains(&message_text.trim())
|
||||
|| message_text.trim() == "/clear";
|
||||
|
||||
return Ok(Box::pin(async_stream::try_stream! {
|
||||
yield AgentEvent::Message(user_message);
|
||||
yield AgentEvent::Message(response);
|
||||
|
||||
// After commands that modify history, notify UI that history was replaced
|
||||
if modifies_history {
|
||||
let updated_session = SessionManager::get_session(&session_config.id, true)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch updated session: {}", e))?;
|
||||
let updated_conversation = updated_session
|
||||
.conversation
|
||||
.ok_or_else(|| anyhow!("Session has no conversation after history modification"))?;
|
||||
yield AgentEvent::HistoryReplaced(updated_conversation);
|
||||
}
|
||||
}));
|
||||
}
|
||||
Some(resolved_message) => {
|
||||
SessionManager::add_message(
|
||||
&session_config.id,
|
||||
&user_message.clone().with_visibility(true, false),
|
||||
)
|
||||
.await?;
|
||||
SessionManager::add_message(
|
||||
&session_config.id,
|
||||
&resolved_message.clone().with_visibility(false, true),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
None => {
|
||||
SessionManager::add_message(&session_config.id, &user_message).await?;
|
||||
}
|
||||
}
|
||||
let session = SessionManager::get_session(&session_config.id, true).await?;
|
||||
let conversation = session
|
||||
@@ -828,40 +846,37 @@ impl Agent {
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("Session {} has no conversation", session_config.id))?;
|
||||
|
||||
let needs_auto_compact = !is_manual_compact
|
||||
&& check_if_compaction_needed(
|
||||
self.provider().await?.as_ref(),
|
||||
&conversation,
|
||||
None,
|
||||
&session,
|
||||
)
|
||||
.await?;
|
||||
let needs_auto_compact = check_if_compaction_needed(
|
||||
self.provider().await?.as_ref(),
|
||||
&conversation,
|
||||
None,
|
||||
&session,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let conversation_to_compact = conversation.clone();
|
||||
|
||||
Ok(Box::pin(async_stream::try_stream! {
|
||||
let final_conversation = if !needs_auto_compact && !is_manual_compact {
|
||||
let final_conversation = if !needs_auto_compact {
|
||||
conversation
|
||||
} else {
|
||||
if !is_manual_compact {
|
||||
let config = crate::config::Config::global();
|
||||
let threshold = config
|
||||
.get_param::<f64>("GOOSE_AUTO_COMPACT_THRESHOLD")
|
||||
.unwrap_or(DEFAULT_COMPACTION_THRESHOLD);
|
||||
let threshold_percentage = (threshold * 100.0) as u32;
|
||||
let config = Config::global();
|
||||
let threshold = config
|
||||
.get_param::<f64>("GOOSE_AUTO_COMPACT_THRESHOLD")
|
||||
.unwrap_or(DEFAULT_COMPACTION_THRESHOLD);
|
||||
let threshold_percentage = (threshold * 100.0) as u32;
|
||||
|
||||
let inline_msg = format!(
|
||||
"Exceeded auto-compact threshold of {}%. Performing auto-compaction...",
|
||||
threshold_percentage
|
||||
);
|
||||
let inline_msg = format!(
|
||||
"Exceeded auto-compact threshold of {}%. Performing auto-compaction...",
|
||||
threshold_percentage
|
||||
);
|
||||
|
||||
yield AgentEvent::Message(
|
||||
Message::assistant().with_system_notification(
|
||||
SystemNotificationType::InlineMessage,
|
||||
inline_msg,
|
||||
)
|
||||
);
|
||||
}
|
||||
yield AgentEvent::Message(
|
||||
Message::assistant().with_system_notification(
|
||||
SystemNotificationType::InlineMessage,
|
||||
inline_msg,
|
||||
)
|
||||
);
|
||||
|
||||
yield AgentEvent::Message(
|
||||
Message::assistant().with_system_notification(
|
||||
@@ -870,7 +885,7 @@ impl Agent {
|
||||
)
|
||||
);
|
||||
|
||||
match compact_messages(self.provider().await?.as_ref(), &conversation_to_compact, is_manual_compact).await {
|
||||
match compact_messages(self.provider().await?.as_ref(), &conversation_to_compact, false).await {
|
||||
Ok((compacted_conversation, summarization_usage)) => {
|
||||
SessionManager::replace_conversation(&session_config.id, &compacted_conversation).await?;
|
||||
Self::update_session_metrics(&session_config, &summarization_usage, true).await?;
|
||||
@@ -897,11 +912,9 @@ impl Agent {
|
||||
}
|
||||
};
|
||||
|
||||
if !is_manual_compact {
|
||||
let mut reply_stream = self.reply_internal(final_conversation, session_config, session, cancel_token).await?;
|
||||
while let Some(event) = reply_stream.next().await {
|
||||
yield event?;
|
||||
}
|
||||
let mut reply_stream = self.reply_internal(final_conversation, session_config, session, cancel_token).await?;
|
||||
while let Some(event) = reply_stream.next().await {
|
||||
yield event?;
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use crate::context_mgmt::compact_messages;
|
||||
use crate::conversation::message::{Message, SystemNotificationType};
|
||||
use crate::recipe::build_recipe::build_recipe_from_template_with_positional_params;
|
||||
use crate::session::SessionManager;
|
||||
|
||||
use super::Agent;
|
||||
|
||||
pub const COMPACT_TRIGGERS: &[&str] =
|
||||
&["/compact", "Please compact this conversation", "/summarize"];
|
||||
|
||||
pub struct CommandDef {
|
||||
pub name: &'static str,
|
||||
pub description: &'static str,
|
||||
}
|
||||
|
||||
static COMMANDS: &[CommandDef] = &[
|
||||
CommandDef {
|
||||
name: "prompts",
|
||||
description: "List available prompts, optionally filtered by extension",
|
||||
},
|
||||
CommandDef {
|
||||
name: "prompt",
|
||||
description: "Execute a prompt or show its info with --info",
|
||||
},
|
||||
CommandDef {
|
||||
name: "compact",
|
||||
description: "Compact the conversation history",
|
||||
},
|
||||
CommandDef {
|
||||
name: "clear",
|
||||
description: "Clear the conversation history",
|
||||
},
|
||||
];
|
||||
|
||||
pub fn list_commands() -> &'static [CommandDef] {
|
||||
COMMANDS
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
pub async fn execute_command(&self, message_text: &str, session_id: &str) -> Option<Message> {
|
||||
let mut trimmed = message_text.trim().to_string();
|
||||
|
||||
if COMPACT_TRIGGERS.contains(&trimmed.as_str()) {
|
||||
trimmed = COMPACT_TRIGGERS[0].to_string();
|
||||
}
|
||||
|
||||
if !trimmed.starts_with('/') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let command_str = trimmed.strip_prefix('/').unwrap_or(&trimmed);
|
||||
let (command, params) = command_str
|
||||
.split_once(' ')
|
||||
.map(|(cmd, p)| (cmd, p.trim()))
|
||||
.unwrap_or((command_str, ""));
|
||||
|
||||
let params: Vec<&str> = if params.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
params.split_whitespace().collect()
|
||||
};
|
||||
|
||||
let result = match command {
|
||||
"prompts" => self.handle_prompts_command(¶ms, session_id).await,
|
||||
"prompt" => self.handle_prompt_command(¶ms, session_id).await,
|
||||
"compact" => self.handle_compact_command(session_id).await,
|
||||
"clear" => self.handle_clear_command(session_id).await,
|
||||
_ => {
|
||||
self.handle_recipe_command(command, ¶ms, session_id)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
Some(Message::assistant().with_text(format!("Error executing /{}: {}", command, e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_compact_command(&self, session_id: &str) -> Result<Option<Message>> {
|
||||
let session = SessionManager::get_session(session_id, true).await?;
|
||||
let conversation = session
|
||||
.conversation
|
||||
.ok_or_else(|| anyhow!("Session has no conversation"))?;
|
||||
|
||||
let (compacted_conversation, _usage) = compact_messages(
|
||||
self.provider().await?.as_ref(),
|
||||
&conversation,
|
||||
true, // is_manual_compact
|
||||
)
|
||||
.await?;
|
||||
|
||||
SessionManager::replace_conversation(session_id, &compacted_conversation).await?;
|
||||
|
||||
Ok(Some(Message::assistant().with_system_notification(
|
||||
SystemNotificationType::InlineMessage,
|
||||
"Compaction complete",
|
||||
)))
|
||||
}
|
||||
|
||||
async fn handle_clear_command(&self, session_id: &str) -> Result<Option<Message>> {
|
||||
use crate::conversation::Conversation;
|
||||
|
||||
SessionManager::replace_conversation(session_id, &Conversation::default()).await?;
|
||||
|
||||
SessionManager::update_session(session_id)
|
||||
.total_tokens(Some(0))
|
||||
.input_tokens(Some(0))
|
||||
.output_tokens(Some(0))
|
||||
.apply()
|
||||
.await?;
|
||||
|
||||
Ok(Some(Message::assistant().with_system_notification(
|
||||
SystemNotificationType::InlineMessage,
|
||||
"Conversation cleared",
|
||||
)))
|
||||
}
|
||||
|
||||
async fn handle_prompts_command(
|
||||
&self,
|
||||
params: &[&str],
|
||||
_session_id: &str,
|
||||
) -> Result<Option<Message>> {
|
||||
let extension_filter = params.first().map(|s| s.to_string());
|
||||
|
||||
let prompts = self.list_extension_prompts().await;
|
||||
|
||||
if let Some(filter) = &extension_filter {
|
||||
if !prompts.contains_key(filter) {
|
||||
let error_msg = format!("Extension '{}' not found", filter);
|
||||
return Ok(Some(Message::assistant().with_text(error_msg)));
|
||||
}
|
||||
}
|
||||
|
||||
let filtered_prompts: HashMap<String, Vec<String>> = prompts
|
||||
.into_iter()
|
||||
.filter(|(ext, _)| extension_filter.as_ref().is_none_or(|f| f == ext))
|
||||
.map(|(extension, prompt_list)| {
|
||||
let names = prompt_list.into_iter().map(|p| p.name).collect();
|
||||
(extension, names)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut output = String::new();
|
||||
if filtered_prompts.is_empty() {
|
||||
output.push_str("No prompts available.\n");
|
||||
} else {
|
||||
output.push_str("Available prompts:\n\n");
|
||||
for (extension, prompt_names) in filtered_prompts {
|
||||
output.push_str(&format!("**{}**:\n", extension));
|
||||
for name in prompt_names {
|
||||
output.push_str(&format!(" - {}\n", name));
|
||||
}
|
||||
output.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(Message::assistant().with_text(output)))
|
||||
}
|
||||
|
||||
async fn handle_prompt_command(
|
||||
&self,
|
||||
params: &[&str],
|
||||
session_id: &str,
|
||||
) -> Result<Option<Message>> {
|
||||
if params.is_empty() {
|
||||
return Ok(Some(
|
||||
Message::assistant().with_text("Prompt name argument is required"),
|
||||
));
|
||||
}
|
||||
|
||||
let prompt_name = params[0].to_string();
|
||||
let is_info = params.get(1).map(|s| *s == "--info").unwrap_or(false);
|
||||
|
||||
if is_info {
|
||||
let prompts = self.list_extension_prompts().await;
|
||||
let mut prompt_info = None;
|
||||
|
||||
for (extension, prompt_list) in prompts {
|
||||
if let Some(prompt) = prompt_list.iter().find(|p| p.name == prompt_name) {
|
||||
let mut output = format!("**Prompt: {}**\n\n", prompt.name);
|
||||
if let Some(desc) = &prompt.description {
|
||||
output.push_str(&format!("Description: {}\n\n", desc));
|
||||
}
|
||||
output.push_str(&format!("Extension: {}\n\n", extension));
|
||||
|
||||
if let Some(args) = &prompt.arguments {
|
||||
output.push_str("Arguments:\n");
|
||||
for arg in args {
|
||||
output.push_str(&format!(" - {}", arg.name));
|
||||
if let Some(desc) = &arg.description {
|
||||
output.push_str(&format!(": {}", desc));
|
||||
}
|
||||
output.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
prompt_info = Some(output);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(Some(Message::assistant().with_text(
|
||||
prompt_info.unwrap_or_else(|| format!("Prompt '{}' not found", prompt_name)),
|
||||
)));
|
||||
}
|
||||
|
||||
let mut arguments = HashMap::new();
|
||||
for param in params.iter().skip(1) {
|
||||
if let Some((key, value)) = param.split_once('=') {
|
||||
let value = value.trim_matches('"');
|
||||
arguments.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let arguments_value = serde_json::to_value(arguments)
|
||||
.map_err(|e| anyhow!("Failed to serialize arguments: {}", e))?;
|
||||
|
||||
match self.get_prompt(&prompt_name, arguments_value).await {
|
||||
Ok(prompt_result) => {
|
||||
for (i, prompt_message) in prompt_result.messages.into_iter().enumerate() {
|
||||
let msg = Message::from(prompt_message);
|
||||
|
||||
let expected_role = if i % 2 == 0 {
|
||||
rmcp::model::Role::User
|
||||
} else {
|
||||
rmcp::model::Role::Assistant
|
||||
};
|
||||
|
||||
if msg.role != expected_role {
|
||||
let error_msg = format!(
|
||||
"Expected {:?} message at position {}, but found {:?}",
|
||||
expected_role, i, msg.role
|
||||
);
|
||||
return Ok(Some(Message::assistant().with_text(error_msg)));
|
||||
}
|
||||
|
||||
SessionManager::add_message(session_id, &msg).await?;
|
||||
}
|
||||
|
||||
let last_message = SessionManager::get_session(session_id, true)
|
||||
.await?
|
||||
.conversation
|
||||
.ok_or_else(|| anyhow!("No conversation found"))?
|
||||
.messages()
|
||||
.last()
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("No messages in conversation"))?;
|
||||
|
||||
Ok(Some(last_message))
|
||||
}
|
||||
Err(e) => Ok(Some(
|
||||
Message::assistant().with_text(format!("Error getting prompt: {}", e)),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_recipe_command(
|
||||
&self,
|
||||
command: &str,
|
||||
params: &[&str],
|
||||
_session_id: &str,
|
||||
) -> Result<Option<Message>> {
|
||||
let full_command = format!("/{}", command);
|
||||
let recipe_path = match crate::slash_commands::get_recipe_for_command(&full_command) {
|
||||
Some(path) => path,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
if !recipe_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let recipe_content = std::fs::read_to_string(&recipe_path)
|
||||
.map_err(|e| anyhow!("Failed to read recipe file: {}", e))?;
|
||||
|
||||
let recipe_dir = recipe_path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("Recipe path has no parent directory"))?;
|
||||
|
||||
let param_values: Vec<String> = params.iter().map(|s| s.to_string()).collect();
|
||||
|
||||
let recipe = match build_recipe_from_template_with_positional_params(
|
||||
recipe_content,
|
||||
recipe_dir,
|
||||
param_values,
|
||||
None::<fn(&str, &str) -> Result<String>>,
|
||||
) {
|
||||
Ok(recipe) => recipe,
|
||||
Err(crate::recipe::build_recipe::RecipeError::MissingParams { parameters }) => {
|
||||
return Ok(Some(Message::assistant().with_text(format!(
|
||||
"Recipe requires {} parameter(s): {}. Provided: {}",
|
||||
parameters.len(),
|
||||
parameters.join(", "),
|
||||
params.len()
|
||||
))));
|
||||
}
|
||||
Err(e) => return Err(anyhow!("Failed to build recipe: {}", e)),
|
||||
};
|
||||
|
||||
let prompt = [recipe.instructions.as_deref(), recipe.prompt.as_deref()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
|
||||
Ok(Some(Message::user().with_text(prompt)))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod agent;
|
||||
pub(crate) mod chatrecall_extension;
|
||||
pub(crate) mod code_execution_extension;
|
||||
pub mod execute_commands;
|
||||
pub mod extension;
|
||||
pub mod extension_malware_check;
|
||||
pub mod extension_manager;
|
||||
@@ -27,7 +28,8 @@ mod tool_route_manager;
|
||||
mod tool_router_index_manager;
|
||||
pub mod types;
|
||||
|
||||
pub use agent::{Agent, AgentEvent, MANUAL_COMPACT_TRIGGERS};
|
||||
pub use agent::{Agent, AgentEvent};
|
||||
pub use execute_commands::COMPACT_TRIGGERS;
|
||||
pub use extension::ExtensionConfig;
|
||||
pub use extension_manager::ExtensionManager;
|
||||
pub use prompt_manager::PromptManager;
|
||||
|
||||
@@ -77,6 +77,41 @@ where
|
||||
Ok(recipe)
|
||||
}
|
||||
|
||||
pub fn build_recipe_from_template_with_positional_params<F>(
|
||||
recipe_content: String,
|
||||
recipe_dir: &Path,
|
||||
params: Vec<String>,
|
||||
user_prompt_fn: Option<F>,
|
||||
) -> Result<Recipe, RecipeError>
|
||||
where
|
||||
F: Fn(&str, &str) -> Result<String, anyhow::Error>,
|
||||
{
|
||||
let recipe_dir_str = recipe_dir.display().to_string();
|
||||
|
||||
let recipe_parameters =
|
||||
validate_recipe_template_from_content(&recipe_content, Some(recipe_dir_str.clone()))
|
||||
.map_err(|source| RecipeError::TemplateRendering { source })?
|
||||
.parameters;
|
||||
|
||||
let param_pairs: Vec<(String, String)> = if let Some(recipe_params) = &recipe_parameters {
|
||||
if params.len() < recipe_params.len() {
|
||||
let param_keys: Vec<String> = recipe_params.iter().map(|p| p.key.clone()).collect();
|
||||
return Err(RecipeError::MissingParams {
|
||||
parameters: param_keys,
|
||||
});
|
||||
}
|
||||
recipe_params
|
||||
.iter()
|
||||
.zip(params.iter())
|
||||
.map(|(rp, p)| (rp.key.clone(), p.clone()))
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
build_recipe_from_template(recipe_content, recipe_dir, param_pairs, user_prompt_fn)
|
||||
}
|
||||
|
||||
pub fn apply_values_to_parameters<F>(
|
||||
user_params: &[(String, String)],
|
||||
recipe_parameters: Option<Vec<RecipeParameter>>,
|
||||
|
||||
@@ -61,7 +61,7 @@ const TOKEN_LIMIT_DEFAULT = 128000; // fallback for custom models that the backe
|
||||
const TOOLS_MAX_SUGGESTED = 60; // max number of tools before we show a warning
|
||||
|
||||
// Manual compact trigger message - must match backend constant
|
||||
const MANUAL_COMPACT_TRIGGER = 'Please compact this conversation';
|
||||
const MANUAL_COMPACT_TRIGGER = '/compact';
|
||||
|
||||
interface ModelLimit {
|
||||
pattern: string;
|
||||
|
||||
Reference in New Issue
Block a user