mirror of
https://github.com/jnsahaj/lumen.git
synced 2025-08-04 19:08:21 +00:00
feat: Parse operate AI response and execute command
This commit is contained in:
parent
61e13f9905
commit
6c35341c9a
3 changed files with 147 additions and 23 deletions
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue