rusty_commit/providers/
flowise.rs1use 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 #[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 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
150pub 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 }
173
174 fn default_model(&self) -> Option<&'static str> {
175 None }
177}