slf/gitai/
mod.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#![allow(unused)]

use anyhow::Result;
use async_trait::async_trait;
use providers::{OLLAMA_HOST, OLLAMA_MODEL};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

pub mod config;
pub mod git;
pub mod providers;

#[async_trait]
pub trait Provider: Send + Sync {
    async fn generate_commit_message(&self, diff: &str) -> Result<String>;
    fn name(&self) -> &'static str;
}

pub struct GitAI {
    config: GitAIConfig,
    provider: Box<dyn Provider>,
}

impl GitAI {
    pub async fn new(config_path: Option<PathBuf>) -> Result<Self> {
        let config = GitAIConfig::load_from(config_path).await?;
        let provider = providers::create_provider(&config)?;

        Ok(Self { config, provider })
    }

    pub fn config(&self) -> &GitAIConfig {
        &self.config
    }

    pub fn config_mut(&mut self) -> &mut GitAIConfig {
        &mut self.config
    }

    pub async fn generate_commit_message(&self, diff: &str) -> Result<String> {
        self.provider.generate_commit_message(diff).await
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct GitAIConfig {
    pub provider: String,
    pub openai_api_key: Option<String>,
    pub anthropic_api_key: Option<String>,
    pub gemini_api_key: Option<String>,
    pub ollama_host: Option<String>,
    pub ollama_model: Option<String>,
    pub assistant_thread_id: Option<String>,
    pub project_context: Option<String>,
}

impl GitAIConfig {
    pub async fn load() -> Result<Self> {
        Self::load_from(None).await
    }

    pub async fn load_from(path: Option<PathBuf>) -> Result<Self> {
        config::load_config(path)
    }

    pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
        match key {
            "provider" => self.provider = value.to_string(),
            "openai_api_key" => self.openai_api_key = Some(value.to_string()),
            "anthropic_api_key" => self.anthropic_api_key = Some(value.to_string()),
            "gemini_api_key" => self.gemini_api_key = Some(value.to_string()),
            "ollama_host" => self.ollama_host = Some(value.to_string()),
            "ollama_model" => self.ollama_model = Some(value.to_string()),
            "assistant_thread_id" => self.assistant_thread_id = Some(value.to_string()),
            "project_context" => self.project_context = Some(value.to_string()),
            _ => return Err(anyhow::anyhow!("Unknown config key: {}", key)),
        }
        Ok(())
    }

    pub fn get(&self, key: &str) -> Option<String> {
        match key {
            "provider" => Some(self.provider.clone()),
            "openai_api_key" => self.openai_api_key.clone(),
            "anthropic_api_key" => self.anthropic_api_key.clone(),
            "gemini_api_key" => self.gemini_api_key.clone(),
            "ollama_host" => self.ollama_host.clone(),
            "ollama_model" => self.ollama_model.clone(),
            "assistant_thread_id" => self.assistant_thread_id.clone(),
            "project_context" => self.project_context.clone(),
            _ => None,
        }
    }

    pub async fn save(&self) -> Result<()> {
        self.save_to(None).await
    }

    pub async fn save_to(&self, path: Option<PathBuf>) -> Result<()> {
        config::save_config(self, path)
    }

    pub fn list(&self) -> Vec<(&str, String)> {
        let mut items = vec![("provider", self.provider.clone())];

        if let Some(key) = &self.openai_api_key {
            items.push(("openai_api_key", key.clone()));
        }
        if let Some(key) = &self.anthropic_api_key {
            items.push(("anthropic_api_key", key.clone()));
        }
        if let Some(key) = &self.gemini_api_key {
            items.push(("gemini_api_key", key.clone()));
        }
        if let Some(host) = &self.ollama_host {
            items.push(("ollama_host", host.clone()));
        }
        if let Some(model) = &self.ollama_model {
            items.push(("ollama_model", model.clone()));
        }
        if let Some(id) = &self.assistant_thread_id {
            items.push(("assistant_thread_id", id.clone()));
        }
        if let Some(ctx) = &self.project_context {
            items.push(("project_context", ctx.clone()));
        }

        items
    }
}

impl Default for GitAIConfig {
    fn default() -> Self {
        Self {
            provider: "ollama".to_string(),
            openai_api_key: None,
            anthropic_api_key: None,
            gemini_api_key: None,
            ollama_host: Some(OLLAMA_HOST.to_string()),
            ollama_model: Some(OLLAMA_MODEL.to_string()),
            assistant_thread_id: None,
            project_context: None,
        }
    }
}