feat: Parse operate AI response and execute command

This commit is contained in:
Sahaj Jain 2025-03-20 16:07:19 +05:30
parent 61e13f9905
commit 6c35341c9a
3 changed files with 147 additions and 23 deletions

View file

@ -150,30 +150,19 @@ impl AIPrompt {
pub fn build_operate_prompt(query: &str) -> Result<Self, AIPromptError> {
let system_prompt = String::from(indoc! {"
You are a Git operations assistant that helps users execute the right Git commands.
When given a request:
1. Determine the precise Git command to accomplish the task
2. Provide a brief, clear explanation of what the command does
3. Include any warnings about potential data loss or destructive operations
4. Format your response using the specified XML tags
You're a Git assistant that provides commands with clear explanations.
- Include warnings ONLY for destructive commands (reset, push --force, clean, etc.)
- Omit warning tag completely for safe commands
"});
let user_prompt = formatdoc! {"
Generate the appropriate Git command for this request:
Request: {query}
Respond with:
<command>The exact Git command to execute</command>
<explanation>A brief explanation of what this command does and why it's appropriate</explanation>
<warning>Any warnings about potential data loss or side effects (if applicable)</warning>
Make sure the command works as expected. If the request is ambiguous, choose the most likely interpretation.
For time-based operations, use standard Git time formats.
Generate Git command for: {query}
<command>Git command</command>
<explanation>Brief explanation</explanation>
<warning>Required for destructive commands only - omit for safe commands</warning>
",
query = query
};
Ok(AIPrompt {
system_prompt,
user_prompt,

View file

@ -46,9 +46,7 @@ impl CommandType {
draft_config,
context,
}),
CommandType::Operate { query } => {
Box::new(OperateCommand { query })
}
CommandType::Operate { query } => Box::new(OperateCommand { query }),
})
}
}
@ -125,4 +123,30 @@ impl LumenCommand {
}
Ok(())
}
fn execute_bash_command(command: &str) -> Result<(), LumenError> {
let output = std::process::Command::new("sh")
.arg("-c")
.arg(command)
.output()?;
if !output.status.success() {
let mut stderr = String::from_utf8(output.stderr)?;
stderr.pop();
return Err(LumenError::CommandError(stderr));
}
println!("{}", String::from_utf8(output.stdout)?);
Ok(())
}
// ask for (y/N) confirmation to execute the command
fn execute_bash_command_with_confirmation(command: &str) -> Result<(), LumenError> {
let mut input = String::new();
println!("{} (y/N)", command);
std::io::stdin().read_line(&mut input)?;
if input.trim().to_lowercase() != "y" {
return Err(LumenError::CommandError("Aborted".to_string()));
}
LumenCommand::execute_bash_command(command)
}
}

View file

@ -1,5 +1,21 @@
use async_trait::async_trait;
use spinoff::{spinners, Color, Spinner};
use std::io::{self, Write};
use thiserror::Error;
#[derive(Debug)]
pub struct OperateResult {
pub command: String,
pub explanation: String,
pub warning: Option<String>,
}
#[derive(Error, Debug)]
#[error("Failed to extract {field} from AI response: {message}")]
pub struct ExtractError {
field: String,
message: String,
}
use crate::{error::LumenError, provider::LumenProvider};
@ -9,6 +25,99 @@ pub struct OperateCommand {
pub query: String,
}
pub fn extract_operate_response(ai_response: &str) -> Result<OperateResult, ExtractError> {
// Helper function to extract content between XML tags
fn extract_tag(text: &str, tag_name: &str) -> Result<String, ExtractError> {
let start_tag = format!("<{}>", tag_name);
let end_tag = format!("</{}>", tag_name);
if let Some(start) = text.find(&start_tag) {
if let Some(end) = text.find(&end_tag) {
let content_start = start + start_tag.len();
if content_start < end {
return Ok(text[content_start..end].trim().to_string());
}
}
}
Err(ExtractError {
field: tag_name.to_string(),
message: format!("Could not find valid <{}> tags", tag_name),
})
}
// Extract required fields
let command = extract_tag(ai_response, "command")?;
let explanation = extract_tag(ai_response, "explanation")?;
// Warning is optional
let warning = match extract_tag(ai_response, "warning") {
Ok(warning_text) if !warning_text.is_empty() => Some(warning_text),
_ => None,
};
Ok(OperateResult {
command,
explanation,
warning,
})
}
pub fn process_operation(result: OperateResult) -> Result<(), io::Error> {
// Display the explanation
println!("\n--- What this will do ---");
println!("{}", result.explanation);
// Display warnings if any and prompt for confirmation
if let Some(warning) = result.warning {
// print warning in yellow colour
println!("\n\x1b[33mWarning: {}\x1b[0m", warning);
}
print!("\n{} [y/N] ", result.command);
io::stdout().flush()?; // Ensure prompt is shown immediately
let mut input = String::new();
io::stdin().read_line(&mut input)?;
println!();
if !input.trim().eq_ignore_ascii_case("y") {
println!("Operation canceled.");
return Ok(());
}
// Using a shell to execute the git command
#[cfg(target_family = "unix")]
let output = std::process::Command::new("sh")
.arg("-c")
.arg(&result.command)
.output()?;
#[cfg(target_family = "windows")]
let output = std::process::Command::new("cmd")
.arg("/C")
.arg(&result.command)
.output()?;
// Print command output
if !output.stdout.is_empty() {
io::stdout().write_all(&output.stdout)?;
}
if !output.stderr.is_empty() {
io::stderr().write_all(&output.stderr)?;
}
if !output.status.success() {
eprintln!(
"\nCommand failed with exit code: {:?}",
output.status.code()
);
}
Ok(())
}
#[async_trait]
impl Command for OperateCommand {
async fn execute(&self, provider: &LumenProvider) -> Result<(), LumenError> {
@ -18,9 +127,11 @@ impl Command for OperateCommand {
let mut spinner = Spinner::new(spinners::Dots, spinner_text, Color::Blue);
let result = provider.operate(self).await?;
let operate_result = extract_operate_response(&result)
.map_err(|e| LumenError::CommandError(e.to_string()))?;
spinner.success("Done");
LumenCommand::print_with_mdcat(result)?;
process_operation(operate_result)?;
Ok(())
}
}