use crate::{config::LLMConfig, Result, ScraperError};
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::Client;
use serde_json::json;
use std::time::Duration;
use tracing::{debug, instrument};
pub struct LLMProcessor {
client: Client,
config: LLMConfig,
}
#[derive(Debug)]
pub struct ProcessedResponse {
pub content: String,
pub token_count: usize,
pub processing_time: Duration,
pub model: String,
}
impl LLMProcessor {
pub fn new(config: LLMConfig) -> Self {
Self {
client: Client::new(),
config,
}
}
fn create_progress_bar(&self, msg: &str) -> ProgressBar {
let spinner = ProgressBar::new_spinner();
spinner.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")
.unwrap(),
);
spinner.enable_steady_tick(Duration::from_millis(120));
spinner.set_message(msg.to_string());
spinner
}
#[instrument(skip(self, prompt), fields(prompt_length = prompt.len()))]
pub async fn process(&self, prompt: &str, model: &str) -> Result<String> {
let response = self.process_with_details(prompt, model).await?;
Ok(response.content)
}
pub async fn process_with_details(&self, prompt: &str, model: &str) -> Result<ProcessedResponse> {
debug!("Processing LLM request with prompt: {}", prompt);
let spinner = self.create_progress_bar("Preparing LLM request...");
let start_time = std::time::Instant::now();
let request = json!({
"system" : String::from(
"You are a helpful assistant that analyzes text content to answer questions. \
you will receive a lot of content and a statement or a query, Your responses should be \
about the question or query or statement that was given as a prompt and nothing more :\n\
1. Make your reply Accurate and based on the provided content\n\
2. Well-structured and easy to understand\n\
3. Directly addressing the original question or prompt\n\
4. Including relevant citations when appropriate"
),
"model": model,
"prompt": prompt,
"temperature": self.config.temperature,
"max_tokens": self.config.max_tokens,
"stream": false
});
spinner.set_message(format!("Sending request to {}...", model));
let response = match self.client
.post(&self.config.endpoint)
.json(&request)
.send()
.await
{
Ok(resp) => resp,
Err(e) => {
spinner.finish_with_message("❌ LLM request failed!");
return Err(ScraperError::LLMError(e.to_string()));
}
};
spinner.set_message("Processing response...");
let result: serde_json::Value = match response.json().await {
Ok(json) => json,
Err(e) => {
spinner.finish_with_message("❌ Failed to parse LLM response!");
return Err(ScraperError::LLMError(e.to_string()));
}
};
let response_text = result["response"]
.as_str()
.map(String::from)
.ok_or_else(|| {
spinner.finish_with_message("❌ Invalid LLM response format!");
ScraperError::LLMError("Invalid LLM response format".to_string())
})?;
let processing_time = start_time.elapsed();
let token_estimate = response_text.split_whitespace().count();
spinner.finish_with_message(format!(
"✨ Generated response (~{} tokens) in {:.2?}",
token_estimate,
processing_time
));
Ok(ProcessedResponse {
content: response_text,
token_count: token_estimate,
processing_time,
model: model.to_string(),
})
}
}