use anyhow::{Context, Result};
use async_trait::async_trait;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use super::prompt::build_prompt;
use super::AIProvider;
use crate::config::Config;
use crate::utils::retry::retry_async;
pub struct FlowiseProvider {
client: Client,
api_url: String,
api_key: Option<String>,
}
#[derive(Serialize)]
struct FlowiseRequest {
question: String,
#[serde(skip_serializing_if = "Option::is_none")]
history: Option<Vec<FlowiseMessage>>,
}
#[derive(Serialize, Deserialize, Clone)]
struct FlowiseMessage {
message: String,
#[serde(rename = "type")]
message_type: String,
}
#[derive(Deserialize)]
struct FlowiseResponse {
text: String,
#[serde(rename = "sessionId")]
#[allow(dead_code)]
session_id: Option<String>,
}
impl FlowiseProvider {
pub fn new(config: &Config) -> Result<Self> {
let client = Client::new();
let api_url = config
.api_url
.as_deref()
.unwrap_or("http://localhost:3000")
.to_string();
let api_key = config.api_key.clone();
Ok(Self {
client,
api_url,
api_key,
})
}
#[allow(dead_code)]
pub fn from_account(
account: &crate::config::accounts::AccountConfig,
_api_key: &str,
config: &Config,
) -> Result<Self> {
let client = Client::new();
let api_url = account
.api_url
.as_deref()
.or(config.api_url.as_deref())
.unwrap_or("http://localhost:3000")
.to_string();
Ok(Self {
client,
api_url,
api_key: None,
})
}
}
#[async_trait]
impl AIProvider for FlowiseProvider {
async fn generate_commit_message(
&self,
diff: &str,
context: Option<&str>,
full_gitmoji: bool,
config: &Config,
) -> Result<String> {
let prompt = build_prompt(diff, context, config, full_gitmoji);
let request = FlowiseRequest {
question: prompt,
history: None,
};
let flowise_response: FlowiseResponse = retry_async(|| async {
let url = format!("{}/api/v1/prediction/flowise", self.api_url);
let mut req = self.client.post(&url).json(&request);
if let Some(ref key) = self.api_key {
req = req.header("Authorization", format!("Bearer {}", key));
}
let response = req
.send()
.await
.context("Failed to connect to Flowise server. Is Flowise running?")?;
if !response.status().is_success() {
let error_text = response.text().await?;
if error_text.contains("Unauthorized") || error_text.contains("401") {
return Err(anyhow::anyhow!(
"Invalid Flowise API key. Please check your configuration."
));
}
return Err(anyhow::anyhow!("Flowise API error: {}", error_text));
}
let flowise_response: FlowiseResponse = response
.json()
.await
.context("Failed to parse Flowise response")?;
Ok(flowise_response)
})
.await
.context("Failed to generate commit message from Flowise after retries")?;
let message = flowise_response.text.trim().to_string();
if message.is_empty() {
anyhow::bail!("Flowise returned an empty response");
}
Ok(message)
}
}
pub struct FlowiseProviderBuilder;
impl super::registry::ProviderBuilder for FlowiseProviderBuilder {
fn name(&self) -> &'static str {
"flowise"
}
fn aliases(&self) -> Vec<&'static str> {
vec!["flowise-ai"]
}
fn category(&self) -> super::registry::ProviderCategory {
super::registry::ProviderCategory::Local
}
fn create(&self, config: &Config) -> Result<Box<dyn super::AIProvider>> {
Ok(Box::new(FlowiseProvider::new(config)?))
}
fn requires_api_key(&self) -> bool {
false }
fn default_model(&self) -> Option<&'static str> {
None }
}