-
Notifications
You must be signed in to change notification settings - Fork 66
Improve natural language package install parsing #764
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -76,6 +76,19 @@ pub enum PackageCommand { | |||||||||||||||||||||||||||||||||||||||
| Search { query: String }, | ||||||||||||||||||||||||||||||||||||||||
| /// Install a package | ||||||||||||||||||||||||||||||||||||||||
| Install { package: String }, | ||||||||||||||||||||||||||||||||||||||||
| /// Install a natural-language package profile | ||||||||||||||||||||||||||||||||||||||||
| NaturalInstall { | ||||||||||||||||||||||||||||||||||||||||
| query: String, | ||||||||||||||||||||||||||||||||||||||||
| packages: Vec<String>, | ||||||||||||||||||||||||||||||||||||||||
| confidence: u8, | ||||||||||||||||||||||||||||||||||||||||
| reasoning: String, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| /// Ask the user to clarify an ambiguous package request | ||||||||||||||||||||||||||||||||||||||||
| Clarify { | ||||||||||||||||||||||||||||||||||||||||
| query: String, | ||||||||||||||||||||||||||||||||||||||||
| reason: String, | ||||||||||||||||||||||||||||||||||||||||
| suggestions: Vec<String>, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| /// Remove a package | ||||||||||||||||||||||||||||||||||||||||
| Remove { package: String }, | ||||||||||||||||||||||||||||||||||||||||
| /// List installed packages | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -99,9 +112,14 @@ pub enum PackageCommand { | |||||||||||||||||||||||||||||||||||||||
| impl PackageCommand { | ||||||||||||||||||||||||||||||||||||||||
| /// Parse a command string into a PackageCommand | ||||||||||||||||||||||||||||||||||||||||
| pub fn parse(input: &str) -> Self { | ||||||||||||||||||||||||||||||||||||||||
| let input_lower = input.to_lowercase(); | ||||||||||||||||||||||||||||||||||||||||
| let normalized = normalize_package_request(input); | ||||||||||||||||||||||||||||||||||||||||
| let input_lower = normalized.to_lowercase(); | ||||||||||||||||||||||||||||||||||||||||
| let words: Vec<&str> = input.split_whitespace().collect(); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if let Some(command) = parse_natural_install(input, &input_lower) { | ||||||||||||||||||||||||||||||||||||||||
| return command; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Search | ||||||||||||||||||||||||||||||||||||||||
| if input_lower.contains("search") || input_lower.contains("find package") { | ||||||||||||||||||||||||||||||||||||||||
| let query = words | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -118,7 +136,7 @@ impl PackageCommand { | |||||||||||||||||||||||||||||||||||||||
| if input_lower.contains("install") { | ||||||||||||||||||||||||||||||||||||||||
| let package = words | ||||||||||||||||||||||||||||||||||||||||
| .iter() | ||||||||||||||||||||||||||||||||||||||||
| .skip_while(|&w| w.to_lowercase() != "install") | ||||||||||||||||||||||||||||||||||||||||
| .skip_while(|&w| normalize_package_request(w).to_lowercase() != "install") | ||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calling .skip_while(|&w| {
let w_lower = w.to_lowercase();
let trimmed = w_lower.trim_matches(|c: char| !c.is_alphanumeric() && c != '-');
!matches!(trimmed, "install" | "instal" | "isntall" | "isntal")
}) |
||||||||||||||||||||||||||||||||||||||||
| .nth(1) | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
136
to
140
|
||||||||||||||||||||||||||||||||||||||||
| .map(|s| s.to_string()) | ||||||||||||||||||||||||||||||||||||||||
| .or_else(|| words.last().map(|s| s.to_string())) | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -203,6 +221,150 @@ impl PackageCommand { | |||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| fn normalize_package_request(input: &str) -> String { | ||||||||||||||||||||||||||||||||||||||||
| input | ||||||||||||||||||||||||||||||||||||||||
| .to_lowercase() | ||||||||||||||||||||||||||||||||||||||||
| .split_whitespace() | ||||||||||||||||||||||||||||||||||||||||
| .map(|word| match word.trim_matches(|c: char| !c.is_alphanumeric() && c != '-') { | ||||||||||||||||||||||||||||||||||||||||
| "instal" | "isntall" | "isntal" => "install", | ||||||||||||||||||||||||||||||||||||||||
| "pyhton" | "pythn" => "python", | ||||||||||||||||||||||||||||||||||||||||
| "dockr" | "docer" => "docker", | ||||||||||||||||||||||||||||||||||||||||
| "kubernets" | "kubernetess" | "k8s" => "kubernetes", | ||||||||||||||||||||||||||||||||||||||||
| "ngnix" => "nginx", | ||||||||||||||||||||||||||||||||||||||||
| "machne" => "machine", | ||||||||||||||||||||||||||||||||||||||||
| "lerning" | "leaning" => "learning", | ||||||||||||||||||||||||||||||||||||||||
| other => other, | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| .collect::<Vec<_>>() | ||||||||||||||||||||||||||||||||||||||||
| .join(" ") | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| fn parse_natural_install(original: &str, input: &str) -> Option<PackageCommand> { | ||||||||||||||||||||||||||||||||||||||||
| let is_install_request = input.contains("install") | ||||||||||||||||||||||||||||||||||||||||
| || input.contains("set up") | ||||||||||||||||||||||||||||||||||||||||
| || input.contains("setup") | ||||||||||||||||||||||||||||||||||||||||
| || input.contains("i need") | ||||||||||||||||||||||||||||||||||||||||
| || input.contains("something for"); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if !is_install_request { | ||||||||||||||||||||||||||||||||||||||||
| return None; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+243
to
+251
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if input.contains("web server") { | ||||||||||||||||||||||||||||||||||||||||
| return Some(PackageCommand::Clarify { | ||||||||||||||||||||||||||||||||||||||||
| query: original.to_string(), | ||||||||||||||||||||||||||||||||||||||||
| reason: "A web server could mean a reverse proxy, static file server, or application runtime.".to_string(), | ||||||||||||||||||||||||||||||||||||||||
| suggestions: vec![ | ||||||||||||||||||||||||||||||||||||||||
| "install nginx for a common production reverse proxy".to_string(), | ||||||||||||||||||||||||||||||||||||||||
| "install apache2 for a traditional HTTP server".to_string(), | ||||||||||||||||||||||||||||||||||||||||
| "install caddy for automatic HTTPS".to_string(), | ||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if input.contains("machine learning") || input.contains("ml") { | ||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The check
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| return Some(PackageCommand::NaturalInstall { | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+265
to
+266
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested fix- if input.contains("machine learning") || input.contains("ml") {
+ let tokens: std::collections::HashSet<&str> = input.split_whitespace().collect();
+ if input.contains("machine learning") || tokens.contains("ml") {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Comment on lines
+265
to
+266
|
||||||||||||||||||||||||||||||||||||||||
| query: original.to_string(), | ||||||||||||||||||||||||||||||||||||||||
| packages: vec![ | ||||||||||||||||||||||||||||||||||||||||
| "python3", | ||||||||||||||||||||||||||||||||||||||||
| "python3-pip", | ||||||||||||||||||||||||||||||||||||||||
| "python3-venv", | ||||||||||||||||||||||||||||||||||||||||
| "python3-numpy", | ||||||||||||||||||||||||||||||||||||||||
| "python3-scipy", | ||||||||||||||||||||||||||||||||||||||||
| "python3-pandas", | ||||||||||||||||||||||||||||||||||||||||
| "python3-sklearn", | ||||||||||||||||||||||||||||||||||||||||
| "jupyter-notebook", | ||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+268
to
+277
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Natural-install package lists are distro-specific but executed across all package managers. Profiles use apt-style names ( Also applies to: 293-299, 311-312, 452-466 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| .into_iter() | ||||||||||||||||||||||||||||||||||||||||
| .map(str::to_string) | ||||||||||||||||||||||||||||||||||||||||
| .collect(), | ||||||||||||||||||||||||||||||||||||||||
| confidence: 86, | ||||||||||||||||||||||||||||||||||||||||
| reasoning: "I understood this as a Python-based machine learning workstation setup." | ||||||||||||||||||||||||||||||||||||||||
| .to_string(), | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+266
to
+283
|
||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if input.contains("python development") | ||||||||||||||||||||||||||||||||||||||||
| || input.contains("python dev") | ||||||||||||||||||||||||||||||||||||||||
| || input.contains("python environment") | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| return Some(PackageCommand::NaturalInstall { | ||||||||||||||||||||||||||||||||||||||||
| query: original.to_string(), | ||||||||||||||||||||||||||||||||||||||||
| packages: vec![ | ||||||||||||||||||||||||||||||||||||||||
| "python3", | ||||||||||||||||||||||||||||||||||||||||
| "python3-pip", | ||||||||||||||||||||||||||||||||||||||||
| "python3-venv", | ||||||||||||||||||||||||||||||||||||||||
| "python3-dev", | ||||||||||||||||||||||||||||||||||||||||
| "build-essential", | ||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||
| .into_iter() | ||||||||||||||||||||||||||||||||||||||||
| .map(str::to_string) | ||||||||||||||||||||||||||||||||||||||||
| .collect(), | ||||||||||||||||||||||||||||||||||||||||
| confidence: 91, | ||||||||||||||||||||||||||||||||||||||||
| reasoning: "I understood this as a Python development environment with compiler headers, pip, and virtualenv support.".to_string(), | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if input.contains("docker") && input.contains("kubernetes") { | ||||||||||||||||||||||||||||||||||||||||
| return Some(PackageCommand::NaturalInstall { | ||||||||||||||||||||||||||||||||||||||||
| query: original.to_string(), | ||||||||||||||||||||||||||||||||||||||||
| packages: vec!["docker.io", "docker-compose-plugin", "kubectl"] | ||||||||||||||||||||||||||||||||||||||||
| .into_iter() | ||||||||||||||||||||||||||||||||||||||||
| .map(str::to_string) | ||||||||||||||||||||||||||||||||||||||||
| .collect(), | ||||||||||||||||||||||||||||||||||||||||
| confidence: 82, | ||||||||||||||||||||||||||||||||||||||||
| reasoning: "I understood this as container tooling plus the Kubernetes command-line client.".to_string(), | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if input.contains("nginx") { | ||||||||||||||||||||||||||||||||||||||||
| return Some(PackageCommand::NaturalInstall { | ||||||||||||||||||||||||||||||||||||||||
| query: original.to_string(), | ||||||||||||||||||||||||||||||||||||||||
| packages: vec!["nginx"].into_iter().map(str::to_string).collect(), | ||||||||||||||||||||||||||||||||||||||||
| confidence: 94, | ||||||||||||||||||||||||||||||||||||||||
| reasoning: "I understood this as a request for the nginx web server.".to_string(), | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if input.contains("python") { | ||||||||||||||||||||||||||||||||||||||||
| return Some(PackageCommand::NaturalInstall { | ||||||||||||||||||||||||||||||||||||||||
| query: original.to_string(), | ||||||||||||||||||||||||||||||||||||||||
| packages: vec!["python3", "python3-pip", "python3-venv"] | ||||||||||||||||||||||||||||||||||||||||
| .into_iter() | ||||||||||||||||||||||||||||||||||||||||
| .map(str::to_string) | ||||||||||||||||||||||||||||||||||||||||
| .collect(), | ||||||||||||||||||||||||||||||||||||||||
| confidence: 78, | ||||||||||||||||||||||||||||||||||||||||
| reasoning: "I understood this as a general Python runtime request.".to_string(), | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if input.contains("docker") { | ||||||||||||||||||||||||||||||||||||||||
| return Some(PackageCommand::NaturalInstall { | ||||||||||||||||||||||||||||||||||||||||
| query: original.to_string(), | ||||||||||||||||||||||||||||||||||||||||
| packages: vec!["docker.io", "docker-compose-plugin"] | ||||||||||||||||||||||||||||||||||||||||
| .into_iter() | ||||||||||||||||||||||||||||||||||||||||
| .map(str::to_string) | ||||||||||||||||||||||||||||||||||||||||
| .collect(), | ||||||||||||||||||||||||||||||||||||||||
| confidence: 84, | ||||||||||||||||||||||||||||||||||||||||
| reasoning: "I understood this as a Docker engine and Compose plugin setup.".to_string(), | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if input.contains("something for") || input.contains("i need") || input.contains("set up") { | ||||||||||||||||||||||||||||||||||||||||
| return Some(PackageCommand::Clarify { | ||||||||||||||||||||||||||||||||||||||||
| query: original.to_string(), | ||||||||||||||||||||||||||||||||||||||||
| reason: "The request describes a goal, but not enough constraints to choose packages safely.".to_string(), | ||||||||||||||||||||||||||||||||||||||||
| suggestions: vec![ | ||||||||||||||||||||||||||||||||||||||||
| "name the language or framework".to_string(), | ||||||||||||||||||||||||||||||||||||||||
| "say whether this is for a desktop, server, or container host".to_string(), | ||||||||||||||||||||||||||||||||||||||||
| "ask for a known profile such as machine learning or python development".to_string(), | ||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| None | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// Package agent for package management | ||||||||||||||||||||||||||||||||||||||||
| pub struct PackageAgent { | ||||||||||||||||||||||||||||||||||||||||
| capabilities: Vec<AgentCapability>, | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -271,31 +433,69 @@ impl PackageAgent { | |||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// Install a package (returns command to run, requires confirmation) | ||||||||||||||||||||||||||||||||||||||||
| fn install_package(&self, package: &str) -> AgentResponse { | ||||||||||||||||||||||||||||||||||||||||
| self.install_packages(&[package.to_string()], None, None, None) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// Install packages (returns command to run, requires confirmation) | ||||||||||||||||||||||||||||||||||||||||
| fn install_packages( | ||||||||||||||||||||||||||||||||||||||||
| &self, | ||||||||||||||||||||||||||||||||||||||||
| packages: &[String], | ||||||||||||||||||||||||||||||||||||||||
| confidence: Option<u8>, | ||||||||||||||||||||||||||||||||||||||||
| reasoning: Option<&str>, | ||||||||||||||||||||||||||||||||||||||||
| query: Option<&str>, | ||||||||||||||||||||||||||||||||||||||||
| ) -> AgentResponse { | ||||||||||||||||||||||||||||||||||||||||
| if packages.is_empty() || packages.iter().any(|p| p.trim().is_empty()) { | ||||||||||||||||||||||||||||||||||||||||
| return AgentResponse::error("No package name was provided".to_string()); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| let package_list = packages.join(" "); | ||||||||||||||||||||||||||||||||||||||||
| let cmd_str = match self.package_manager { | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+447
to
452
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate package identifiers with an allow-list before command construction.
Suggested fix fn install_packages(
&self,
packages: &[String],
confidence: Option<u8>,
reasoning: Option<&str>,
query: Option<&str>,
) -> AgentResponse {
if packages.is_empty() || packages.iter().any(|p| p.trim().is_empty()) {
return AgentResponse::error("No package name was provided".to_string());
}
+ if packages
+ .iter()
+ .any(|p| !p.chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '.' || c == '-' || c == '_'))
+ {
+ return AgentResponse::error("Invalid package name format".to_string());
+ }
let package_list = packages.join(" ");As per coding guidelines, "Use allow-lists for input validation to prevent injection attacks". 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| PackageManager::Apt => format!("sudo apt install -y {}", package), | ||||||||||||||||||||||||||||||||||||||||
| PackageManager::Pacman => format!("sudo pacman -S {}", package), | ||||||||||||||||||||||||||||||||||||||||
| PackageManager::Dnf => format!("sudo dnf install -y {}", package), | ||||||||||||||||||||||||||||||||||||||||
| PackageManager::Yum => format!("sudo yum install -y {}", package), | ||||||||||||||||||||||||||||||||||||||||
| PackageManager::Zypper => format!("sudo zypper install -y {}", package), | ||||||||||||||||||||||||||||||||||||||||
| PackageManager::Brew => format!("brew install {}", package), | ||||||||||||||||||||||||||||||||||||||||
| PackageManager::Nix => format!("nix-env -iA nixpkgs.{}", package), | ||||||||||||||||||||||||||||||||||||||||
| PackageManager::Apt => format!("sudo apt install -y {}", package_list), | ||||||||||||||||||||||||||||||||||||||||
| PackageManager::Pacman => format!("sudo pacman -S {}", package_list), | ||||||||||||||||||||||||||||||||||||||||
| PackageManager::Dnf => format!("sudo dnf install -y {}", package_list), | ||||||||||||||||||||||||||||||||||||||||
| PackageManager::Yum => format!("sudo yum install -y {}", package_list), | ||||||||||||||||||||||||||||||||||||||||
| PackageManager::Zypper => format!("sudo zypper install -y {}", package_list), | ||||||||||||||||||||||||||||||||||||||||
| PackageManager::Brew => format!("brew install {}", package_list), | ||||||||||||||||||||||||||||||||||||||||
| PackageManager::Nix => { | ||||||||||||||||||||||||||||||||||||||||
| let attrs = packages | ||||||||||||||||||||||||||||||||||||||||
| .iter() | ||||||||||||||||||||||||||||||||||||||||
| .map(|package| format!("nixpkgs.{}", package)) | ||||||||||||||||||||||||||||||||||||||||
| .collect::<Vec<_>>() | ||||||||||||||||||||||||||||||||||||||||
| .join(" "); | ||||||||||||||||||||||||||||||||||||||||
| format!("nix-env -iA {}", attrs) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| PackageManager::Unknown => { | ||||||||||||||||||||||||||||||||||||||||
| return AgentResponse::error("No package manager detected".to_string()); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Return the command to execute - requires user confirmation | ||||||||||||||||||||||||||||||||||||||||
| let intro = match (query, confidence, reasoning) { | ||||||||||||||||||||||||||||||||||||||||
| (Some(query), Some(confidence), Some(reasoning)) => format!( | ||||||||||||||||||||||||||||||||||||||||
| "I understood you want: {}\nReasoning: {}\nConfidence: {}%\nPackages: {}\n\n", | ||||||||||||||||||||||||||||||||||||||||
| query, reasoning, confidence, package_list | ||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||
| _ => String::new(), | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| AgentResponse::success(format!( | ||||||||||||||||||||||||||||||||||||||||
| "To install '{}', run:\n\n {}\n\nThis requires confirmation before execution.", | ||||||||||||||||||||||||||||||||||||||||
| package, cmd_str | ||||||||||||||||||||||||||||||||||||||||
| "{}To install '{}', run:\n\n {}\n\nThis requires confirmation before execution.", | ||||||||||||||||||||||||||||||||||||||||
| intro, package_list, cmd_str | ||||||||||||||||||||||||||||||||||||||||
| )) | ||||||||||||||||||||||||||||||||||||||||
| .with_commands(vec![cmd_str]) | ||||||||||||||||||||||||||||||||||||||||
| .with_suggestions(vec![ | ||||||||||||||||||||||||||||||||||||||||
| format!("search {}", package), | ||||||||||||||||||||||||||||||||||||||||
| format!("info {}", package), | ||||||||||||||||||||||||||||||||||||||||
| format!("search {}", packages[0]), | ||||||||||||||||||||||||||||||||||||||||
| format!("info {}", packages[0]), | ||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| fn clarify_install(&self, query: &str, reason: &str, suggestions: Vec<String>) -> AgentResponse { | ||||||||||||||||||||||||||||||||||||||||
| AgentResponse::success(format!( | ||||||||||||||||||||||||||||||||||||||||
| "I understood the request, but need clarification before choosing packages.\nRequest: {}\nReasoning: {}\nConfidence: 45%", | ||||||||||||||||||||||||||||||||||||||||
| query, reason | ||||||||||||||||||||||||||||||||||||||||
| )) | ||||||||||||||||||||||||||||||||||||||||
| .with_suggestions(suggestions) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// Remove a package (returns command to run, requires confirmation) | ||||||||||||||||||||||||||||||||||||||||
| fn remove_package(&self, package: &str) -> AgentResponse { | ||||||||||||||||||||||||||||||||||||||||
| let cmd_str = match self.package_manager { | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -567,6 +767,7 @@ impl Agent for PackageAgent { | |||||||||||||||||||||||||||||||||||||||
| let cmd_lower = request.command.to_lowercase(); | ||||||||||||||||||||||||||||||||||||||||
| cmd_lower.contains("package") | ||||||||||||||||||||||||||||||||||||||||
| || cmd_lower.contains("install") | ||||||||||||||||||||||||||||||||||||||||
| || cmd_lower.contains("instal") | ||||||||||||||||||||||||||||||||||||||||
| || cmd_lower.contains("uninstall") | ||||||||||||||||||||||||||||||||||||||||
| || cmd_lower.contains("upgrade") | ||||||||||||||||||||||||||||||||||||||||
| || cmd_lower.contains("apt ") | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -580,6 +781,22 @@ impl Agent for PackageAgent { | |||||||||||||||||||||||||||||||||||||||
| match command { | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::Search { query } => self.search_packages(&query), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::Install { package } => self.install_package(&package), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::NaturalInstall { | ||||||||||||||||||||||||||||||||||||||||
| query, | ||||||||||||||||||||||||||||||||||||||||
| packages, | ||||||||||||||||||||||||||||||||||||||||
| confidence, | ||||||||||||||||||||||||||||||||||||||||
| reasoning, | ||||||||||||||||||||||||||||||||||||||||
| } => self.install_packages( | ||||||||||||||||||||||||||||||||||||||||
| &packages, | ||||||||||||||||||||||||||||||||||||||||
| Some(confidence), | ||||||||||||||||||||||||||||||||||||||||
| Some(&reasoning), | ||||||||||||||||||||||||||||||||||||||||
| Some(&query), | ||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::Clarify { | ||||||||||||||||||||||||||||||||||||||||
| query, | ||||||||||||||||||||||||||||||||||||||||
| reason, | ||||||||||||||||||||||||||||||||||||||||
| suggestions, | ||||||||||||||||||||||||||||||||||||||||
| } => self.clarify_install(&query, &reason, suggestions), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::Remove { package } => self.remove_package(&package), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::ListInstalled { filter } => self.list_installed(filter.as_deref()), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::CheckUpdates => self.check_updates(), | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -626,6 +843,85 @@ mod tests { | |||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||
| fn test_parse_machine_learning_profile() { | ||||||||||||||||||||||||||||||||||||||||
| assert!(matches!( | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::parse("install something for machine learning"), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::NaturalInstall { packages, confidence, .. } | ||||||||||||||||||||||||||||||||||||||||
| if confidence >= 80 && packages.contains(&"python3-sklearn".to_string()) | ||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||
| fn test_parse_web_server_ambiguity() { | ||||||||||||||||||||||||||||||||||||||||
| assert!(matches!( | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::parse("I need a web server"), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::Clarify { suggestions, .. } if suggestions.len() >= 3 | ||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||
| fn test_parse_python_development_environment() { | ||||||||||||||||||||||||||||||||||||||||
| assert!(matches!( | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::parse("set up python development environment"), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::NaturalInstall { packages, confidence, .. } | ||||||||||||||||||||||||||||||||||||||||
| if confidence >= 90 && packages.contains(&"python3-dev".to_string()) | ||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||
| fn test_parse_docker_and_kubernetes() { | ||||||||||||||||||||||||||||||||||||||||
| assert!(matches!( | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::parse("install docker and kubernetes"), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::NaturalInstall { packages, .. } | ||||||||||||||||||||||||||||||||||||||||
| if packages == vec!["docker.io".to_string(), "docker-compose-plugin".to_string(), "kubectl".to_string()] | ||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||
| fn test_typo_install_python() { | ||||||||||||||||||||||||||||||||||||||||
| assert!(matches!( | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::parse("instal pyhton"), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::NaturalInstall { packages, .. } | ||||||||||||||||||||||||||||||||||||||||
| if packages.contains(&"python3".to_string()) | ||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||
| fn test_typo_install_nginx() { | ||||||||||||||||||||||||||||||||||||||||
| assert!(matches!( | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::parse("instal ngnix"), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::NaturalInstall { packages, confidence, .. } | ||||||||||||||||||||||||||||||||||||||||
| if confidence >= 90 && packages == vec!["nginx".to_string()] | ||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||
| fn test_unknown_goal_requires_clarification() { | ||||||||||||||||||||||||||||||||||||||||
| assert!(matches!( | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::parse("set up something useful"), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::Clarify { reason, .. } if reason.contains("not enough constraints") | ||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||
| fn test_general_python_profile() { | ||||||||||||||||||||||||||||||||||||||||
| assert!(matches!( | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::parse("I need python"), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::NaturalInstall { packages, confidence, .. } | ||||||||||||||||||||||||||||||||||||||||
| if confidence >= 70 && packages.contains(&"python3-venv".to_string()) | ||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||
| fn test_general_docker_profile() { | ||||||||||||||||||||||||||||||||||||||||
| assert!(matches!( | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::parse("set up docker"), | ||||||||||||||||||||||||||||||||||||||||
| PackageCommand::NaturalInstall { packages, confidence, .. } | ||||||||||||||||||||||||||||||||||||||||
| if confidence >= 80 && packages.contains(&"docker-compose-plugin".to_string()) | ||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||
| fn test_parse_check_updates() { | ||||||||||||||||||||||||||||||||||||||||
| assert!(matches!( | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Natural-language parsing currently preempts explicit
install <package>commands.Running
parse_natural_installbefore explicit install parsing causes regressions likeinstall docker-composebeing interpreted as a profile instead of a direct package install.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents