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!("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
151pub 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 }
174
175 fn default_model(&self) -> Option<&'static str> {
176 None }
178}