Skip to main content

rusty_commit/providers/
flowise.rs

1//! Flowise Provider - Self-hosted LLM workflow platform
2//!
3//! Flowise is a low-code/no-code drag & drop workflow builder for LLMs.
4//! This provider connects to a self-hosted Flowise instance.
5//!
6//! Setup:
7//! 1. Install Flowise: `npm install -g flowise`
8//! 2. Start Flowise: `npx flowise start`
9//! 3. Configure rco: `rco config set RCO_AI_PROVIDER=flowise RCO_API_URL=http://localhost:3000`
10//!
11//! Docs: https://docs.flowiseai.com/
12
13use anyhow::{Context, Result};
14use async_trait::async_trait;
15use reqwest::Client;
16use serde::{Deserialize, Serialize};
17
18use super::{build_prompt, AIProvider};
19use crate::config::Config;
20use crate::utils::retry::retry_async;
21
22pub struct FlowiseProvider {
23    client: Client,
24    api_url: String,
25    api_key: Option<String>,
26}
27
28#[derive(Serialize)]
29struct FlowiseRequest {
30    question: String,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    history: Option<Vec<FlowiseMessage>>,
33}
34
35#[derive(Serialize, Deserialize, Clone)]
36struct FlowiseMessage {
37    message: String,
38    #[serde(rename = "type")]
39    message_type: String,
40}
41
42#[derive(Deserialize)]
43struct FlowiseResponse {
44    text: String,
45    #[serde(rename = "sessionId")]
46    #[allow(dead_code)]
47    session_id: Option<String>,
48}
49
50impl FlowiseProvider {
51    pub fn new(config: &Config) -> Result<Self> {
52        let client = Client::new();
53        let api_url = config
54            .api_url
55            .as_deref()
56            .unwrap_or("http://localhost:3000")
57            .to_string();
58        let api_key = config.api_key.clone();
59
60        Ok(Self {
61            client,
62            api_url,
63            api_key,
64        })
65    }
66
67    /// Create provider from account configuration
68    #[allow(dead_code)]
69    pub fn from_account(
70        account: &crate::config::accounts::AccountConfig,
71        _api_key: &str,
72        config: &Config,
73    ) -> Result<Self> {
74        let client = Client::new();
75        let api_url = account
76            .api_url
77            .as_deref()
78            .or(config.api_url.as_deref())
79            .unwrap_or("http://localhost:3000")
80            .to_string();
81
82        Ok(Self {
83            client,
84            api_url,
85            api_key: None,
86        })
87    }
88}
89
90#[async_trait]
91impl AIProvider for FlowiseProvider {
92    async fn generate_commit_message(
93        &self,
94        diff: &str,
95        context: Option<&str>,
96        full_gitmoji: bool,
97        config: &Config,
98    ) -> Result<String> {
99        let prompt = build_prompt(diff, context, config, full_gitmoji);
100
101        let request = FlowiseRequest {
102            question: prompt,
103            history: None,
104        };
105
106        let flowise_response: FlowiseResponse = retry_async(|| async {
107            let url = format!("{}/api/v1/prediction/flowise", self.api_url);
108            let mut req = self.client.post(&url).json(&request);
109            
110            // Add API key if available
111            if let Some(ref key) = self.api_key {
112                req = req.header("Authorization", format!("Bearer {}", key));
113            }
114            
115            let response = req
116                .send()
117                .await
118                .context("Failed to connect to Flowise server. Is Flowise running?")?;
119
120            if !response.status().is_success() {
121                let error_text = response.text().await?;
122                if error_text.contains("Unauthorized") || error_text.contains("401") {
123                    return Err(anyhow::anyhow!("Invalid Flowise API key. Please check your configuration."));
124                }
125                return Err(anyhow::anyhow!("Flowise API error: {}", error_text));
126            }
127
128            let flowise_response: FlowiseResponse = response
129                .json()
130                .await
131                .context("Failed to parse Flowise response")?;
132
133            Ok(flowise_response)
134        })
135        .await
136        .context("Failed to generate commit message from Flowise after retries")?;
137
138        let message = flowise_response
139            .text
140            .trim()
141            .to_string();
142
143        if message.is_empty() {
144            anyhow::bail!("Flowise returned an empty response");
145        }
146
147        Ok(message)
148    }
149}
150
151/// ProviderBuilder for Flowise
152pub struct FlowiseProviderBuilder;
153
154impl super::registry::ProviderBuilder for FlowiseProviderBuilder {
155    fn name(&self) -> &'static str {
156        "flowise"
157    }
158
159    fn aliases(&self) -> Vec<&'static str> {
160        vec!["flowise-ai"]
161    }
162
163    fn category(&self) -> super::registry::ProviderCategory {
164        super::registry::ProviderCategory::Local
165    }
166
167    fn create(&self, config: &Config) -> Result<Box<dyn super::AIProvider>> {
168        Ok(Box::new(FlowiseProvider::new(config)?))
169    }
170
171    fn requires_api_key(&self) -> bool {
172        false // Flowise is self-hosted, API key is optional
173    }
174
175    fn default_model(&self) -> Option<&'static str> {
176        None // Flowise uses workflows, not direct model selection
177    }
178}