Skip to content

feat(cli): Implement automatic naming for saved conversations #2233

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
183 changes: 183 additions & 0 deletions crates/chat-cli/benches/benchmarks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#![feature(test)]

extern crate test;
extern crate amazon_q_cli_auto_naming;

use test::Bencher;
use amazon_q_cli_auto_naming::{
Conversation,
SaveConfig,
filename_generator,
topic_extractor,
commands,
security::{SecuritySettings, redact_sensitive_information},
};
use std::path::Path;
use tempfile::tempdir;

/// Benchmark topic extraction with basic extractor
#[bench]
fn bench_basic_topic_extraction(b: &mut Bencher) {
// Create a conversation
let mut conversation = Conversation::new("test-id".to_string());
conversation.add_user_message("I need help with Amazon Q CLI automatic naming feature".to_string());
conversation.add_assistant_message("Sure, I can help you with that. What would you like to know?".to_string(), None);
conversation.add_user_message("How do I save a conversation with an automatically generated filename?".to_string());

b.iter(|| {
topic_extractor::basic::extract_topics(&conversation)
});
}

/// Benchmark topic extraction with enhanced extractor
#[bench]
fn bench_enhanced_topic_extraction(b: &mut Bencher) {
// Create a conversation
let mut conversation = Conversation::new("test-id".to_string());
conversation.add_user_message("I need help with Amazon Q CLI automatic naming feature".to_string());
conversation.add_assistant_message("Sure, I can help you with that. What would you like to know?".to_string(), None);
conversation.add_user_message("How do I save a conversation with an automatically generated filename?".to_string());

b.iter(|| {
topic_extractor::enhanced::extract_topics(&conversation)
});
}

/// Benchmark topic extraction with advanced extractor
#[bench]
fn bench_advanced_topic_extraction(b: &mut Bencher) {
// Create a conversation
let mut conversation = Conversation::new("test-id".to_string());
conversation.add_user_message("I need help with Amazon Q CLI automatic naming feature".to_string());
conversation.add_assistant_message("Sure, I can help you with that. What would you like to know?".to_string(), None);
conversation.add_user_message("How do I save a conversation with an automatically generated filename?".to_string());

b.iter(|| {
topic_extractor::advanced::extract_topics(&conversation)
});
}

/// Benchmark filename generation
#[bench]
fn bench_filename_generation(b: &mut Bencher) {
// Create a conversation
let mut conversation = Conversation::new("test-id".to_string());
conversation.add_user_message("I need help with Amazon Q CLI automatic naming feature".to_string());
conversation.add_assistant_message("Sure, I can help you with that. What would you like to know?".to_string(), None);
conversation.add_user_message("How do I save a conversation with an automatically generated filename?".to_string());

b.iter(|| {
filename_generator::generate_filename(&conversation)
});
}

/// Benchmark filename generation with advanced extractor
#[bench]
fn bench_filename_generation_with_advanced_extractor(b: &mut Bencher) {
// Create a conversation
let mut conversation = Conversation::new("test-id".to_string());
conversation.add_user_message("I need help with Amazon Q CLI automatic naming feature".to_string());
conversation.add_assistant_message("Sure, I can help you with that. What would you like to know?".to_string(), None);
conversation.add_user_message("How do I save a conversation with an automatically generated filename?".to_string());

b.iter(|| {
filename_generator::generate_filename_with_extractor(&conversation, &topic_extractor::advanced::extract_topics)
});
}

/// Benchmark filename generation with custom format
#[bench]
fn bench_filename_generation_with_custom_format(b: &mut Bencher) {
// Create a conversation
let mut conversation = Conversation::new("test-id".to_string());
conversation.add_user_message("I need help with Amazon Q CLI automatic naming feature".to_string());
conversation.add_assistant_message("Sure, I can help you with that. What would you like to know?".to_string(), None);
conversation.add_user_message("How do I save a conversation with an automatically generated filename?".to_string());

// Create a save config with a custom format
let mut config = SaveConfig::new("/tmp/config.json");
config.set_filename_format(amazon_q_cli_auto_naming::save_config::FilenameFormat::Custom(
String::from("{main_topic}-{sub_topic}-{action_type}-{date}")
)).unwrap();

b.iter(|| {
filename_generator::generate_filename_with_config(&conversation, &config)
});
}

/// Benchmark sensitive information redaction
#[bench]
fn bench_sensitive_information_redaction(b: &mut Bencher) {
// Create a text with sensitive information
let text = "My credit card is 1234-5678-9012-3456, my SSN is 123-45-6789, \
my API key is abcdefghijklmnopqrstuvwxyz1234567890abcdef, \
my AWS key is AKIAIOSFODNN7EXAMPLE, \
password = secret123";

b.iter(|| {
redact_sensitive_information(text)
});
}

/// Benchmark save command
#[bench]
fn bench_save_command(b: &mut Bencher) {
// Create a temporary directory for testing
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("config.json");

// Create a save config with a default path
let mut config = SaveConfig::new(&config_path);
let default_path = temp_dir.path().join("qChats").to_string_lossy().to_string();
config.set_default_path(&default_path).unwrap();

// Create a conversation
let mut conversation = Conversation::new("test-id".to_string());
conversation.add_user_message("I need help with Amazon Q CLI automatic naming feature".to_string());
conversation.add_assistant_message("Sure, I can help you with that. What would you like to know?".to_string(), None);
conversation.add_user_message("How do I save a conversation with an automatically generated filename?".to_string());

// Create a unique path for each iteration
let mut counter = 0;

b.iter(|| {
// Create a unique path for this iteration
let path = temp_dir.path().join(format!("test{}.q.json", counter));
counter += 1;

// Call the save command with the path
let args = vec![path.to_string_lossy().to_string()];
commands::save::handle_save_command(&args, &conversation, &config)
});
}

/// Benchmark save command with redaction
#[bench]
fn bench_save_command_with_redaction(b: &mut Bencher) {
// Create a temporary directory for testing
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("config.json");

// Create a save config with a default path
let mut config = SaveConfig::new(&config_path);
let default_path = temp_dir.path().join("qChats").to_string_lossy().to_string();
config.set_default_path(&default_path).unwrap();

// Create a conversation with sensitive information
let mut conversation = Conversation::new("test-id".to_string());
conversation.add_user_message("My credit card is 1234-5678-9012-3456".to_string());
conversation.add_assistant_message("I'll help you with that.".to_string(), None);

// Create a unique path for each iteration
let mut counter = 0;

b.iter(|| {
// Create a unique path for this iteration
let path = temp_dir.path().join(format!("test{}.q.json", counter));
counter += 1;

// Call the save command with the path and redaction option
let args = vec![path.to_string_lossy().to_string(), "--redact".to_string()];
commands::save::handle_save_command(&args, &conversation, &config)
});
}
169 changes: 169 additions & 0 deletions crates/chat-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// commands/mod.rs
// Command registration for Amazon Q CLI automatic naming feature

pub mod save;

use std::collections::HashMap;
use crate::conversation::Conversation;
use crate::save_config::SaveConfig;
use self::save::handle_save_command;

/// Command handler function type
pub type CommandHandler = fn(&[String], &Conversation, &SaveConfig) -> Result<String, Box<dyn std::error::Error>>;

/// Command registry
pub struct CommandRegistry {
/// Registered commands
commands: HashMap<String, CommandHandler>,

/// Save configuration
config: SaveConfig,
}

impl CommandRegistry {
/// Create a new command registry
pub fn new(config: SaveConfig) -> Self {
let mut registry = Self {
commands: HashMap::new(),
config,
};

// Register the save command
registry.register_save_command();

registry
}

/// Register the save command
fn register_save_command(&mut self) {
self.commands.insert("save".to_string(), |args, conv, config| {
handle_save_command(args, conv, config)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
});
}

/// Execute a command
pub fn execute_command(
&self,
command: &str,
args: &[String],
conversation: &Conversation,
) -> Result<String, Box<dyn std::error::Error>> {
if let Some(handler) = self.commands.get(command) {
handler(args, conversation, &self.config)
} else {
Err(format!("Unknown command: {}", command).into())
}
}

/// Get help text for a command
pub fn get_help_text(&self, command: &str) -> Option<String> {
match command {
"save" => Some(
"/save [path]\n Save the current conversation.\n\n \
Without arguments: Automatically generates a filename and saves to the default location.\n \
With directory path: Saves to the specified directory with an auto-generated filename.\n \
With full path: Saves to the specified path with the given filename.\n\n \
Examples:\n \
/save\n \
/save ~/my-conversations/\n \
/save ~/my-conversations/important-chat.q.json".to_string()
),
_ => None,
}
}

/// Get the list of available commands
pub fn get_commands(&self) -> Vec<String> {
self.commands.keys().cloned().collect()
}

/// Get the save configuration
pub fn get_config(&self) -> &SaveConfig {
&self.config
}

/// Get a mutable reference to the save configuration
pub fn get_config_mut(&mut self) -> &mut SaveConfig {
&mut self.config
}
}

#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use crate::tests::mocks::create_mock_conversation;

#[test]
fn test_register_save_command() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("config.json");
let config = SaveConfig::new(&config_path);

let registry = CommandRegistry::new(config);

assert!(registry.commands.contains_key("save"));
}

#[test]
fn test_execute_save_command() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("config.json");
let mut config = SaveConfig::new(&config_path);
let default_path = temp_dir.path().join("qChats").to_string_lossy().to_string();
config.set_default_path(&default_path).unwrap();

let registry = CommandRegistry::new(config);
let conv = create_mock_conversation("amazon_q_cli");

let result = registry.execute_command("save", &[], &conv);

assert!(result.is_ok());
let save_path = result.unwrap();
assert!(save_path.starts_with(&default_path));
}

#[test]
fn test_unknown_command() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("config.json");
let config = SaveConfig::new(&config_path);

let registry = CommandRegistry::new(config);
let conv = create_mock_conversation("amazon_q_cli");

let result = registry.execute_command("unknown", &[], &conv);

assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "Unknown command: unknown");
}

#[test]
fn test_get_help_text() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("config.json");
let config = SaveConfig::new(&config_path);

let registry = CommandRegistry::new(config);

let help_text = registry.get_help_text("save");
assert!(help_text.is_some());
assert!(help_text.unwrap().contains("/save [path]"));

let help_text = registry.get_help_text("unknown");
assert!(help_text.is_none());
}

#[test]
fn test_get_commands() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("config.json");
let config = SaveConfig::new(&config_path);

let registry = CommandRegistry::new(config);

let commands = registry.get_commands();
assert!(commands.contains(&"save".to_string()));
}
}
Loading