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::prompt::build_prompt;
19use super::AIProvider;
20use crate::config::Config;
21use crate::utils::retry::retry_async;
22
23pub struct FlowiseProvider {
24    client: Client,
25    api_url: String,
26    api_key: Option<String>,
27}
28
29#[derive(Serialize)]
30struct FlowiseRequest {
31    question: String,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    history: Option<Vec<FlowiseMessage>>,
34}
35
36#[derive(Serialize, Deserialize, Clone)]
37struct FlowiseMessage {
38    message: String,
39    #[serde(rename = "type")]
40    message_type: String,
41}
42
43#[derive(Deserialize)]
44struct FlowiseResponse {
45    text: String,
46    #[serde(rename = "sessionId")]
47    #[allow(dead_code)]
48    session_id: Option<String>,
49}
50
51impl FlowiseProvider {
52    pub fn new(config: &Config) -> Result<Self> {
53        let client = Client::new();
54        let api_url = config
55            .api_url
56            .as_deref()
57            .unwrap_or("http://localhost:3000")
58            .to_string();
59        let api_key = config.api_key.clone();
60
61        Ok(Self {
62            client,
63            api_url,
64            api_key,
65        })
66    }
67
68    /// Create provider from account configuration
69    #[allow(dead_code)]
70    pub fn from_account(
71        account: &crate::config::accounts::AccountConfig,
72        _api_key: &str,
73        config: &Config,
74    ) -> Result<Self> {
75        let client = Client::new();
76        let api_url = account
77            .api_url
78            .as_deref()
79            .or(config.api_url.as_deref())
80            .unwrap_or("http://localhost:3000")
81            .to_string();
82
83        Ok(Self {
84            client,
85            api_url,
86            api_key: None,
87        })
88    }
89}
90
91#[async_trait]
92impl AIProvider for FlowiseProvider {
93    async fn generate_commit_message(
94        &self,
95        diff: &str,
96        context: Option<&str>,
97        full_gitmoji: bool,
98        config: &Config,
99    ) -> Result<String> {
100        let prompt = build_prompt(diff, context, config, full_gitmoji);
101
102        let request = FlowiseRequest {
103            question: prompt,
104            history: None,
105        };
106
107        let flowise_response: FlowiseResponse = retry_async(|| async {
108            let url = format!("{}/api/v1/prediction/flowise", self.api_url);
109            let mut req = self.client.post(&url).json(&request);
110
111            // Add API key if available
112            if let Some(ref key) = self.api_key {
113                req = req.header("Authorization", format!("Bearer {}", key));
114            }
115
116            let response = req
117                .send()
118                .await
119                .context("Failed to connect to Flowise server. Is Flowise running?")?;
120
121            if !response.status().is_success() {
122                let error_text = response.text().await?;
123                if error_text.contains("Unauthorized") || error_text.contains("401") {
124                    return Err(anyhow::anyhow!(
125                        "Invalid Flowise API key. Please check your configuration."
126                    ));
127                }
128                return Err(anyhow::anyhow!("Flowise API error: {}", error_text));
129            }
130
131            let flowise_response: FlowiseResponse = response
132                .json()
133                .await
134                .context("Failed to parse Flowise response")?;
135
136            Ok(flowise_response)
137        })
138        .await
139        .context("Failed to generate commit message from Flowise after retries")?;
140
141        let message = flowise_response.text.trim().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}