rusty_commit/providers/
flowise.rs1use 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 #[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 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
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}