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!(
124                        "Invalid Flowise API key. Please check your configuration."
125                    ));
126                }
127                return Err(anyhow::anyhow!("Flowise API error: {}", error_text));
128            }
129
130            let flowise_response: FlowiseResponse = response
131                .json()
132                .await
133                .context("Failed to parse Flowise response")?;
134
135            Ok(flowise_response)
136        })
137        .await
138        .context("Failed to generate commit message from Flowise after retries")?;
139
140        let message = flowise_response.text.trim().to_string();
141
142        if message.is_empty() {
143            anyhow::bail!("Flowise returned an empty response");
144        }
145
146        Ok(message)
147    }
148}
149
150/// ProviderBuilder for Flowise
151pub struct FlowiseProviderBuilder;
152
153impl super::registry::ProviderBuilder for FlowiseProviderBuilder {
154    fn name(&self) -> &'static str {
155        "flowise"
156    }
157
158    fn aliases(&self) -> Vec<&'static str> {
159        vec!["flowise-ai"]
160    }
161
162    fn category(&self) -> super::registry::ProviderCategory {
163        super::registry::ProviderCategory::Local
164    }
165
166    fn create(&self, config: &Config) -> Result<Box<dyn super::AIProvider>> {
167        Ok(Box::new(FlowiseProvider::new(config)?))
168    }
169
170    fn requires_api_key(&self) -> bool {
171        false // Flowise is self-hosted, API key is optional
172    }
173
174    fn default_model(&self) -> Option<&'static str> {
175        None // Flowise uses workflows, not direct model selection
176    }
177}