use anyhow::Result;
use async_openai::types::{
ChatCompletionRequestMessage, ChatCompletionRequestSystemMessage,
ChatCompletionRequestSystemMessageContent, ChatCompletionRequestUserMessage,
ChatCompletionRequestUserMessageContent, CreateChatCompletionRequest,
};
use tracing::debug;
use crate::ai::prompts::{AI_PIPE_PROMPT, AI_REDIRECT_PROMPT};
use crate::ai::stream::process_ai_pipe_stream;
impl super::JarvisAI {
pub async fn process_ai_pipe(&self, stdin_text: &str, prompt: &str) -> Result<String> {
let char_count = stdin_text.chars().count();
let limit = self.ai_pipe_max_chars;
debug!(
prompt = %prompt,
input_chars = char_count,
limit = limit,
"process_ai_pipe() called"
);
if char_count > limit {
anyhow::bail!(
"input text exceeds the {limit} chars limit ({char_count} chars). \
Use 'head' or 'tail' to reduce input, or increase 'ai_pipe_max_chars' in config.toml."
);
}
let user_message = format!("[User Instruction]\n{prompt}\n\n[Input Text]\n{stdin_text}");
let messages: Vec<ChatCompletionRequestMessage> = vec![
ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage {
content: ChatCompletionRequestSystemMessageContent::Text(
AI_PIPE_PROMPT.to_string(),
),
name: None,
}),
ChatCompletionRequestMessage::User(ChatCompletionRequestUserMessage {
content: ChatCompletionRequestUserMessageContent::Text(user_message),
name: None,
}),
];
let request = CreateChatCompletionRequest {
model: self.model.clone(),
messages,
stream: Some(true),
temperature: Some(self.temperature),
..Default::default()
};
let raw = process_ai_pipe_stream(&self.client, request, self.markdown_rendering).await?;
Ok(sanitize_ai_pipe_output(&raw))
}
pub async fn process_ai_redirect(&self, stdin_text: &str, prompt: &str) -> Result<String> {
let char_count = stdin_text.chars().count();
let limit = self.ai_redirect_max_chars;
debug!(
prompt = %prompt,
input_chars = char_count,
limit = limit,
"process_ai_redirect() called"
);
if char_count > limit {
anyhow::bail!(
"input text exceeds the {limit} chars limit ({char_count} chars). \
Use 'head' or 'tail' to reduce input, or increase 'ai_redirect_max_chars' in config.toml."
);
}
let user_message = format!("[User Instruction]\n{prompt}\n\n[Input Text]\n{stdin_text}");
let messages: Vec<ChatCompletionRequestMessage> = vec![
ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage {
content: ChatCompletionRequestSystemMessageContent::Text(
AI_REDIRECT_PROMPT.to_string(),
),
name: None,
}),
ChatCompletionRequestMessage::User(ChatCompletionRequestUserMessage {
content: ChatCompletionRequestUserMessageContent::Text(user_message),
name: None,
}),
];
let request = CreateChatCompletionRequest {
model: self.model.clone(),
messages,
stream: Some(true),
temperature: Some(self.temperature),
..Default::default()
};
let raw = process_ai_pipe_stream(&self.client, request, self.markdown_rendering).await?;
Ok(raw)
}
}
fn sanitize_ai_pipe_output(text: &str) -> String {
let trimmed = text.trim();
if trimmed.starts_with("```") && trimmed.ends_with("```") && trimmed.len() > 6 {
let inner = &trimmed[3..trimmed.len() - 3];
if let Some(newline_pos) = inner.find('\n') {
return inner[newline_pos + 1..].trim().to_string();
}
}
trimmed.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_strips_code_fence_with_language() {
let input = "```json\n{\"key\": \"value\"}\n```";
assert_eq!(sanitize_ai_pipe_output(input), "{\"key\": \"value\"}");
}
#[test]
fn sanitize_strips_code_fence_without_language() {
let input = "```\nhello world\n```";
assert_eq!(sanitize_ai_pipe_output(input), "hello world");
}
#[test]
fn sanitize_preserves_plain_text() {
let input = "hello world\nsecond line";
assert_eq!(sanitize_ai_pipe_output(input), "hello world\nsecond line");
}
#[test]
fn sanitize_trims_whitespace() {
let input = " \n hello world \n ";
assert_eq!(sanitize_ai_pipe_output(input), "hello world");
}
#[test]
fn sanitize_handles_empty_string() {
assert_eq!(sanitize_ai_pipe_output(""), "");
assert_eq!(sanitize_ai_pipe_output(" "), "");
}
}